All user data for FoundryVTT. Includes worlds, systems, modules, and any asset in the "foundryuserdata" directory. Does NOT include the FoundryVTT installation itself.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

23993 lines
846 KiB

1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
  1. /**
  2. * Base configuration application for advancements that can be extended by other types to implement custom
  3. * editing interfaces.
  4. *
  5. * @param {Advancement} advancement The advancement item being edited.
  6. * @param {object} [options={}] Additional options passed to FormApplication.
  7. * @param {string} [options.dropKeyPath=null] Path within advancement configuration where dropped items are stored.
  8. * If populated, will enable default drop & delete behavior.
  9. */
  10. class AdvancementConfig extends FormApplication {
  11. constructor(advancement, options={}) {
  12. super(advancement, options);
  13. this.#advancementId = advancement.id;
  14. this.item = advancement.item;
  15. }
  16. /* -------------------------------------------- */
  17. /**
  18. * The ID of the advancement being created or edited.
  19. * @type {string}
  20. */
  21. #advancementId;
  22. /* -------------------------------------------- */
  23. /**
  24. * Parent item to which this advancement belongs.
  25. * @type {Item5e}
  26. */
  27. item;
  28. /* -------------------------------------------- */
  29. /** @inheritDoc */
  30. static get defaultOptions() {
  31. return foundry.utils.mergeObject(super.defaultOptions, {
  32. classes: ["dnd5e", "advancement", "dialog"],
  33. template: "systems/dnd5e/templates/advancement/advancement-config.hbs",
  34. width: 400,
  35. height: "auto",
  36. submitOnChange: true,
  37. closeOnSubmit: false,
  38. dropKeyPath: null
  39. });
  40. }
  41. /* -------------------------------------------- */
  42. /**
  43. * The advancement being created or edited.
  44. * @type {Advancement}
  45. */
  46. get advancement() {
  47. return this.item.advancement.byId[this.#advancementId];
  48. }
  49. /* -------------------------------------------- */
  50. /** @inheritDoc */
  51. get title() {
  52. const type = this.advancement.constructor.metadata.title;
  53. return `${game.i18n.format("DND5E.AdvancementConfigureTitle", { item: this.item.name })}: ${type}`;
  54. }
  55. /* -------------------------------------------- */
  56. /** @inheritdoc */
  57. async close(options={}) {
  58. await super.close(options);
  59. delete this.advancement.apps[this.appId];
  60. }
  61. /* -------------------------------------------- */
  62. /** @inheritdoc */
  63. getData() {
  64. const levels = Object.fromEntries(Array.fromRange(CONFIG.DND5E.maxLevel + 1).map(l => [l, l]));
  65. if ( ["class", "subclass"].includes(this.item.type) ) delete levels[0];
  66. else levels[0] = game.i18n.localize("DND5E.AdvancementLevelAnyHeader");
  67. const context = {
  68. CONFIG: CONFIG.DND5E,
  69. ...this.advancement.toObject(false),
  70. src: this.advancement.toObject(),
  71. default: {
  72. title: this.advancement.constructor.metadata.title,
  73. icon: this.advancement.constructor.metadata.icon
  74. },
  75. levels,
  76. showClassRestrictions: this.item.type === "class",
  77. showLevelSelector: !this.advancement.constructor.metadata.multiLevel
  78. };
  79. return context;
  80. }
  81. /* -------------------------------------------- */
  82. /**
  83. * Perform any changes to configuration data before it is saved to the advancement.
  84. * @param {object} configuration Configuration object.
  85. * @returns {object} Modified configuration.
  86. */
  87. async prepareConfigurationUpdate(configuration) {
  88. return configuration;
  89. }
  90. /* -------------------------------------------- */
  91. /** @inheritdoc */
  92. activateListeners(html) {
  93. super.activateListeners(html);
  94. // Remove an item from the list
  95. if ( this.options.dropKeyPath ) html.on("click", "[data-action='delete']", this._onItemDelete.bind(this));
  96. }
  97. /* -------------------------------------------- */
  98. /** @inheritdoc */
  99. render(force=false, options={}) {
  100. this.advancement.apps[this.appId] = this;
  101. return super.render(force, options);
  102. }
  103. /* -------------------------------------------- */
  104. /** @inheritdoc */
  105. async _updateObject(event, formData) {
  106. let updates = foundry.utils.expandObject(formData);
  107. if ( updates.configuration ) updates.configuration = await this.prepareConfigurationUpdate(updates.configuration);
  108. await this.advancement.update(updates);
  109. }
  110. /* -------------------------------------------- */
  111. /**
  112. * Helper method to take an object and apply updates that remove any empty keys.
  113. * @param {object} object Object to be cleaned.
  114. * @returns {object} Copy of object with only non false-ish values included and others marked
  115. * using `-=` syntax to be removed by update process.
  116. * @protected
  117. */
  118. static _cleanedObject(object) {
  119. return Object.entries(object).reduce((obj, [key, value]) => {
  120. if ( value ) obj[key] = value;
  121. else obj[`-=${key}`] = null;
  122. return obj;
  123. }, {});
  124. }
  125. /* -------------------------------------------- */
  126. /* Drag & Drop for Item Pools */
  127. /* -------------------------------------------- */
  128. /**
  129. * Handle deleting an existing Item entry from the Advancement.
  130. * @param {Event} event The originating click event.
  131. * @returns {Promise<Item5e>} The updated parent Item after the application re-renders.
  132. * @protected
  133. */
  134. async _onItemDelete(event) {
  135. event.preventDefault();
  136. const uuidToDelete = event.currentTarget.closest("[data-item-uuid]")?.dataset.itemUuid;
  137. if ( !uuidToDelete ) return;
  138. const items = foundry.utils.getProperty(this.advancement.configuration, this.options.dropKeyPath);
  139. const updates = { configuration: await this.prepareConfigurationUpdate({
  140. [this.options.dropKeyPath]: items.filter(uuid => uuid !== uuidToDelete)
  141. }) };
  142. await this.advancement.update(updates);
  143. }
  144. /* -------------------------------------------- */
  145. /** @inheritdoc */
  146. _canDragDrop() {
  147. return this.isEditable;
  148. }
  149. /* -------------------------------------------- */
  150. /** @inheritdoc */
  151. async _onDrop(event) {
  152. if ( !this.options.dropKeyPath ) throw new Error(
  153. "AdvancementConfig#options.dropKeyPath must be configured or #_onDrop must be overridden to support"
  154. + " drag and drop on advancement config items."
  155. );
  156. // Try to extract the data
  157. const data = TextEditor.getDragEventData(event);
  158. if ( data?.type !== "Item" ) return false;
  159. const item = await Item.implementation.fromDropData(data);
  160. try {
  161. this._validateDroppedItem(event, item);
  162. } catch(err) {
  163. return ui.notifications.error(err.message);
  164. }
  165. const existingItems = foundry.utils.getProperty(this.advancement.configuration, this.options.dropKeyPath);
  166. // Abort if this uuid is the parent item
  167. if ( item.uuid === this.item.uuid ) {
  168. return ui.notifications.error(game.i18n.localize("DND5E.AdvancementItemGrantRecursiveWarning"));
  169. }
  170. // Abort if this uuid exists already
  171. if ( existingItems.includes(item.uuid) ) {
  172. return ui.notifications.warn(game.i18n.localize("DND5E.AdvancementItemGrantDuplicateWarning"));
  173. }
  174. await this.advancement.update({[`configuration.${this.options.dropKeyPath}`]: [...existingItems, item.uuid]});
  175. }
  176. /* -------------------------------------------- */
  177. /**
  178. * Called when an item is dropped to validate the Item before it is saved. An error should be thrown
  179. * if the item is invalid.
  180. * @param {Event} event Triggering drop event.
  181. * @param {Item5e} item The materialized Item that was dropped.
  182. * @throws An error if the item is invalid.
  183. * @protected
  184. */
  185. _validateDroppedItem(event, item) {}
  186. }
  187. /**
  188. * Base class for the advancement interface displayed by the advancement prompt that should be subclassed by
  189. * individual advancement types.
  190. *
  191. * @param {Item5e} item Item to which the advancement belongs.
  192. * @param {string} advancementId ID of the advancement this flow modifies.
  193. * @param {number} level Level for which to configure this flow.
  194. * @param {object} [options={}] Application rendering options.
  195. */
  196. class AdvancementFlow extends FormApplication {
  197. constructor(item, advancementId, level, options={}) {
  198. super({}, options);
  199. /**
  200. * The item that houses the Advancement.
  201. * @type {Item5e}
  202. */
  203. this.item = item;
  204. /**
  205. * ID of the advancement this flow modifies.
  206. * @type {string}
  207. * @private
  208. */
  209. this._advancementId = advancementId;
  210. /**
  211. * Level for which to configure this flow.
  212. * @type {number}
  213. */
  214. this.level = level;
  215. /**
  216. * Data retained by the advancement manager during a reverse step. If restoring data using Advancement#restore,
  217. * this data should be used when displaying the flow's form.
  218. * @type {object|null}
  219. */
  220. this.retainedData = null;
  221. }
  222. /* -------------------------------------------- */
  223. /** @inheritdoc */
  224. static get defaultOptions() {
  225. return foundry.utils.mergeObject(super.defaultOptions, {
  226. template: "systems/dnd5e/templates/advancement/advancement-flow.hbs",
  227. popOut: false
  228. });
  229. }
  230. /* -------------------------------------------- */
  231. /** @inheritdoc */
  232. get id() {
  233. return `actor-${this.advancement.item.id}-advancement-${this.advancement.id}-${this.level}`;
  234. }
  235. /* -------------------------------------------- */
  236. /** @inheritdoc */
  237. get title() {
  238. return this.advancement.title;
  239. }
  240. /* -------------------------------------------- */
  241. /**
  242. * The Advancement object this flow modifies.
  243. * @type {Advancement|null}
  244. */
  245. get advancement() {
  246. return this.item.advancement?.byId[this._advancementId] ?? null;
  247. }
  248. /* -------------------------------------------- */
  249. /**
  250. * Set the retained data for this flow. This method gives the flow a chance to do any additional prep
  251. * work required for the retained data before the application is rendered.
  252. * @param {object} data Retained data associated with this flow.
  253. */
  254. async retainData(data) {
  255. this.retainedData = data;
  256. }
  257. /* -------------------------------------------- */
  258. /** @inheritdoc */
  259. getData() {
  260. return {
  261. appId: this.id,
  262. advancement: this.advancement,
  263. type: this.advancement.constructor.typeName,
  264. title: this.title,
  265. summary: this.advancement.summaryForLevel(this.level),
  266. level: this.level
  267. };
  268. }
  269. /* -------------------------------------------- */
  270. /** @inheritdoc */
  271. async _updateObject(event, formData) {
  272. await this.advancement.apply(this.level, formData);
  273. }
  274. }
  275. /**
  276. * Data Model variant with some extra methods to support template mix-ins.
  277. *
  278. * **Note**: This uses some advanced Javascript techniques that are not necessary for most data models.
  279. * Please refer to the [advancement data models]{@link BaseAdvancement} for an example of a more typical usage.
  280. *
  281. * In template.json, each Actor or Item type can incorporate several templates which are chunks of data that are
  282. * common across all the types that use them. One way to represent them in the schema for a given Document type is to
  283. * duplicate schema definitions for the templates and write them directly into the Data Model for the Document type.
  284. * This works fine for small templates or systems that do not need many Document types but for more complex systems
  285. * this boilerplate can become prohibitive.
  286. *
  287. * Here we have opted to instead create a separate Data Model for each template available. These define their own
  288. * schemas which are then mixed-in to the final schema for the Document type's Data Model. A Document type Data Model
  289. * can define its own schema unique to it, and then add templates in direct correspondence to those in template.json
  290. * via SystemDataModel.mixin.
  291. */
  292. class SystemDataModel extends foundry.abstract.DataModel {
  293. /** @inheritdoc */
  294. static _enableV10Validation = true;
  295. /**
  296. * System type that this system data model represents (e.g. "character", "npc", "vehicle").
  297. * @type {string}
  298. */
  299. static _systemType;
  300. /* -------------------------------------------- */
  301. /**
  302. * Base templates used for construction.
  303. * @type {*[]}
  304. * @private
  305. */
  306. static _schemaTemplates = [];
  307. /* -------------------------------------------- */
  308. /**
  309. * A list of properties that should not be mixed-in to the final type.
  310. * @type {Set<string>}
  311. * @private
  312. */
  313. static _immiscible = new Set(["length", "mixed", "name", "prototype", "migrateData", "defineSchema"]);
  314. /* -------------------------------------------- */
  315. /** @inheritdoc */
  316. static defineSchema() {
  317. const schema = {};
  318. for ( const template of this._schemaTemplates ) {
  319. if ( !template.defineSchema ) {
  320. throw new Error(`Invalid dnd5e template mixin ${template} defined on class ${this.constructor}`);
  321. }
  322. this.mergeSchema(schema, template.defineSchema());
  323. }
  324. return schema;
  325. }
  326. /* -------------------------------------------- */
  327. /**
  328. * Merge two schema definitions together as well as possible.
  329. * @param {DataSchema} a First schema that forms the basis for the merge. *Will be mutated.*
  330. * @param {DataSchema} b Second schema that will be merged in, overwriting any non-mergeable properties.
  331. * @returns {DataSchema} Fully merged schema.
  332. */
  333. static mergeSchema(a, b) {
  334. Object.assign(a, b);
  335. return a;
  336. }
  337. /* -------------------------------------------- */
  338. /** @inheritdoc */
  339. static migrateData(source) {
  340. for ( const template of this._schemaTemplates ) {
  341. template.migrateData?.(source);
  342. }
  343. return super.migrateData(source);
  344. }
  345. /* -------------------------------------------- */
  346. /** @inheritdoc */
  347. validate(options={}) {
  348. if ( this.constructor._enableV10Validation === false ) return true;
  349. return super.validate(options);
  350. }
  351. /* -------------------------------------------- */
  352. /**
  353. * Mix multiple templates with the base type.
  354. * @param {...*} templates Template classes to mix.
  355. * @returns {typeof SystemDataModel} Final prepared type.
  356. */
  357. static mixin(...templates) {
  358. const Base = class extends this {};
  359. Object.defineProperty(Base, "_schemaTemplates", {
  360. value: Object.seal([...this._schemaTemplates, ...templates]),
  361. writable: false,
  362. configurable: false
  363. });
  364. for ( const template of templates ) {
  365. // Take all static methods and fields from template and mix in to base class
  366. for ( const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(template)) ) {
  367. if ( this._immiscible.has(key) ) continue;
  368. Object.defineProperty(Base, key, descriptor);
  369. }
  370. // Take all instance methods and fields from template and mix in to base class
  371. for ( const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(template.prototype)) ) {
  372. if ( ["constructor"].includes(key) ) continue;
  373. Object.defineProperty(Base.prototype, key, descriptor);
  374. }
  375. }
  376. return Base;
  377. }
  378. }
  379. /* -------------------------------------------- */
  380. /**
  381. * Data Model variant that does not export fields with an `undefined` value during `toObject(true)`.
  382. */
  383. class SparseDataModel extends foundry.abstract.DataModel {
  384. /** @inheritdoc */
  385. toObject(source=true) {
  386. if ( !source ) return super.toObject(source);
  387. const clone = foundry.utils.flattenObject(this._source);
  388. // Remove any undefined keys from the source data
  389. Object.keys(clone).filter(k => clone[k] === undefined).forEach(k => delete clone[k]);
  390. return foundry.utils.expandObject(clone);
  391. }
  392. }
  393. /**
  394. * Data field that selects the appropriate advancement data model if available, otherwise defaults to generic
  395. * `ObjectField` to prevent issues with custom advancement types that aren't currently loaded.
  396. */
  397. class AdvancementField extends foundry.data.fields.ObjectField {
  398. /**
  399. * Get the BaseAdvancement definition for the specified advancement type.
  400. * @param {string} type The Advancement type.
  401. * @returns {typeof BaseAdvancement|null} The BaseAdvancement class, or null.
  402. */
  403. getModelForType(type) {
  404. return CONFIG.DND5E.advancementTypes[type] ?? null;
  405. }
  406. /* -------------------------------------------- */
  407. /** @inheritdoc */
  408. _cleanType(value, options) {
  409. if ( !(typeof value === "object") ) value = {};
  410. const cls = this.getModelForType(value.type);
  411. if ( cls ) return cls.cleanData(value, options);
  412. return value;
  413. }
  414. /* -------------------------------------------- */
  415. /** @inheritdoc */
  416. initialize(value, model, options={}) {
  417. const cls = this.getModelForType(value.type);
  418. if ( cls ) return new cls(value, {parent: model, ...options});
  419. return foundry.utils.deepClone(value);
  420. }
  421. }
  422. /* -------------------------------------------- */
  423. /**
  424. * Data field that automatically selects the Advancement-specific configuration or value data models.
  425. *
  426. * @param {Advancement} advancementType Advancement class to which this field belongs.
  427. */
  428. class AdvancementDataField extends foundry.data.fields.ObjectField {
  429. constructor(advancementType, options={}) {
  430. super(options);
  431. this.advancementType = advancementType;
  432. }
  433. /* -------------------------------------------- */
  434. /** @inheritdoc */
  435. static get _defaults() {
  436. return foundry.utils.mergeObject(super._defaults, {required: true});
  437. }
  438. /**
  439. * Get the DataModel definition for the specified field as defined in metadata.
  440. * @returns {typeof DataModel|null} The DataModel class, or null.
  441. */
  442. getModel() {
  443. return this.advancementType.metadata?.dataModels?.[this.name];
  444. }
  445. /* -------------------------------------------- */
  446. /**
  447. * Get the defaults object for the specified field as defined in metadata.
  448. * @returns {object}
  449. */
  450. getDefaults() {
  451. return this.advancementType.metadata?.defaults?.[this.name] ?? {};
  452. }
  453. /* -------------------------------------------- */
  454. /** @inheritdoc */
  455. _cleanType(value, options) {
  456. if ( !(typeof value === "object") ) value = {};
  457. // Use a defined DataModel
  458. const cls = this.getModel();
  459. if ( cls ) return cls.cleanData(value, options);
  460. if ( options.partial ) return value;
  461. // Use the defined defaults
  462. const defaults = this.getDefaults();
  463. return foundry.utils.mergeObject(defaults, value, {inplace: false});
  464. }
  465. /* -------------------------------------------- */
  466. /** @inheritdoc */
  467. initialize(value, model, options={}) {
  468. const cls = this.getModel();
  469. if ( cls ) return new cls(value, {parent: model, ...options});
  470. return foundry.utils.deepClone(value);
  471. }
  472. }
  473. /* -------------------------------------------- */
  474. /**
  475. * @typedef {StringFieldOptions} FormulaFieldOptions
  476. * @property {boolean} [deterministic=false] Is this formula not allowed to have dice values?
  477. */
  478. /**
  479. * Special case StringField which represents a formula.
  480. *
  481. * @param {FormulaFieldOptions} [options={}] Options which configure the behavior of the field.
  482. * @property {boolean} deterministic=false Is this formula not allowed to have dice values?
  483. */
  484. class FormulaField extends foundry.data.fields.StringField {
  485. /** @inheritdoc */
  486. static get _defaults() {
  487. return foundry.utils.mergeObject(super._defaults, {
  488. deterministic: false
  489. });
  490. }
  491. /* -------------------------------------------- */
  492. /** @inheritdoc */
  493. _validateType(value) {
  494. if ( this.options.deterministic ) {
  495. const roll = new Roll(value);
  496. if ( !roll.isDeterministic ) throw new Error("must not contain dice terms");
  497. Roll.safeEval(roll.formula);
  498. }
  499. else Roll.validate(value);
  500. super._validateType(value);
  501. }
  502. }
  503. /* -------------------------------------------- */
  504. /**
  505. * Special case StringField that includes automatic validation for identifiers.
  506. */
  507. class IdentifierField extends foundry.data.fields.StringField {
  508. /** @override */
  509. _validateType(value) {
  510. if ( !dnd5e.utils.validators.isValidIdentifier(value) ) {
  511. throw new Error(game.i18n.localize("DND5E.IdentifierError"));
  512. }
  513. }
  514. }
  515. /* -------------------------------------------- */
  516. /**
  517. * @callback MappingFieldInitialValueBuilder
  518. * @param {string} key The key within the object where this new value is being generated.
  519. * @param {*} initial The generic initial data provided by the contained model.
  520. * @param {object} existing Any existing mapping data.
  521. * @returns {object} Value to use as default for this key.
  522. */
  523. /**
  524. * @typedef {DataFieldOptions} MappingFieldOptions
  525. * @property {string[]} [initialKeys] Keys that will be created if no data is provided.
  526. * @property {MappingFieldInitialValueBuilder} [initialValue] Function to calculate the initial value for a key.
  527. * @property {boolean} [initialKeysOnly=false] Should the keys in the initialized data be limited to the keys provided
  528. * by `options.initialKeys`?
  529. */
  530. /**
  531. * A subclass of ObjectField that represents a mapping of keys to the provided DataField type.
  532. *
  533. * @param {DataField} model The class of DataField which should be embedded in this field.
  534. * @param {MappingFieldOptions} [options={}] Options which configure the behavior of the field.
  535. * @property {string[]} [initialKeys] Keys that will be created if no data is provided.
  536. * @property {MappingFieldInitialValueBuilder} [initialValue] Function to calculate the initial value for a key.
  537. * @property {boolean} [initialKeysOnly=false] Should the keys in the initialized data be limited to the keys provided
  538. * by `options.initialKeys`?
  539. */
  540. class MappingField extends foundry.data.fields.ObjectField {
  541. constructor(model, options) {
  542. if ( !(model instanceof foundry.data.fields.DataField) ) {
  543. throw new Error("MappingField must have a DataField as its contained element");
  544. }
  545. super(options);
  546. /**
  547. * The embedded DataField definition which is contained in this field.
  548. * @type {DataField}
  549. */
  550. this.model = model;
  551. }
  552. /* -------------------------------------------- */
  553. /** @inheritdoc */
  554. static get _defaults() {
  555. return foundry.utils.mergeObject(super._defaults, {
  556. initialKeys: null,
  557. initialValue: null,
  558. initialKeysOnly: false
  559. });
  560. }
  561. /* -------------------------------------------- */
  562. /** @inheritdoc */
  563. _cleanType(value, options) {
  564. Object.entries(value).forEach(([k, v]) => value[k] = this.model.clean(v, options));
  565. return value;
  566. }
  567. /* -------------------------------------------- */
  568. /** @inheritdoc */
  569. getInitialValue(data) {
  570. let keys = this.initialKeys;
  571. const initial = super.getInitialValue(data);
  572. if ( !keys || !foundry.utils.isEmpty(initial) ) return initial;
  573. if ( !(keys instanceof Array) ) keys = Object.keys(keys);
  574. for ( const key of keys ) initial[key] = this._getInitialValueForKey(key);
  575. return initial;
  576. }
  577. /* -------------------------------------------- */
  578. /**
  579. * Get the initial value for the provided key.
  580. * @param {string} key Key within the object being built.
  581. * @param {object} [object] Any existing mapping data.
  582. * @returns {*} Initial value based on provided field type.
  583. */
  584. _getInitialValueForKey(key, object) {
  585. const initial = this.model.getInitialValue();
  586. return this.initialValue?.(key, initial, object) ?? initial;
  587. }
  588. /* -------------------------------------------- */
  589. /** @override */
  590. _validateType(value, options={}) {
  591. if ( foundry.utils.getType(value) !== "Object" ) throw new Error("must be an Object");
  592. const errors = this._validateValues(value, options);
  593. if ( !foundry.utils.isEmpty(errors) ) throw new foundry.data.fields.ModelValidationError(errors);
  594. }
  595. /* -------------------------------------------- */
  596. /**
  597. * Validate each value of the object.
  598. * @param {object} value The object to validate.
  599. * @param {object} options Validation options.
  600. * @returns {Object<Error>} An object of value-specific errors by key.
  601. */
  602. _validateValues(value, options) {
  603. const errors = {};
  604. for ( const [k, v] of Object.entries(value) ) {
  605. const error = this.model.validate(v, options);
  606. if ( error ) errors[k] = error;
  607. }
  608. return errors;
  609. }
  610. /* -------------------------------------------- */
  611. /** @override */
  612. initialize(value, model, options={}) {
  613. if ( !value ) return value;
  614. const obj = {};
  615. const initialKeys = (this.initialKeys instanceof Array) ? this.initialKeys : Object.keys(this.initialKeys ?? {});
  616. const keys = this.initialKeysOnly ? initialKeys : Object.keys(value);
  617. for ( const key of keys ) {
  618. const data = value[key] ?? this._getInitialValueForKey(key, value);
  619. obj[key] = this.model.initialize(data, model, options);
  620. }
  621. return obj;
  622. }
  623. /* -------------------------------------------- */
  624. /** @inheritdoc */
  625. _getField(path) {
  626. if ( path.length === 0 ) return this;
  627. else if ( path.length === 1 ) return this.model;
  628. path.shift();
  629. return this.model._getField(path);
  630. }
  631. }
  632. var fields = /*#__PURE__*/Object.freeze({
  633. __proto__: null,
  634. AdvancementDataField: AdvancementDataField,
  635. AdvancementField: AdvancementField,
  636. FormulaField: FormulaField,
  637. IdentifierField: IdentifierField,
  638. MappingField: MappingField
  639. });
  640. class BaseAdvancement extends SparseDataModel {
  641. /**
  642. * Name of this advancement type that will be stored in config and used for lookups.
  643. * @type {string}
  644. * @protected
  645. */
  646. static get typeName() {
  647. return this.name.replace(/Advancement$/, "");
  648. }
  649. /* -------------------------------------------- */
  650. /** @inheritdoc */
  651. static defineSchema() {
  652. return {
  653. _id: new foundry.data.fields.DocumentIdField({initial: () => foundry.utils.randomID()}),
  654. type: new foundry.data.fields.StringField({
  655. required: true, initial: this.typeName, validate: v => v === this.typeName,
  656. validationError: `must be the same as the Advancement type name ${this.typeName}`
  657. }),
  658. configuration: new AdvancementDataField(this, {required: true}),
  659. value: new AdvancementDataField(this, {required: true}),
  660. level: new foundry.data.fields.NumberField({
  661. integer: true, initial: this.metadata?.multiLevel ? undefined : 1, min: 0, label: "DND5E.Level"
  662. }),
  663. title: new foundry.data.fields.StringField({initial: undefined, label: "DND5E.AdvancementCustomTitle"}),
  664. icon: new foundry.data.fields.FilePathField({
  665. initial: undefined, categories: ["IMAGE"], label: "DND5E.AdvancementCustomIcon"
  666. }),
  667. classRestriction: new foundry.data.fields.StringField({
  668. initial: undefined, choices: ["primary", "secondary"], label: "DND5E.AdvancementClassRestriction"
  669. })
  670. };
  671. }
  672. }
  673. /**
  674. * Error that can be thrown during the advancement update preparation process.
  675. */
  676. class AdvancementError extends Error {
  677. constructor(...args) {
  678. super(...args);
  679. this.name = "AdvancementError";
  680. }
  681. }
  682. /**
  683. * Abstract base class which various advancement types can subclass.
  684. * @param {Item5e} item Item to which this advancement belongs.
  685. * @param {object} [data={}] Raw data stored in the advancement object.
  686. * @param {object} [options={}] Options which affect DataModel construction.
  687. * @abstract
  688. */
  689. class Advancement extends BaseAdvancement {
  690. constructor(data, {parent=null, ...options}={}) {
  691. if ( parent instanceof Item ) parent = parent.system;
  692. super(data, {parent, ...options});
  693. /**
  694. * A collection of Application instances which should be re-rendered whenever this document is updated.
  695. * The keys of this object are the application ids and the values are Application instances. Each
  696. * Application in this object will have its render method called by {@link Document#render}.
  697. * @type {Object<Application>}
  698. */
  699. Object.defineProperty(this, "apps", {
  700. value: {},
  701. writable: false,
  702. enumerable: false
  703. });
  704. }
  705. /* -------------------------------------------- */
  706. /** @inheritdoc */
  707. _initialize(options) {
  708. super._initialize(options);
  709. return this.prepareData();
  710. }
  711. static ERROR = AdvancementError;
  712. /* -------------------------------------------- */
  713. /**
  714. * Information on how an advancement type is configured.
  715. *
  716. * @typedef {object} AdvancementMetadata
  717. * @property {object} dataModels
  718. * @property {DataModel} configuration Data model used for validating configuration data.
  719. * @property {DataModel} value Data model used for validating value data.
  720. * @property {number} order Number used to determine default sorting order of advancement items.
  721. * @property {string} icon Icon used for this advancement type if no user icon is specified.
  722. * @property {string} title Title to be displayed if no user title is specified.
  723. * @property {string} hint Description of this type shown in the advancement selection dialog.
  724. * @property {boolean} multiLevel Can this advancement affect more than one level? If this is set to true,
  725. * the level selection control in the configuration window is hidden and the
  726. * advancement should provide its own implementation of `Advancement#levels`
  727. * and potentially its own level configuration interface.
  728. * @property {Set<string>} validItemTypes Set of types to which this advancement can be added.
  729. * @property {object} apps
  730. * @property {*} apps.config Subclass of AdvancementConfig that allows for editing of this advancement type.
  731. * @property {*} apps.flow Subclass of AdvancementFlow that is displayed while fulfilling this advancement.
  732. */
  733. /**
  734. * Configuration information for this advancement type.
  735. * @type {AdvancementMetadata}
  736. */
  737. static get metadata() {
  738. return {
  739. order: 100,
  740. icon: "icons/svg/upgrade.svg",
  741. title: game.i18n.localize("DND5E.AdvancementTitle"),
  742. hint: "",
  743. multiLevel: false,
  744. validItemTypes: new Set(["background", "class", "subclass"]),
  745. apps: {
  746. config: AdvancementConfig,
  747. flow: AdvancementFlow
  748. }
  749. };
  750. }
  751. /* -------------------------------------------- */
  752. /* Instance Properties */
  753. /* -------------------------------------------- */
  754. /**
  755. * Unique identifier for this advancement within its item.
  756. * @type {string}
  757. */
  758. get id() {
  759. return this._id;
  760. }
  761. /* -------------------------------------------- */
  762. /**
  763. * Globally unique identifier for this advancement.
  764. * @type {string}
  765. */
  766. get uuid() {
  767. return `${this.item.uuid}.Advancement.${this.id}`;
  768. }
  769. /* -------------------------------------------- */
  770. /**
  771. * Item to which this advancement belongs.
  772. * @type {Item5e}
  773. */
  774. get item() {
  775. return this.parent.parent;
  776. }
  777. /* -------------------------------------------- */
  778. /**
  779. * Actor to which this advancement's item belongs, if the item is embedded.
  780. * @type {Actor5e|null}
  781. */
  782. get actor() {
  783. return this.item.parent ?? null;
  784. }
  785. /* -------------------------------------------- */
  786. /**
  787. * List of levels in which this advancement object should be displayed. Will be a list of class levels if this
  788. * advancement is being applied to classes or subclasses, otherwise a list of character levels.
  789. * @returns {number[]}
  790. */
  791. get levels() {
  792. return this.level !== undefined ? [this.level] : [];
  793. }
  794. /* -------------------------------------------- */
  795. /**
  796. * Should this advancement be applied to a class based on its class restriction setting? This will always return
  797. * true for advancements that are not within an embedded class item.
  798. * @type {boolean}
  799. * @protected
  800. */
  801. get appliesToClass() {
  802. const originalClass = this.item.isOriginalClass;
  803. return (originalClass === null) || !this.classRestriction
  804. || (this.classRestriction === "primary" && originalClass)
  805. || (this.classRestriction === "secondary" && !originalClass);
  806. }
  807. /* -------------------------------------------- */
  808. /* Preparation Methods */
  809. /* -------------------------------------------- */
  810. /**
  811. * Prepare data for the Advancement.
  812. */
  813. prepareData() {
  814. this.title = this.title || this.constructor.metadata.title;
  815. this.icon = this.icon || this.constructor.metadata.icon;
  816. }
  817. /* -------------------------------------------- */
  818. /* Display Methods */
  819. /* -------------------------------------------- */
  820. /**
  821. * Has the player made choices for this advancement at the specified level?
  822. * @param {number} level Level for which to check configuration.
  823. * @returns {boolean} Have any available choices been made?
  824. */
  825. configuredForLevel(level) {
  826. return true;
  827. }
  828. /* -------------------------------------------- */
  829. /**
  830. * Value used for sorting this advancement at a certain level.
  831. * @param {number} level Level for which this entry is being sorted.
  832. * @returns {string} String that can be used for sorting.
  833. */
  834. sortingValueForLevel(level) {
  835. return `${this.constructor.metadata.order.paddedString(4)} ${this.titleForLevel(level)}`;
  836. }
  837. /* -------------------------------------------- */
  838. /**
  839. * Title displayed in advancement list for a specific level.
  840. * @param {number} level Level for which to generate a title.
  841. * @param {object} [options={}]
  842. * @param {object} [options.configMode=false] Is the advancement's item sheet in configuration mode? When in
  843. * config mode, the choices already made on this actor should not
  844. * be displayed.
  845. * @returns {string} HTML title with any level-specific information.
  846. */
  847. titleForLevel(level, { configMode=false }={}) {
  848. return this.title;
  849. }
  850. /* -------------------------------------------- */
  851. /**
  852. * Summary content displayed beneath the title in the advancement list.
  853. * @param {number} level Level for which to generate the summary.
  854. * @param {object} [options={}]
  855. * @param {object} [options.configMode=false] Is the advancement's item sheet in configuration mode? When in
  856. * config mode, the choices already made on this actor should not
  857. * be displayed.
  858. * @returns {string} HTML content of the summary.
  859. */
  860. summaryForLevel(level, { configMode=false }={}) {
  861. return "";
  862. }
  863. /* -------------------------------------------- */
  864. /**
  865. * Render all of the Application instances which are connected to this advancement.
  866. * @param {boolean} [force=false] Force rendering
  867. * @param {object} [context={}] Optional context
  868. */
  869. render(force=false, context={}) {
  870. for ( const app of Object.values(this.apps) ) app.render(force, context);
  871. }
  872. /* -------------------------------------------- */
  873. /* Editing Methods */
  874. /* -------------------------------------------- */
  875. /**
  876. * Update this advancement.
  877. * @param {object} updates Updates to apply to this advancement.
  878. * @returns {Promise<Advancement>} This advancement after updates have been applied.
  879. */
  880. async update(updates) {
  881. await this.item.updateAdvancement(this.id, updates);
  882. return this;
  883. }
  884. /* -------------------------------------------- */
  885. /**
  886. * Update this advancement's data on the item without performing a database commit.
  887. * @param {object} updates Updates to apply to this advancement.
  888. * @returns {Advancement} This advancement after updates have been applied.
  889. */
  890. updateSource(updates) {
  891. super.updateSource(updates);
  892. return this;
  893. }
  894. /* -------------------------------------------- */
  895. /**
  896. * Can an advancement of this type be added to the provided item?
  897. * @param {Item5e} item Item to check against.
  898. * @returns {boolean} Should this be enabled as an option on the `AdvancementSelection` dialog?
  899. */
  900. static availableForItem(item) {
  901. return true;
  902. }
  903. /* -------------------------------------------- */
  904. /**
  905. * Serialize salient information for this Advancement when dragging it.
  906. * @returns {object} An object of drag data.
  907. */
  908. toDragData() {
  909. const dragData = { type: "Advancement" };
  910. if ( this.id ) dragData.uuid = this.uuid;
  911. else dragData.data = this.toObject();
  912. return dragData;
  913. }
  914. /* -------------------------------------------- */
  915. /* Application Methods */
  916. /* -------------------------------------------- */
  917. /**
  918. * Locally apply this advancement to the actor.
  919. * @param {number} level Level being advanced.
  920. * @param {object} data Data from the advancement form.
  921. * @abstract
  922. */
  923. async apply(level, data) { }
  924. /* -------------------------------------------- */
  925. /**
  926. * Locally apply this advancement from stored data, if possible. If stored data can not be restored for any reason,
  927. * throw an AdvancementError to display the advancement flow UI.
  928. * @param {number} level Level being advanced.
  929. * @param {object} data Data from `Advancement#reverse` needed to restore this advancement.
  930. * @abstract
  931. */
  932. async restore(level, data) { }
  933. /* -------------------------------------------- */
  934. /**
  935. * Locally remove this advancement's changes from the actor.
  936. * @param {number} level Level being removed.
  937. * @returns {object} Data that can be passed to the `Advancement#restore` method to restore this reversal.
  938. * @abstract
  939. */
  940. async reverse(level) { }
  941. }
  942. /**
  943. * Configuration application for ability score improvements.
  944. */
  945. class AbilityScoreImprovementConfig extends AdvancementConfig {
  946. /** @inheritdoc */
  947. static get defaultOptions() {
  948. return foundry.utils.mergeObject(super.defaultOptions, {
  949. template: "systems/dnd5e/templates/advancement/ability-score-improvement-config.hbs"
  950. });
  951. }
  952. /* -------------------------------------------- */
  953. /** @inheritdoc */
  954. getData() {
  955. const abilities = Object.entries(CONFIG.DND5E.abilities).reduce((obj, [key, data]) => {
  956. if ( !this.advancement.canImprove(key) ) return obj;
  957. const fixed = this.advancement.configuration.fixed[key] ?? 0;
  958. obj[key] = {
  959. key,
  960. name: `configuration.fixed.${key}`,
  961. label: data.label,
  962. value: fixed,
  963. canIncrease: true,
  964. canDecrease: true
  965. };
  966. return obj;
  967. }, {});
  968. return foundry.utils.mergeObject(super.getData(), {
  969. abilities,
  970. points: {
  971. key: "points",
  972. name: "configuration.points",
  973. label: game.i18n.localize("DND5E.AdvancementAbilityScoreImprovementPoints"),
  974. min: 0,
  975. value: this.advancement.configuration.points,
  976. canIncrease: true,
  977. canDecrease: this.advancement.configuration.points > 0
  978. }
  979. });
  980. }
  981. /* -------------------------------------------- */
  982. /** @inheritdoc */
  983. activateListeners(html) {
  984. super.activateListeners(html);
  985. html.find(".adjustment-button").click(this._onClickButton.bind(this));
  986. }
  987. /* -------------------------------------------- */
  988. /**
  989. * Handle clicking the plus and minus buttons.
  990. * @param {Event} event Triggering click event.
  991. */
  992. _onClickButton(event) {
  993. event.preventDefault();
  994. const action = event.currentTarget.dataset.action;
  995. const input = event.currentTarget.closest("li").querySelector("input");
  996. if ( action === "decrease" ) input.valueAsNumber -= 1;
  997. else if ( action === "increase" ) input.valueAsNumber += 1;
  998. this.submit();
  999. }
  1000. }
  1001. /**
  1002. * Inline application that presents the player with a choice between ability score improvement and taking a feat.
  1003. */
  1004. class AbilityScoreImprovementFlow extends AdvancementFlow {
  1005. /**
  1006. * Player assignments to abilities.
  1007. * @type {Object<string, number>}
  1008. */
  1009. assignments = {};
  1010. /* -------------------------------------------- */
  1011. /**
  1012. * The dropped feat item.
  1013. * @type {Item5e}
  1014. */
  1015. feat;
  1016. /* -------------------------------------------- */
  1017. /** @inheritdoc */
  1018. static get defaultOptions() {
  1019. return foundry.utils.mergeObject(super.defaultOptions, {
  1020. dragDrop: [{ dropSelector: "form" }],
  1021. template: "systems/dnd5e/templates/advancement/ability-score-improvement-flow.hbs"
  1022. });
  1023. }
  1024. /* -------------------------------------------- */
  1025. /** @inheritdoc */
  1026. async retainData(data) {
  1027. await super.retainData(data);
  1028. this.assignments = this.retainedData.assignments ?? {};
  1029. const featUuid = Object.values(this.retainedData.feat ?? {})[0];
  1030. if ( featUuid ) this.feat = await fromUuid(featUuid);
  1031. }
  1032. /* -------------------------------------------- */
  1033. /** @inheritdoc */
  1034. async getData() {
  1035. const points = {
  1036. assigned: Object.keys(CONFIG.DND5E.abilities).reduce((assigned, key) => {
  1037. if ( !this.advancement.canImprove(key) || this.advancement.configuration.fixed[key] ) return assigned;
  1038. return assigned + (this.assignments[key] ?? 0);
  1039. }, 0),
  1040. total: this.advancement.configuration.points
  1041. };
  1042. points.available = points.total - points.assigned;
  1043. const formatter = new Intl.NumberFormat(game.i18n.lang, { signDisplay: "always" });
  1044. const abilities = Object.entries(CONFIG.DND5E.abilities).reduce((obj, [key, data]) => {
  1045. if ( !this.advancement.canImprove(key) ) return obj;
  1046. const ability = this.advancement.actor.system.abilities[key];
  1047. const fixed = this.advancement.configuration.fixed[key] ?? 0;
  1048. const value = Math.min(ability.value + ((fixed || this.assignments[key]) ?? 0), ability.max);
  1049. const max = fixed ? value : Math.min(value + points.available, ability.max);
  1050. obj[key] = {
  1051. key, max, value,
  1052. name: `abilities.${key}`,
  1053. label: data.label,
  1054. initial: ability.value,
  1055. min: fixed ? max : ability.value,
  1056. delta: (value - ability.value) ? formatter.format(value - ability.value) : null,
  1057. showDelta: true,
  1058. isDisabled: !!this.feat,
  1059. isFixed: !!fixed,
  1060. canIncrease: (value < max) && !fixed && !this.feat,
  1061. canDecrease: (value > ability.value) && !fixed && !this.feat
  1062. };
  1063. return obj;
  1064. }, {});
  1065. const pluralRule = new Intl.PluralRules(game.i18n.lang).select(points.available);
  1066. return foundry.utils.mergeObject(super.getData(), {
  1067. abilities, points,
  1068. feat: this.feat,
  1069. staticIncrease: !this.advancement.configuration.points,
  1070. pointsRemaining: game.i18n.format(
  1071. `DND5E.AdvancementAbilityScoreImprovementPointsRemaining.${pluralRule}`, {points: points.available}
  1072. )
  1073. });
  1074. }
  1075. /* -------------------------------------------- */
  1076. /** @inheritdoc */
  1077. activateListeners(html) {
  1078. super.activateListeners(html);
  1079. html.find(".adjustment-button").click(this._onClickButton.bind(this));
  1080. html.find("a[data-uuid]").click(this._onClickFeature.bind(this));
  1081. html.find("[data-action='delete']").click(this._onItemDelete.bind(this));
  1082. }
  1083. /* -------------------------------------------- */
  1084. /** @inheritdoc */
  1085. _onChangeInput(event) {
  1086. super._onChangeInput(event);
  1087. const input = event.currentTarget;
  1088. const key = input.closest("[data-score]").dataset.score;
  1089. const clampedValue = Math.clamped(input.valueAsNumber, Number(input.min), Number(input.max));
  1090. this.assignments[key] = clampedValue - Number(input.dataset.initial);
  1091. this.render();
  1092. }
  1093. /* -------------------------------------------- */
  1094. /**
  1095. * Handle clicking the plus and minus buttons.
  1096. * @param {Event} event Triggering click event.
  1097. */
  1098. _onClickButton(event) {
  1099. event.preventDefault();
  1100. const action = event.currentTarget.dataset.action;
  1101. const key = event.currentTarget.closest("li").dataset.score;
  1102. this.assignments[key] ??= 0;
  1103. if ( action === "decrease" ) this.assignments[key] -= 1;
  1104. else if ( action === "increase" ) this.assignments[key] += 1;
  1105. else return;
  1106. this.render();
  1107. }
  1108. /* -------------------------------------------- */
  1109. /**
  1110. * Handle clicking on a feature during item grant to preview the feature.
  1111. * @param {MouseEvent} event The triggering event.
  1112. * @protected
  1113. */
  1114. async _onClickFeature(event) {
  1115. event.preventDefault();
  1116. const uuid = event.currentTarget.dataset.uuid;
  1117. const item = await fromUuid(uuid);
  1118. item?.sheet.render(true);
  1119. }
  1120. /* -------------------------------------------- */
  1121. /** @inheritdoc */
  1122. async _updateObject(event, formData) {
  1123. // TODO: Pass through retained feat data
  1124. await this.advancement.apply(this.level, {
  1125. type: this.feat ? "feat" : "asi",
  1126. assignments: this.assignments,
  1127. featUuid: this.feat?.uuid,
  1128. retainedItems: this.retainedData?.retainedItems
  1129. });
  1130. }
  1131. /* -------------------------------------------- */
  1132. /* Drag & Drop */
  1133. /* -------------------------------------------- */
  1134. /**
  1135. * Handle deleting a dropped feat.
  1136. * @param {Event} event The originating click event.
  1137. * @protected
  1138. */
  1139. async _onItemDelete(event) {
  1140. event.preventDefault();
  1141. this.feat = null;
  1142. this.render();
  1143. }
  1144. /* -------------------------------------------- */
  1145. /** @inheritdoc */
  1146. async _onDrop(event) {
  1147. if ( !this.advancement.allowFeat ) return false;
  1148. // Try to extract the data
  1149. let data;
  1150. try {
  1151. data = JSON.parse(event.dataTransfer.getData("text/plain"));
  1152. } catch(err) {
  1153. return false;
  1154. }
  1155. if ( data.type !== "Item" ) return false;
  1156. const item = await Item.implementation.fromDropData(data);
  1157. if ( (item.type !== "feat") || (item.system.type.value !== "feat") ) return ui.notifications.error(
  1158. game.i18n.localize("DND5E.AdvancementAbilityScoreImprovementFeatWarning")
  1159. );
  1160. this.feat = item;
  1161. this.render();
  1162. }
  1163. }
  1164. /**
  1165. * Data model for the Ability Score Improvement advancement configuration.
  1166. *
  1167. * @property {number} points Number of points that can be assigned to any score.
  1168. * @property {Object<string, number>} fixed Number of points automatically assigned to a certain score.
  1169. */
  1170. class AbilityScoreImprovementConfigurationData extends foundry.abstract.DataModel {
  1171. /** @inheritdoc */
  1172. static defineSchema() {
  1173. return {
  1174. // TODO: This should default to 2 if added to a class, or 0 if added to anything else
  1175. points: new foundry.data.fields.NumberField({
  1176. integer: true, min: 0, initial: 2,
  1177. label: "DND5E.AdvancementAbilityScoreImprovementPoints",
  1178. hint: "DND5E.AdvancementAbilityScoreImprovementPointsHint"
  1179. }),
  1180. fixed: new MappingField(
  1181. new foundry.data.fields.NumberField({nullable: false, integer: true, initial: 0}),
  1182. {label: "DND5E.AdvancementAbilityScoreImprovementFixed"}
  1183. )
  1184. };
  1185. }
  1186. }
  1187. /**
  1188. * Data model for the Ability Score Improvement advancement value.
  1189. *
  1190. * @property {string} type When on a class, whether the player chose ASI or a Feat.
  1191. * @property {Object<string, number>} Points assigned to individual scores.
  1192. * @property {Object<string, string>} Feat that was selected.
  1193. */
  1194. class AbilityScoreImprovementValueData extends SparseDataModel {
  1195. /** @inheritdoc */
  1196. static defineSchema() {
  1197. return {
  1198. type: new foundry.data.fields.StringField({
  1199. required: true, initial: "asi", choices: ["asi", "feat"]
  1200. }),
  1201. assignments: new MappingField(new foundry.data.fields.NumberField({
  1202. nullable: false, integer: true
  1203. }), {required: false, initial: undefined}),
  1204. feat: new MappingField(new foundry.data.fields.StringField(), {
  1205. required: false, initial: undefined, label: "DND5E.Feature.Feat"
  1206. })
  1207. };
  1208. }
  1209. }
  1210. /**
  1211. * Advancement that presents the player with the option of improving their ability scores or selecting a feat.
  1212. */
  1213. class AbilityScoreImprovementAdvancement extends Advancement {
  1214. /** @inheritdoc */
  1215. static get metadata() {
  1216. return foundry.utils.mergeObject(super.metadata, {
  1217. dataModels: {
  1218. configuration: AbilityScoreImprovementConfigurationData,
  1219. value: AbilityScoreImprovementValueData
  1220. },
  1221. order: 20,
  1222. icon: "systems/dnd5e/icons/svg/ability-score-improvement.svg",
  1223. title: game.i18n.localize("DND5E.AdvancementAbilityScoreImprovementTitle"),
  1224. hint: game.i18n.localize("DND5E.AdvancementAbilityScoreImprovementHint"),
  1225. validItemTypes: new Set(["background", "class"]),
  1226. apps: {
  1227. config: AbilityScoreImprovementConfig,
  1228. flow: AbilityScoreImprovementFlow
  1229. }
  1230. });
  1231. }
  1232. /* -------------------------------------------- */
  1233. /* Instance Properties */
  1234. /* -------------------------------------------- */
  1235. /**
  1236. * Does this advancement allow feats, or just ability score improvements?
  1237. * @type {boolean}
  1238. */
  1239. get allowFeat() {
  1240. return (this.item.type === "class") && game.settings.get("dnd5e", "allowFeats");
  1241. }
  1242. /* -------------------------------------------- */
  1243. /**
  1244. * Information on the ASI points available.
  1245. * @type {{ assigned: number, total: number }}
  1246. */
  1247. get points() {
  1248. return {
  1249. assigned: Object.entries(this.value.assignments ?? {}).reduce((n, [abl, c]) => {
  1250. if ( this.canImprove(abl) ) n += c;
  1251. return n;
  1252. }, 0),
  1253. total: this.configuration.points + Object.entries(this.configuration.fixed).reduce((t, [abl, v]) => {
  1254. if ( this.canImprove(abl) ) t += v;
  1255. return t;
  1256. }, 0)
  1257. };
  1258. }
  1259. /* -------------------------------------------- */
  1260. /* Instance Methods */
  1261. /* -------------------------------------------- */
  1262. /**
  1263. * Is this ability allowed to be improved?
  1264. * @param {string} ability The ability key.
  1265. * @returns {boolean}
  1266. */
  1267. canImprove(ability) {
  1268. return CONFIG.DND5E.abilities[ability]?.improvement !== false;
  1269. }
  1270. /* -------------------------------------------- */
  1271. /* Display Methods */
  1272. /* -------------------------------------------- */
  1273. /** @inheritdoc */
  1274. titleForLevel(level, { configMode=false }={}) {
  1275. if ( this.value.selected !== "feat" ) return this.title;
  1276. return game.i18n.localize("DND5E.Feature.Feat");
  1277. }
  1278. /* -------------------------------------------- */
  1279. /** @inheritdoc */
  1280. summaryForLevel(level, { configMode=false }={}) {
  1281. if ( (this.value.type === "feat") && this.value.feat ) {
  1282. const id = Object.keys(this.value.feat)[0];
  1283. const feat = this.actor.items.get(id);
  1284. if ( feat ) return feat.toAnchor({classes: ["content-link"]}).outerHTML;
  1285. } else if ( (this.value.type === "asi") && this.value.assignments ) {
  1286. const formatter = new Intl.NumberFormat(game.i18n.lang, { signDisplay: "always" });
  1287. return Object.entries(this.value.assignments).reduce((html, [key, value]) => {
  1288. const name = CONFIG.DND5E.abilities[key]?.label ?? key;
  1289. html += `<span class="tag">${name} <strong>${formatter.format(value)}</strong></span>\n`;
  1290. return html;
  1291. }, "");
  1292. }
  1293. return "";
  1294. }
  1295. /* -------------------------------------------- */
  1296. /* Application Methods */
  1297. /* -------------------------------------------- */
  1298. /** @inheritdoc */
  1299. async apply(level, data) {
  1300. if ( data.type === "asi" ) {
  1301. const assignments = foundry.utils.mergeObject(this.configuration.fixed, data.assignments, {inplace: false});
  1302. const updates = {};
  1303. for ( const key of Object.keys(assignments) ) {
  1304. const ability = this.actor.system.abilities[key];
  1305. if ( !ability || !this.canImprove(key) ) continue;
  1306. assignments[key] = Math.min(assignments[key], ability.max - ability.value);
  1307. if ( assignments[key] ) updates[`system.abilities.${key}.value`] = ability.value + assignments[key];
  1308. else delete assignments[key];
  1309. }
  1310. data.assignments = assignments;
  1311. data.feat = null;
  1312. this.actor.updateSource(updates);
  1313. }
  1314. else {
  1315. let itemData = data.retainedItems?.[data.featUuid];
  1316. if ( !itemData ) {
  1317. const source = await fromUuid(data.featUuid);
  1318. if ( source ) {
  1319. itemData = source.clone({
  1320. _id: foundry.utils.randomID(),
  1321. "flags.dnd5e.sourceId": data.featUuid,
  1322. "flags.dnd5e.advancementOrigin": `${this.item.id}.${this.id}`
  1323. }, {keepId: true}).toObject();
  1324. }
  1325. }
  1326. data.assignments = null;
  1327. if ( itemData ) {
  1328. data.feat = { [itemData._id]: data.featUuid };
  1329. this.actor.updateSource({items: [itemData]});
  1330. }
  1331. }
  1332. this.updateSource({value: data});
  1333. }
  1334. /* -------------------------------------------- */
  1335. /** @inheritdoc */
  1336. restore(level, data) {
  1337. data.featUuid = Object.values(data.feat ?? {})[0];
  1338. this.apply(level, data);
  1339. }
  1340. /* -------------------------------------------- */
  1341. /** @inheritdoc */
  1342. reverse(level) {
  1343. const source = this.value.toObject();
  1344. if ( this.value.type === "asi" ) {
  1345. const updates = {};
  1346. for ( const [key, change] of Object.entries(this.value.assignments ?? {}) ) {
  1347. const ability = this.actor.system.abilities[key];
  1348. if ( !ability || !this.canImprove(key) ) continue;
  1349. updates[`system.abilities.${key}.value`] = ability.value - change;
  1350. }
  1351. this.actor.updateSource(updates);
  1352. }
  1353. else {
  1354. const [id, uuid] = Object.entries(this.value.feat ?? {})[0] ?? [];
  1355. const item = this.actor.items.get(id);
  1356. if ( item ) source.retainedItems = {[uuid]: item.toObject()};
  1357. this.actor.items.delete(id);
  1358. }
  1359. this.updateSource({ "value.assignments": null, "value.feat": null });
  1360. return source;
  1361. }
  1362. }
  1363. /**
  1364. * Configuration application for hit points.
  1365. */
  1366. class HitPointsConfig extends AdvancementConfig {
  1367. /** @inheritdoc */
  1368. static get defaultOptions() {
  1369. return foundry.utils.mergeObject(super.defaultOptions, {
  1370. template: "systems/dnd5e/templates/advancement/hit-points-config.hbs"
  1371. });
  1372. }
  1373. /* -------------------------------------------- */
  1374. /** @inheritdoc */
  1375. getData() {
  1376. return foundry.utils.mergeObject(super.getData(), {
  1377. hitDie: this.advancement.hitDie
  1378. });
  1379. }
  1380. }
  1381. /**
  1382. * Inline application that presents hit points selection upon level up.
  1383. */
  1384. class HitPointsFlow extends AdvancementFlow {
  1385. /** @inheritdoc */
  1386. static get defaultOptions() {
  1387. return foundry.utils.mergeObject(super.defaultOptions, {
  1388. template: "systems/dnd5e/templates/advancement/hit-points-flow.hbs"
  1389. });
  1390. }
  1391. /* -------------------------------------------- */
  1392. /** @inheritdoc */
  1393. getData() {
  1394. const source = this.retainedData ?? this.advancement.value;
  1395. const value = source[this.level];
  1396. // If value is empty, `useAverage` should default to the value selected at the previous level
  1397. let useAverage = value === "avg";
  1398. if ( !value ) {
  1399. const lastValue = source[this.level - 1];
  1400. if ( lastValue === "avg" ) useAverage = true;
  1401. }
  1402. return foundry.utils.mergeObject(super.getData(), {
  1403. isFirstClassLevel: (this.level === 1) && this.advancement.item.isOriginalClass,
  1404. hitDie: this.advancement.hitDie,
  1405. dieValue: this.advancement.hitDieValue,
  1406. data: {
  1407. value: Number.isInteger(value) ? value : "",
  1408. useAverage
  1409. }
  1410. });
  1411. }
  1412. /* -------------------------------------------- */
  1413. /** @inheritdoc */
  1414. activateListeners(html) {
  1415. this.form.querySelector(".averageCheckbox")?.addEventListener("change", event => {
  1416. this.form.querySelector(".rollResult").disabled = event.target.checked;
  1417. this.form.querySelector(".rollButton").disabled = event.target.checked;
  1418. this._updateRollResult();
  1419. });
  1420. this.form.querySelector(".rollButton")?.addEventListener("click", async () => {
  1421. const roll = await this.advancement.actor.rollClassHitPoints(this.advancement.item);
  1422. this.form.querySelector(".rollResult").value = roll.total;
  1423. });
  1424. this._updateRollResult();
  1425. }
  1426. /* -------------------------------------------- */
  1427. /**
  1428. * Update the roll result display when the average result is taken.
  1429. * @protected
  1430. */
  1431. _updateRollResult() {
  1432. if ( !this.form.elements.useAverage?.checked ) return;
  1433. this.form.elements.value.value = (this.advancement.hitDieValue / 2) + 1;
  1434. }
  1435. /* -------------------------------------------- */
  1436. /** @inheritdoc */
  1437. _updateObject(event, formData) {
  1438. let value;
  1439. if ( formData.useMax ) value = "max";
  1440. else if ( formData.useAverage ) value = "avg";
  1441. else if ( Number.isInteger(formData.value) ) value = parseInt(formData.value);
  1442. if ( value !== undefined ) return this.advancement.apply(this.level, { [this.level]: value });
  1443. this.form.querySelector(".rollResult")?.classList.add("error");
  1444. const errorType = formData.value ? "Invalid" : "Empty";
  1445. throw new Advancement.ERROR(game.i18n.localize(`DND5E.AdvancementHitPoints${errorType}Error`));
  1446. }
  1447. }
  1448. /* -------------------------------------------- */
  1449. /* Formulas */
  1450. /* -------------------------------------------- */
  1451. /**
  1452. * Convert a bonus value to a simple integer for displaying on the sheet.
  1453. * @param {number|string|null} bonus Bonus formula.
  1454. * @param {object} [data={}] Data to use for replacing @ strings.
  1455. * @returns {number} Simplified bonus as an integer.
  1456. * @protected
  1457. */
  1458. function simplifyBonus(bonus, data={}) {
  1459. if ( !bonus ) return 0;
  1460. if ( Number.isNumeric(bonus) ) return Number(bonus);
  1461. try {
  1462. const roll = new Roll(bonus, data);
  1463. return roll.isDeterministic ? Roll.safeEval(roll.formula) : 0;
  1464. } catch(error) {
  1465. console.error(error);
  1466. return 0;
  1467. }
  1468. }
  1469. /* -------------------------------------------- */
  1470. /* Object Helpers */
  1471. /* -------------------------------------------- */
  1472. /**
  1473. * Sort the provided object by its values or by an inner sortKey.
  1474. * @param {object} obj The object to sort.
  1475. * @param {string} [sortKey] An inner key upon which to sort.
  1476. * @returns {object} A copy of the original object that has been sorted.
  1477. */
  1478. function sortObjectEntries(obj, sortKey) {
  1479. let sorted = Object.entries(obj);
  1480. if ( sortKey ) sorted = sorted.sort((a, b) => a[1][sortKey].localeCompare(b[1][sortKey]));
  1481. else sorted = sorted.sort((a, b) => a[1].localeCompare(b[1]));
  1482. return Object.fromEntries(sorted);
  1483. }
  1484. /* -------------------------------------------- */
  1485. /**
  1486. * Retrieve the indexed data for a Document using its UUID. Will never return a result for embedded documents.
  1487. * @param {string} uuid The UUID of the Document index to retrieve.
  1488. * @returns {object} Document's index if one could be found.
  1489. */
  1490. function indexFromUuid(uuid) {
  1491. const parts = uuid.split(".");
  1492. let index;
  1493. // Compendium Documents
  1494. if ( parts[0] === "Compendium" ) {
  1495. const [, scope, packName, id] = parts;
  1496. const pack = game.packs.get(`${scope}.${packName}`);
  1497. index = pack?.index.get(id);
  1498. }
  1499. // World Documents
  1500. else if ( parts.length < 3 ) {
  1501. const [docName, id] = parts;
  1502. const collection = CONFIG[docName].collection.instance;
  1503. index = collection.get(id);
  1504. }
  1505. return index || null;
  1506. }
  1507. /* -------------------------------------------- */
  1508. /**
  1509. * Creates an HTML document link for the provided UUID.
  1510. * @param {string} uuid UUID for which to produce the link.
  1511. * @returns {string} Link to the item or empty string if item wasn't found.
  1512. */
  1513. function linkForUuid(uuid) {
  1514. return TextEditor._createContentLink(["", "UUID", uuid]).outerHTML;
  1515. }
  1516. /* -------------------------------------------- */
  1517. /* Validators */
  1518. /* -------------------------------------------- */
  1519. /**
  1520. * Ensure the provided string contains only the characters allowed in identifiers.
  1521. * @param {string} identifier
  1522. * @returns {boolean}
  1523. */
  1524. function isValidIdentifier(identifier) {
  1525. return /^([a-z0-9_-]+)$/i.test(identifier);
  1526. }
  1527. const validators = {
  1528. isValidIdentifier: isValidIdentifier
  1529. };
  1530. /* -------------------------------------------- */
  1531. /* Handlebars Template Helpers */
  1532. /* -------------------------------------------- */
  1533. /**
  1534. * Define a set of template paths to pre-load. Pre-loaded templates are compiled and cached for fast access when
  1535. * rendering. These paths will also be available as Handlebars partials by using the file name
  1536. * (e.g. "dnd5e.actor-traits").
  1537. * @returns {Promise}
  1538. */
  1539. async function preloadHandlebarsTemplates() {
  1540. const partials = [
  1541. // Shared Partials
  1542. "systems/dnd5e/templates/actors/parts/active-effects.hbs",
  1543. "systems/dnd5e/templates/apps/parts/trait-list.hbs",
  1544. // Actor Sheet Partials
  1545. "systems/dnd5e/templates/actors/parts/actor-traits.hbs",
  1546. "systems/dnd5e/templates/actors/parts/actor-inventory.hbs",
  1547. "systems/dnd5e/templates/actors/parts/actor-features.hbs",
  1548. "systems/dnd5e/templates/actors/parts/actor-spellbook.hbs",
  1549. "systems/dnd5e/templates/actors/parts/actor-warnings.hbs",
  1550. // Item Sheet Partials
  1551. "systems/dnd5e/templates/items/parts/item-action.hbs",
  1552. "systems/dnd5e/templates/items/parts/item-activation.hbs",
  1553. "systems/dnd5e/templates/items/parts/item-advancement.hbs",
  1554. "systems/dnd5e/templates/items/parts/item-description.hbs",
  1555. "systems/dnd5e/templates/items/parts/item-mountable.hbs",
  1556. "systems/dnd5e/templates/items/parts/item-spellcasting.hbs",
  1557. "systems/dnd5e/templates/items/parts/item-summary.hbs",
  1558. // Journal Partials
  1559. "systems/dnd5e/templates/journal/parts/journal-table.hbs",
  1560. // Advancement Partials
  1561. "systems/dnd5e/templates/advancement/parts/advancement-ability-score-control.hbs",
  1562. "systems/dnd5e/templates/advancement/parts/advancement-controls.hbs",
  1563. "systems/dnd5e/templates/advancement/parts/advancement-spell-config.hbs"
  1564. ];
  1565. const paths = {};
  1566. for ( const path of partials ) {
  1567. paths[path.replace(".hbs", ".html")] = path;
  1568. paths[`dnd5e.${path.split("/").pop().replace(".hbs", "")}`] = path;
  1569. }
  1570. return loadTemplates(paths);
  1571. }
  1572. /* -------------------------------------------- */
  1573. /**
  1574. * A helper that fetch the appropriate item context from root and adds it to the first block parameter.
  1575. * @param {object} context Current evaluation context.
  1576. * @param {object} options Handlebars options.
  1577. * @returns {string}
  1578. */
  1579. function itemContext(context, options) {
  1580. if ( arguments.length !== 2 ) throw new Error("#dnd5e-itemContext requires exactly one argument");
  1581. if ( foundry.utils.getType(context) === "function" ) context = context.call(this);
  1582. const ctx = options.data.root.itemContext?.[context.id];
  1583. if ( !ctx ) {
  1584. const inverse = options.inverse(this);
  1585. if ( inverse ) return options.inverse(this);
  1586. }
  1587. return options.fn(context, { data: options.data, blockParams: [ctx] });
  1588. }
  1589. /* -------------------------------------------- */
  1590. /**
  1591. * Register custom Handlebars helpers used by 5e.
  1592. */
  1593. function registerHandlebarsHelpers() {
  1594. Handlebars.registerHelper({
  1595. getProperty: foundry.utils.getProperty,
  1596. "dnd5e-linkForUuid": linkForUuid,
  1597. "dnd5e-itemContext": itemContext
  1598. });
  1599. }
  1600. /* -------------------------------------------- */
  1601. /* Config Pre-Localization */
  1602. /* -------------------------------------------- */
  1603. /**
  1604. * Storage for pre-localization configuration.
  1605. * @type {object}
  1606. * @private
  1607. */
  1608. const _preLocalizationRegistrations = {};
  1609. /**
  1610. * Mark the provided config key to be pre-localized during the init stage.
  1611. * @param {string} configKeyPath Key path within `CONFIG.DND5E` to localize.
  1612. * @param {object} [options={}]
  1613. * @param {string} [options.key] If each entry in the config enum is an object,
  1614. * localize and sort using this property.
  1615. * @param {string[]} [options.keys=[]] Array of localization keys. First key listed will be used for sorting
  1616. * if multiple are provided.
  1617. * @param {boolean} [options.sort=false] Sort this config enum, using the key if set.
  1618. */
  1619. function preLocalize(configKeyPath, { key, keys=[], sort=false }={}) {
  1620. if ( key ) keys.unshift(key);
  1621. _preLocalizationRegistrations[configKeyPath] = { keys, sort };
  1622. }
  1623. /* -------------------------------------------- */
  1624. /**
  1625. * Execute previously defined pre-localization tasks on the provided config object.
  1626. * @param {object} config The `CONFIG.DND5E` object to localize and sort. *Will be mutated.*
  1627. */
  1628. function performPreLocalization(config) {
  1629. for ( const [keyPath, settings] of Object.entries(_preLocalizationRegistrations) ) {
  1630. const target = foundry.utils.getProperty(config, keyPath);
  1631. _localizeObject(target, settings.keys);
  1632. if ( settings.sort ) foundry.utils.setProperty(config, keyPath, sortObjectEntries(target, settings.keys[0]));
  1633. }
  1634. }
  1635. /* -------------------------------------------- */
  1636. /**
  1637. * Localize the values of a configuration object by translating them in-place.
  1638. * @param {object} obj The configuration object to localize.
  1639. * @param {string[]} [keys] List of inner keys that should be localized if this is an object.
  1640. * @private
  1641. */
  1642. function _localizeObject(obj, keys) {
  1643. for ( const [k, v] of Object.entries(obj) ) {
  1644. const type = typeof v;
  1645. if ( type === "string" ) {
  1646. obj[k] = game.i18n.localize(v);
  1647. continue;
  1648. }
  1649. if ( type !== "object" ) {
  1650. console.error(new Error(
  1651. `Pre-localized configuration values must be a string or object, ${type} found for "${k}" instead.`
  1652. ));
  1653. continue;
  1654. }
  1655. if ( !keys?.length ) {
  1656. console.error(new Error(
  1657. "Localization keys must be provided for pre-localizing when target is an object."
  1658. ));
  1659. continue;
  1660. }
  1661. for ( const key of keys ) {
  1662. if ( !v[key] ) continue;
  1663. v[key] = game.i18n.localize(v[key]);
  1664. }
  1665. }
  1666. }
  1667. /* -------------------------------------------- */
  1668. /* Migration */
  1669. /* -------------------------------------------- */
  1670. /**
  1671. * Synchronize the spells for all Actors in some collection with source data from an Item compendium pack.
  1672. * @param {CompendiumCollection} actorPack An Actor compendium pack which will be updated
  1673. * @param {CompendiumCollection} spellsPack An Item compendium pack which provides source data for spells
  1674. * @returns {Promise<void>}
  1675. */
  1676. async function synchronizeActorSpells(actorPack, spellsPack) {
  1677. // Load all actors and spells
  1678. const actors = await actorPack.getDocuments();
  1679. const spells = await spellsPack.getDocuments();
  1680. const spellsMap = spells.reduce((obj, item) => {
  1681. obj[item.name] = item;
  1682. return obj;
  1683. }, {});
  1684. // Unlock the pack
  1685. await actorPack.configure({locked: false});
  1686. // Iterate over actors
  1687. SceneNavigation.displayProgressBar({label: "Synchronizing Spell Data", pct: 0});
  1688. for ( const [i, actor] of actors.entries() ) {
  1689. const {toDelete, toCreate} = _synchronizeActorSpells(actor, spellsMap);
  1690. if ( toDelete.length ) await actor.deleteEmbeddedDocuments("Item", toDelete);
  1691. if ( toCreate.length ) await actor.createEmbeddedDocuments("Item", toCreate, {keepId: true});
  1692. console.debug(`${actor.name} | Synchronized ${toCreate.length} spells`);
  1693. SceneNavigation.displayProgressBar({label: actor.name, pct: ((i / actors.length) * 100).toFixed(0)});
  1694. }
  1695. // Re-lock the pack
  1696. await actorPack.configure({locked: true});
  1697. SceneNavigation.displayProgressBar({label: "Synchronizing Spell Data", pct: 100});
  1698. }
  1699. /* -------------------------------------------- */
  1700. /**
  1701. * A helper function to synchronize spell data for a specific Actor.
  1702. * @param {Actor5e} actor
  1703. * @param {Object<string,Item5e>} spellsMap
  1704. * @returns {{toDelete: string[], toCreate: object[]}}
  1705. * @private
  1706. */
  1707. function _synchronizeActorSpells(actor, spellsMap) {
  1708. const spells = actor.itemTypes.spell;
  1709. const toDelete = [];
  1710. const toCreate = [];
  1711. if ( !spells.length ) return {toDelete, toCreate};
  1712. for ( const spell of spells ) {
  1713. const source = spellsMap[spell.name];
  1714. if ( !source ) {
  1715. console.warn(`${actor.name} | ${spell.name} | Does not exist in spells compendium pack`);
  1716. continue;
  1717. }
  1718. // Combine source data with the preparation and uses data from the actor
  1719. const spellData = source.toObject();
  1720. const {preparation, uses, save} = spell.toObject().system;
  1721. Object.assign(spellData.system, {preparation, uses});
  1722. spellData.system.save.dc = save.dc;
  1723. foundry.utils.setProperty(spellData, "flags.core.sourceId", source.uuid);
  1724. // Record spells to be deleted and created
  1725. toDelete.push(spell.id);
  1726. toCreate.push(spellData);
  1727. }
  1728. return {toDelete, toCreate};
  1729. }
  1730. var utils = /*#__PURE__*/Object.freeze({
  1731. __proto__: null,
  1732. indexFromUuid: indexFromUuid,
  1733. linkForUuid: linkForUuid,
  1734. performPreLocalization: performPreLocalization,
  1735. preLocalize: preLocalize,
  1736. preloadHandlebarsTemplates: preloadHandlebarsTemplates,
  1737. registerHandlebarsHelpers: registerHandlebarsHelpers,
  1738. simplifyBonus: simplifyBonus,
  1739. sortObjectEntries: sortObjectEntries,
  1740. synchronizeActorSpells: synchronizeActorSpells,
  1741. validators: validators
  1742. });
  1743. /**
  1744. * Advancement that presents the player with the option to roll hit points at each level or select the average value.
  1745. * Keeps track of player hit point rolls or selection for each class level. **Can only be added to classes and each
  1746. * class can only have one.**
  1747. */
  1748. class HitPointsAdvancement extends Advancement {
  1749. /** @inheritdoc */
  1750. static get metadata() {
  1751. return foundry.utils.mergeObject(super.metadata, {
  1752. order: 10,
  1753. icon: "systems/dnd5e/icons/svg/hit-points.svg",
  1754. title: game.i18n.localize("DND5E.AdvancementHitPointsTitle"),
  1755. hint: game.i18n.localize("DND5E.AdvancementHitPointsHint"),
  1756. multiLevel: true,
  1757. validItemTypes: new Set(["class"]),
  1758. apps: {
  1759. config: HitPointsConfig,
  1760. flow: HitPointsFlow
  1761. }
  1762. });
  1763. }
  1764. /* -------------------------------------------- */
  1765. /* Instance Properties */
  1766. /* -------------------------------------------- */
  1767. /** @inheritdoc */
  1768. get levels() {
  1769. return Array.fromRange(CONFIG.DND5E.maxLevel + 1).slice(1);
  1770. }
  1771. /* -------------------------------------------- */
  1772. /**
  1773. * Shortcut to the hit die used by the class.
  1774. * @returns {string}
  1775. */
  1776. get hitDie() {
  1777. return this.item.system.hitDice;
  1778. }
  1779. /* -------------------------------------------- */
  1780. /**
  1781. * The face value of the hit die used.
  1782. * @returns {number}
  1783. */
  1784. get hitDieValue() {
  1785. return Number(this.hitDie.substring(1));
  1786. }
  1787. /* -------------------------------------------- */
  1788. /* Display Methods */
  1789. /* -------------------------------------------- */
  1790. /** @inheritdoc */
  1791. configuredForLevel(level) {
  1792. return this.valueForLevel(level) !== null;
  1793. }
  1794. /* -------------------------------------------- */
  1795. /** @inheritdoc */
  1796. titleForLevel(level, { configMode=false }={}) {
  1797. const hp = this.valueForLevel(level);
  1798. if ( !hp || configMode ) return this.title;
  1799. return `${this.title}: <strong>${hp}</strong>`;
  1800. }
  1801. /* -------------------------------------------- */
  1802. /**
  1803. * Hit points given at the provided level.
  1804. * @param {number} level Level for which to get hit points.
  1805. * @returns {number|null} Hit points for level or null if none have been taken.
  1806. */
  1807. valueForLevel(level) {
  1808. return this.constructor.valueForLevel(this.value, this.hitDieValue, level);
  1809. }
  1810. /* -------------------------------------------- */
  1811. /**
  1812. * Hit points given at the provided level.
  1813. * @param {object} data Contents of `value` used to determine this value.
  1814. * @param {number} hitDieValue Face value of the hit die used by this advancement.
  1815. * @param {number} level Level for which to get hit points.
  1816. * @returns {number|null} Hit points for level or null if none have been taken.
  1817. */
  1818. static valueForLevel(data, hitDieValue, level) {
  1819. const value = data[level];
  1820. if ( !value ) return null;
  1821. if ( value === "max" ) return hitDieValue;
  1822. if ( value === "avg" ) return (hitDieValue / 2) + 1;
  1823. return value;
  1824. }
  1825. /* -------------------------------------------- */
  1826. /**
  1827. * Total hit points provided by this advancement.
  1828. * @returns {number} Hit points currently selected.
  1829. */
  1830. total() {
  1831. return Object.keys(this.value).reduce((total, level) => total + this.valueForLevel(parseInt(level)), 0);
  1832. }
  1833. /* -------------------------------------------- */
  1834. /**
  1835. * Total hit points taking the provided ability modifier into account, with a minimum of 1 per level.
  1836. * @param {number} mod Modifier to add per level.
  1837. * @returns {number} Total hit points plus modifier.
  1838. */
  1839. getAdjustedTotal(mod) {
  1840. return Object.keys(this.value).reduce((total, level) => {
  1841. return total + Math.max(this.valueForLevel(parseInt(level)) + mod, 1);
  1842. }, 0);
  1843. }
  1844. /* -------------------------------------------- */
  1845. /* Editing Methods */
  1846. /* -------------------------------------------- */
  1847. /** @inheritdoc */
  1848. static availableForItem(item) {
  1849. return !item.advancement.byType.HitPoints?.length;
  1850. }
  1851. /* -------------------------------------------- */
  1852. /* Application Methods */
  1853. /* -------------------------------------------- */
  1854. /**
  1855. * Add the ability modifier and any bonuses to the provided hit points value to get the number to apply.
  1856. * @param {number} value Hit points taken at a given level.
  1857. * @returns {number} Hit points adjusted with ability modifier and per-level bonuses.
  1858. */
  1859. #getApplicableValue(value) {
  1860. const abilityId = CONFIG.DND5E.hitPointsAbility || "con";
  1861. value = Math.max(value + (this.actor.system.abilities[abilityId]?.mod ?? 0), 1);
  1862. value += simplifyBonus(this.actor.system.attributes.hp.bonuses.level, this.actor.getRollData());
  1863. return value;
  1864. }
  1865. /* -------------------------------------------- */
  1866. /** @inheritdoc */
  1867. apply(level, data) {
  1868. let value = this.constructor.valueForLevel(data, this.hitDieValue, level);
  1869. if ( value === undefined ) return;
  1870. this.actor.updateSource({
  1871. "system.attributes.hp.value": this.actor.system.attributes.hp.value + this.#getApplicableValue(value)
  1872. });
  1873. this.updateSource({ value: data });
  1874. }
  1875. /* -------------------------------------------- */
  1876. /** @inheritdoc */
  1877. restore(level, data) {
  1878. this.apply(level, data);
  1879. }
  1880. /* -------------------------------------------- */
  1881. /** @inheritdoc */
  1882. reverse(level) {
  1883. let value = this.valueForLevel(level);
  1884. if ( value === undefined ) return;
  1885. this.actor.updateSource({
  1886. "system.attributes.hp.value": this.actor.system.attributes.hp.value - this.#getApplicableValue(value)
  1887. });
  1888. const source = { [level]: this.value[level] };
  1889. this.updateSource({ [`value.-=${level}`]: null });
  1890. return source;
  1891. }
  1892. }
  1893. /**
  1894. * Configuration application for item grants.
  1895. */
  1896. class ItemGrantConfig extends AdvancementConfig {
  1897. /** @inheritdoc */
  1898. static get defaultOptions() {
  1899. return foundry.utils.mergeObject(super.defaultOptions, {
  1900. classes: ["dnd5e", "advancement", "item-grant"],
  1901. dragDrop: [{ dropSelector: ".drop-target" }],
  1902. dropKeyPath: "items",
  1903. template: "systems/dnd5e/templates/advancement/item-grant-config.hbs"
  1904. });
  1905. }
  1906. /* -------------------------------------------- */
  1907. /** @inheritdoc */
  1908. getData(options={}) {
  1909. const context = super.getData(options);
  1910. context.showSpellConfig = context.configuration.items.map(uuid => fromUuidSync(uuid)).some(i => i?.type === "spell");
  1911. return context;
  1912. }
  1913. /* -------------------------------------------- */
  1914. /** @inheritdoc */
  1915. _validateDroppedItem(event, item) {
  1916. this.advancement._validateItemType(item);
  1917. }
  1918. }
  1919. /**
  1920. * Inline application that presents the player with a list of items to be added.
  1921. */
  1922. class ItemGrantFlow extends AdvancementFlow {
  1923. /** @inheritdoc */
  1924. static get defaultOptions() {
  1925. return foundry.utils.mergeObject(super.defaultOptions, {
  1926. template: "systems/dnd5e/templates/advancement/item-grant-flow.hbs"
  1927. });
  1928. }
  1929. /* -------------------------------------------- */
  1930. /**
  1931. * Produce the rendering context for this flow.
  1932. * @returns {object}
  1933. */
  1934. async getContext() {
  1935. const config = this.advancement.configuration.items;
  1936. const added = this.retainedData?.items.map(i => foundry.utils.getProperty(i, "flags.dnd5e.sourceId"))
  1937. ?? this.advancement.value.added;
  1938. const checked = new Set(Object.values(added ?? {}));
  1939. return {
  1940. optional: this.advancement.configuration.optional,
  1941. items: (await Promise.all(config.map(uuid => fromUuid(uuid)))).reduce((arr, item) => {
  1942. if ( !item ) return arr;
  1943. item.checked = added ? checked.has(item.uuid) : true;
  1944. arr.push(item);
  1945. return arr;
  1946. }, [])
  1947. };
  1948. }
  1949. /* -------------------------------------------- */
  1950. /** @inheritdoc */
  1951. async getData(options={}) {
  1952. return foundry.utils.mergeObject(super.getData(options), await this.getContext());
  1953. }
  1954. /* -------------------------------------------- */
  1955. /** @inheritdoc */
  1956. activateListeners(html) {
  1957. super.activateListeners(html);
  1958. html.find("a[data-uuid]").click(this._onClickFeature.bind(this));
  1959. }
  1960. /* -------------------------------------------- */
  1961. /**
  1962. * Handle clicking on a feature during item grant to preview the feature.
  1963. * @param {MouseEvent} event The triggering event.
  1964. * @protected
  1965. */
  1966. async _onClickFeature(event) {
  1967. event.preventDefault();
  1968. const uuid = event.currentTarget.dataset.uuid;
  1969. const item = await fromUuid(uuid);
  1970. item?.sheet.render(true);
  1971. }
  1972. /* -------------------------------------------- */
  1973. /** @inheritdoc */
  1974. async _updateObject(event, formData) {
  1975. const retainedData = this.retainedData?.items.reduce((obj, i) => {
  1976. obj[foundry.utils.getProperty(i, "flags.dnd5e.sourceId")] = i;
  1977. return obj;
  1978. }, {});
  1979. await this.advancement.apply(this.level, formData, retainedData);
  1980. }
  1981. }
  1982. class SpellConfigurationData extends foundry.abstract.DataModel {
  1983. /** @inheritdoc */
  1984. static defineSchema() {
  1985. return {
  1986. ability: new foundry.data.fields.StringField({label: "DND5E.AbilityModifier"}),
  1987. preparation: new foundry.data.fields.StringField({label: "DND5E.SpellPreparationMode"}),
  1988. uses: new foundry.data.fields.SchemaField({
  1989. max: new FormulaField({deterministic: true, label: "DND5E.UsesMax"}),
  1990. per: new foundry.data.fields.StringField({label: "DND5E.UsesPeriod"})
  1991. }, {label: "DND5E.LimitedUses"})
  1992. };
  1993. }
  1994. /* -------------------------------------------- */
  1995. /**
  1996. * Changes that this spell configuration indicates should be performed on spells.
  1997. * @type {object}
  1998. */
  1999. get spellChanges() {
  2000. const updates = {};
  2001. if ( this.ability ) updates["system.ability"] = this.ability;
  2002. if ( this.preparation ) updates["system.preparation.mode"] = this.preparation;
  2003. if ( this.uses.max && this.uses.per ) {
  2004. updates["system.uses.max"] = this.uses.max;
  2005. updates["system.uses.per"] = this.uses.per;
  2006. if ( Number.isNumeric(this.uses.max) ) updates["system.uses.value"] = parseInt(this.uses.max);
  2007. else {
  2008. try {
  2009. const rollData = this.parent.parent.actor.getRollData({ deterministic: true });
  2010. const formula = Roll.replaceFormulaData(this.uses.max, rollData, {missing: 0});
  2011. updates["system.uses.value"] = Roll.safeEval(formula);
  2012. } catch(e) { }
  2013. }
  2014. }
  2015. return updates;
  2016. }
  2017. }
  2018. class ItemGrantConfigurationData extends foundry.abstract.DataModel {
  2019. /** @inheritdoc */
  2020. static defineSchema() {
  2021. return {
  2022. items: new foundry.data.fields.ArrayField(new foundry.data.fields.StringField(), {
  2023. required: true, label: "DOCUMENT.Items"
  2024. }),
  2025. optional: new foundry.data.fields.BooleanField({
  2026. required: true, label: "DND5E.AdvancementItemGrantOptional", hint: "DND5E.AdvancementItemGrantOptionalHint"
  2027. }),
  2028. spell: new foundry.data.fields.EmbeddedDataField(SpellConfigurationData, {
  2029. required: true, nullable: true, initial: null
  2030. })
  2031. };
  2032. }
  2033. }
  2034. /**
  2035. * Advancement that automatically grants one or more items to the player. Presents the player with the option of
  2036. * skipping any or all of the items.
  2037. */
  2038. class ItemGrantAdvancement extends Advancement {
  2039. /** @inheritdoc */
  2040. static get metadata() {
  2041. return foundry.utils.mergeObject(super.metadata, {
  2042. dataModels: {
  2043. configuration: ItemGrantConfigurationData
  2044. },
  2045. order: 40,
  2046. icon: "systems/dnd5e/icons/svg/item-grant.svg",
  2047. title: game.i18n.localize("DND5E.AdvancementItemGrantTitle"),
  2048. hint: game.i18n.localize("DND5E.AdvancementItemGrantHint"),
  2049. apps: {
  2050. config: ItemGrantConfig,
  2051. flow: ItemGrantFlow
  2052. }
  2053. });
  2054. }
  2055. /* -------------------------------------------- */
  2056. /**
  2057. * The item types that are supported in Item Grant.
  2058. * @type {Set<string>}
  2059. */
  2060. static VALID_TYPES = new Set(["feat", "spell", "consumable", "backpack", "equipment", "loot", "tool", "weapon"]);
  2061. /* -------------------------------------------- */
  2062. /* Display Methods */
  2063. /* -------------------------------------------- */
  2064. /** @inheritdoc */
  2065. configuredForLevel(level) {
  2066. return !foundry.utils.isEmpty(this.value);
  2067. }
  2068. /* -------------------------------------------- */
  2069. /** @inheritdoc */
  2070. summaryForLevel(level, { configMode=false }={}) {
  2071. // Link to compendium items
  2072. if ( !this.value.added || configMode ) {
  2073. return this.configuration.items.reduce((html, uuid) => html + dnd5e.utils.linkForUuid(uuid), "");
  2074. }
  2075. // Link to items on the actor
  2076. else {
  2077. return Object.keys(this.value.added).map(id => {
  2078. const item = this.actor.items.get(id);
  2079. return item?.toAnchor({classes: ["content-link"]}).outerHTML ?? "";
  2080. }).join("");
  2081. }
  2082. }
  2083. /* -------------------------------------------- */
  2084. /* Application Methods */
  2085. /* -------------------------------------------- */
  2086. /**
  2087. * Location where the added items are stored for the specified level.
  2088. * @param {number} level Level being advanced.
  2089. * @returns {string}
  2090. */
  2091. storagePath(level) {
  2092. return "value.added";
  2093. }
  2094. /* -------------------------------------------- */
  2095. /**
  2096. * Locally apply this advancement to the actor.
  2097. * @param {number} level Level being advanced.
  2098. * @param {object} data Data from the advancement form.
  2099. * @param {object} [retainedData={}] Item data grouped by UUID. If present, this data will be used rather than
  2100. * fetching new data from the source.
  2101. */
  2102. async apply(level, data, retainedData={}) {
  2103. const items = [];
  2104. const updates = {};
  2105. const spellChanges = this.configuration.spell?.spellChanges ?? {};
  2106. for ( const [uuid, selected] of Object.entries(data) ) {
  2107. if ( !selected ) continue;
  2108. let itemData = retainedData[uuid];
  2109. if ( !itemData ) {
  2110. const source = await fromUuid(uuid);
  2111. if ( !source ) continue;
  2112. itemData = source.clone({
  2113. _id: foundry.utils.randomID(),
  2114. "flags.dnd5e.sourceId": uuid,
  2115. "flags.dnd5e.advancementOrigin": `${this.item.id}.${this.id}`
  2116. }, {keepId: true}).toObject();
  2117. }
  2118. if ( itemData.type === "spell" ) foundry.utils.mergeObject(itemData, spellChanges);
  2119. items.push(itemData);
  2120. updates[itemData._id] = uuid;
  2121. }
  2122. this.actor.updateSource({items});
  2123. this.updateSource({[this.storagePath(level)]: updates});
  2124. }
  2125. /* -------------------------------------------- */
  2126. /** @inheritdoc */
  2127. restore(level, data) {
  2128. const updates = {};
  2129. for ( const item of data.items ) {
  2130. this.actor.updateSource({items: [item]});
  2131. updates[item._id] = item.flags.dnd5e.sourceId;
  2132. }
  2133. this.updateSource({[this.storagePath(level)]: updates});
  2134. }
  2135. /* -------------------------------------------- */
  2136. /** @inheritdoc */
  2137. reverse(level) {
  2138. const items = [];
  2139. const keyPath = this.storagePath(level);
  2140. for ( const id of Object.keys(foundry.utils.getProperty(this, keyPath) ?? {}) ) {
  2141. const item = this.actor.items.get(id);
  2142. if ( item ) items.push(item.toObject());
  2143. this.actor.items.delete(id);
  2144. }
  2145. this.updateSource({[keyPath.replace(/\.([\w\d]+)$/, ".-=$1")]: null});
  2146. return { items };
  2147. }
  2148. /* -------------------------------------------- */
  2149. /**
  2150. * Verify that the provided item can be used with this advancement based on the configuration.
  2151. * @param {Item5e} item Item that needs to be tested.
  2152. * @param {object} config
  2153. * @param {boolean} [config.strict=true] Should an error be thrown when an invalid type is encountered?
  2154. * @returns {boolean} Is this type valid?
  2155. * @throws An error if the item is invalid and strict is `true`.
  2156. */
  2157. _validateItemType(item, { strict=true }={}) {
  2158. if ( this.constructor.VALID_TYPES.has(item.type) ) return true;
  2159. const type = game.i18n.localize(CONFIG.Item.typeLabels[item.type]);
  2160. if ( strict ) throw new Error(game.i18n.format("DND5E.AdvancementItemTypeInvalidWarning", {type}));
  2161. return false;
  2162. }
  2163. }
  2164. /**
  2165. * Configuration application for item choices.
  2166. */
  2167. class ItemChoiceConfig extends AdvancementConfig {
  2168. /** @inheritdoc */
  2169. static get defaultOptions() {
  2170. return foundry.utils.mergeObject(super.defaultOptions, {
  2171. classes: ["dnd5e", "advancement", "item-choice", "two-column"],
  2172. dragDrop: [{ dropSelector: ".drop-target" }],
  2173. dropKeyPath: "pool",
  2174. template: "systems/dnd5e/templates/advancement/item-choice-config.hbs",
  2175. width: 540
  2176. });
  2177. }
  2178. /* -------------------------------------------- */
  2179. /** @inheritdoc */
  2180. getData(options={}) {
  2181. const context = {
  2182. ...super.getData(options),
  2183. showSpellConfig: this.advancement.configuration.type === "spell",
  2184. validTypes: this.advancement.constructor.VALID_TYPES.reduce((obj, type) => {
  2185. obj[type] = game.i18n.localize(CONFIG.Item.typeLabels[type]);
  2186. return obj;
  2187. }, {})
  2188. };
  2189. if ( this.advancement.configuration.type === "feat" ) {
  2190. const selectedType = CONFIG.DND5E.featureTypes[this.advancement.configuration.restriction.type];
  2191. context.typeRestriction = {
  2192. typeLabel: game.i18n.localize("DND5E.ItemFeatureType"),
  2193. typeOptions: CONFIG.DND5E.featureTypes,
  2194. subtypeLabel: game.i18n.format("DND5E.ItemFeatureSubtype", {category: selectedType?.label}),
  2195. subtypeOptions: selectedType?.subtypes
  2196. };
  2197. }
  2198. return context;
  2199. }
  2200. /* -------------------------------------------- */
  2201. /** @inheritdoc */
  2202. async prepareConfigurationUpdate(configuration) {
  2203. if ( configuration.choices ) configuration.choices = this.constructor._cleanedObject(configuration.choices);
  2204. // Ensure items are still valid if type restriction or spell restriction are changed
  2205. const pool = [];
  2206. for ( const uuid of (configuration.pool ?? this.advancement.configuration.pool) ) {
  2207. if ( this.advancement._validateItemType(await fromUuid(uuid), {
  2208. type: configuration.type, restriction: configuration.restriction ?? {}, strict: false
  2209. }) ) pool.push(uuid);
  2210. }
  2211. configuration.pool = pool;
  2212. return configuration;
  2213. }
  2214. /* -------------------------------------------- */
  2215. /** @inheritdoc */
  2216. _validateDroppedItem(event, item) {
  2217. this.advancement._validateItemType(item);
  2218. }
  2219. }
  2220. /**
  2221. * Object describing the proficiency for a specific ability or skill.
  2222. *
  2223. * @param {number} proficiency Actor's flat proficiency bonus based on their current level.
  2224. * @param {number} multiplier Value by which to multiply the actor's base proficiency value.
  2225. * @param {boolean} [roundDown] Should half-values be rounded up or down?
  2226. */
  2227. class Proficiency {
  2228. constructor(proficiency, multiplier, roundDown=true) {
  2229. /**
  2230. * Base proficiency value of the actor.
  2231. * @type {number}
  2232. * @private
  2233. */
  2234. this._baseProficiency = Number(proficiency ?? 0);
  2235. /**
  2236. * Value by which to multiply the actor's base proficiency value.
  2237. * @type {number}
  2238. */
  2239. this.multiplier = Number(multiplier ?? 0);
  2240. /**
  2241. * Direction decimal results should be rounded ("up" or "down").
  2242. * @type {string}
  2243. */
  2244. this.rounding = roundDown ? "down" : "up";
  2245. }
  2246. /* -------------------------------------------- */
  2247. /**
  2248. * Calculate an actor's proficiency modifier based on level or CR.
  2249. * @param {number} level Level or CR To use for calculating proficiency modifier.
  2250. * @returns {number} Proficiency modifier.
  2251. */
  2252. static calculateMod(level) {
  2253. return Math.floor((level + 7) / 4);
  2254. }
  2255. /* -------------------------------------------- */
  2256. /**
  2257. * Flat proficiency value regardless of proficiency mode.
  2258. * @type {number}
  2259. */
  2260. get flat() {
  2261. const roundMethod = (this.rounding === "down") ? Math.floor : Math.ceil;
  2262. return roundMethod(this.multiplier * this._baseProficiency);
  2263. }
  2264. /* -------------------------------------------- */
  2265. /**
  2266. * Dice-based proficiency value regardless of proficiency mode.
  2267. * @type {string}
  2268. */
  2269. get dice() {
  2270. if ( (this._baseProficiency === 0) || (this.multiplier === 0) ) return "0";
  2271. const roundTerm = (this.rounding === "down") ? "floor" : "ceil";
  2272. if ( this.multiplier === 0.5 ) {
  2273. return `${roundTerm}(1d${this._baseProficiency * 2} / 2)`;
  2274. } else {
  2275. return `${this.multiplier}d${this._baseProficiency * 2}`;
  2276. }
  2277. }
  2278. /* -------------------------------------------- */
  2279. /**
  2280. * Either flat or dice proficiency term based on configured setting.
  2281. * @type {string}
  2282. */
  2283. get term() {
  2284. return (game.settings.get("dnd5e", "proficiencyModifier") === "dice") ? this.dice : String(this.flat);
  2285. }
  2286. /* -------------------------------------------- */
  2287. /**
  2288. * Whether the proficiency is greater than zero.
  2289. * @type {boolean}
  2290. */
  2291. get hasProficiency() {
  2292. return (this._baseProficiency > 0) && (this.multiplier > 0);
  2293. }
  2294. /* -------------------------------------------- */
  2295. /**
  2296. * Override the default `toString` method to return flat proficiency for backwards compatibility in formula.
  2297. * @returns {string} Flat proficiency value.
  2298. */
  2299. toString() {
  2300. return this.term;
  2301. }
  2302. }
  2303. /* -------------------------------------------- */
  2304. /* D20 Roll */
  2305. /* -------------------------------------------- */
  2306. /**
  2307. * Configuration data for a D20 roll.
  2308. *
  2309. * @typedef {object} D20RollConfiguration
  2310. *
  2311. * @property {string[]} [parts=[]] The dice roll component parts, excluding the initial d20.
  2312. * @property {object} [data={}] Data that will be used when parsing this roll.
  2313. * @property {Event} [event] The triggering event for this roll.
  2314. *
  2315. * ## D20 Properties
  2316. * @property {boolean} [advantage] Apply advantage to this roll (unless overridden by modifier keys or dialog)?
  2317. * @property {boolean} [disadvantage] Apply disadvantage to this roll (unless overridden by modifier keys or dialog)?
  2318. * @property {number|null} [critical=20] The value of the d20 result which represents a critical success,
  2319. * `null` will prevent critical successes.
  2320. * @property {number|null} [fumble=1] The value of the d20 result which represents a critical failure,
  2321. * `null` will prevent critical failures.
  2322. * @property {number} [targetValue] The value of the d20 result which should represent a successful roll.
  2323. *
  2324. * ## Flags
  2325. * @property {boolean} [elvenAccuracy] Allow Elven Accuracy to modify this roll?
  2326. * @property {boolean} [halflingLucky] Allow Halfling Luck to modify this roll?
  2327. * @property {boolean} [reliableTalent] Allow Reliable Talent to modify this roll?
  2328. *
  2329. * ## Roll Configuration Dialog
  2330. * @property {boolean} [fastForward] Should the roll configuration dialog be skipped?
  2331. * @property {boolean} [chooseModifier=false] If the configuration dialog is shown, should the ability modifier be
  2332. * configurable within that interface?
  2333. * @property {string} [template] The HTML template used to display the roll configuration dialog.
  2334. * @property {string} [title] Title of the roll configuration dialog.
  2335. * @property {object} [dialogOptions] Additional options passed to the roll configuration dialog.
  2336. *
  2337. * ## Chat Message
  2338. * @property {boolean} [chatMessage=true] Should a chat message be created for this roll?
  2339. * @property {object} [messageData={}] Additional data which is applied to the created chat message.
  2340. * @property {string} [rollMode] Value of `CONST.DICE_ROLL_MODES` to apply as default for the chat message.
  2341. * @property {object} [flavor] Flavor text to use in the created chat message.
  2342. */
  2343. /**
  2344. * A standardized helper function for managing core 5e d20 rolls.
  2345. * Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward".
  2346. * This chooses the default options of a normal attack with no bonus, Advantage, or Disadvantage respectively
  2347. *
  2348. * @param {D20RollConfiguration} configuration Configuration data for the D20 roll.
  2349. * @returns {Promise<D20Roll|null>} The evaluated D20Roll, or null if the workflow was cancelled.
  2350. */
  2351. async function d20Roll({
  2352. parts=[], data={}, event,
  2353. advantage, disadvantage, critical=20, fumble=1, targetValue,
  2354. elvenAccuracy, halflingLucky, reliableTalent,
  2355. fastForward, chooseModifier=false, template, title, dialogOptions,
  2356. chatMessage=true, messageData={}, rollMode, flavor
  2357. }={}) {
  2358. // Handle input arguments
  2359. const formula = ["1d20"].concat(parts).join(" + ");
  2360. const {advantageMode, isFF} = CONFIG.Dice.D20Roll.determineAdvantageMode({
  2361. advantage, disadvantage, fastForward, event
  2362. });
  2363. const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
  2364. if ( chooseModifier && !isFF ) {
  2365. data.mod = "@mod";
  2366. if ( "abilityCheckBonus" in data ) data.abilityCheckBonus = "@abilityCheckBonus";
  2367. }
  2368. // Construct the D20Roll instance
  2369. const roll = new CONFIG.Dice.D20Roll(formula, data, {
  2370. flavor: flavor || title,
  2371. advantageMode,
  2372. defaultRollMode,
  2373. rollMode,
  2374. critical,
  2375. fumble,
  2376. targetValue,
  2377. elvenAccuracy,
  2378. halflingLucky,
  2379. reliableTalent
  2380. });
  2381. // Prompt a Dialog to further configure the D20Roll
  2382. if ( !isFF ) {
  2383. const configured = await roll.configureDialog({
  2384. title,
  2385. chooseModifier,
  2386. defaultRollMode,
  2387. defaultAction: advantageMode,
  2388. defaultAbility: data?.item?.ability || data?.defaultAbility,
  2389. template
  2390. }, dialogOptions);
  2391. if ( configured === null ) return null;
  2392. } else roll.options.rollMode ??= defaultRollMode;
  2393. // Evaluate the configured roll
  2394. await roll.evaluate({async: true});
  2395. // Create a Chat Message
  2396. if ( roll && chatMessage ) await roll.toMessage(messageData);
  2397. return roll;
  2398. }
  2399. /* -------------------------------------------- */
  2400. /* Damage Roll */
  2401. /* -------------------------------------------- */
  2402. /**
  2403. * Configuration data for a damage roll.
  2404. *
  2405. * @typedef {object} DamageRollConfiguration
  2406. *
  2407. * @property {string[]} [parts=[]] The dice roll component parts.
  2408. * @property {object} [data={}] Data that will be used when parsing this roll.
  2409. * @property {Event} [event] The triggering event for this roll.
  2410. *
  2411. * ## Critical Handling
  2412. * @property {boolean} [allowCritical=true] Is this damage roll allowed to be rolled as critical?
  2413. * @property {boolean} [critical] Apply critical to this roll (unless overridden by modifier key or dialog)?
  2414. * @property {number} [criticalBonusDice] A number of bonus damage dice that are added for critical hits.
  2415. * @property {number} [criticalMultiplier] Multiplier to use when calculating critical damage.
  2416. * @property {boolean} [multiplyNumeric] Should numeric terms be multiplied when this roll criticals?
  2417. * @property {boolean} [powerfulCritical] Should the critical dice be maximized rather than rolled?
  2418. * @property {string} [criticalBonusDamage] An extra damage term that is applied only on a critical hit.
  2419. *
  2420. * ## Roll Configuration Dialog
  2421. * @property {boolean} [fastForward] Should the roll configuration dialog be skipped?
  2422. * @property {string} [template] The HTML template used to render the roll configuration dialog.
  2423. * @property {string} [title] Title of the roll configuration dialog.
  2424. * @property {object} [dialogOptions] Additional options passed to the roll configuration dialog.
  2425. *
  2426. * ## Chat Message
  2427. * @property {boolean} [chatMessage=true] Should a chat message be created for this roll?
  2428. * @property {object} [messageData={}] Additional data which is applied to the created chat message.
  2429. * @property {string} [rollMode] Value of `CONST.DICE_ROLL_MODES` to apply as default for the chat message.
  2430. * @property {string} [flavor] Flavor text to use in the created chat message.
  2431. */
  2432. /**
  2433. * A standardized helper function for managing core 5e damage rolls.
  2434. * Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward".
  2435. * This chooses the default options of a normal attack with no bonus, Critical, or no bonus respectively
  2436. *
  2437. * @param {DamageRollConfiguration} configuration Configuration data for the Damage roll.
  2438. * @returns {Promise<DamageRoll|null>} The evaluated DamageRoll, or null if the workflow was canceled.
  2439. */
  2440. async function damageRoll({
  2441. parts=[], data={}, event,
  2442. allowCritical=true, critical, criticalBonusDice, criticalMultiplier,
  2443. multiplyNumeric, powerfulCritical, criticalBonusDamage,
  2444. fastForward, template, title, dialogOptions,
  2445. chatMessage=true, messageData={}, rollMode, flavor
  2446. }={}) {
  2447. // Handle input arguments
  2448. const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
  2449. // Construct the DamageRoll instance
  2450. const formula = parts.join(" + ");
  2451. const {isCritical, isFF} = _determineCriticalMode({critical, fastForward, event});
  2452. const roll = new CONFIG.Dice.DamageRoll(formula, data, {
  2453. flavor: flavor || title,
  2454. rollMode,
  2455. critical: isFF ? isCritical : false,
  2456. criticalBonusDice,
  2457. criticalMultiplier,
  2458. criticalBonusDamage,
  2459. multiplyNumeric: multiplyNumeric ?? game.settings.get("dnd5e", "criticalDamageModifiers"),
  2460. powerfulCritical: powerfulCritical ?? game.settings.get("dnd5e", "criticalDamageMaxDice")
  2461. });
  2462. // Prompt a Dialog to further configure the DamageRoll
  2463. if ( !isFF ) {
  2464. const configured = await roll.configureDialog({
  2465. title,
  2466. defaultRollMode: defaultRollMode,
  2467. defaultCritical: isCritical,
  2468. template,
  2469. allowCritical
  2470. }, dialogOptions);
  2471. if ( configured === null ) return null;
  2472. }
  2473. // Evaluate the configured roll
  2474. await roll.evaluate({async: true});
  2475. // Create a Chat Message
  2476. if ( roll && chatMessage ) await roll.toMessage(messageData);
  2477. return roll;
  2478. }
  2479. /* -------------------------------------------- */
  2480. /**
  2481. * Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
  2482. * @param {object} [config]
  2483. * @param {Event} [config.event] Event that triggered the roll.
  2484. * @param {boolean} [config.critical] Is this roll treated as a critical by default?
  2485. * @param {boolean} [config.fastForward] Should the roll dialog be skipped?
  2486. * @returns {{isFF: boolean, isCritical: boolean}} Whether the roll is fast-forward, and whether it is a critical hit
  2487. */
  2488. function _determineCriticalMode({event, critical=false, fastForward}={}) {
  2489. const isFF = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
  2490. if ( event?.altKey ) critical = true;
  2491. return {isFF: !!isFF, isCritical: critical};
  2492. }
  2493. /**
  2494. * A helper Dialog subclass for rolling Hit Dice on short rest.
  2495. *
  2496. * @param {Actor5e} actor Actor that is taking the short rest.
  2497. * @param {object} [dialogData={}] An object of dialog data which configures how the modal window is rendered.
  2498. * @param {object} [options={}] Dialog rendering options.
  2499. */
  2500. class ShortRestDialog extends Dialog {
  2501. constructor(actor, dialogData={}, options={}) {
  2502. super(dialogData, options);
  2503. /**
  2504. * Store a reference to the Actor document which is resting
  2505. * @type {Actor}
  2506. */
  2507. this.actor = actor;
  2508. /**
  2509. * Track the most recently used HD denomination for re-rendering the form
  2510. * @type {string}
  2511. */
  2512. this._denom = null;
  2513. }
  2514. /* -------------------------------------------- */
  2515. /** @inheritDoc */
  2516. static get defaultOptions() {
  2517. return foundry.utils.mergeObject(super.defaultOptions, {
  2518. template: "systems/dnd5e/templates/apps/short-rest.hbs",
  2519. classes: ["dnd5e", "dialog"]
  2520. });
  2521. }
  2522. /* -------------------------------------------- */
  2523. /** @inheritDoc */
  2524. getData() {
  2525. const data = super.getData();
  2526. // Determine Hit Dice
  2527. data.availableHD = this.actor.items.reduce((hd, item) => {
  2528. if ( item.type === "class" ) {
  2529. const {levels, hitDice, hitDiceUsed} = item.system;
  2530. const denom = hitDice ?? "d6";
  2531. const available = parseInt(levels ?? 1) - parseInt(hitDiceUsed ?? 0);
  2532. hd[denom] = denom in hd ? hd[denom] + available : available;
  2533. }
  2534. return hd;
  2535. }, {});
  2536. data.canRoll = this.actor.system.attributes.hd > 0;
  2537. data.denomination = this._denom;
  2538. // Determine rest type
  2539. const variant = game.settings.get("dnd5e", "restVariant");
  2540. data.promptNewDay = variant !== "epic"; // It's never a new day when only resting 1 minute
  2541. data.newDay = false; // It may be a new day, but not by default
  2542. return data;
  2543. }
  2544. /* -------------------------------------------- */
  2545. /** @inheritDoc */
  2546. activateListeners(html) {
  2547. super.activateListeners(html);
  2548. let btn = html.find("#roll-hd");
  2549. btn.click(this._onRollHitDie.bind(this));
  2550. }
  2551. /* -------------------------------------------- */
  2552. /**
  2553. * Handle rolling a Hit Die as part of a Short Rest action
  2554. * @param {Event} event The triggering click event
  2555. * @protected
  2556. */
  2557. async _onRollHitDie(event) {
  2558. event.preventDefault();
  2559. const btn = event.currentTarget;
  2560. this._denom = btn.form.hd.value;
  2561. await this.actor.rollHitDie(this._denom);
  2562. this.render();
  2563. }
  2564. /* -------------------------------------------- */
  2565. /**
  2566. * A helper constructor function which displays the Short Rest dialog and returns a Promise once it's workflow has
  2567. * been resolved.
  2568. * @param {object} [options={}]
  2569. * @param {Actor5e} [options.actor] Actor that is taking the short rest.
  2570. * @returns {Promise} Promise that resolves when the rest is completed or rejects when canceled.
  2571. */
  2572. static async shortRestDialog({ actor }={}) {
  2573. return new Promise((resolve, reject) => {
  2574. const dlg = new this(actor, {
  2575. title: `${game.i18n.localize("DND5E.ShortRest")}: ${actor.name}`,
  2576. buttons: {
  2577. rest: {
  2578. icon: '<i class="fas fa-bed"></i>',
  2579. label: game.i18n.localize("DND5E.Rest"),
  2580. callback: html => {
  2581. let newDay = false;
  2582. if ( game.settings.get("dnd5e", "restVariant") !== "epic" ) {
  2583. newDay = html.find('input[name="newDay"]')[0].checked;
  2584. }
  2585. resolve(newDay);
  2586. }
  2587. },
  2588. cancel: {
  2589. icon: '<i class="fas fa-times"></i>',
  2590. label: game.i18n.localize("Cancel"),
  2591. callback: reject
  2592. }
  2593. },
  2594. close: reject
  2595. });
  2596. dlg.render(true);
  2597. });
  2598. }
  2599. }
  2600. /**
  2601. * A helper Dialog subclass for completing a long rest.
  2602. *
  2603. * @param {Actor5e} actor Actor that is taking the long rest.
  2604. * @param {object} [dialogData={}] An object of dialog data which configures how the modal window is rendered.
  2605. * @param {object} [options={}] Dialog rendering options.
  2606. */
  2607. class LongRestDialog extends Dialog {
  2608. constructor(actor, dialogData={}, options={}) {
  2609. super(dialogData, options);
  2610. this.actor = actor;
  2611. }
  2612. /* -------------------------------------------- */
  2613. /** @inheritDoc */
  2614. static get defaultOptions() {
  2615. return foundry.utils.mergeObject(super.defaultOptions, {
  2616. template: "systems/dnd5e/templates/apps/long-rest.hbs",
  2617. classes: ["dnd5e", "dialog"]
  2618. });
  2619. }
  2620. /* -------------------------------------------- */
  2621. /** @inheritDoc */
  2622. getData() {
  2623. const data = super.getData();
  2624. const variant = game.settings.get("dnd5e", "restVariant");
  2625. data.promptNewDay = variant !== "gritty"; // It's always a new day when resting 1 week
  2626. data.newDay = variant === "normal"; // It's probably a new day when resting normally (8 hours)
  2627. return data;
  2628. }
  2629. /* -------------------------------------------- */
  2630. /**
  2631. * A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's
  2632. * workflow has been resolved.
  2633. * @param {object} [options={}]
  2634. * @param {Actor5e} [options.actor] Actor that is taking the long rest.
  2635. * @returns {Promise} Promise that resolves when the rest is completed or rejects when canceled.
  2636. */
  2637. static async longRestDialog({ actor } = {}) {
  2638. return new Promise((resolve, reject) => {
  2639. const dlg = new this(actor, {
  2640. title: `${game.i18n.localize("DND5E.LongRest")}: ${actor.name}`,
  2641. buttons: {
  2642. rest: {
  2643. icon: '<i class="fas fa-bed"></i>',
  2644. label: game.i18n.localize("DND5E.Rest"),
  2645. callback: html => {
  2646. let newDay = true;
  2647. if (game.settings.get("dnd5e", "restVariant") !== "gritty") {
  2648. newDay = html.find('input[name="newDay"]')[0].checked;
  2649. }
  2650. resolve(newDay);
  2651. }
  2652. },
  2653. cancel: {
  2654. icon: '<i class="fas fa-times"></i>',
  2655. label: game.i18n.localize("Cancel"),
  2656. callback: reject
  2657. }
  2658. },
  2659. default: "rest",
  2660. close: reject
  2661. });
  2662. dlg.render(true);
  2663. });
  2664. }
  2665. }
  2666. /**
  2667. * Cached version of the base items compendia indices with the needed subtype fields.
  2668. * @type {object}
  2669. * @private
  2670. */
  2671. const _cachedIndices = {};
  2672. /* -------------------------------------------- */
  2673. /* Trait Lists */
  2674. /* -------------------------------------------- */
  2675. /**
  2676. * Get the key path to the specified trait on an actor.
  2677. * @param {string} trait Trait as defined in `CONFIG.DND5E.traits`.
  2678. * @returns {string} Key path to this trait's object within an actor's system data.
  2679. */
  2680. function actorKeyPath(trait) {
  2681. const traitConfig = CONFIG.DND5E.traits[trait];
  2682. if ( traitConfig.actorKeyPath ) return traitConfig.actorKeyPath;
  2683. return `traits.${trait}`;
  2684. }
  2685. /* -------------------------------------------- */
  2686. /**
  2687. * Fetch the categories object for the specified trait.
  2688. * @param {string} trait Trait as defined in `CONFIG.DND5E.traits`.
  2689. * @returns {object} Trait categories defined within `CONFIG.DND5E`.
  2690. */
  2691. function categories(trait) {
  2692. const traitConfig = CONFIG.DND5E.traits[trait];
  2693. return CONFIG.DND5E[traitConfig.configKey ?? trait];
  2694. }
  2695. /* -------------------------------------------- */
  2696. /**
  2697. * Get a list of choices for a specific trait.
  2698. * @param {string} trait Trait as defined in `CONFIG.DND5E.traits`.
  2699. * @param {Set<string>} [chosen=[]] Optional list of keys to be marked as chosen.
  2700. * @returns {object} Object mapping proficiency ids to choice objects.
  2701. */
  2702. async function choices(trait, chosen=new Set()) {
  2703. const traitConfig = CONFIG.DND5E.traits[trait];
  2704. if ( foundry.utils.getType(chosen) === "Array" ) chosen = new Set(chosen);
  2705. let data = Object.entries(categories(trait)).reduce((obj, [key, label]) => {
  2706. obj[key] = { label, chosen: chosen.has(key) };
  2707. return obj;
  2708. }, {});
  2709. if ( traitConfig.children ) {
  2710. for ( const [categoryKey, childrenKey] of Object.entries(traitConfig.children) ) {
  2711. const children = CONFIG.DND5E[childrenKey];
  2712. if ( !children || !data[categoryKey] ) continue;
  2713. data[categoryKey].children = Object.entries(children).reduce((obj, [key, label]) => {
  2714. obj[key] = { label, chosen: chosen.has(key) };
  2715. return obj;
  2716. }, {});
  2717. }
  2718. }
  2719. if ( traitConfig.subtypes ) {
  2720. const keyPath = `system.${traitConfig.subtypes.keyPath}`;
  2721. const map = CONFIG.DND5E[`${trait}ProficienciesMap`];
  2722. // Merge all IDs lists together
  2723. const ids = traitConfig.subtypes.ids.reduce((obj, key) => {
  2724. if ( CONFIG.DND5E[key] ) Object.assign(obj, CONFIG.DND5E[key]);
  2725. return obj;
  2726. }, {});
  2727. // Fetch base items for all IDs
  2728. const baseItems = await Promise.all(Object.entries(ids).map(async ([key, id]) => {
  2729. const index = await getBaseItem(id);
  2730. return [key, index];
  2731. }));
  2732. // Sort base items as children of categories based on subtypes
  2733. for ( const [key, index] of baseItems ) {
  2734. if ( !index ) continue;
  2735. // Get the proper subtype, using proficiency map if needed
  2736. let type = foundry.utils.getProperty(index, keyPath);
  2737. if ( map?.[type] ) type = map[type];
  2738. const entry = { label: index.name, chosen: chosen.has(key) };
  2739. // No category for this type, add at top level
  2740. if ( !data[type] ) data[key] = entry;
  2741. // Add as child to appropriate category
  2742. else {
  2743. data[type].children ??= {};
  2744. data[type].children[key] = entry;
  2745. }
  2746. }
  2747. }
  2748. // Sort Categories
  2749. if ( traitConfig.sortCategories ) data = dnd5e.utils.sortObjectEntries(data, "label");
  2750. // Sort Children
  2751. for ( const category of Object.values(data) ) {
  2752. if ( !category.children ) continue;
  2753. category.children = dnd5e.utils.sortObjectEntries(category.children, "label");
  2754. }
  2755. return data;
  2756. }
  2757. /* -------------------------------------------- */
  2758. /**
  2759. * Fetch an item for the provided ID. If the provided ID contains a compendium pack name
  2760. * it will be fetched from that pack, otherwise it will be fetched from the compendium defined
  2761. * in `DND5E.sourcePacks.ITEMS`.
  2762. * @param {string} identifier Simple ID or compendium name and ID separated by a dot.
  2763. * @param {object} [options]
  2764. * @param {boolean} [options.indexOnly] If set to true, only the index data will be fetched (will never return
  2765. * Promise).
  2766. * @param {boolean} [options.fullItem] If set to true, the full item will be returned as long as `indexOnly` is
  2767. * false.
  2768. * @returns {Promise<Item5e>|object} Promise for a `Document` if `indexOnly` is false & `fullItem` is true,
  2769. * otherwise else a simple object containing the minimal index data.
  2770. */
  2771. function getBaseItem(identifier, { indexOnly=false, fullItem=false }={}) {
  2772. let pack = CONFIG.DND5E.sourcePacks.ITEMS;
  2773. let [scope, collection, id] = identifier.split(".");
  2774. if ( scope && collection ) pack = `${scope}.${collection}`;
  2775. if ( !id ) id = identifier;
  2776. const packObject = game.packs.get(pack);
  2777. // Full Item5e document required, always async.
  2778. if ( fullItem && !indexOnly ) return packObject?.getDocument(id);
  2779. const cache = _cachedIndices[pack];
  2780. const loading = cache instanceof Promise;
  2781. // Return extended index if cached, otherwise normal index, guaranteed to never be async.
  2782. if ( indexOnly ) {
  2783. const index = packObject?.index.get(id);
  2784. return loading ? index : cache?.[id] ?? index;
  2785. }
  2786. // Returned cached version of extended index if available.
  2787. if ( loading ) return cache.then(() => _cachedIndices[pack][id]);
  2788. else if ( cache ) return cache[id];
  2789. if ( !packObject ) return;
  2790. // Build the extended index and return a promise for the data
  2791. const promise = packObject.getIndex({ fields: traitIndexFields() }).then(index => {
  2792. const store = index.reduce((obj, entry) => {
  2793. obj[entry._id] = entry;
  2794. return obj;
  2795. }, {});
  2796. _cachedIndices[pack] = store;
  2797. return store[id];
  2798. });
  2799. _cachedIndices[pack] = promise;
  2800. return promise;
  2801. }
  2802. /* -------------------------------------------- */
  2803. /**
  2804. * List of fields on items that should be indexed for retrieving subtypes.
  2805. * @returns {string[]} Index list to pass to `Compendium#getIndex`.
  2806. * @protected
  2807. */
  2808. function traitIndexFields() {
  2809. const fields = [];
  2810. for ( const traitConfig of Object.values(CONFIG.DND5E.traits) ) {
  2811. if ( !traitConfig.subtypes ) continue;
  2812. fields.push(`system.${traitConfig.subtypes.keyPath}`);
  2813. }
  2814. return fields;
  2815. }
  2816. /* -------------------------------------------- */
  2817. /* Localized Formatting Methods */
  2818. /* -------------------------------------------- */
  2819. /**
  2820. * Get the localized label for a specific trait type.
  2821. * @param {string} trait Trait as defined in `CONFIG.DND5E.traits`.
  2822. * @param {number} [count] Count used to determine pluralization. If no count is provided, will default to
  2823. * the 'other' pluralization.
  2824. * @returns {string} Localized label.
  2825. */
  2826. function traitLabel(trait, count) {
  2827. let typeCap;
  2828. if ( trait.length === 2 ) typeCap = trait.toUpperCase();
  2829. else typeCap = trait.capitalize();
  2830. const pluralRule = ( count !== undefined ) ? new Intl.PluralRules(game.i18n.lang).select(count) : "other";
  2831. return game.i18n.localize(`DND5E.Trait${typeCap}Plural.${pluralRule}`);
  2832. }
  2833. /* -------------------------------------------- */
  2834. /**
  2835. * Retrieve the proper display label for the provided key.
  2836. * @param {string} trait Trait as defined in `CONFIG.DND5E.traits`.
  2837. * @param {string} key Key for which to generate the label.
  2838. * @returns {string} Retrieved label.
  2839. */
  2840. function keyLabel(trait, key) {
  2841. const traitConfig = CONFIG.DND5E.traits[trait];
  2842. if ( categories(trait)[key] ) {
  2843. const category = categories(trait)[key];
  2844. if ( !traitConfig.labelKey ) return category;
  2845. return foundry.utils.getProperty(category, traitConfig.labelKey);
  2846. }
  2847. for ( const childrenKey of Object.values(traitConfig.children ?? {}) ) {
  2848. if ( CONFIG.DND5E[childrenKey]?.[key] ) return CONFIG.DND5E[childrenKey]?.[key];
  2849. }
  2850. for ( const idsKey of traitConfig.subtypes?.ids ?? [] ) {
  2851. if ( !CONFIG.DND5E[idsKey]?.[key] ) continue;
  2852. const index = getBaseItem(CONFIG.DND5E[idsKey][key], { indexOnly: true });
  2853. if ( index ) return index.name;
  2854. else break;
  2855. }
  2856. return key;
  2857. }
  2858. /* -------------------------------------------- */
  2859. /**
  2860. * Create a human readable description of the provided choice.
  2861. * @param {string} trait Trait as defined in `CONFIG.DND5E.traits`.
  2862. * @param {TraitChoice} choice Data for a specific choice.
  2863. * @returns {string}
  2864. */
  2865. function choiceLabel(trait, choice) {
  2866. // Select from any trait values
  2867. if ( !choice.pool ) {
  2868. return game.i18n.format("DND5E.TraitConfigChooseAny", {
  2869. count: choice.count,
  2870. type: traitLabel(trait, choice.count).toLowerCase()
  2871. });
  2872. }
  2873. // Select from a list of options
  2874. const choices = choice.pool.map(key => keyLabel(trait, key));
  2875. const listFormatter = new Intl.ListFormat(game.i18n.lang, { type: "disjunction" });
  2876. return game.i18n.format("DND5E.TraitConfigChooseList", {
  2877. count: choice.count,
  2878. list: listFormatter.format(choices)
  2879. });
  2880. }
  2881. var trait = /*#__PURE__*/Object.freeze({
  2882. __proto__: null,
  2883. actorKeyPath: actorKeyPath,
  2884. categories: categories,
  2885. choiceLabel: choiceLabel,
  2886. choices: choices,
  2887. getBaseItem: getBaseItem,
  2888. keyLabel: keyLabel,
  2889. traitIndexFields: traitIndexFields,
  2890. traitLabel: traitLabel
  2891. });
  2892. /**
  2893. * Extend the base Actor class to implement additional system-specific logic.
  2894. */
  2895. class Actor5e extends Actor {
  2896. /**
  2897. * The data source for Actor5e.classes allowing it to be lazily computed.
  2898. * @type {Object<Item5e>}
  2899. * @private
  2900. */
  2901. _classes;
  2902. /* -------------------------------------------- */
  2903. /* Properties */
  2904. /* -------------------------------------------- */
  2905. /**
  2906. * A mapping of classes belonging to this Actor.
  2907. * @type {Object<Item5e>}
  2908. */
  2909. get classes() {
  2910. if ( this._classes !== undefined ) return this._classes;
  2911. if ( !["character", "npc"].includes(this.type) ) return this._classes = {};
  2912. return this._classes = this.items.filter(item => item.type === "class").reduce((obj, cls) => {
  2913. obj[cls.identifier] = cls;
  2914. return obj;
  2915. }, {});
  2916. }
  2917. /* -------------------------------------------- */
  2918. /**
  2919. * Is this Actor currently polymorphed into some other creature?
  2920. * @type {boolean}
  2921. */
  2922. get isPolymorphed() {
  2923. return this.getFlag("dnd5e", "isPolymorphed") || false;
  2924. }
  2925. /* -------------------------------------------- */
  2926. /**
  2927. * The Actor's currently equipped armor, if any.
  2928. * @type {Item5e|null}
  2929. */
  2930. get armor() {
  2931. return this.system.attributes.ac.equippedArmor ?? null;
  2932. }
  2933. /* -------------------------------------------- */
  2934. /**
  2935. * The Actor's currently equipped shield, if any.
  2936. * @type {Item5e|null}
  2937. */
  2938. get shield() {
  2939. return this.system.attributes.ac.equippedShield ?? null;
  2940. }
  2941. /* -------------------------------------------- */
  2942. /* Methods */
  2943. /* -------------------------------------------- */
  2944. /** @inheritdoc */
  2945. _initializeSource(source, options={}) {
  2946. source = super._initializeSource(source, options);
  2947. if ( !source._id || !options.pack || dnd5e.moduleArt.suppressArt ) return source;
  2948. const uuid = `Compendium.${options.pack}.${source._id}`;
  2949. const art = game.dnd5e.moduleArt.map.get(uuid);
  2950. if ( art?.actor || art?.token ) {
  2951. if ( art.actor ) source.img = art.actor;
  2952. if ( typeof art.token === "string" ) source.prototypeToken.texture.src = art.token;
  2953. else if ( art.token ) foundry.utils.mergeObject(source.prototypeToken, art.token);
  2954. const biography = source.system.details?.biography;
  2955. if ( art.credit && biography ) {
  2956. if ( typeof biography.value !== "string" ) biography.value = "";
  2957. biography.value += `<p>${art.credit}</p>`;
  2958. }
  2959. }
  2960. return source;
  2961. }
  2962. /* -------------------------------------------- */
  2963. /** @inheritDoc */
  2964. prepareData() {
  2965. // Do not attempt to prepare non-system types.
  2966. if ( !game.template.Actor.types.includes(this.type) ) return;
  2967. this._classes = undefined;
  2968. this._preparationWarnings = [];
  2969. super.prepareData();
  2970. this.items.forEach(item => item.prepareFinalAttributes());
  2971. }
  2972. /* -------------------------------------------- */
  2973. /** @inheritDoc */
  2974. prepareBaseData() {
  2975. // Delegate preparation to type-subclass
  2976. if ( this.type === "group" ) { // Eventually other types will also support this
  2977. return this.system._prepareBaseData();
  2978. }
  2979. this._prepareBaseArmorClass();
  2980. // Type-specific preparation
  2981. switch ( this.type ) {
  2982. case "character":
  2983. return this._prepareCharacterData();
  2984. case "npc":
  2985. return this._prepareNPCData();
  2986. case "vehicle":
  2987. return this._prepareVehicleData();
  2988. }
  2989. }
  2990. /* --------------------------------------------- */
  2991. /** @inheritDoc */
  2992. applyActiveEffects() {
  2993. this._prepareScaleValues();
  2994. // The Active Effects do not have access to their parent at preparation time, so we wait until this stage to
  2995. // determine whether they are suppressed or not.
  2996. this.effects.forEach(e => e.determineSuppression());
  2997. return super.applyActiveEffects();
  2998. }
  2999. /* -------------------------------------------- */
  3000. /** @inheritDoc */
  3001. prepareDerivedData() {
  3002. // Delegate preparation to type-subclass
  3003. if ( this.type === "group" ) { // Eventually other types will also support this
  3004. return this.system._prepareDerivedData();
  3005. }
  3006. const flags = this.flags.dnd5e || {};
  3007. this.labels = {};
  3008. // Retrieve data for polymorphed actors
  3009. let originalSaves = null;
  3010. let originalSkills = null;
  3011. if ( this.isPolymorphed ) {
  3012. const transformOptions = flags.transformOptions;
  3013. const original = game.actors?.get(flags.originalActor);
  3014. if ( original ) {
  3015. if ( transformOptions.mergeSaves ) originalSaves = original.system.abilities;
  3016. if ( transformOptions.mergeSkills ) originalSkills = original.system.skills;
  3017. }
  3018. }
  3019. // Prepare abilities, skills, & everything else
  3020. const globalBonuses = this.system.bonuses?.abilities ?? {};
  3021. const rollData = this.getRollData();
  3022. const checkBonus = simplifyBonus(globalBonuses?.check, rollData);
  3023. this._prepareAbilities(rollData, globalBonuses, checkBonus, originalSaves);
  3024. this._prepareSkills(rollData, globalBonuses, checkBonus, originalSkills);
  3025. this._prepareTools(rollData, globalBonuses, checkBonus);
  3026. this._prepareArmorClass();
  3027. this._prepareEncumbrance();
  3028. this._prepareHitPoints(rollData);
  3029. this._prepareInitiative(rollData, checkBonus);
  3030. this._prepareSpellcasting();
  3031. }
  3032. /* -------------------------------------------- */
  3033. /**
  3034. * Return the amount of experience required to gain a certain character level.
  3035. * @param {number} level The desired level.
  3036. * @returns {number} The XP required.
  3037. */
  3038. getLevelExp(level) {
  3039. const levels = CONFIG.DND5E.CHARACTER_EXP_LEVELS;
  3040. return levels[Math.min(level, levels.length - 1)];
  3041. }
  3042. /* -------------------------------------------- */
  3043. /**
  3044. * Return the amount of experience granted by killing a creature of a certain CR.
  3045. * @param {number} cr The creature's challenge rating.
  3046. * @returns {number} The amount of experience granted per kill.
  3047. */
  3048. getCRExp(cr) {
  3049. if ( cr < 1.0 ) return Math.max(200 * cr, 10);
  3050. return CONFIG.DND5E.CR_EXP_LEVELS[cr];
  3051. }
  3052. /* -------------------------------------------- */
  3053. /**
  3054. * @inheritdoc
  3055. * @param {object} [options]
  3056. * @param {boolean} [options.deterministic] Whether to force deterministic values for data properties that could be
  3057. * either a die term or a flat term.
  3058. */
  3059. getRollData({ deterministic=false }={}) {
  3060. const data = {...super.getRollData()};
  3061. if ( this.type === "group" ) return data;
  3062. data.prof = new Proficiency(this.system.attributes.prof, 1);
  3063. if ( deterministic ) data.prof = data.prof.flat;
  3064. data.attributes = foundry.utils.deepClone(data.attributes);
  3065. data.attributes.spellmod = data.abilities[data.attributes.spellcasting || "int"]?.mod ?? 0;
  3066. data.classes = {};
  3067. for ( const [identifier, cls] of Object.entries(this.classes) ) {
  3068. data.classes[identifier] = {...cls.system};
  3069. if ( cls.subclass ) data.classes[identifier].subclass = cls.subclass.system;
  3070. }
  3071. return data;
  3072. }
  3073. /* -------------------------------------------- */
  3074. /* Base Data Preparation Helpers */
  3075. /* -------------------------------------------- */
  3076. /**
  3077. * Initialize derived AC fields for Active Effects to target.
  3078. * Mutates the system.attributes.ac object.
  3079. * @protected
  3080. */
  3081. _prepareBaseArmorClass() {
  3082. const ac = this.system.attributes.ac;
  3083. ac.armor = 10;
  3084. ac.shield = ac.bonus = ac.cover = 0;
  3085. }
  3086. /* -------------------------------------------- */
  3087. /**
  3088. * Derive any values that have been scaled by the Advancement system.
  3089. * Mutates the value of the `system.scale` object.
  3090. * @protected
  3091. */
  3092. _prepareScaleValues() {
  3093. this.system.scale = Object.entries(this.classes).reduce((scale, [identifier, cls]) => {
  3094. scale[identifier] = cls.scaleValues;
  3095. if ( cls.subclass ) scale[cls.subclass.identifier] = cls.subclass.scaleValues;
  3096. return scale;
  3097. }, {});
  3098. }
  3099. /* -------------------------------------------- */
  3100. /**
  3101. * Perform any Character specific preparation.
  3102. * Mutates several aspects of the system data object.
  3103. * @protected
  3104. */
  3105. _prepareCharacterData() {
  3106. this.system.details.level = 0;
  3107. this.system.attributes.hd = 0;
  3108. this.system.attributes.attunement.value = 0;
  3109. for ( const item of this.items ) {
  3110. // Class levels & hit dice
  3111. if ( item.type === "class" ) {
  3112. const classLevels = parseInt(item.system.levels) || 1;
  3113. this.system.details.level += classLevels;
  3114. this.system.attributes.hd += classLevels - (parseInt(item.system.hitDiceUsed) || 0);
  3115. }
  3116. // Attuned items
  3117. else if ( item.system.attunement === CONFIG.DND5E.attunementTypes.ATTUNED ) {
  3118. this.system.attributes.attunement.value += 1;
  3119. }
  3120. }
  3121. // Character proficiency bonus
  3122. this.system.attributes.prof = Proficiency.calculateMod(this.system.details.level);
  3123. // Experience required for next level
  3124. const xp = this.system.details.xp;
  3125. xp.max = this.getLevelExp(this.system.details.level || 1);
  3126. const prior = this.getLevelExp(this.system.details.level - 1 || 0);
  3127. const required = xp.max - prior;
  3128. const pct = Math.round((xp.value - prior) * 100 / required);
  3129. xp.pct = Math.clamped(pct, 0, 100);
  3130. }
  3131. /* -------------------------------------------- */
  3132. /**
  3133. * Perform any NPC specific preparation.
  3134. * Mutates several aspects of the system data object.
  3135. * @protected
  3136. */
  3137. _prepareNPCData() {
  3138. const cr = this.system.details.cr;
  3139. // Attuned items
  3140. this.system.attributes.attunement.value = this.items.filter(i => {
  3141. return i.system.attunement === CONFIG.DND5E.attunementTypes.ATTUNED;
  3142. }).length;
  3143. // Kill Experience
  3144. this.system.details.xp ??= {};
  3145. this.system.details.xp.value = this.getCRExp(cr);
  3146. // Proficiency
  3147. this.system.attributes.prof = Proficiency.calculateMod(Math.max(cr, 1));
  3148. // Spellcaster Level
  3149. if ( this.system.attributes.spellcasting && !Number.isNumeric(this.system.details.spellLevel) ) {
  3150. this.system.details.spellLevel = Math.max(cr, 1);
  3151. }
  3152. }
  3153. /* -------------------------------------------- */
  3154. /**
  3155. * Perform any Vehicle specific preparation.
  3156. * Mutates several aspects of the system data object.
  3157. * @protected
  3158. */
  3159. _prepareVehicleData() {
  3160. this.system.attributes.prof = 0;
  3161. }
  3162. /* -------------------------------------------- */
  3163. /* Derived Data Preparation Helpers */
  3164. /* -------------------------------------------- */
  3165. /**
  3166. * Prepare abilities.
  3167. * @param {object} bonusData Data produced by `getRollData` to be applied to bonus formulas.
  3168. * @param {object} globalBonuses Global bonus data.
  3169. * @param {number} checkBonus Global ability check bonus.
  3170. * @param {object} originalSaves A transformed actor's original actor's abilities.
  3171. * @protected
  3172. */
  3173. _prepareAbilities(bonusData, globalBonuses, checkBonus, originalSaves) {
  3174. const flags = this.flags.dnd5e ?? {};
  3175. const dcBonus = simplifyBonus(this.system.bonuses?.spell?.dc, bonusData);
  3176. const saveBonus = simplifyBonus(globalBonuses.save, bonusData);
  3177. for ( const [id, abl] of Object.entries(this.system.abilities) ) {
  3178. if ( flags.diamondSoul ) abl.proficient = 1; // Diamond Soul is proficient in all saves
  3179. abl.mod = Math.floor((abl.value - 10) / 2);
  3180. const isRA = this._isRemarkableAthlete(id);
  3181. abl.checkProf = new Proficiency(this.system.attributes.prof, (isRA || flags.jackOfAllTrades) ? 0.5 : 0, !isRA);
  3182. const saveBonusAbl = simplifyBonus(abl.bonuses?.save, bonusData);
  3183. abl.saveBonus = saveBonusAbl + saveBonus;
  3184. abl.saveProf = new Proficiency(this.system.attributes.prof, abl.proficient);
  3185. const checkBonusAbl = simplifyBonus(abl.bonuses?.check, bonusData);
  3186. abl.checkBonus = checkBonusAbl + checkBonus;
  3187. abl.save = abl.mod + abl.saveBonus;
  3188. if ( Number.isNumeric(abl.saveProf.term) ) abl.save += abl.saveProf.flat;
  3189. abl.dc = 8 + abl.mod + this.system.attributes.prof + dcBonus;
  3190. if ( !Number.isFinite(abl.max) ) abl.max = CONFIG.DND5E.maxAbilityScore;
  3191. // If we merged saves when transforming, take the highest bonus here.
  3192. if ( originalSaves && abl.proficient ) abl.save = Math.max(abl.save, originalSaves[id].save);
  3193. }
  3194. }
  3195. /* -------------------------------------------- */
  3196. /**
  3197. * Prepare skill checks. Mutates the values of system.skills.
  3198. * @param {object} bonusData Data produced by `getRollData` to be applied to bonus formulas.
  3199. * @param {object} globalBonuses Global bonus data.
  3200. * @param {number} checkBonus Global ability check bonus.
  3201. * @param {object} originalSkills A transformed actor's original actor's skills.
  3202. * @protected
  3203. */
  3204. _prepareSkills(bonusData, globalBonuses, checkBonus, originalSkills) {
  3205. if ( this.type === "vehicle" ) return;
  3206. const flags = this.flags.dnd5e ?? {};
  3207. // Skill modifiers
  3208. const feats = CONFIG.DND5E.characterFlags;
  3209. const skillBonus = simplifyBonus(globalBonuses.skill, bonusData);
  3210. for ( const [id, skl] of Object.entries(this.system.skills) ) {
  3211. const ability = this.system.abilities[skl.ability];
  3212. const baseBonus = simplifyBonus(skl.bonuses?.check, bonusData);
  3213. let roundDown = true;
  3214. // Remarkable Athlete
  3215. if ( this._isRemarkableAthlete(skl.ability) && (skl.value < 0.5) ) {
  3216. skl.value = 0.5;
  3217. roundDown = false;
  3218. }
  3219. // Jack of All Trades
  3220. else if ( flags.jackOfAllTrades && (skl.value < 0.5) ) {
  3221. skl.value = 0.5;
  3222. }
  3223. // Polymorph Skill Proficiencies
  3224. if ( originalSkills ) {
  3225. skl.value = Math.max(skl.value, originalSkills[id].value);
  3226. }
  3227. // Compute modifier
  3228. const checkBonusAbl = simplifyBonus(ability?.bonuses?.check, bonusData);
  3229. skl.bonus = baseBonus + checkBonus + checkBonusAbl + skillBonus;
  3230. skl.mod = ability?.mod ?? 0;
  3231. skl.prof = new Proficiency(this.system.attributes.prof, skl.value, roundDown);
  3232. skl.proficient = skl.value;
  3233. skl.total = skl.mod + skl.bonus;
  3234. if ( Number.isNumeric(skl.prof.term) ) skl.total += skl.prof.flat;
  3235. // Compute passive bonus
  3236. const passive = flags.observantFeat && (feats.observantFeat.skills.includes(id)) ? 5 : 0;
  3237. const passiveBonus = simplifyBonus(skl.bonuses?.passive, bonusData);
  3238. skl.passive = 10 + skl.mod + skl.bonus + skl.prof.flat + passive + passiveBonus;
  3239. }
  3240. }
  3241. /* -------------------------------------------- */
  3242. /**
  3243. * Prepare tool checks. Mutates the values of system.tools.
  3244. * @param {object} bonusData Data produced by `getRollData` to be applied to bonus formulae.
  3245. * @param {object} globalBonuses Global bonus data.
  3246. * @param {number} checkBonus Global ability check bonus.
  3247. * @protected
  3248. */
  3249. _prepareTools(bonusData, globalBonuses, checkBonus) {
  3250. if ( this.type === "vehicle" ) return;
  3251. const flags = this.flags.dnd5e ?? {};
  3252. for ( const tool of Object.values(this.system.tools) ) {
  3253. const ability = this.system.abilities[tool.ability];
  3254. const baseBonus = simplifyBonus(tool.bonuses.check, bonusData);
  3255. let roundDown = true;
  3256. // Remarkable Athlete.
  3257. if ( this._isRemarkableAthlete(tool.ability) && (tool.value < 0.5) ) {
  3258. tool.value = 0.5;
  3259. roundDown = false;
  3260. }
  3261. // Jack of All Trades.
  3262. else if ( flags.jackOfAllTrades && (tool.value < 0.5) ) tool.value = 0.5;
  3263. const checkBonusAbl = simplifyBonus(ability?.bonuses?.check, bonusData);
  3264. tool.bonus = baseBonus + checkBonus + checkBonusAbl;
  3265. tool.mod = ability?.mod ?? 0;
  3266. tool.prof = new Proficiency(this.system.attributes.prof, tool.value, roundDown);
  3267. tool.total = tool.mod + tool.bonus;
  3268. if ( Number.isNumeric(tool.prof.term) ) tool.total += tool.prof.flat;
  3269. }
  3270. }
  3271. /* -------------------------------------------- */
  3272. /**
  3273. * Prepare a character's AC value from their equipped armor and shield.
  3274. * Mutates the value of the `system.attributes.ac` object.
  3275. */
  3276. _prepareArmorClass() {
  3277. const ac = this.system.attributes.ac;
  3278. // Apply automatic migrations for older data structures
  3279. let cfg = CONFIG.DND5E.armorClasses[ac.calc];
  3280. if ( !cfg ) {
  3281. ac.calc = "flat";
  3282. if ( Number.isNumeric(ac.value) ) ac.flat = Number(ac.value);
  3283. cfg = CONFIG.DND5E.armorClasses.flat;
  3284. }
  3285. // Identify Equipped Items
  3286. const armorTypes = new Set(Object.keys(CONFIG.DND5E.armorTypes));
  3287. const {armors, shields} = this.itemTypes.equipment.reduce((obj, equip) => {
  3288. const armor = equip.system.armor;
  3289. if ( !equip.system.equipped || !armorTypes.has(armor?.type) ) return obj;
  3290. if ( armor.type === "shield" ) obj.shields.push(equip);
  3291. else obj.armors.push(equip);
  3292. return obj;
  3293. }, {armors: [], shields: []});
  3294. // Determine base AC
  3295. switch ( ac.calc ) {
  3296. // Flat AC (no additional bonuses)
  3297. case "flat":
  3298. ac.value = Number(ac.flat);
  3299. return;
  3300. // Natural AC (includes bonuses)
  3301. case "natural":
  3302. ac.base = Number(ac.flat);
  3303. break;
  3304. default:
  3305. let formula = ac.calc === "custom" ? ac.formula : cfg.formula;
  3306. if ( armors.length ) {
  3307. if ( armors.length > 1 ) this._preparationWarnings.push({
  3308. message: game.i18n.localize("DND5E.WarnMultipleArmor"), type: "warning"
  3309. });
  3310. const armorData = armors[0].system.armor;
  3311. const isHeavy = armorData.type === "heavy";
  3312. ac.armor = armorData.value ?? ac.armor;
  3313. ac.dex = isHeavy ? 0 : Math.min(armorData.dex ?? Infinity, this.system.abilities.dex?.mod ?? 0);
  3314. ac.equippedArmor = armors[0];
  3315. }
  3316. else ac.dex = this.system.abilities.dex?.mod ?? 0;
  3317. const rollData = this.getRollData({ deterministic: true });
  3318. rollData.attributes.ac = ac;
  3319. try {
  3320. const replaced = Roll.replaceFormulaData(formula, rollData);
  3321. ac.base = Roll.safeEval(replaced);
  3322. } catch(err) {
  3323. this._preparationWarnings.push({
  3324. message: game.i18n.localize("DND5E.WarnBadACFormula"), link: "armor", type: "error"
  3325. });
  3326. const replaced = Roll.replaceFormulaData(CONFIG.DND5E.armorClasses.default.formula, rollData);
  3327. ac.base = Roll.safeEval(replaced);
  3328. }
  3329. break;
  3330. }
  3331. // Equipped Shield
  3332. if ( shields.length ) {
  3333. if ( shields.length > 1 ) this._preparationWarnings.push({
  3334. message: game.i18n.localize("DND5E.WarnMultipleShields"), type: "warning"
  3335. });
  3336. ac.shield = shields[0].system.armor.value ?? 0;
  3337. ac.equippedShield = shields[0];
  3338. }
  3339. // Compute total AC and return
  3340. ac.value = ac.base + ac.shield + ac.bonus + ac.cover;
  3341. }
  3342. /* -------------------------------------------- */
  3343. /**
  3344. * Prepare the level and percentage of encumbrance for an Actor.
  3345. * Optionally include the weight of carried currency by applying the standard rule from the PHB pg. 143.
  3346. * Mutates the value of the `system.attributes.encumbrance` object.
  3347. * @protected
  3348. */
  3349. _prepareEncumbrance() {
  3350. const encumbrance = this.system.attributes.encumbrance ??= {};
  3351. // Get the total weight from items
  3352. const physicalItems = ["weapon", "equipment", "consumable", "tool", "backpack", "loot"];
  3353. let weight = this.items.reduce((weight, i) => {
  3354. if ( !physicalItems.includes(i.type) ) return weight;
  3355. const q = i.system.quantity || 0;
  3356. const w = i.system.weight || 0;
  3357. return weight + (q * w);
  3358. }, 0);
  3359. // [Optional] add Currency Weight (for non-transformed actors)
  3360. const currency = this.system.currency;
  3361. if ( game.settings.get("dnd5e", "currencyWeight") && currency ) {
  3362. const numCoins = Object.values(currency).reduce((val, denom) => val + Math.max(denom, 0), 0);
  3363. const currencyPerWeight = game.settings.get("dnd5e", "metricWeightUnits")
  3364. ? CONFIG.DND5E.encumbrance.currencyPerWeight.metric
  3365. : CONFIG.DND5E.encumbrance.currencyPerWeight.imperial;
  3366. weight += numCoins / currencyPerWeight;
  3367. }
  3368. // Determine the Encumbrance size class
  3369. let mod = {tiny: 0.5, sm: 1, med: 1, lg: 2, huge: 4, grg: 8}[this.system.traits.size] || 1;
  3370. if ( this.flags.dnd5e?.powerfulBuild ) mod = Math.min(mod * 2, 8);
  3371. const strengthMultiplier = game.settings.get("dnd5e", "metricWeightUnits")
  3372. ? CONFIG.DND5E.encumbrance.strMultiplier.metric
  3373. : CONFIG.DND5E.encumbrance.strMultiplier.imperial;
  3374. // Populate final Encumbrance values
  3375. encumbrance.value = weight.toNearest(0.1);
  3376. encumbrance.max = ((this.system.abilities.str?.value ?? 10) * strengthMultiplier * mod).toNearest(0.1);
  3377. encumbrance.pct = Math.clamped((encumbrance.value * 100) / encumbrance.max, 0, 100);
  3378. encumbrance.encumbered = encumbrance.pct > (200 / 3);
  3379. }
  3380. /* -------------------------------------------- */
  3381. /**
  3382. * Prepare hit points for characters.
  3383. * @param {object} rollData Data produced by `getRollData` to be applied to bonus formulas.
  3384. * @protected
  3385. */
  3386. _prepareHitPoints(rollData) {
  3387. if ( this.type !== "character" || (this.system._source.attributes.hp.max !== null) ) return;
  3388. const hp = this.system.attributes.hp;
  3389. const abilityId = CONFIG.DND5E.hitPointsAbility || "con";
  3390. const abilityMod = (this.system.abilities[abilityId]?.mod ?? 0);
  3391. const base = Object.values(this.classes).reduce((total, item) => {
  3392. const advancement = item.advancement.byType.HitPoints?.[0];
  3393. return total + (advancement?.getAdjustedTotal(abilityMod) ?? 0);
  3394. }, 0);
  3395. const levelBonus = simplifyBonus(hp.bonuses.level, rollData) * this.system.details.level;
  3396. const overallBonus = simplifyBonus(hp.bonuses.overall, rollData);
  3397. hp.max = base + levelBonus + overallBonus;
  3398. }
  3399. /* -------------------------------------------- */
  3400. /**
  3401. * Prepare the initiative data for an actor.
  3402. * Mutates the value of the system.attributes.init object.
  3403. * @param {object} bonusData Data produced by getRollData to be applied to bonus formulas
  3404. * @param {number} globalCheckBonus Global ability check bonus
  3405. * @protected
  3406. */
  3407. _prepareInitiative(bonusData, globalCheckBonus=0) {
  3408. const init = this.system.attributes.init ??= {};
  3409. const flags = this.flags.dnd5e || {};
  3410. // Compute initiative modifier
  3411. const abilityId = init.ability || CONFIG.DND5E.initiativeAbility;
  3412. const ability = this.system.abilities?.[abilityId] || {};
  3413. init.mod = ability.mod ?? 0;
  3414. // Initiative proficiency
  3415. const prof = this.system.attributes.prof ?? 0;
  3416. const ra = flags.remarkableAthlete && ["str", "dex", "con"].includes(abilityId);
  3417. init.prof = new Proficiency(prof, (flags.jackOfAllTrades || ra) ? 0.5 : 0, !ra);
  3418. // Total initiative includes all numeric terms
  3419. const initBonus = simplifyBonus(init.bonus, bonusData);
  3420. const abilityBonus = simplifyBonus(ability.bonuses?.check, bonusData);
  3421. init.total = init.mod + initBonus + abilityBonus + globalCheckBonus
  3422. + (flags.initiativeAlert ? 5 : 0)
  3423. + (Number.isNumeric(init.prof.term) ? init.prof.flat : 0);
  3424. }
  3425. /* -------------------------------------------- */
  3426. /* Spellcasting Preparation */
  3427. /* -------------------------------------------- */
  3428. /**
  3429. * Prepare data related to the spell-casting capabilities of the Actor.
  3430. * Mutates the value of the system.spells object.
  3431. * @protected
  3432. */
  3433. _prepareSpellcasting() {
  3434. if ( !this.system.spells ) return;
  3435. // Spellcasting DC
  3436. const spellcastingAbility = this.system.abilities[this.system.attributes.spellcasting];
  3437. this.system.attributes.spelldc = spellcastingAbility ? spellcastingAbility.dc : 8 + this.system.attributes.prof;
  3438. // Translate the list of classes into spellcasting progression
  3439. const progression = { slot: 0, pact: 0 };
  3440. const types = {};
  3441. // NPCs don't get spell levels from classes
  3442. if ( this.type === "npc" ) {
  3443. progression.slot = this.system.details.spellLevel ?? 0;
  3444. types.leveled = 1;
  3445. }
  3446. else {
  3447. // Grab all classes with spellcasting
  3448. const classes = this.items.filter(cls => {
  3449. if ( cls.type !== "class" ) return false;
  3450. const type = cls.spellcasting.type;
  3451. if ( !type ) return false;
  3452. types[type] ??= 0;
  3453. types[type] += 1;
  3454. return true;
  3455. });
  3456. for ( const cls of classes ) this.constructor.computeClassProgression(
  3457. progression, cls, { actor: this, count: types[cls.spellcasting.type] }
  3458. );
  3459. }
  3460. for ( const type of Object.keys(CONFIG.DND5E.spellcastingTypes) ) {
  3461. this.constructor.prepareSpellcastingSlots(this.system.spells, type, progression, { actor: this });
  3462. }
  3463. }
  3464. /* -------------------------------------------- */
  3465. /**
  3466. * Contribute to the actor's spellcasting progression.
  3467. * @param {object} progression Spellcasting progression data. *Will be mutated.*
  3468. * @param {Item5e} cls Class for whom this progression is being computed.
  3469. * @param {object} [config={}]
  3470. * @param {Actor5e|null} [config.actor] Actor for whom the data is being prepared.
  3471. * @param {SpellcastingDescription} [config.spellcasting] Spellcasting descriptive object.
  3472. * @param {number} [config.count=1] Number of classes with this type of spellcasting.
  3473. */
  3474. static computeClassProgression(progression, cls, {actor, spellcasting, count=1}={}) {
  3475. const type = cls.spellcasting.type;
  3476. spellcasting = spellcasting ?? cls.spellcasting;
  3477. /**
  3478. * A hook event that fires while computing the spellcasting progression for each class on each actor.
  3479. * The actual hook names include the spellcasting type (e.g. `dnd5e.computeLeveledProgression`).
  3480. * @param {object} progression Spellcasting progression data. *Will be mutated.*
  3481. * @param {Actor5e|null} [actor] Actor for whom the data is being prepared.
  3482. * @param {Item5e} cls Class for whom this progression is being computed.
  3483. * @param {SpellcastingDescription} spellcasting Spellcasting descriptive object.
  3484. * @param {number} count Number of classes with this type of spellcasting.
  3485. * @returns {boolean} Explicitly return false to prevent default progression from being calculated.
  3486. * @function dnd5e.computeSpellcastingProgression
  3487. * @memberof hookEvents
  3488. */
  3489. const allowed = Hooks.call(
  3490. `dnd5e.compute${type.capitalize()}Progression`, progression, actor, cls, spellcasting, count
  3491. );
  3492. if ( allowed && (type === "pact") ) {
  3493. this.computePactProgression(progression, actor, cls, spellcasting, count);
  3494. } else if ( allowed && (type === "leveled") ) {
  3495. this.computeLeveledProgression(progression, actor, cls, spellcasting, count);
  3496. }
  3497. }
  3498. /* -------------------------------------------- */
  3499. /**
  3500. * Contribute to the actor's spellcasting progression for a class with leveled spellcasting.
  3501. * @param {object} progression Spellcasting progression data. *Will be mutated.*
  3502. * @param {Actor5e} actor Actor for whom the data is being prepared.
  3503. * @param {Item5e} cls Class for whom this progression is being computed.
  3504. * @param {SpellcastingDescription} spellcasting Spellcasting descriptive object.
  3505. * @param {number} count Number of classes with this type of spellcasting.
  3506. */
  3507. static computeLeveledProgression(progression, actor, cls, spellcasting, count) {
  3508. const prog = CONFIG.DND5E.spellcastingTypes.leveled.progression[spellcasting.progression];
  3509. if ( !prog ) return;
  3510. const rounding = prog.roundUp ? Math.ceil : Math.floor;
  3511. progression.slot += rounding(spellcasting.levels / prog.divisor ?? 1);
  3512. // Single-classed, non-full progression rounds up, rather than down.
  3513. if ( (count === 1) && (prog.divisor > 1) && progression.slot ) {
  3514. progression.slot = Math.ceil(spellcasting.levels / prog.divisor);
  3515. }
  3516. }
  3517. /* -------------------------------------------- */
  3518. /**
  3519. * Contribute to the actor's spellcasting progression for a class with pact spellcasting.
  3520. * @param {object} progression Spellcasting progression data. *Will be mutated.*
  3521. * @param {Actor5e} actor Actor for whom the data is being prepared.
  3522. * @param {Item5e} cls Class for whom this progression is being computed.
  3523. * @param {SpellcastingDescription} spellcasting Spellcasting descriptive object.
  3524. * @param {number} count Number of classes with this type of spellcasting.
  3525. */
  3526. static computePactProgression(progression, actor, cls, spellcasting, count) {
  3527. progression.pact += spellcasting.levels;
  3528. }
  3529. /* -------------------------------------------- */
  3530. /**
  3531. * Prepare actor's spell slots using progression data.
  3532. * @param {object} spells The `data.spells` object within actor's data. *Will be mutated.*
  3533. * @param {string} type Type of spellcasting slots being prepared.
  3534. * @param {object} progression Spellcasting progression data.
  3535. * @param {object} [config]
  3536. * @param {Actor5e} [config.actor] Actor for whom the data is being prepared.
  3537. */
  3538. static prepareSpellcastingSlots(spells, type, progression, {actor}={}) {
  3539. /**
  3540. * A hook event that fires to convert the provided spellcasting progression into spell slots.
  3541. * The actual hook names include the spellcasting type (e.g. `dnd5e.prepareLeveledSlots`).
  3542. * @param {object} spells The `data.spells` object within actor's data. *Will be mutated.*
  3543. * @param {Actor5e} actor Actor for whom the data is being prepared.
  3544. * @param {object} progression Spellcasting progression data.
  3545. * @returns {boolean} Explicitly return false to prevent default preparation from being performed.
  3546. * @function dnd5e.prepareSpellcastingSlots
  3547. * @memberof hookEvents
  3548. */
  3549. const allowed = Hooks.call(`dnd5e.prepare${type.capitalize()}Slots`, spells, actor, progression);
  3550. if ( allowed && (type === "pact") ) this.preparePactSlots(spells, actor, progression);
  3551. else if ( allowed && (type === "leveled") ) this.prepareLeveledSlots(spells, actor, progression);
  3552. }
  3553. /* -------------------------------------------- */
  3554. /**
  3555. * Prepare leveled spell slots using progression data.
  3556. * @param {object} spells The `data.spells` object within actor's data. *Will be mutated.*
  3557. * @param {Actor5e} actor Actor for whom the data is being prepared.
  3558. * @param {object} progression Spellcasting progression data.
  3559. */
  3560. static prepareLeveledSlots(spells, actor, progression) {
  3561. const levels = Math.clamped(progression.slot, 0, CONFIG.DND5E.maxLevel);
  3562. const slots = CONFIG.DND5E.SPELL_SLOT_TABLE[Math.min(levels, CONFIG.DND5E.SPELL_SLOT_TABLE.length) - 1] ?? [];
  3563. for ( const level of Array.fromRange(Object.keys(CONFIG.DND5E.spellLevels).length - 1, 1) ) {
  3564. const slot = spells[`spell${level}`] ??= { value: 0 };
  3565. slot.max = Number.isNumeric(slot.override) ? Math.max(parseInt(slot.override), 0) : slots[level - 1] ?? 0;
  3566. }
  3567. }
  3568. /* -------------------------------------------- */
  3569. /**
  3570. * Prepare pact spell slots using progression data.
  3571. * @param {object} spells The `data.spells` object within actor's data. *Will be mutated.*
  3572. * @param {Actor5e} actor Actor for whom the data is being prepared.
  3573. * @param {object} progression Spellcasting progression data.
  3574. */
  3575. static preparePactSlots(spells, actor, progression) {
  3576. // Pact spell data:
  3577. // - pact.level: Slot level for pact casting
  3578. // - pact.max: Total number of pact slots
  3579. // - pact.value: Currently available pact slots
  3580. // - pact.override: Override number of available spell slots
  3581. let pactLevel = Math.clamped(progression.pact, 0, CONFIG.DND5E.maxLevel);
  3582. spells.pact ??= {};
  3583. const override = Number.isNumeric(spells.pact.override) ? parseInt(spells.pact.override) : null;
  3584. // Pact slot override
  3585. if ( (pactLevel === 0) && (actor.type === "npc") && (override !== null) ) {
  3586. pactLevel = actor.system.details.spellLevel;
  3587. }
  3588. const [, pactConfig] = Object.entries(CONFIG.DND5E.pactCastingProgression)
  3589. .reverse().find(([l]) => Number(l) <= pactLevel) ?? [];
  3590. if ( pactConfig ) {
  3591. spells.pact.level = pactConfig.level;
  3592. if ( override === null ) spells.pact.max = pactConfig.slots;
  3593. else spells.pact.max = Math.max(override, 1);
  3594. spells.pact.value = Math.min(spells.pact.value, spells.pact.max);
  3595. }
  3596. else {
  3597. spells.pact.max = override || 0;
  3598. spells.pact.level = spells.pact.max > 0 ? 1 : 0;
  3599. }
  3600. }
  3601. /* -------------------------------------------- */
  3602. /* Event Handlers */
  3603. /* -------------------------------------------- */
  3604. /** @inheritdoc */
  3605. async _preCreate(data, options, user) {
  3606. await super._preCreate(data, options, user);
  3607. const sourceId = this.getFlag("core", "sourceId");
  3608. if ( sourceId?.startsWith("Compendium.") ) return;
  3609. // Configure prototype token settings
  3610. const prototypeToken = {};
  3611. if ( "size" in (this.system.traits || {}) ) {
  3612. const size = CONFIG.DND5E.tokenSizes[this.system.traits.size || "med"];
  3613. if ( !foundry.utils.hasProperty(data, "prototypeToken.width") ) prototypeToken.width = size;
  3614. if ( !foundry.utils.hasProperty(data, "prototypeToken.height") ) prototypeToken.height = size;
  3615. }
  3616. if ( this.type === "character" ) Object.assign(prototypeToken, {
  3617. sight: { enabled: true }, actorLink: true, disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY
  3618. });
  3619. this.updateSource({ prototypeToken });
  3620. }
  3621. /* -------------------------------------------- */
  3622. /** @inheritdoc */
  3623. async _preUpdate(changed, options, user) {
  3624. await super._preUpdate(changed, options, user);
  3625. // Apply changes in Actor size to Token width/height
  3626. if ( "size" in (this.system.traits || {}) ) {
  3627. const newSize = foundry.utils.getProperty(changed, "system.traits.size");
  3628. if ( newSize && (newSize !== this.system.traits?.size) ) {
  3629. let size = CONFIG.DND5E.tokenSizes[newSize];
  3630. if ( !foundry.utils.hasProperty(changed, "prototypeToken.width") ) {
  3631. changed.prototypeToken ||= {};
  3632. changed.prototypeToken.height = size;
  3633. changed.prototypeToken.width = size;
  3634. }
  3635. }
  3636. }
  3637. // Reset death save counters
  3638. if ( "hp" in (this.system.attributes || {}) ) {
  3639. const isDead = this.system.attributes.hp.value <= 0;
  3640. if ( isDead && (foundry.utils.getProperty(changed, "system.attributes.hp.value") > 0) ) {
  3641. foundry.utils.setProperty(changed, "system.attributes.death.success", 0);
  3642. foundry.utils.setProperty(changed, "system.attributes.death.failure", 0);
  3643. }
  3644. }
  3645. }
  3646. /* -------------------------------------------- */
  3647. /**
  3648. * Assign a class item as the original class for the Actor based on which class has the most levels.
  3649. * @returns {Promise<Actor5e>} Instance of the updated actor.
  3650. * @protected
  3651. */
  3652. _assignPrimaryClass() {
  3653. const classes = this.itemTypes.class.sort((a, b) => b.system.levels - a.system.levels);
  3654. const newPC = classes[0]?.id || "";
  3655. return this.update({"system.details.originalClass": newPC});
  3656. }
  3657. /* -------------------------------------------- */
  3658. /* Gameplay Mechanics */
  3659. /* -------------------------------------------- */
  3660. /** @override */
  3661. async modifyTokenAttribute(attribute, value, isDelta, isBar) {
  3662. if ( attribute === "attributes.hp" ) {
  3663. const hp = this.system.attributes.hp;
  3664. const delta = isDelta ? (-1 * value) : (hp.value + hp.temp) - value;
  3665. return this.applyDamage(delta);
  3666. }
  3667. return super.modifyTokenAttribute(attribute, value, isDelta, isBar);
  3668. }
  3669. /* -------------------------------------------- */
  3670. /**
  3671. * Apply a certain amount of damage or healing to the health pool for Actor
  3672. * @param {number} amount An amount of damage (positive) or healing (negative) to sustain
  3673. * @param {number} multiplier A multiplier which allows for resistance, vulnerability, or healing
  3674. * @returns {Promise<Actor5e>} A Promise which resolves once the damage has been applied
  3675. */
  3676. async applyDamage(amount=0, multiplier=1) {
  3677. amount = Math.floor(parseInt(amount) * multiplier);
  3678. const hp = this.system.attributes.hp;
  3679. if ( !hp ) return this; // Group actors don't have HP at the moment
  3680. // Deduct damage from temp HP first
  3681. const tmp = parseInt(hp.temp) || 0;
  3682. const dt = amount > 0 ? Math.min(tmp, amount) : 0;
  3683. // Remaining goes to health
  3684. const tmpMax = parseInt(hp.tempmax) || 0;
  3685. const dh = Math.clamped(hp.value - (amount - dt), 0, Math.max(0, hp.max + tmpMax));
  3686. // Update the Actor
  3687. const updates = {
  3688. "system.attributes.hp.temp": tmp - dt,
  3689. "system.attributes.hp.value": dh
  3690. };
  3691. // Delegate damage application to a hook
  3692. // TODO replace this in the future with a better modifyTokenAttribute function in the core
  3693. const allowed = Hooks.call("modifyTokenAttribute", {
  3694. attribute: "attributes.hp",
  3695. value: amount,
  3696. isDelta: false,
  3697. isBar: true
  3698. }, updates);
  3699. return allowed !== false ? this.update(updates, {dhp: -amount}) : this;
  3700. }
  3701. /* -------------------------------------------- */
  3702. /**
  3703. * Apply a certain amount of temporary hit point, but only if it's more than the actor currently has.
  3704. * @param {number} amount An amount of temporary hit points to set
  3705. * @returns {Promise<Actor5e>} A Promise which resolves once the temp HP has been applied
  3706. */
  3707. async applyTempHP(amount=0) {
  3708. amount = parseInt(amount);
  3709. const hp = this.system.attributes.hp;
  3710. // Update the actor if the new amount is greater than the current
  3711. const tmp = parseInt(hp.temp) || 0;
  3712. return amount > tmp ? this.update({"system.attributes.hp.temp": amount}) : this;
  3713. }
  3714. /* -------------------------------------------- */
  3715. /**
  3716. * Get a color used to represent the current hit points of an Actor.
  3717. * @param {number} current The current HP value
  3718. * @param {number} max The maximum HP value
  3719. * @returns {Color} The color used to represent the HP percentage
  3720. */
  3721. static getHPColor(current, max) {
  3722. const pct = Math.clamped(current, 0, max) / max;
  3723. return Color.fromRGB([(1-(pct/2)), pct, 0]);
  3724. }
  3725. /* -------------------------------------------- */
  3726. /**
  3727. * Determine whether the provided ability is usable for remarkable athlete.
  3728. * @param {string} ability Ability type to check.
  3729. * @returns {boolean} Whether the actor has the remarkable athlete flag and the ability is physical.
  3730. * @private
  3731. */
  3732. _isRemarkableAthlete(ability) {
  3733. return this.getFlag("dnd5e", "remarkableAthlete")
  3734. && CONFIG.DND5E.characterFlags.remarkableAthlete.abilities.includes(ability);
  3735. }
  3736. /* -------------------------------------------- */
  3737. /* Rolling */
  3738. /* -------------------------------------------- */
  3739. /**
  3740. * Roll a Skill Check
  3741. * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus
  3742. * @param {string} skillId The skill id (e.g. "ins")
  3743. * @param {object} options Options which configure how the skill check is rolled
  3744. * @returns {Promise<D20Roll>} A Promise which resolves to the created Roll instance
  3745. */
  3746. async rollSkill(skillId, options={}) {
  3747. const skl = this.system.skills[skillId];
  3748. const abl = this.system.abilities[skl.ability];
  3749. const globalBonuses = this.system.bonuses?.abilities ?? {};
  3750. const parts = ["@mod", "@abilityCheckBonus"];
  3751. const data = this.getRollData();
  3752. // Add ability modifier
  3753. data.mod = skl.mod;
  3754. data.defaultAbility = skl.ability;
  3755. // Include proficiency bonus
  3756. if ( skl.prof.hasProficiency ) {
  3757. parts.push("@prof");
  3758. data.prof = skl.prof.term;
  3759. }
  3760. // Global ability check bonus
  3761. if ( globalBonuses.check ) {
  3762. parts.push("@checkBonus");
  3763. data.checkBonus = Roll.replaceFormulaData(globalBonuses.check, data);
  3764. }
  3765. // Ability-specific check bonus
  3766. if ( abl?.bonuses?.check ) data.abilityCheckBonus = Roll.replaceFormulaData(abl.bonuses.check, data);
  3767. else data.abilityCheckBonus = 0;
  3768. // Skill-specific skill bonus
  3769. if ( skl.bonuses?.check ) {
  3770. const checkBonusKey = `${skillId}CheckBonus`;
  3771. parts.push(`@${checkBonusKey}`);
  3772. data[checkBonusKey] = Roll.replaceFormulaData(skl.bonuses.check, data);
  3773. }
  3774. // Global skill check bonus
  3775. if ( globalBonuses.skill ) {
  3776. parts.push("@skillBonus");
  3777. data.skillBonus = Roll.replaceFormulaData(globalBonuses.skill, data);
  3778. }
  3779. // Reliable Talent applies to any skill check we have full or better proficiency in
  3780. const reliableTalent = (skl.value >= 1 && this.getFlag("dnd5e", "reliableTalent"));
  3781. // Roll and return
  3782. const flavor = game.i18n.format("DND5E.SkillPromptTitle", {skill: CONFIG.DND5E.skills[skillId]?.label ?? ""});
  3783. const rollData = foundry.utils.mergeObject({
  3784. data: data,
  3785. title: `${flavor}: ${this.name}`,
  3786. flavor,
  3787. chooseModifier: true,
  3788. halflingLucky: this.getFlag("dnd5e", "halflingLucky"),
  3789. reliableTalent,
  3790. messageData: {
  3791. speaker: options.speaker || ChatMessage.getSpeaker({actor: this}),
  3792. "flags.dnd5e.roll": {type: "skill", skillId }
  3793. }
  3794. }, options);
  3795. rollData.parts = parts.concat(options.parts ?? []);
  3796. /**
  3797. * A hook event that fires before a skill check is rolled for an Actor.
  3798. * @function dnd5e.preRollSkill
  3799. * @memberof hookEvents
  3800. * @param {Actor5e} actor Actor for which the skill check is being rolled.
  3801. * @param {D20RollConfiguration} config Configuration data for the pending roll.
  3802. * @param {string} skillId ID of the skill being rolled as defined in `DND5E.skills`.
  3803. * @returns {boolean} Explicitly return `false` to prevent skill check from being rolled.
  3804. */
  3805. if ( Hooks.call("dnd5e.preRollSkill", this, rollData, skillId) === false ) return;
  3806. const roll = await d20Roll(rollData);
  3807. /**
  3808. * A hook event that fires after a skill check has been rolled for an Actor.
  3809. * @function dnd5e.rollSkill
  3810. * @memberof hookEvents
  3811. * @param {Actor5e} actor Actor for which the skill check has been rolled.
  3812. * @param {D20Roll} roll The resulting roll.
  3813. * @param {string} skillId ID of the skill that was rolled as defined in `DND5E.skills`.
  3814. */
  3815. if ( roll ) Hooks.callAll("dnd5e.rollSkill", this, roll, skillId);
  3816. return roll;
  3817. }
  3818. /* -------------------------------------------- */
  3819. /**
  3820. * Roll a Tool Check.
  3821. * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonuses.
  3822. * @param {string} toolId The identifier of the tool being rolled.
  3823. * @param {object} options Options which configure how the tool check is rolled.
  3824. * @returns {Promise<D20Roll>} A Promise which resolves to the created Roll instance.
  3825. */
  3826. async rollToolCheck(toolId, options={}) {
  3827. // Prepare roll data.
  3828. const tool = this.system.tools[toolId];
  3829. const ability = this.system.abilities[options.ability || (tool?.ability ?? "int")];
  3830. const globalBonuses = this.system.bonuses?.abilities ?? {};
  3831. const parts = ["@mod", "@abilityCheckBonus"];
  3832. const data = this.getRollData();
  3833. // Add ability modifier.
  3834. data.mod = tool?.mod ?? 0;
  3835. data.defaultAbility = options.ability || (tool?.ability ?? "int");
  3836. // Add proficiency.
  3837. const prof = options.prof ?? tool?.prof;
  3838. if ( prof?.hasProficiency ) {
  3839. parts.push("@prof");
  3840. data.prof = prof.term;
  3841. }
  3842. // Global ability check bonus.
  3843. if ( globalBonuses.check ) {
  3844. parts.push("@checkBonus");
  3845. data.checkBonus = Roll.replaceFormulaData(globalBonuses.check, data);
  3846. }
  3847. // Ability-specific check bonus.
  3848. if ( ability?.bonuses.check ) data.abilityCheckBonus = Roll.replaceFormulaData(ability.bonuses.check, data);
  3849. else data.abilityCheckBonus = 0;
  3850. // Tool-specific check bonus.
  3851. if ( tool?.bonuses.check || options.bonus ) {
  3852. parts.push("@toolBonus");
  3853. const bonus = [];
  3854. if ( tool?.bonuses.check ) bonus.push(Roll.replaceFormulaData(tool.bonuses.check, data));
  3855. if ( options.bonus ) bonus.push(Roll.replaceFormulaData(options.bonus, data));
  3856. data.toolBonus = bonus.join(" + ");
  3857. }
  3858. const flavor = game.i18n.format("DND5E.ToolPromptTitle", {tool: keyLabel("tool", toolId) ?? ""});
  3859. const rollData = foundry.utils.mergeObject({
  3860. data, flavor,
  3861. title: `${flavor}: ${this.name}`,
  3862. chooseModifier: true,
  3863. halflingLucky: this.getFlag("dnd5e", "halflingLucky"),
  3864. messageData: {
  3865. speaker: options.speaker || ChatMessage.implementation.getSpeaker({actor: this}),
  3866. "flags.dnd5e.roll": {type: "tool", toolId}
  3867. }
  3868. }, options);
  3869. rollData.parts = parts.concat(options.parts ?? []);
  3870. /**
  3871. * A hook event that fires before a tool check is rolled for an Actor.
  3872. * @function dnd5e.preRollRool
  3873. * @memberof hookEvents
  3874. * @param {Actor5e} actor Actor for which the tool check is being rolled.
  3875. * @param {D20RollConfiguration} config Configuration data for the pending roll.
  3876. * @param {string} toolId Identifier of the tool being rolled.
  3877. * @returns {boolean} Explicitly return `false` to prevent skill check from being rolled.
  3878. */
  3879. if ( Hooks.call("dnd5e.preRollToolCheck", this, rollData, toolId) === false ) return;
  3880. const roll = await d20Roll(rollData);
  3881. /**
  3882. * A hook event that fires after a tool check has been rolled for an Actor.
  3883. * @function dnd5e.rollTool
  3884. * @memberof hookEvents
  3885. * @param {Actor5e} actor Actor for which the tool check has been rolled.
  3886. * @param {D20Roll} roll The resulting roll.
  3887. * @param {string} toolId Identifier of the tool that was rolled.
  3888. */
  3889. if ( roll ) Hooks.callAll("dnd5e.rollToolCheck", this, roll, toolId);
  3890. return roll;
  3891. }
  3892. /* -------------------------------------------- */
  3893. /**
  3894. * Roll a generic ability test or saving throw.
  3895. * Prompt the user for input on which variety of roll they want to do.
  3896. * @param {string} abilityId The ability id (e.g. "str")
  3897. * @param {object} options Options which configure how ability tests or saving throws are rolled
  3898. */
  3899. rollAbility(abilityId, options={}) {
  3900. const label = CONFIG.DND5E.abilities[abilityId]?.label ?? "";
  3901. new Dialog({
  3902. title: `${game.i18n.format("DND5E.AbilityPromptTitle", {ability: label})}: ${this.name}`,
  3903. content: `<p>${game.i18n.format("DND5E.AbilityPromptText", {ability: label})}</p>`,
  3904. buttons: {
  3905. test: {
  3906. label: game.i18n.localize("DND5E.ActionAbil"),
  3907. callback: () => this.rollAbilityTest(abilityId, options)
  3908. },
  3909. save: {
  3910. label: game.i18n.localize("DND5E.ActionSave"),
  3911. callback: () => this.rollAbilitySave(abilityId, options)
  3912. }
  3913. }
  3914. }).render(true);
  3915. }
  3916. /* -------------------------------------------- */
  3917. /**
  3918. * Roll an Ability Test
  3919. * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus
  3920. * @param {string} abilityId The ability ID (e.g. "str")
  3921. * @param {object} options Options which configure how ability tests are rolled
  3922. * @returns {Promise<D20Roll>} A Promise which resolves to the created Roll instance
  3923. */
  3924. async rollAbilityTest(abilityId, options={}) {
  3925. const label = CONFIG.DND5E.abilities[abilityId]?.label ?? "";
  3926. const abl = this.system.abilities[abilityId];
  3927. const globalBonuses = this.system.bonuses?.abilities ?? {};
  3928. const parts = [];
  3929. const data = this.getRollData();
  3930. // Add ability modifier
  3931. parts.push("@mod");
  3932. data.mod = abl?.mod ?? 0;
  3933. // Include proficiency bonus
  3934. if ( abl?.checkProf.hasProficiency ) {
  3935. parts.push("@prof");
  3936. data.prof = abl.checkProf.term;
  3937. }
  3938. // Add ability-specific check bonus
  3939. if ( abl?.bonuses?.check ) {
  3940. const checkBonusKey = `${abilityId}CheckBonus`;
  3941. parts.push(`@${checkBonusKey}`);
  3942. data[checkBonusKey] = Roll.replaceFormulaData(abl.bonuses.check, data);
  3943. }
  3944. // Add global actor bonus
  3945. if ( globalBonuses.check ) {
  3946. parts.push("@checkBonus");
  3947. data.checkBonus = Roll.replaceFormulaData(globalBonuses.check, data);
  3948. }
  3949. // Roll and return
  3950. const flavor = game.i18n.format("DND5E.AbilityPromptTitle", {ability: label});
  3951. const rollData = foundry.utils.mergeObject({
  3952. data,
  3953. title: `${flavor}: ${this.name}`,
  3954. flavor,
  3955. halflingLucky: this.getFlag("dnd5e", "halflingLucky"),
  3956. messageData: {
  3957. speaker: options.speaker || ChatMessage.getSpeaker({actor: this}),
  3958. "flags.dnd5e.roll": {type: "ability", abilityId }
  3959. }
  3960. }, options);
  3961. rollData.parts = parts.concat(options.parts ?? []);
  3962. /**
  3963. * A hook event that fires before an ability test is rolled for an Actor.
  3964. * @function dnd5e.preRollAbilityTest
  3965. * @memberof hookEvents
  3966. * @param {Actor5e} actor Actor for which the ability test is being rolled.
  3967. * @param {D20RollConfiguration} config Configuration data for the pending roll.
  3968. * @param {string} abilityId ID of the ability being rolled as defined in `DND5E.abilities`.
  3969. * @returns {boolean} Explicitly return `false` to prevent ability test from being rolled.
  3970. */
  3971. if ( Hooks.call("dnd5e.preRollAbilityTest", this, rollData, abilityId) === false ) return;
  3972. const roll = await d20Roll(rollData);
  3973. /**
  3974. * A hook event that fires after an ability test has been rolled for an Actor.
  3975. * @function dnd5e.rollAbilityTest
  3976. * @memberof hookEvents
  3977. * @param {Actor5e} actor Actor for which the ability test has been rolled.
  3978. * @param {D20Roll} roll The resulting roll.
  3979. * @param {string} abilityId ID of the ability that was rolled as defined in `DND5E.abilities`.
  3980. */
  3981. if ( roll ) Hooks.callAll("dnd5e.rollAbilityTest", this, roll, abilityId);
  3982. return roll;
  3983. }
  3984. /* -------------------------------------------- */
  3985. /**
  3986. * Roll an Ability Saving Throw
  3987. * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus
  3988. * @param {string} abilityId The ability ID (e.g. "str")
  3989. * @param {object} options Options which configure how ability tests are rolled
  3990. * @returns {Promise<D20Roll>} A Promise which resolves to the created Roll instance
  3991. */
  3992. async rollAbilitySave(abilityId, options={}) {
  3993. const label = CONFIG.DND5E.abilities[abilityId]?.label ?? "";
  3994. const abl = this.system.abilities[abilityId];
  3995. const globalBonuses = this.system.bonuses?.abilities ?? {};
  3996. const parts = [];
  3997. const data = this.getRollData();
  3998. // Add ability modifier
  3999. parts.push("@mod");
  4000. data.mod = abl?.mod ?? 0;
  4001. // Include proficiency bonus
  4002. if ( abl?.saveProf.hasProficiency ) {
  4003. parts.push("@prof");
  4004. data.prof = abl.saveProf.term;
  4005. }
  4006. // Include ability-specific saving throw bonus
  4007. if ( abl?.bonuses?.save ) {
  4008. const saveBonusKey = `${abilityId}SaveBonus`;
  4009. parts.push(`@${saveBonusKey}`);
  4010. data[saveBonusKey] = Roll.replaceFormulaData(abl.bonuses.save, data);
  4011. }
  4012. // Include a global actor ability save bonus
  4013. if ( globalBonuses.save ) {
  4014. parts.push("@saveBonus");
  4015. data.saveBonus = Roll.replaceFormulaData(globalBonuses.save, data);
  4016. }
  4017. // Roll and return
  4018. const flavor = game.i18n.format("DND5E.SavePromptTitle", {ability: label});
  4019. const rollData = foundry.utils.mergeObject({
  4020. data,
  4021. title: `${flavor}: ${this.name}`,
  4022. flavor,
  4023. halflingLucky: this.getFlag("dnd5e", "halflingLucky"),
  4024. messageData: {
  4025. speaker: options.speaker || ChatMessage.getSpeaker({actor: this}),
  4026. "flags.dnd5e.roll": {type: "save", abilityId }
  4027. }
  4028. }, options);
  4029. rollData.parts = parts.concat(options.parts ?? []);
  4030. /**
  4031. * A hook event that fires before an ability save is rolled for an Actor.
  4032. * @function dnd5e.preRollAbilitySave
  4033. * @memberof hookEvents
  4034. * @param {Actor5e} actor Actor for which the ability save is being rolled.
  4035. * @param {D20RollConfiguration} config Configuration data for the pending roll.
  4036. * @param {string} abilityId ID of the ability being rolled as defined in `DND5E.abilities`.
  4037. * @returns {boolean} Explicitly return `false` to prevent ability save from being rolled.
  4038. */
  4039. if ( Hooks.call("dnd5e.preRollAbilitySave", this, rollData, abilityId) === false ) return;
  4040. const roll = await d20Roll(rollData);
  4041. /**
  4042. * A hook event that fires after an ability save has been rolled for an Actor.
  4043. * @function dnd5e.rollAbilitySave
  4044. * @memberof hookEvents
  4045. * @param {Actor5e} actor Actor for which the ability save has been rolled.
  4046. * @param {D20Roll} roll The resulting roll.
  4047. * @param {string} abilityId ID of the ability that was rolled as defined in `DND5E.abilities`.
  4048. */
  4049. if ( roll ) Hooks.callAll("dnd5e.rollAbilitySave", this, roll, abilityId);
  4050. return roll;
  4051. }
  4052. /* -------------------------------------------- */
  4053. /**
  4054. * Perform a death saving throw, rolling a d20 plus any global save bonuses
  4055. * @param {object} options Additional options which modify the roll
  4056. * @returns {Promise<D20Roll|null>} A Promise which resolves to the Roll instance
  4057. */
  4058. async rollDeathSave(options={}) {
  4059. const death = this.system.attributes.death;
  4060. // Display a warning if we are not at zero HP or if we already have reached 3
  4061. if ( (this.system.attributes.hp.value > 0) || (death.failure >= 3) || (death.success >= 3) ) {
  4062. ui.notifications.warn(game.i18n.localize("DND5E.DeathSaveUnnecessary"));
  4063. return null;
  4064. }
  4065. // Evaluate a global saving throw bonus
  4066. const speaker = options.speaker || ChatMessage.getSpeaker({actor: this});
  4067. const globalBonuses = this.system.bonuses?.abilities ?? {};
  4068. const parts = [];
  4069. const data = this.getRollData();
  4070. // Diamond Soul adds proficiency
  4071. if ( this.getFlag("dnd5e", "diamondSoul") ) {
  4072. parts.push("@prof");
  4073. data.prof = new Proficiency(this.system.attributes.prof, 1).term;
  4074. }
  4075. // Include a global actor ability save bonus
  4076. if ( globalBonuses.save ) {
  4077. parts.push("@saveBonus");
  4078. data.saveBonus = Roll.replaceFormulaData(globalBonuses.save, data);
  4079. }
  4080. // Evaluate the roll
  4081. const flavor = game.i18n.localize("DND5E.DeathSavingThrow");
  4082. const rollData = foundry.utils.mergeObject({
  4083. data,
  4084. title: `${flavor}: ${this.name}`,
  4085. flavor,
  4086. halflingLucky: this.getFlag("dnd5e", "halflingLucky"),
  4087. targetValue: 10,
  4088. messageData: {
  4089. speaker: speaker,
  4090. "flags.dnd5e.roll": {type: "death"}
  4091. }
  4092. }, options);
  4093. rollData.parts = parts.concat(options.parts ?? []);
  4094. /**
  4095. * A hook event that fires before a death saving throw is rolled for an Actor.
  4096. * @function dnd5e.preRollDeathSave
  4097. * @memberof hookEvents
  4098. * @param {Actor5e} actor Actor for which the death saving throw is being rolled.
  4099. * @param {D20RollConfiguration} config Configuration data for the pending roll.
  4100. * @returns {boolean} Explicitly return `false` to prevent death saving throw from being rolled.
  4101. */
  4102. if ( Hooks.call("dnd5e.preRollDeathSave", this, rollData) === false ) return;
  4103. const roll = await d20Roll(rollData);
  4104. if ( !roll ) return null;
  4105. // Take action depending on the result
  4106. const details = {};
  4107. // Save success
  4108. if ( roll.total >= (roll.options.targetValue ?? 10) ) {
  4109. let successes = (death.success || 0) + 1;
  4110. // Critical Success = revive with 1hp
  4111. if ( roll.isCritical ) {
  4112. details.updates = {
  4113. "system.attributes.death.success": 0,
  4114. "system.attributes.death.failure": 0,
  4115. "system.attributes.hp.value": 1
  4116. };
  4117. details.chatString = "DND5E.DeathSaveCriticalSuccess";
  4118. }
  4119. // 3 Successes = survive and reset checks
  4120. else if ( successes === 3 ) {
  4121. details.updates = {
  4122. "system.attributes.death.success": 0,
  4123. "system.attributes.death.failure": 0
  4124. };
  4125. details.chatString = "DND5E.DeathSaveSuccess";
  4126. }
  4127. // Increment successes
  4128. else details.updates = {"system.attributes.death.success": Math.clamped(successes, 0, 3)};
  4129. }
  4130. // Save failure
  4131. else {
  4132. let failures = (death.failure || 0) + (roll.isFumble ? 2 : 1);
  4133. details.updates = {"system.attributes.death.failure": Math.clamped(failures, 0, 3)};
  4134. if ( failures >= 3 ) { // 3 Failures = death
  4135. details.chatString = "DND5E.DeathSaveFailure";
  4136. }
  4137. }
  4138. /**
  4139. * A hook event that fires after a death saving throw has been rolled for an Actor, but before
  4140. * updates have been performed.
  4141. * @function dnd5e.rollDeathSave
  4142. * @memberof hookEvents
  4143. * @param {Actor5e} actor Actor for which the death saving throw has been rolled.
  4144. * @param {D20Roll} roll The resulting roll.
  4145. * @param {object} details
  4146. * @param {object} details.updates Updates that will be applied to the actor as a result of this save.
  4147. * @param {string} details.chatString Localizable string displayed in the create chat message. If not set, then
  4148. * no chat message will be displayed.
  4149. * @returns {boolean} Explicitly return `false` to prevent updates from being performed.
  4150. */
  4151. if ( Hooks.call("dnd5e.rollDeathSave", this, roll, details) === false ) return roll;
  4152. if ( !foundry.utils.isEmpty(details.updates) ) await this.update(details.updates);
  4153. // Display success/failure chat message
  4154. if ( details.chatString ) {
  4155. let chatData = { content: game.i18n.format(details.chatString, {name: this.name}), speaker };
  4156. ChatMessage.applyRollMode(chatData, roll.options.rollMode);
  4157. await ChatMessage.create(chatData);
  4158. }
  4159. // Return the rolled result
  4160. return roll;
  4161. }
  4162. /* -------------------------------------------- */
  4163. /**
  4164. * Get an un-evaluated D20Roll instance used to roll initiative for this Actor.
  4165. * @param {object} [options] Options which modify the roll
  4166. * @param {D20Roll.ADV_MODE} [options.advantageMode] A specific advantage mode to apply
  4167. * @param {string} [options.flavor] Special flavor text to apply
  4168. * @returns {D20Roll} The constructed but unevaluated D20Roll
  4169. */
  4170. getInitiativeRoll(options={}) {
  4171. // Use a temporarily cached initiative roll
  4172. if ( this._cachedInitiativeRoll ) return this._cachedInitiativeRoll.clone();
  4173. // Obtain required data
  4174. const init = this.system.attributes?.init;
  4175. const abilityId = init?.ability || CONFIG.DND5E.initiativeAbility;
  4176. const data = this.getRollData();
  4177. const flags = this.flags.dnd5e || {};
  4178. if ( flags.initiativeAdv ) options.advantageMode ??= dnd5e.dice.D20Roll.ADV_MODE.ADVANTAGE;
  4179. // Standard initiative formula
  4180. const parts = ["1d20"];
  4181. // Special initiative bonuses
  4182. if ( init ) {
  4183. parts.push(init.mod);
  4184. if ( init.prof.term !== "0" ) {
  4185. parts.push("@prof");
  4186. data.prof = init.prof.term;
  4187. }
  4188. if ( init.bonus ) {
  4189. parts.push("@bonus");
  4190. data.bonus = Roll.replaceFormulaData(init.bonus, data);
  4191. }
  4192. }
  4193. // Ability check bonuses
  4194. if ( "abilities" in this.system ) {
  4195. const abilityBonus = this.system.abilities[abilityId]?.bonuses?.check;
  4196. if ( abilityBonus ) {
  4197. parts.push("@abilityBonus");
  4198. data.abilityBonus = Roll.replaceFormulaData(abilityBonus, data);
  4199. }
  4200. }
  4201. // Global check bonus
  4202. if ( "bonuses" in this.system ) {
  4203. const globalCheckBonus = this.system.bonuses.abilities?.check;
  4204. if ( globalCheckBonus ) {
  4205. parts.push("@globalBonus");
  4206. data.globalBonus = Roll.replaceFormulaData(globalCheckBonus, data);
  4207. }
  4208. }
  4209. // Alert feat
  4210. if ( flags.initiativeAlert ) {
  4211. parts.push("@alertBonus");
  4212. data.alertBonus = 5;
  4213. }
  4214. // Ability score tiebreaker
  4215. const tiebreaker = game.settings.get("dnd5e", "initiativeDexTiebreaker");
  4216. if ( tiebreaker && ("abilities" in this.system) ) {
  4217. const abilityValue = this.system.abilities[abilityId]?.value;
  4218. if ( Number.isNumeric(abilityValue) ) parts.push(String(abilityValue / 100));
  4219. }
  4220. options = foundry.utils.mergeObject({
  4221. flavor: options.flavor ?? game.i18n.localize("DND5E.Initiative"),
  4222. halflingLucky: flags.halflingLucky ?? false,
  4223. critical: null,
  4224. fumble: null
  4225. }, options);
  4226. // Create the d20 roll
  4227. const formula = parts.join(" + ");
  4228. return new CONFIG.Dice.D20Roll(formula, data, options);
  4229. }
  4230. /* -------------------------------------------- */
  4231. /**
  4232. * Roll initiative for this Actor with a dialog that provides an opportunity to elect advantage or other bonuses.
  4233. * @param {object} [rollOptions] Options forwarded to the Actor#getInitiativeRoll method
  4234. * @returns {Promise<void>} A promise which resolves once initiative has been rolled for the Actor
  4235. */
  4236. async rollInitiativeDialog(rollOptions={}) {
  4237. // Create and configure the Initiative roll
  4238. const roll = this.getInitiativeRoll(rollOptions);
  4239. const choice = await roll.configureDialog({
  4240. defaultRollMode: game.settings.get("core", "rollMode"),
  4241. title: `${game.i18n.localize("DND5E.InitiativeRoll")}: ${this.name}`,
  4242. chooseModifier: false,
  4243. defaultAction: rollOptions.advantageMode ?? dnd5e.dice.D20Roll.ADV_MODE.NORMAL
  4244. });
  4245. if ( choice === null ) return; // Closed dialog
  4246. // Temporarily cache the configured roll and use it to roll initiative for the Actor
  4247. this._cachedInitiativeRoll = roll;
  4248. await this.rollInitiative({createCombatants: true});
  4249. delete this._cachedInitiativeRoll;
  4250. }
  4251. /* -------------------------------------------- */
  4252. /** @inheritdoc */
  4253. async rollInitiative(options={}) {
  4254. /**
  4255. * A hook event that fires before initiative is rolled for an Actor.
  4256. * @function dnd5e.preRollInitiative
  4257. * @memberof hookEvents
  4258. * @param {Actor5e} actor The Actor that is rolling initiative.
  4259. * @param {D20Roll} roll The initiative roll.
  4260. */
  4261. if ( Hooks.call("dnd5e.preRollInitiative", this, this._cachedInitiativeRoll) === false ) return;
  4262. const combat = await super.rollInitiative(options);
  4263. const combatants = this.isToken ? this.getActiveTokens(false, true).reduce((arr, t) => {
  4264. const combatant = game.combat.getCombatantByToken(t.id);
  4265. if ( combatant ) arr.push(combatant);
  4266. return arr;
  4267. }, []) : [game.combat.getCombatantByActor(this.id)];
  4268. /**
  4269. * A hook event that fires after an Actor has rolled for initiative.
  4270. * @function dnd5e.rollInitiative
  4271. * @memberof hookEvents
  4272. * @param {Actor5e} actor The Actor that rolled initiative.
  4273. * @param {Combatant[]} combatants The associated Combatants in the Combat.
  4274. */
  4275. Hooks.callAll("dnd5e.rollInitiative", this, combatants);
  4276. return combat;
  4277. }
  4278. /* -------------------------------------------- */
  4279. /**
  4280. * Roll a hit die of the appropriate type, gaining hit points equal to the die roll plus your CON modifier.
  4281. * @param {string} [denomination] The hit denomination of hit die to roll. Example "d8".
  4282. * If no denomination is provided, the first available HD will be used
  4283. * @param {object} options Additional options which modify the roll.
  4284. * @returns {Promise<Roll|null>} The created Roll instance, or null if no hit die was rolled
  4285. */
  4286. async rollHitDie(denomination, options={}) {
  4287. // If no denomination was provided, choose the first available
  4288. let cls = null;
  4289. if ( !denomination ) {
  4290. cls = this.itemTypes.class.find(c => c.system.hitDiceUsed < c.system.levels);
  4291. if ( !cls ) return null;
  4292. denomination = cls.system.hitDice;
  4293. }
  4294. // Otherwise, locate a class (if any) which has an available hit die of the requested denomination
  4295. else cls = this.items.find(i => {
  4296. return (i.system.hitDice === denomination) && ((i.system.hitDiceUsed || 0) < (i.system.levels || 1));
  4297. });
  4298. // If no class is available, display an error notification
  4299. if ( !cls ) {
  4300. ui.notifications.error(game.i18n.format("DND5E.HitDiceWarn", {name: this.name, formula: denomination}));
  4301. return null;
  4302. }
  4303. // Prepare roll data
  4304. const flavor = game.i18n.localize("DND5E.HitDiceRoll");
  4305. const rollConfig = foundry.utils.mergeObject({
  4306. formula: `max(0, 1${denomination} + @abilities.con.mod)`,
  4307. data: this.getRollData(),
  4308. chatMessage: true,
  4309. messageData: {
  4310. speaker: ChatMessage.getSpeaker({actor: this}),
  4311. flavor,
  4312. title: `${flavor}: ${this.name}`,
  4313. rollMode: game.settings.get("core", "rollMode"),
  4314. "flags.dnd5e.roll": {type: "hitDie"}
  4315. }
  4316. }, options);
  4317. /**
  4318. * A hook event that fires before a hit die is rolled for an Actor.
  4319. * @function dnd5e.preRollHitDie
  4320. * @memberof hookEvents
  4321. * @param {Actor5e} actor Actor for which the hit die is to be rolled.
  4322. * @param {object} config Configuration data for the pending roll.
  4323. * @param {string} config.formula Formula that will be rolled.
  4324. * @param {object} config.data Data used when evaluating the roll.
  4325. * @param {boolean} config.chatMessage Should a chat message be created for this roll?
  4326. * @param {object} config.messageData Data used to create the chat message.
  4327. * @param {string} denomination Size of hit die to be rolled.
  4328. * @returns {boolean} Explicitly return `false` to prevent hit die from being rolled.
  4329. */
  4330. if ( Hooks.call("dnd5e.preRollHitDie", this, rollConfig, denomination) === false ) return;
  4331. const roll = await new Roll(rollConfig.formula, rollConfig.data).roll({async: true});
  4332. if ( rollConfig.chatMessage ) roll.toMessage(rollConfig.messageData);
  4333. const hp = this.system.attributes.hp;
  4334. const dhp = Math.min(Math.max(0, hp.max + (hp.tempmax ?? 0)) - hp.value, roll.total);
  4335. const updates = {
  4336. actor: {"system.attributes.hp.value": hp.value + dhp},
  4337. class: {"system.hitDiceUsed": cls.system.hitDiceUsed + 1}
  4338. };
  4339. /**
  4340. * A hook event that fires after a hit die has been rolled for an Actor, but before updates have been performed.
  4341. * @function dnd5e.rollHitDie
  4342. * @memberof hookEvents
  4343. * @param {Actor5e} actor Actor for which the hit die has been rolled.
  4344. * @param {Roll} roll The resulting roll.
  4345. * @param {object} updates
  4346. * @param {object} updates.actor Updates that will be applied to the actor.
  4347. * @param {object} updates.class Updates that will be applied to the class.
  4348. * @returns {boolean} Explicitly return `false` to prevent updates from being performed.
  4349. */
  4350. if ( Hooks.call("dnd5e.rollHitDie", this, roll, updates) === false ) return roll;
  4351. // Re-evaluate dhp in the event that it was changed in the previous hook
  4352. const updateOptions = { dhp: (updates.actor?.["system.attributes.hp.value"] ?? hp.value) - hp.value };
  4353. // Perform updates
  4354. if ( !foundry.utils.isEmpty(updates.actor) ) await this.update(updates.actor, updateOptions);
  4355. if ( !foundry.utils.isEmpty(updates.class) ) await cls.update(updates.class);
  4356. return roll;
  4357. }
  4358. /* -------------------------------------------- */
  4359. /**
  4360. * Roll hit points for a specific class as part of a level-up workflow.
  4361. * @param {Item5e} item The class item whose hit dice to roll.
  4362. * @param {object} options
  4363. * @param {boolean} [options.chatMessage=true] Display the chat message for this roll.
  4364. * @returns {Promise<Roll>} The completed roll.
  4365. * @see {@link dnd5e.preRollClassHitPoints}
  4366. */
  4367. async rollClassHitPoints(item, { chatMessage=true }={}) {
  4368. if ( item.type !== "class" ) throw new Error("Hit points can only be rolled for a class item.");
  4369. const rollData = {
  4370. formula: `1${item.system.hitDice}`,
  4371. data: item.getRollData(),
  4372. chatMessage
  4373. };
  4374. const flavor = game.i18n.format("DND5E.AdvancementHitPointsRollMessage", { class: item.name });
  4375. const messageData = {
  4376. title: `${flavor}: ${this.name}`,
  4377. flavor,
  4378. speaker: ChatMessage.getSpeaker({ actor: this }),
  4379. "flags.dnd5e.roll": { type: "hitPoints" }
  4380. };
  4381. /**
  4382. * A hook event that fires before hit points are rolled for a character's class.
  4383. * @function dnd5e.preRollClassHitPoints
  4384. * @memberof hookEvents
  4385. * @param {Actor5e} actor Actor for which the hit points are being rolled.
  4386. * @param {Item5e} item The class item whose hit dice will be rolled.
  4387. * @param {object} rollData
  4388. * @param {string} rollData.formula The string formula to parse.
  4389. * @param {object} rollData.data The data object against which to parse attributes within the formula.
  4390. * @param {object} messageData The data object to use when creating the message.
  4391. */
  4392. Hooks.callAll("dnd5e.preRollClassHitPoints", this, item, rollData, messageData);
  4393. const roll = new Roll(rollData.formula, rollData.data);
  4394. await roll.evaluate({async: true});
  4395. /**
  4396. * A hook event that fires after hit points haven been rolled for a character's class.
  4397. * @function dnd5e.rollClassHitPoints
  4398. * @memberof hookEvents
  4399. * @param {Actor5e} actor Actor for which the hit points have been rolled.
  4400. * @param {Roll} roll The resulting roll.
  4401. */
  4402. Hooks.callAll("dnd5e.rollClassHitPoints", this, roll);
  4403. if ( rollData.chatMessage ) await roll.toMessage(messageData);
  4404. return roll;
  4405. }
  4406. /* -------------------------------------------- */
  4407. /**
  4408. * Roll hit points for an NPC based on the HP formula.
  4409. * @param {object} options
  4410. * @param {boolean} [options.chatMessage=true] Display the chat message for this roll.
  4411. * @returns {Promise<Roll>} The completed roll.
  4412. * @see {@link dnd5e.preRollNPCHitPoints}
  4413. */
  4414. async rollNPCHitPoints({ chatMessage=true }={}) {
  4415. if ( this.type !== "npc" ) throw new Error("NPC hit points can only be rolled for NPCs");
  4416. const rollData = {
  4417. formula: this.system.attributes.hp.formula,
  4418. data: this.getRollData(),
  4419. chatMessage
  4420. };
  4421. const flavor = game.i18n.format("DND5E.HPFormulaRollMessage");
  4422. const messageData = {
  4423. title: `${flavor}: ${this.name}`,
  4424. flavor,
  4425. speaker: ChatMessage.getSpeaker({ actor: this }),
  4426. "flags.dnd5e.roll": { type: "hitPoints" }
  4427. };
  4428. /**
  4429. * A hook event that fires before hit points are rolled for an NPC.
  4430. * @function dnd5e.preRollNPCHitPoints
  4431. * @memberof hookEvents
  4432. * @param {Actor5e} actor Actor for which the hit points are being rolled.
  4433. * @param {object} rollData
  4434. * @param {string} rollData.formula The string formula to parse.
  4435. * @param {object} rollData.data The data object against which to parse attributes within the formula.
  4436. * @param {object} messageData The data object to use when creating the message.
  4437. */
  4438. Hooks.callAll("dnd5e.preRollNPCHitPoints", this, rollData, messageData);
  4439. const roll = new Roll(rollData.formula, rollData.data);
  4440. await roll.evaluate({async: true});
  4441. /**
  4442. * A hook event that fires after hit points are rolled for an NPC.
  4443. * @function dnd5e.rollNPCHitPoints
  4444. * @memberof hookEvents
  4445. * @param {Actor5e} actor Actor for which the hit points have been rolled.
  4446. * @param {Roll} roll The resulting roll.
  4447. */
  4448. Hooks.callAll("dnd5e.rollNPCHitPoints", this, roll);
  4449. if ( rollData.chatMessage ) await roll.toMessage(messageData);
  4450. return roll;
  4451. }
  4452. /* -------------------------------------------- */
  4453. /* Resting */
  4454. /* -------------------------------------------- */
  4455. /**
  4456. * Configuration options for a rest.
  4457. *
  4458. * @typedef {object} RestConfiguration
  4459. * @property {boolean} dialog Present a dialog window which allows for rolling hit dice as part of the
  4460. * Short Rest and selecting whether a new day has occurred.
  4461. * @property {boolean} chat Should a chat message be created to summarize the results of the rest?
  4462. * @property {boolean} newDay Does this rest carry over to a new day?
  4463. * @property {boolean} [autoHD] Should hit dice be spent automatically during a short rest?
  4464. * @property {number} [autoHDThreshold] How many hit points should be missing before hit dice are
  4465. * automatically spent during a short rest.
  4466. */
  4467. /**
  4468. * Results from a rest operation.
  4469. *
  4470. * @typedef {object} RestResult
  4471. * @property {number} dhp Hit points recovered during the rest.
  4472. * @property {number} dhd Hit dice recovered or spent during the rest.
  4473. * @property {object} updateData Updates applied to the actor.
  4474. * @property {object[]} updateItems Updates applied to actor's items.
  4475. * @property {boolean} longRest Whether the rest type was a long rest.
  4476. * @property {boolean} newDay Whether a new day occurred during the rest.
  4477. * @property {Roll[]} rolls Any rolls that occurred during the rest process, not including hit dice.
  4478. */
  4479. /* -------------------------------------------- */
  4480. /**
  4481. * Take a short rest, possibly spending hit dice and recovering resources, item uses, and pact slots.
  4482. * @param {RestConfiguration} [config] Configuration options for a short rest.
  4483. * @returns {Promise<RestResult>} A Promise which resolves once the short rest workflow has completed.
  4484. */
  4485. async shortRest(config={}) {
  4486. config = foundry.utils.mergeObject({
  4487. dialog: true, chat: true, newDay: false, autoHD: false, autoHDThreshold: 3
  4488. }, config);
  4489. /**
  4490. * A hook event that fires before a short rest is started.
  4491. * @function dnd5e.preShortRest
  4492. * @memberof hookEvents
  4493. * @param {Actor5e} actor The actor that is being rested.
  4494. * @param {RestConfiguration} config Configuration options for the rest.
  4495. * @returns {boolean} Explicitly return `false` to prevent the rest from being started.
  4496. */
  4497. if ( Hooks.call("dnd5e.preShortRest", this, config) === false ) return;
  4498. // Take note of the initial hit points and number of hit dice the Actor has
  4499. const hd0 = this.system.attributes.hd;
  4500. const hp0 = this.system.attributes.hp.value;
  4501. // Display a Dialog for rolling hit dice
  4502. if ( config.dialog ) {
  4503. try { config.newDay = await ShortRestDialog.shortRestDialog({actor: this, canRoll: hd0 > 0});
  4504. } catch(err) { return; }
  4505. }
  4506. // Automatically spend hit dice
  4507. else if ( config.autoHD ) await this.autoSpendHitDice({ threshold: config.autoHDThreshold });
  4508. // Return the rest result
  4509. const dhd = this.system.attributes.hd - hd0;
  4510. const dhp = this.system.attributes.hp.value - hp0;
  4511. return this._rest(config.chat, config.newDay, false, dhd, dhp);
  4512. }
  4513. /* -------------------------------------------- */
  4514. /**
  4515. * Take a long rest, recovering hit points, hit dice, resources, item uses, and spell slots.
  4516. * @param {RestConfiguration} [config] Configuration options for a long rest.
  4517. * @returns {Promise<RestResult>} A Promise which resolves once the long rest workflow has completed.
  4518. */
  4519. async longRest(config={}) {
  4520. config = foundry.utils.mergeObject({
  4521. dialog: true, chat: true, newDay: true
  4522. }, config);
  4523. /**
  4524. * A hook event that fires before a long rest is started.
  4525. * @function dnd5e.preLongRest
  4526. * @memberof hookEvents
  4527. * @param {Actor5e} actor The actor that is being rested.
  4528. * @param {RestConfiguration} config Configuration options for the rest.
  4529. * @returns {boolean} Explicitly return `false` to prevent the rest from being started.
  4530. */
  4531. if ( Hooks.call("dnd5e.preLongRest", this, config) === false ) return;
  4532. if ( config.dialog ) {
  4533. try { config.newDay = await LongRestDialog.longRestDialog({actor: this}); }
  4534. catch(err) { return; }
  4535. }
  4536. return this._rest(config.chat, config.newDay, true);
  4537. }
  4538. /* -------------------------------------------- */
  4539. /**
  4540. * Perform all of the changes needed for a short or long rest.
  4541. *
  4542. * @param {boolean} chat Summarize the results of the rest workflow as a chat message.
  4543. * @param {boolean} newDay Has a new day occurred during this rest?
  4544. * @param {boolean} longRest Is this a long rest?
  4545. * @param {number} [dhd=0] Number of hit dice spent during so far during the rest.
  4546. * @param {number} [dhp=0] Number of hit points recovered so far during the rest.
  4547. * @returns {Promise<RestResult>} Consolidated results of the rest workflow.
  4548. * @private
  4549. */
  4550. async _rest(chat, newDay, longRest, dhd=0, dhp=0) {
  4551. let hitPointsRecovered = 0;
  4552. let hitPointUpdates = {};
  4553. let hitDiceRecovered = 0;
  4554. let hitDiceUpdates = [];
  4555. const rolls = [];
  4556. // Recover hit points & hit dice on long rest
  4557. if ( longRest ) {
  4558. ({ updates: hitPointUpdates, hitPointsRecovered } = this._getRestHitPointRecovery());
  4559. ({ updates: hitDiceUpdates, hitDiceRecovered } = this._getRestHitDiceRecovery());
  4560. }
  4561. // Figure out the rest of the changes
  4562. const result = {
  4563. dhd: dhd + hitDiceRecovered,
  4564. dhp: dhp + hitPointsRecovered,
  4565. updateData: {
  4566. ...hitPointUpdates,
  4567. ...this._getRestResourceRecovery({ recoverShortRestResources: !longRest, recoverLongRestResources: longRest }),
  4568. ...this._getRestSpellRecovery({ recoverSpells: longRest })
  4569. },
  4570. updateItems: [
  4571. ...hitDiceUpdates,
  4572. ...(await this._getRestItemUsesRecovery({ recoverLongRestUses: longRest, recoverDailyUses: newDay, rolls }))
  4573. ],
  4574. longRest,
  4575. newDay
  4576. };
  4577. result.rolls = rolls;
  4578. /**
  4579. * A hook event that fires after rest result is calculated, but before any updates are performed.
  4580. * @function dnd5e.preRestCompleted
  4581. * @memberof hookEvents
  4582. * @param {Actor5e} actor The actor that is being rested.
  4583. * @param {RestResult} result Details on the rest to be completed.
  4584. * @returns {boolean} Explicitly return `false` to prevent the rest updates from being performed.
  4585. */
  4586. if ( Hooks.call("dnd5e.preRestCompleted", this, result) === false ) return result;
  4587. // Perform updates
  4588. await this.update(result.updateData);
  4589. await this.updateEmbeddedDocuments("Item", result.updateItems);
  4590. // Display a Chat Message summarizing the rest effects
  4591. if ( chat ) await this._displayRestResultMessage(result, longRest);
  4592. /**
  4593. * A hook event that fires when the rest process is completed for an actor.
  4594. * @function dnd5e.restCompleted
  4595. * @memberof hookEvents
  4596. * @param {Actor5e} actor The actor that just completed resting.
  4597. * @param {RestResult} result Details on the rest completed.
  4598. */
  4599. Hooks.callAll("dnd5e.restCompleted", this, result);
  4600. // Return data summarizing the rest effects
  4601. return result;
  4602. }
  4603. /* -------------------------------------------- */
  4604. /**
  4605. * Display a chat message with the result of a rest.
  4606. *
  4607. * @param {RestResult} result Result of the rest operation.
  4608. * @param {boolean} [longRest=false] Is this a long rest?
  4609. * @returns {Promise<ChatMessage>} Chat message that was created.
  4610. * @protected
  4611. */
  4612. async _displayRestResultMessage(result, longRest=false) {
  4613. const { dhd, dhp, newDay } = result;
  4614. const diceRestored = dhd !== 0;
  4615. const healthRestored = dhp !== 0;
  4616. const length = longRest ? "Long" : "Short";
  4617. // Summarize the rest duration
  4618. let restFlavor;
  4619. switch (game.settings.get("dnd5e", "restVariant")) {
  4620. case "normal":
  4621. restFlavor = (longRest && newDay) ? "DND5E.LongRestOvernight" : `DND5E.${length}RestNormal`;
  4622. break;
  4623. case "gritty":
  4624. restFlavor = (!longRest && newDay) ? "DND5E.ShortRestOvernight" : `DND5E.${length}RestGritty`;
  4625. break;
  4626. case "epic":
  4627. restFlavor = `DND5E.${length}RestEpic`;
  4628. break;
  4629. }
  4630. // Determine the chat message to display
  4631. let message;
  4632. if ( diceRestored && healthRestored ) message = `DND5E.${length}RestResult`;
  4633. else if ( longRest && !diceRestored && healthRestored ) message = "DND5E.LongRestResultHitPoints";
  4634. else if ( longRest && diceRestored && !healthRestored ) message = "DND5E.LongRestResultHitDice";
  4635. else message = `DND5E.${length}RestResultShort`;
  4636. // Create a chat message
  4637. let chatData = {
  4638. user: game.user.id,
  4639. speaker: {actor: this, alias: this.name},
  4640. flavor: game.i18n.localize(restFlavor),
  4641. rolls: result.rolls,
  4642. content: game.i18n.format(message, {
  4643. name: this.name,
  4644. dice: longRest ? dhd : -dhd,
  4645. health: dhp
  4646. })
  4647. };
  4648. ChatMessage.applyRollMode(chatData, game.settings.get("core", "rollMode"));
  4649. return ChatMessage.create(chatData);
  4650. }
  4651. /* -------------------------------------------- */
  4652. /**
  4653. * Automatically spend hit dice to recover hit points up to a certain threshold.
  4654. * @param {object} [options]
  4655. * @param {number} [options.threshold=3] A number of missing hit points which would trigger an automatic HD roll.
  4656. * @returns {Promise<number>} Number of hit dice spent.
  4657. */
  4658. async autoSpendHitDice({ threshold=3 }={}) {
  4659. const hp = this.system.attributes.hp;
  4660. const max = Math.max(0, hp.max + hp.tempmax);
  4661. let diceRolled = 0;
  4662. while ( (this.system.attributes.hp.value + threshold) <= max ) {
  4663. const r = await this.rollHitDie();
  4664. if ( r === null ) break;
  4665. diceRolled += 1;
  4666. }
  4667. return diceRolled;
  4668. }
  4669. /* -------------------------------------------- */
  4670. /**
  4671. * Recovers actor hit points and eliminates any temp HP.
  4672. * @param {object} [options]
  4673. * @param {boolean} [options.recoverTemp=true] Reset temp HP to zero.
  4674. * @param {boolean} [options.recoverTempMax=true] Reset temp max HP to zero.
  4675. * @returns {object} Updates to the actor and change in hit points.
  4676. * @protected
  4677. */
  4678. _getRestHitPointRecovery({recoverTemp=true, recoverTempMax=true}={}) {
  4679. const hp = this.system.attributes.hp;
  4680. let max = hp.max;
  4681. let updates = {};
  4682. if ( recoverTempMax ) updates["system.attributes.hp.tempmax"] = 0;
  4683. else max = Math.max(0, max + (hp.tempmax || 0));
  4684. updates["system.attributes.hp.value"] = max;
  4685. if ( recoverTemp ) updates["system.attributes.hp.temp"] = 0;
  4686. return { updates, hitPointsRecovered: max - hp.value };
  4687. }
  4688. /* -------------------------------------------- */
  4689. /**
  4690. * Recovers actor resources.
  4691. * @param {object} [options]
  4692. * @param {boolean} [options.recoverShortRestResources=true] Recover resources that recharge on a short rest.
  4693. * @param {boolean} [options.recoverLongRestResources=true] Recover resources that recharge on a long rest.
  4694. * @returns {object} Updates to the actor.
  4695. * @protected
  4696. */
  4697. _getRestResourceRecovery({recoverShortRestResources=true, recoverLongRestResources=true}={}) {
  4698. let updates = {};
  4699. for ( let [k, r] of Object.entries(this.system.resources) ) {
  4700. if ( Number.isNumeric(r.max) && ((recoverShortRestResources && r.sr) || (recoverLongRestResources && r.lr)) ) {
  4701. updates[`system.resources.${k}.value`] = Number(r.max);
  4702. }
  4703. }
  4704. return updates;
  4705. }
  4706. /* -------------------------------------------- */
  4707. /**
  4708. * Recovers spell slots and pact slots.
  4709. * @param {object} [options]
  4710. * @param {boolean} [options.recoverPact=true] Recover all expended pact slots.
  4711. * @param {boolean} [options.recoverSpells=true] Recover all expended spell slots.
  4712. * @returns {object} Updates to the actor.
  4713. * @protected
  4714. */
  4715. _getRestSpellRecovery({recoverPact=true, recoverSpells=true}={}) {
  4716. const spells = this.system.spells;
  4717. let updates = {};
  4718. if ( recoverPact ) {
  4719. const pact = spells.pact;
  4720. updates["system.spells.pact.value"] = pact.override || pact.max;
  4721. }
  4722. if ( recoverSpells ) {
  4723. for ( let [k, v] of Object.entries(spells) ) {
  4724. updates[`system.spells.${k}.value`] = Number.isNumeric(v.override) ? v.override : (v.max ?? 0);
  4725. }
  4726. }
  4727. return updates;
  4728. }
  4729. /* -------------------------------------------- */
  4730. /**
  4731. * Recovers class hit dice during a long rest.
  4732. *
  4733. * @param {object} [options]
  4734. * @param {number} [options.maxHitDice] Maximum number of hit dice to recover.
  4735. * @returns {object} Array of item updates and number of hit dice recovered.
  4736. * @protected
  4737. */
  4738. _getRestHitDiceRecovery({maxHitDice}={}) {
  4739. // Determine the number of hit dice which may be recovered
  4740. if ( maxHitDice === undefined ) maxHitDice = Math.max(Math.floor(this.system.details.level / 2), 1);
  4741. // Sort classes which can recover HD, assuming players prefer recovering larger HD first.
  4742. const sortedClasses = Object.values(this.classes).sort((a, b) => {
  4743. return (parseInt(b.system.hitDice.slice(1)) || 0) - (parseInt(a.system.hitDice.slice(1)) || 0);
  4744. });
  4745. // Update hit dice usage
  4746. let updates = [];
  4747. let hitDiceRecovered = 0;
  4748. for ( let item of sortedClasses ) {
  4749. const hitDiceUsed = item.system.hitDiceUsed;
  4750. if ( (hitDiceRecovered < maxHitDice) && (hitDiceUsed > 0) ) {
  4751. let delta = Math.min(hitDiceUsed || 0, maxHitDice - hitDiceRecovered);
  4752. hitDiceRecovered += delta;
  4753. updates.push({_id: item.id, "system.hitDiceUsed": hitDiceUsed - delta});
  4754. }
  4755. }
  4756. return { updates, hitDiceRecovered };
  4757. }
  4758. /* -------------------------------------------- */
  4759. /**
  4760. * Recovers item uses during short or long rests.
  4761. * @param {object} [options]
  4762. * @param {boolean} [options.recoverShortRestUses=true] Recover uses for items that recharge after a short rest.
  4763. * @param {boolean} [options.recoverLongRestUses=true] Recover uses for items that recharge after a long rest.
  4764. * @param {boolean} [options.recoverDailyUses=true] Recover uses for items that recharge on a new day.
  4765. * @param {Roll[]} [options.rolls] Rolls that have been performed as part of this rest.
  4766. * @returns {Promise<object[]>} Array of item updates.
  4767. * @protected
  4768. */
  4769. async _getRestItemUsesRecovery({recoverShortRestUses=true, recoverLongRestUses=true,
  4770. recoverDailyUses=true, rolls}={}) {
  4771. let recovery = [];
  4772. if ( recoverShortRestUses ) recovery.push("sr");
  4773. if ( recoverLongRestUses ) recovery.push("lr");
  4774. if ( recoverDailyUses ) recovery.push("day");
  4775. let updates = [];
  4776. for ( let item of this.items ) {
  4777. const uses = item.system.uses;
  4778. if ( recovery.includes(uses?.per) ) {
  4779. updates.push({_id: item.id, "system.uses.value": uses.max});
  4780. }
  4781. if ( recoverLongRestUses && item.system.recharge?.value ) {
  4782. updates.push({_id: item.id, "system.recharge.charged": true});
  4783. }
  4784. // Items that roll to gain charges on a new day
  4785. if ( recoverDailyUses && uses?.recovery && (uses?.per === "charges") ) {
  4786. const roll = new Roll(uses.recovery, item.getRollData());
  4787. if ( recoverLongRestUses && (game.settings.get("dnd5e", "restVariant") === "gritty") ) {
  4788. roll.alter(7, 0, {multiplyNumeric: true});
  4789. }
  4790. let total = 0;
  4791. try {
  4792. total = (await roll.evaluate({async: true})).total;
  4793. } catch(err) {
  4794. ui.notifications.warn(game.i18n.format("DND5E.ItemRecoveryFormulaWarning", {
  4795. name: item.name,
  4796. formula: uses.recovery
  4797. }));
  4798. }
  4799. const newValue = Math.clamped(uses.value + total, 0, uses.max);
  4800. if ( newValue !== uses.value ) {
  4801. const diff = newValue - uses.value;
  4802. const isMax = newValue === uses.max;
  4803. const locKey = `DND5E.Item${diff < 0 ? "Loss" : "Recovery"}Roll${isMax ? "Max" : ""}`;
  4804. updates.push({_id: item.id, "system.uses.value": newValue});
  4805. rolls.push(roll);
  4806. await roll.toMessage({
  4807. user: game.user.id,
  4808. speaker: {actor: this, alias: this.name},
  4809. flavor: game.i18n.format(locKey, {name: item.name, count: Math.abs(diff)})
  4810. });
  4811. }
  4812. }
  4813. }
  4814. return updates;
  4815. }
  4816. /* -------------------------------------------- */
  4817. /* Conversion & Transformation */
  4818. /* -------------------------------------------- */
  4819. /**
  4820. * Convert all carried currency to the highest possible denomination using configured conversion rates.
  4821. * See CONFIG.DND5E.currencies for configuration.
  4822. * @returns {Promise<Actor5e>}
  4823. */
  4824. convertCurrency() {
  4825. const currency = foundry.utils.deepClone(this.system.currency);
  4826. const currencies = Object.entries(CONFIG.DND5E.currencies);
  4827. currencies.sort((a, b) => a[1].conversion - b[1].conversion);
  4828. // Count total converted units of the base currency
  4829. let basis = currencies.reduce((change, [denomination, config]) => {
  4830. if ( !config.conversion ) return change;
  4831. return change + (currency[denomination] / config.conversion);
  4832. }, 0);
  4833. // Convert base units into the highest denomination possible
  4834. for ( const [denomination, config] of currencies) {
  4835. if ( !config.conversion ) continue;
  4836. const amount = Math.floor(basis * config.conversion);
  4837. currency[denomination] = amount;
  4838. basis -= (amount / config.conversion);
  4839. }
  4840. // Save the updated currency object
  4841. return this.update({"system.currency": currency});
  4842. }
  4843. /* -------------------------------------------- */
  4844. /**
  4845. * Options that determine what properties of the original actor are kept and which are replaced with
  4846. * the target actor.
  4847. *
  4848. * @typedef {object} TransformationOptions
  4849. * @property {boolean} [keepPhysical=false] Keep physical abilities (str, dex, con)
  4850. * @property {boolean} [keepMental=false] Keep mental abilities (int, wis, cha)
  4851. * @property {boolean} [keepSaves=false] Keep saving throw proficiencies
  4852. * @property {boolean} [keepSkills=false] Keep skill proficiencies
  4853. * @property {boolean} [mergeSaves=false] Take the maximum of the save proficiencies
  4854. * @property {boolean} [mergeSkills=false] Take the maximum of the skill proficiencies
  4855. * @property {boolean} [keepClass=false] Keep proficiency bonus
  4856. * @property {boolean} [keepFeats=false] Keep features
  4857. * @property {boolean} [keepSpells=false] Keep spells and spellcasting ability
  4858. * @property {boolean} [keepItems=false] Keep items
  4859. * @property {boolean} [keepBio=false] Keep biography
  4860. * @property {boolean} [keepVision=false] Keep vision
  4861. * @property {boolean} [keepSelf=false] Keep self
  4862. * @property {boolean} [keepAE=false] Keep all effects
  4863. * @property {boolean} [keepOriginAE=true] Keep effects which originate on this actor
  4864. * @property {boolean} [keepOtherOriginAE=true] Keep effects which originate on another actor
  4865. * @property {boolean} [keepSpellAE=true] Keep effects which originate from actors spells
  4866. * @property {boolean} [keepFeatAE=true] Keep effects which originate from actors features
  4867. * @property {boolean} [keepEquipmentAE=true] Keep effects which originate on actors equipment
  4868. * @property {boolean} [keepClassAE=true] Keep effects which originate from actors class/subclass
  4869. * @property {boolean} [keepBackgroundAE=true] Keep effects which originate from actors background
  4870. * @property {boolean} [transformTokens=true] Transform linked tokens too
  4871. */
  4872. /**
  4873. * Transform this Actor into another one.
  4874. *
  4875. * @param {Actor5e} target The target Actor.
  4876. * @param {TransformationOptions} [options={}] Options that determine how the transformation is performed.
  4877. * @param {boolean} [options.renderSheet=true] Render the sheet of the transformed actor after the polymorph
  4878. * @returns {Promise<Array<Token>>|null} Updated token if the transformation was performed.
  4879. */
  4880. async transformInto(target, { keepPhysical=false, keepMental=false, keepSaves=false, keepSkills=false,
  4881. mergeSaves=false, mergeSkills=false, keepClass=false, keepFeats=false, keepSpells=false, keepItems=false,
  4882. keepBio=false, keepVision=false, keepSelf=false, keepAE=false, keepOriginAE=true, keepOtherOriginAE=true,
  4883. keepSpellAE=true, keepEquipmentAE=true, keepFeatAE=true, keepClassAE=true, keepBackgroundAE=true,
  4884. transformTokens=true}={}, {renderSheet=true}={}) {
  4885. // Ensure the player is allowed to polymorph
  4886. const allowed = game.settings.get("dnd5e", "allowPolymorphing");
  4887. if ( !allowed && !game.user.isGM ) {
  4888. return ui.notifications.warn(game.i18n.localize("DND5E.PolymorphWarn"));
  4889. }
  4890. // Get the original Actor data and the new source data
  4891. const o = this.toObject();
  4892. o.flags.dnd5e = o.flags.dnd5e || {};
  4893. o.flags.dnd5e.transformOptions = {mergeSkills, mergeSaves};
  4894. const source = target.toObject();
  4895. if ( keepSelf ) {
  4896. o.img = source.img;
  4897. o.name = `${o.name} (${game.i18n.localize("DND5E.PolymorphSelf")})`;
  4898. }
  4899. // Prepare new data to merge from the source
  4900. const d = foundry.utils.mergeObject(foundry.utils.deepClone({
  4901. type: o.type, // Remain the same actor type
  4902. name: `${o.name} (${source.name})`, // Append the new shape to your old name
  4903. system: source.system, // Get the systemdata model of your new form
  4904. items: source.items, // Get the items of your new form
  4905. effects: o.effects.concat(source.effects), // Combine active effects from both forms
  4906. img: source.img, // New appearance
  4907. ownership: o.ownership, // Use the original actor permissions
  4908. folder: o.folder, // Be displayed in the same sidebar folder
  4909. flags: o.flags, // Use the original actor flags
  4910. prototypeToken: { name: `${o.name} (${source.name})`, texture: {}, sight: {}, detectionModes: [] } // Set a new empty token
  4911. }), keepSelf ? o : {}); // Keeps most of original actor
  4912. // Specifically delete some data attributes
  4913. delete d.system.resources; // Don't change your resource pools
  4914. delete d.system.currency; // Don't lose currency
  4915. delete d.system.bonuses; // Don't lose global bonuses
  4916. if ( keepSpells ) delete d.system.attributes.spellcasting; // Keep spellcasting ability if retaining spells.
  4917. // Specific additional adjustments
  4918. d.system.details.alignment = o.system.details.alignment; // Don't change alignment
  4919. d.system.attributes.exhaustion = o.system.attributes.exhaustion; // Keep your prior exhaustion level
  4920. d.system.attributes.inspiration = o.system.attributes.inspiration; // Keep inspiration
  4921. d.system.spells = o.system.spells; // Keep spell slots
  4922. d.system.attributes.ac.flat = target.system.attributes.ac.value; // Override AC
  4923. // Token appearance updates
  4924. for ( const k of ["width", "height", "alpha", "lockRotation"] ) {
  4925. d.prototypeToken[k] = source.prototypeToken[k];
  4926. }
  4927. for ( const k of ["offsetX", "offsetY", "scaleX", "scaleY", "src", "tint"] ) {
  4928. d.prototypeToken.texture[k] = source.prototypeToken.texture[k];
  4929. }
  4930. for ( const k of ["bar1", "bar2", "displayBars", "displayName", "disposition", "rotation", "elevation"] ) {
  4931. d.prototypeToken[k] = o.prototypeToken[k];
  4932. }
  4933. if ( !keepSelf ) {
  4934. const sightSource = keepVision ? o.prototypeToken : source.prototypeToken;
  4935. for ( const k of ["range", "angle", "visionMode", "color", "attenuation", "brightness", "saturation", "contrast", "enabled"] ) {
  4936. d.prototypeToken.sight[k] = sightSource.sight[k];
  4937. }
  4938. d.prototypeToken.detectionModes = sightSource.detectionModes;
  4939. // Transfer ability scores
  4940. const abilities = d.system.abilities;
  4941. for ( let k of Object.keys(abilities) ) {
  4942. const oa = o.system.abilities[k];
  4943. const prof = abilities[k].proficient;
  4944. const type = CONFIG.DND5E.abilities[k]?.type;
  4945. if ( keepPhysical && (type === "physical") ) abilities[k] = oa;
  4946. else if ( keepMental && (type === "mental") ) abilities[k] = oa;
  4947. // Set saving throw proficiencies.
  4948. if ( keepSaves ) abilities[k].proficient = oa.proficient;
  4949. else if ( mergeSaves ) abilities[k].proficient = Math.max(prof, oa.proficient);
  4950. else abilities[k].proficient = source.system.abilities[k].proficient;
  4951. }
  4952. // Transfer skills
  4953. if ( keepSkills ) d.system.skills = o.system.skills;
  4954. else if ( mergeSkills ) {
  4955. for ( let [k, s] of Object.entries(d.system.skills) ) {
  4956. s.value = Math.max(s.value, o.system.skills[k].value);
  4957. }
  4958. }
  4959. // Keep specific items from the original data
  4960. d.items = d.items.concat(o.items.filter(i => {
  4961. if ( ["class", "subclass"].includes(i.type) ) return keepClass;
  4962. else if ( i.type === "feat" ) return keepFeats;
  4963. else if ( i.type === "spell" ) return keepSpells;
  4964. else return keepItems;
  4965. }));
  4966. // Transfer classes for NPCs
  4967. if ( !keepClass && d.system.details.cr ) {
  4968. const cls = new dnd5e.dataModels.item.ClassData({levels: d.system.details.cr});
  4969. d.items.push({
  4970. type: "class",
  4971. name: game.i18n.localize("DND5E.PolymorphTmpClass"),
  4972. system: cls.toObject()
  4973. });
  4974. }
  4975. // Keep biography
  4976. if ( keepBio ) d.system.details.biography = o.system.details.biography;
  4977. // Keep senses
  4978. if ( keepVision ) d.system.traits.senses = o.system.traits.senses;
  4979. // Remove active effects
  4980. const oEffects = foundry.utils.deepClone(d.effects);
  4981. const originEffectIds = new Set(oEffects.filter(effect => {
  4982. return !effect.origin || effect.origin === this.uuid;
  4983. }).map(e => e._id));
  4984. d.effects = d.effects.filter(e => {
  4985. if ( keepAE ) return true;
  4986. const origin = e.origin?.startsWith("Actor") || e.origin?.startsWith("Item") ? fromUuidSync(e.origin) : {};
  4987. const originIsSelf = origin?.parent?.uuid === this.uuid;
  4988. const isOriginEffect = originEffectIds.has(e._id);
  4989. if ( isOriginEffect ) return keepOriginAE;
  4990. if ( !isOriginEffect && !originIsSelf ) return keepOtherOriginAE;
  4991. if ( origin.type === "spell" ) return keepSpellAE;
  4992. if ( origin.type === "feat" ) return keepFeatAE;
  4993. if ( origin.type === "background" ) return keepBackgroundAE;
  4994. if ( ["subclass", "class"].includes(origin.type) ) return keepClassAE;
  4995. if ( ["equipment", "weapon", "tool", "loot", "backpack"].includes(origin.type) ) return keepEquipmentAE;
  4996. return true;
  4997. });
  4998. }
  4999. // Set a random image if source is configured that way
  5000. if ( source.prototypeToken.randomImg ) {
  5001. const images = await target.getTokenImages();
  5002. d.prototypeToken.texture.src = images[Math.floor(Math.random() * images.length)];
  5003. }
  5004. // Set new data flags
  5005. if ( !this.isPolymorphed || !d.flags.dnd5e.originalActor ) d.flags.dnd5e.originalActor = this.id;
  5006. d.flags.dnd5e.isPolymorphed = true;
  5007. // Gather previous actor data
  5008. const previousActorIds = this.getFlag("dnd5e", "previousActorIds") || [];
  5009. previousActorIds.push(this._id);
  5010. foundry.utils.setProperty(d.flags, "dnd5e.previousActorIds", previousActorIds);
  5011. // Update unlinked Tokens, and grab a copy of any actorData adjustments to re-apply
  5012. if ( this.isToken ) {
  5013. const tokenData = d.prototypeToken;
  5014. delete d.prototypeToken;
  5015. let previousActorData;
  5016. if ( game.dnd5e.isV10 ) {
  5017. tokenData.actorData = d;
  5018. previousActorData = this.token.toObject().actorData;
  5019. } else {
  5020. tokenData.delta = d;
  5021. previousActorData = this.token.delta.toObject();
  5022. }
  5023. foundry.utils.setProperty(tokenData, "flags.dnd5e.previousActorData", previousActorData);
  5024. await this.sheet?.close();
  5025. const update = await this.token.update(tokenData);
  5026. if ( renderSheet ) this.sheet?.render(true);
  5027. return update;
  5028. }
  5029. // Close sheet for non-transformed Actor
  5030. await this.sheet?.close();
  5031. /**
  5032. * A hook event that fires just before the actor is transformed.
  5033. * @function dnd5e.transformActor
  5034. * @memberof hookEvents
  5035. * @param {Actor5e} actor The original actor before transformation.
  5036. * @param {Actor5e} target The target actor into which to transform.
  5037. * @param {object} data The data that will be used to create the new transformed actor.
  5038. * @param {TransformationOptions} options Options that determine how the transformation is performed.
  5039. * @param {object} [options]
  5040. */
  5041. Hooks.callAll("dnd5e.transformActor", this, target, d, {
  5042. keepPhysical, keepMental, keepSaves, keepSkills, mergeSaves, mergeSkills, keepClass, keepFeats, keepSpells,
  5043. keepItems, keepBio, keepVision, keepSelf, keepAE, keepOriginAE, keepOtherOriginAE, keepSpellAE,
  5044. keepEquipmentAE, keepFeatAE, keepClassAE, keepBackgroundAE, transformTokens
  5045. }, {renderSheet});
  5046. // Create new Actor with transformed data
  5047. const newActor = await this.constructor.create(d, {renderSheet});
  5048. // Update placed Token instances
  5049. if ( !transformTokens ) return;
  5050. const tokens = this.getActiveTokens(true);
  5051. const updates = tokens.map(t => {
  5052. const newTokenData = foundry.utils.deepClone(d.prototypeToken);
  5053. newTokenData._id = t.id;
  5054. newTokenData.actorId = newActor.id;
  5055. newTokenData.actorLink = true;
  5056. const dOriginalActor = foundry.utils.getProperty(d, "flags.dnd5e.originalActor");
  5057. foundry.utils.setProperty(newTokenData, "flags.dnd5e.originalActor", dOriginalActor);
  5058. foundry.utils.setProperty(newTokenData, "flags.dnd5e.isPolymorphed", true);
  5059. return newTokenData;
  5060. });
  5061. return canvas.scene?.updateEmbeddedDocuments("Token", updates);
  5062. }
  5063. /* -------------------------------------------- */
  5064. /**
  5065. * If this actor was transformed with transformTokens enabled, then its
  5066. * active tokens need to be returned to their original state. If not, then
  5067. * we can safely just delete this actor.
  5068. * @param {object} [options]
  5069. * @param {boolean} [options.renderSheet=true] Render Sheet after revert the transformation.
  5070. * @returns {Promise<Actor>|null} Original actor if it was reverted.
  5071. */
  5072. async revertOriginalForm({renderSheet=true}={}) {
  5073. if ( !this.isPolymorphed ) return;
  5074. if ( !this.isOwner ) return ui.notifications.warn(game.i18n.localize("DND5E.PolymorphRevertWarn"));
  5075. /**
  5076. * A hook event that fires just before the actor is reverted to original form.
  5077. * @function dnd5e.revertOriginalForm
  5078. * @memberof hookEvents
  5079. * @param {Actor} this The original actor before transformation.
  5080. * @param {object} [options]
  5081. */
  5082. Hooks.callAll("dnd5e.revertOriginalForm", this, {renderSheet});
  5083. const previousActorIds = this.getFlag("dnd5e", "previousActorIds") ?? [];
  5084. const isOriginalActor = !previousActorIds.length;
  5085. const isRendered = this.sheet.rendered;
  5086. // Obtain a reference to the original actor
  5087. const original = game.actors.get(this.getFlag("dnd5e", "originalActor"));
  5088. // If we are reverting an unlinked token, grab the previous actorData, and create a new token
  5089. if ( this.isToken ) {
  5090. const baseActor = original ? original : game.actors.get(this.token.actorId);
  5091. if ( !baseActor ) {
  5092. ui.notifications.warn(game.i18n.format("DND5E.PolymorphRevertNoOriginalActorWarn", {
  5093. reference: this.getFlag("dnd5e", "originalActor")
  5094. }));
  5095. return;
  5096. }
  5097. const prototypeTokenData = await baseActor.getTokenDocument();
  5098. const actorData = this.token.getFlag("dnd5e", "previousActorData");
  5099. const tokenUpdate = this.token.toObject();
  5100. if ( game.dnd5e.isV10 ) tokenUpdate.actorData = actorData ?? {};
  5101. else {
  5102. actorData._id = tokenUpdate.delta._id;
  5103. tokenUpdate.delta = actorData;
  5104. }
  5105. for ( const k of ["width", "height", "alpha", "lockRotation", "name"] ) {
  5106. tokenUpdate[k] = prototypeTokenData[k];
  5107. }
  5108. for ( const k of ["offsetX", "offsetY", "scaleX", "scaleY", "src", "tint"] ) {
  5109. tokenUpdate.texture[k] = prototypeTokenData.texture[k];
  5110. }
  5111. tokenUpdate.sight = prototypeTokenData.sight;
  5112. tokenUpdate.detectionModes = prototypeTokenData.detectionModes;
  5113. await this.sheet.close();
  5114. await canvas.scene?.deleteEmbeddedDocuments("Token", [this.token._id]);
  5115. const token = await TokenDocument.implementation.create(tokenUpdate, {
  5116. parent: canvas.scene, keepId: true, render: true
  5117. });
  5118. if ( isOriginalActor ) {
  5119. await this.unsetFlag("dnd5e", "isPolymorphed");
  5120. await this.unsetFlag("dnd5e", "previousActorIds");
  5121. await this.token.unsetFlag("dnd5e", "previousActorData");
  5122. }
  5123. if ( isRendered && renderSheet ) token.actor?.sheet?.render(true);
  5124. return token;
  5125. }
  5126. if ( !original ) {
  5127. ui.notifications.warn(game.i18n.format("DND5E.PolymorphRevertNoOriginalActorWarn", {
  5128. reference: this.getFlag("dnd5e", "originalActor")
  5129. }));
  5130. return;
  5131. }
  5132. // Get the Tokens which represent this actor
  5133. if ( canvas.ready ) {
  5134. const tokens = this.getActiveTokens(true);
  5135. const tokenData = await original.getTokenDocument();
  5136. const tokenUpdates = tokens.map(t => {
  5137. const update = duplicate(tokenData);
  5138. update._id = t.id;
  5139. delete update.x;
  5140. delete update.y;
  5141. return update;
  5142. });
  5143. await canvas.scene.updateEmbeddedDocuments("Token", tokenUpdates);
  5144. }
  5145. if ( isOriginalActor ) {
  5146. await this.unsetFlag("dnd5e", "isPolymorphed");
  5147. await this.unsetFlag("dnd5e", "previousActorIds");
  5148. }
  5149. // Delete the polymorphed version(s) of the actor, if possible
  5150. if ( game.user.isGM ) {
  5151. const idsToDelete = previousActorIds.filter(id =>
  5152. id !== original.id // Is not original Actor Id
  5153. && game.actors?.get(id) // Actor still exists
  5154. ).concat([this.id]); // Add this id
  5155. await Actor.implementation.deleteDocuments(idsToDelete);
  5156. } else if ( isRendered ) {
  5157. this.sheet?.close();
  5158. }
  5159. if ( isRendered && renderSheet ) original.sheet?.render(isRendered);
  5160. return original;
  5161. }
  5162. /* -------------------------------------------- */
  5163. /**
  5164. * Add additional system-specific sidebar directory context menu options for Actor documents
  5165. * @param {jQuery} html The sidebar HTML
  5166. * @param {Array} entryOptions The default array of context menu options
  5167. */
  5168. static addDirectoryContextOptions(html, entryOptions) {
  5169. entryOptions.push({
  5170. name: "DND5E.PolymorphRestoreTransformation",
  5171. icon: '<i class="fas fa-backward"></i>',
  5172. callback: li => {
  5173. const actor = game.actors.get(li.data("documentId"));
  5174. return actor.revertOriginalForm();
  5175. },
  5176. condition: li => {
  5177. const allowed = game.settings.get("dnd5e", "allowPolymorphing");
  5178. if ( !allowed && !game.user.isGM ) return false;
  5179. const actor = game.actors.get(li.data("documentId"));
  5180. return actor && actor.isPolymorphed;
  5181. }
  5182. });
  5183. }
  5184. /* -------------------------------------------- */
  5185. /**
  5186. * Format a type object into a string.
  5187. * @param {object} typeData The type data to convert to a string.
  5188. * @returns {string}
  5189. */
  5190. static formatCreatureType(typeData) {
  5191. if ( typeof typeData === "string" ) return typeData; // Backwards compatibility
  5192. let localizedType;
  5193. if ( typeData.value === "custom" ) {
  5194. localizedType = typeData.custom;
  5195. } else {
  5196. let code = CONFIG.DND5E.creatureTypes[typeData.value];
  5197. localizedType = game.i18n.localize(typeData.swarm ? `${code}Pl` : code);
  5198. }
  5199. let type = localizedType;
  5200. if ( typeData.swarm ) {
  5201. type = game.i18n.format("DND5E.CreatureSwarmPhrase", {
  5202. size: game.i18n.localize(CONFIG.DND5E.actorSizes[typeData.swarm]),
  5203. type: localizedType
  5204. });
  5205. }
  5206. if (typeData.subtype) type = `${type} (${typeData.subtype})`;
  5207. return type;
  5208. }
  5209. /* -------------------------------------------- */
  5210. /* Event Listeners and Handlers */
  5211. /* -------------------------------------------- */
  5212. /** @inheritdoc */
  5213. _onUpdate(data, options, userId) {
  5214. super._onUpdate(data, options, userId);
  5215. this._displayScrollingDamage(options.dhp);
  5216. }
  5217. /* -------------------------------------------- */
  5218. /**
  5219. * Display changes to health as scrolling combat text.
  5220. * Adapt the font size relative to the Actor's HP total to emphasize more significant blows.
  5221. * @param {number} dhp The change in hit points that was applied
  5222. * @private
  5223. */
  5224. _displayScrollingDamage(dhp) {
  5225. if ( !dhp ) return;
  5226. dhp = Number(dhp);
  5227. const tokens = this.isToken ? [this.token?.object] : this.getActiveTokens(true);
  5228. for ( const t of tokens ) {
  5229. if ( !t.visible || !t.renderable ) continue;
  5230. const pct = Math.clamped(Math.abs(dhp) / this.system.attributes.hp.max, 0, 1);
  5231. canvas.interface.createScrollingText(t.center, dhp.signedString(), {
  5232. anchor: CONST.TEXT_ANCHOR_POINTS.TOP,
  5233. fontSize: 16 + (32 * pct), // Range between [16, 48]
  5234. fill: CONFIG.DND5E.tokenHPColors[dhp < 0 ? "damage" : "healing"],
  5235. stroke: 0x000000,
  5236. strokeThickness: 4,
  5237. jitter: 0.25
  5238. });
  5239. }
  5240. }
  5241. }
  5242. /**
  5243. * Inline application that presents the player with a choice of items.
  5244. */
  5245. class ItemChoiceFlow extends ItemGrantFlow {
  5246. /**
  5247. * Set of selected UUIDs.
  5248. * @type {Set<string>}
  5249. */
  5250. selected;
  5251. /**
  5252. * Cached items from the advancement's pool.
  5253. * @type {Item5e[]}
  5254. */
  5255. pool;
  5256. /**
  5257. * List of dropped items.
  5258. * @type {Item5e[]}
  5259. */
  5260. dropped;
  5261. /* -------------------------------------------- */
  5262. /** @inheritdoc */
  5263. static get defaultOptions() {
  5264. return foundry.utils.mergeObject(super.defaultOptions, {
  5265. dragDrop: [{ dropSelector: ".drop-target" }],
  5266. template: "systems/dnd5e/templates/advancement/item-choice-flow.hbs"
  5267. });
  5268. }
  5269. /* -------------------------------------------- */
  5270. /** @inheritdoc */
  5271. async getContext() {
  5272. this.selected ??= new Set(
  5273. this.retainedData?.items.map(i => foundry.utils.getProperty(i, "flags.dnd5e.sourceId"))
  5274. ?? Object.values(this.advancement.value[this.level] ?? {})
  5275. );
  5276. this.pool ??= await Promise.all(this.advancement.configuration.pool.map(uuid => fromUuid(uuid)));
  5277. if ( !this.dropped ) {
  5278. this.dropped = [];
  5279. for ( const data of this.retainedData?.items ?? [] ) {
  5280. const uuid = foundry.utils.getProperty(data, "flags.dnd5e.sourceId");
  5281. if ( this.pool.find(i => uuid === i.uuid) ) continue;
  5282. const item = await fromUuid(uuid);
  5283. item.dropped = true;
  5284. this.dropped.push(item);
  5285. }
  5286. }
  5287. const max = this.advancement.configuration.choices[this.level];
  5288. const choices = { max, current: this.selected.size, full: this.selected.size >= max };
  5289. const previousLevels = {};
  5290. const previouslySelected = new Set();
  5291. for ( const [level, data] of Object.entries(this.advancement.value.added ?? {}) ) {
  5292. if ( level > this.level ) continue;
  5293. previousLevels[level] = await Promise.all(Object.values(data).map(uuid => fromUuid(uuid)));
  5294. Object.values(data).forEach(uuid => previouslySelected.add(uuid));
  5295. }
  5296. const items = [...this.pool, ...this.dropped].reduce((items, i) => {
  5297. i.checked = this.selected.has(i.uuid);
  5298. i.disabled = !i.checked && choices.full;
  5299. if ( !previouslySelected.has(i.uuid) ) items.push(i);
  5300. return items;
  5301. }, []);
  5302. return { choices, items, previousLevels };
  5303. }
  5304. /* -------------------------------------------- */
  5305. /** @inheritdoc */
  5306. activateListeners(html) {
  5307. super.activateListeners(html);
  5308. html.find(".item-delete").click(this._onItemDelete.bind(this));
  5309. }
  5310. /* -------------------------------------------- */
  5311. /** @inheritdoc */
  5312. _onChangeInput(event) {
  5313. if ( event.target.checked ) this.selected.add(event.target.name);
  5314. else this.selected.delete(event.target.name);
  5315. this.render();
  5316. }
  5317. /* -------------------------------------------- */
  5318. /**
  5319. * Handle deleting a dropped item.
  5320. * @param {Event} event The originating click event.
  5321. * @protected
  5322. */
  5323. async _onItemDelete(event) {
  5324. event.preventDefault();
  5325. const uuidToDelete = event.currentTarget.closest(".item-name")?.querySelector("input")?.name;
  5326. if ( !uuidToDelete ) return;
  5327. this.dropped.findSplice(i => i.uuid === uuidToDelete);
  5328. this.selected.delete(uuidToDelete);
  5329. this.render();
  5330. }
  5331. /* -------------------------------------------- */
  5332. /** @inheritdoc */
  5333. async _onDrop(event) {
  5334. if ( this.selected.size >= this.advancement.configuration.choices[this.level] ) return false;
  5335. // Try to extract the data
  5336. let data;
  5337. try {
  5338. data = JSON.parse(event.dataTransfer.getData("text/plain"));
  5339. } catch(err) {
  5340. return false;
  5341. }
  5342. if ( data.type !== "Item" ) return false;
  5343. const item = await Item.implementation.fromDropData(data);
  5344. try {
  5345. this.advancement._validateItemType(item);
  5346. } catch(err) {
  5347. return ui.notifications.error(err.message);
  5348. }
  5349. // If the item is already been marked as selected, no need to go further
  5350. if ( this.selected.has(item.uuid) ) return false;
  5351. // Check to ensure the dropped item hasn't been selected at a lower level
  5352. for ( const [level, data] of Object.entries(this.advancement.value.added ?? {}) ) {
  5353. if ( level >= this.level ) continue;
  5354. if ( Object.values(data).includes(item.uuid) ) {
  5355. return ui.notifications.error(game.i18n.localize("DND5E.AdvancementItemChoicePreviouslyChosenWarning"));
  5356. }
  5357. }
  5358. // If spell level is restricted to available level, ensure the spell is of the appropriate level
  5359. const spellLevel = this.advancement.configuration.restriction.level;
  5360. if ( (this.advancement.configuration.type === "spell") && spellLevel === "available" ) {
  5361. const maxSlot = this._maxSpellSlotLevel();
  5362. if ( item.system.level > maxSlot ) return ui.notifications.error(game.i18n.format(
  5363. "DND5E.AdvancementItemChoiceSpellLevelAvailableWarning", { level: CONFIG.DND5E.spellLevels[maxSlot] }
  5364. ));
  5365. }
  5366. // Mark the item as selected
  5367. this.selected.add(item.uuid);
  5368. // If the item doesn't already exist in the pool, add it
  5369. if ( !this.pool.find(i => i.uuid === item.uuid) ) {
  5370. this.dropped.push(item);
  5371. item.dropped = true;
  5372. }
  5373. this.render();
  5374. }
  5375. /* -------------------------------------------- */
  5376. /**
  5377. * Determine the maximum spell slot level for the actor to which this advancement is being applied.
  5378. * @returns {number}
  5379. */
  5380. _maxSpellSlotLevel() {
  5381. const spellcasting = this.advancement.item.spellcasting;
  5382. let spells;
  5383. // For advancements on classes or subclasses, use the largest slot available for that class
  5384. if ( spellcasting ) {
  5385. const progression = { slot: 0, pact: {} };
  5386. const maxSpellLevel = CONFIG.DND5E.SPELL_SLOT_TABLE[CONFIG.DND5E.SPELL_SLOT_TABLE.length - 1].length;
  5387. spells = Object.fromEntries(Array.fromRange(maxSpellLevel, 1).map(l => [`spell${l}`, {}]));
  5388. Actor5e.computeClassProgression(progression, this.advancement.item, { spellcasting });
  5389. Actor5e.prepareSpellcastingSlots(spells, spellcasting.type, progression);
  5390. }
  5391. // For all other items, use the largest slot possible
  5392. else spells = this.advancement.actor.system.spells;
  5393. const largestSlot = Object.entries(spells).reduce((slot, [key, data]) => {
  5394. if ( data.max === 0 ) return slot;
  5395. const level = parseInt(key.replace("spell", ""));
  5396. if ( !Number.isNaN(level) && level > slot ) return level;
  5397. return slot;
  5398. }, -1);
  5399. return Math.max(spells.pact?.level ?? 0, largestSlot);
  5400. }
  5401. }
  5402. class ItemChoiceConfigurationData extends foundry.abstract.DataModel {
  5403. static defineSchema() {
  5404. return {
  5405. hint: new foundry.data.fields.StringField({label: "DND5E.AdvancementHint"}),
  5406. choices: new MappingField(new foundry.data.fields.NumberField(), {
  5407. hint: "DND5E.AdvancementItemChoiceLevelsHint"
  5408. }),
  5409. allowDrops: new foundry.data.fields.BooleanField({
  5410. initial: true, label: "DND5E.AdvancementConfigureAllowDrops",
  5411. hint: "DND5E.AdvancementConfigureAllowDropsHint"
  5412. }),
  5413. type: new foundry.data.fields.StringField({
  5414. blank: false, nullable: true, initial: null,
  5415. label: "DND5E.AdvancementItemChoiceType", hint: "DND5E.AdvancementItemChoiceTypeHint"
  5416. }),
  5417. pool: new foundry.data.fields.ArrayField(new foundry.data.fields.StringField(), {label: "DOCUMENT.Items"}),
  5418. spell: new foundry.data.fields.EmbeddedDataField(SpellConfigurationData, {nullable: true, initial: null}),
  5419. restriction: new foundry.data.fields.SchemaField({
  5420. type: new foundry.data.fields.StringField({label: "DND5E.Type"}),
  5421. subtype: new foundry.data.fields.StringField({label: "DND5E.Subtype"}),
  5422. level: new foundry.data.fields.StringField({label: "DND5E.SpellLevel"})
  5423. })
  5424. };
  5425. }
  5426. }
  5427. /**
  5428. * Advancement that presents the player with a choice of multiple items that they can take. Keeps track of which
  5429. * items were selected at which levels.
  5430. */
  5431. class ItemChoiceAdvancement extends ItemGrantAdvancement {
  5432. /** @inheritdoc */
  5433. static get metadata() {
  5434. return foundry.utils.mergeObject(super.metadata, {
  5435. dataModels: {
  5436. configuration: ItemChoiceConfigurationData
  5437. },
  5438. order: 50,
  5439. icon: "systems/dnd5e/icons/svg/item-choice.svg",
  5440. title: game.i18n.localize("DND5E.AdvancementItemChoiceTitle"),
  5441. hint: game.i18n.localize("DND5E.AdvancementItemChoiceHint"),
  5442. multiLevel: true,
  5443. apps: {
  5444. config: ItemChoiceConfig,
  5445. flow: ItemChoiceFlow
  5446. }
  5447. });
  5448. }
  5449. /* -------------------------------------------- */
  5450. /* Instance Properties */
  5451. /* -------------------------------------------- */
  5452. /** @inheritdoc */
  5453. get levels() {
  5454. return Array.from(Object.keys(this.configuration.choices));
  5455. }
  5456. /* -------------------------------------------- */
  5457. /* Display Methods */
  5458. /* -------------------------------------------- */
  5459. /** @inheritdoc */
  5460. configuredForLevel(level) {
  5461. return this.value.added?.[level] !== undefined;
  5462. }
  5463. /* -------------------------------------------- */
  5464. /** @inheritdoc */
  5465. titleForLevel(level, { configMode=false }={}) {
  5466. return `${this.title} <em>(${game.i18n.localize("DND5E.AdvancementChoices")})</em>`;
  5467. }
  5468. /* -------------------------------------------- */
  5469. /** @inheritdoc */
  5470. summaryForLevel(level, { configMode=false }={}) {
  5471. const items = this.value.added?.[level];
  5472. if ( !items || configMode ) return "";
  5473. return Object.values(items).reduce((html, uuid) => html + game.dnd5e.utils.linkForUuid(uuid), "");
  5474. }
  5475. /* -------------------------------------------- */
  5476. /* Application Methods */
  5477. /* -------------------------------------------- */
  5478. /** @inheritdoc */
  5479. storagePath(level) {
  5480. return `value.added.${level}`;
  5481. }
  5482. /* -------------------------------------------- */
  5483. /**
  5484. * Verify that the provided item can be used with this advancement based on the configuration.
  5485. * @param {Item5e} item Item that needs to be tested.
  5486. * @param {object} config
  5487. * @param {string} config.type Type restriction on this advancement.
  5488. * @param {object} config.restriction Additional restrictions to be applied.
  5489. * @param {boolean} [config.strict=true] Should an error be thrown when an invalid type is encountered?
  5490. * @returns {boolean} Is this type valid?
  5491. * @throws An error if the item is invalid and strict is `true`.
  5492. */
  5493. _validateItemType(item, { type, restriction, strict=true }={}) {
  5494. super._validateItemType(item, { strict });
  5495. type ??= this.configuration.type;
  5496. restriction ??= this.configuration.restriction;
  5497. // Type restriction is set and the item type does not match the selected type
  5498. if ( type && (type !== item.type) ) {
  5499. const typeLabel = game.i18n.localize(CONFIG.Item.typeLabels[restriction]);
  5500. if ( strict ) throw new Error(game.i18n.format("DND5E.AdvancementItemChoiceTypeWarning", {type: typeLabel}));
  5501. return false;
  5502. }
  5503. // If additional type restrictions applied, make sure they are valid
  5504. if ( (type === "feat") && restriction.type ) {
  5505. const typeConfig = CONFIG.DND5E.featureTypes[restriction.type];
  5506. const subtype = typeConfig.subtypes?.[restriction.subtype];
  5507. let errorLabel;
  5508. if ( restriction.type !== item.system.type.value ) errorLabel = typeConfig.label;
  5509. else if ( subtype && (restriction.subtype !== item.system.type.subtype) ) errorLabel = subtype;
  5510. if ( errorLabel ) {
  5511. if ( strict ) throw new Error(game.i18n.format("DND5E.AdvancementItemChoiceTypeWarning", {type: errorLabel}));
  5512. return false;
  5513. }
  5514. }
  5515. // If spell level is restricted, ensure the spell is of the appropriate level
  5516. const l = parseInt(restriction.level);
  5517. if ( (type === "spell") && !Number.isNaN(l) && (item.system.level !== l) ) {
  5518. const level = CONFIG.DND5E.spellLevels[l];
  5519. if ( strict ) throw new Error(game.i18n.format("DND5E.AdvancementItemChoiceSpellLevelSpecificWarning", {level}));
  5520. return false;
  5521. }
  5522. return true;
  5523. }
  5524. }
  5525. /**
  5526. * Data model for the Scale Value advancement type.
  5527. *
  5528. * @property {string} identifier Identifier used to select this scale value in roll formulas.
  5529. * @property {string} type Type of data represented by this scale value.
  5530. * @property {object} [distance]
  5531. * @property {string} [distance.units] If distance type is selected, the units each value uses.
  5532. * @property {Object<string, *>} scale Scale values for each level. Value format is determined by type.
  5533. */
  5534. class ScaleValueConfigurationData extends foundry.abstract.DataModel {
  5535. /** @inheritdoc */
  5536. static defineSchema() {
  5537. return {
  5538. identifier: new IdentifierField({required: true, label: "DND5E.Identifier"}),
  5539. type: new foundry.data.fields.StringField({
  5540. required: true, initial: "string", choices: TYPES, label: "DND5E.AdvancementScaleValueTypeLabel"
  5541. }),
  5542. distance: new foundry.data.fields.SchemaField({
  5543. units: new foundry.data.fields.StringField({required: true, label: "DND5E.MovementUnits"})
  5544. }),
  5545. scale: new MappingField(new ScaleValueEntryField(), {required: true})
  5546. };
  5547. }
  5548. /* -------------------------------------------- */
  5549. /** @inheritdoc */
  5550. static migrateData(source) {
  5551. super.migrateData(source);
  5552. if ( source.type === "numeric" ) source.type = "number";
  5553. Object.values(source.scale ?? {}).forEach(v => TYPES[source.type].migrateData(v));
  5554. }
  5555. }
  5556. /**
  5557. * Data field that automatically selects the appropriate ScaleValueType based on the selected type.
  5558. */
  5559. class ScaleValueEntryField extends foundry.data.fields.ObjectField {
  5560. /** @override */
  5561. _cleanType(value, options) {
  5562. if ( !(typeof value === "object") ) value = {};
  5563. // Use a defined DataModel
  5564. const cls = TYPES[options.source?.type];
  5565. if ( cls ) return cls.cleanData(value, options);
  5566. return value;
  5567. }
  5568. /* -------------------------------------------- */
  5569. /** @override */
  5570. initialize(value, model) {
  5571. const cls = TYPES[model.type];
  5572. if ( !value || !cls ) return value;
  5573. return new cls(value, {parent: model});
  5574. }
  5575. /* -------------------------------------------- */
  5576. /** @override */
  5577. toObject(value) {
  5578. return value.toObject(false);
  5579. }
  5580. }
  5581. /**
  5582. * Base scale value data type that stores generic string values.
  5583. *
  5584. * @property {string} value String value.
  5585. */
  5586. class ScaleValueType extends foundry.abstract.DataModel {
  5587. /** @inheritdoc */
  5588. static defineSchema() {
  5589. return {
  5590. value: new foundry.data.fields.StringField({required: true})
  5591. };
  5592. }
  5593. /* -------------------------------------------- */
  5594. /**
  5595. * Information on how a scale value of this type is configured.
  5596. *
  5597. * @typedef {object} ScaleValueTypeMetadata
  5598. * @property {string} label Name of this type.
  5599. * @property {string} hint Hint for this type shown in the scale value configuration.
  5600. * @property {boolean} isNumeric When using the default editing interface, should numeric inputs be used?
  5601. */
  5602. /**
  5603. * Configuration information for this scale value type.
  5604. * @type {ScaleValueTypeMetadata}
  5605. */
  5606. static get metadata() {
  5607. return {
  5608. label: "DND5E.AdvancementScaleValueTypeString",
  5609. hint: "DND5E.AdvancementScaleValueTypeHintString",
  5610. isNumeric: false
  5611. };
  5612. }
  5613. /* -------------------------------------------- */
  5614. /**
  5615. * Attempt to convert another scale value type to this one.
  5616. * @param {ScaleValueType} original Original type to attempt to convert.
  5617. * @param {object} [options] Options which affect DataModel construction.
  5618. * @returns {ScaleValueType|null}
  5619. */
  5620. static convertFrom(original, options) {
  5621. return new this({value: original.formula}, options);
  5622. }
  5623. /* -------------------------------------------- */
  5624. /**
  5625. * This scale value prepared to be used in roll formulas.
  5626. * @type {string|null}
  5627. */
  5628. get formula() { return this.value; }
  5629. /* -------------------------------------------- */
  5630. /**
  5631. * This scale value formatted for display.
  5632. * @type {string|null}
  5633. */
  5634. get display() { return this.formula; }
  5635. /* -------------------------------------------- */
  5636. /**
  5637. * Shortcut to the prepared value when used in roll formulas.
  5638. * @returns {string}
  5639. */
  5640. toString() {
  5641. return this.formula;
  5642. }
  5643. }
  5644. /**
  5645. * Scale value data type that stores numeric values.
  5646. *
  5647. * @property {number} value Numeric value.
  5648. */
  5649. class ScaleValueTypeNumber extends ScaleValueType {
  5650. /** @inheritdoc */
  5651. static defineSchema() {
  5652. return {
  5653. value: new foundry.data.fields.NumberField({required: true})
  5654. };
  5655. }
  5656. /* -------------------------------------------- */
  5657. /** @inheritdoc */
  5658. static get metadata() {
  5659. return foundry.utils.mergeObject(super.metadata, {
  5660. label: "DND5E.AdvancementScaleValueTypeNumber",
  5661. hint: "DND5E.AdvancementScaleValueTypeHintNumber",
  5662. isNumeric: true
  5663. });
  5664. }
  5665. /* -------------------------------------------- */
  5666. /** @inheritdoc */
  5667. static convertFrom(original, options) {
  5668. const value = Number(original.formula);
  5669. if ( Number.isNaN(value) ) return null;
  5670. return new this({value}, options);
  5671. }
  5672. }
  5673. /**
  5674. * Scale value data type that stores challenge ratings.
  5675. *
  5676. * @property {number} value CR value.
  5677. */
  5678. class ScaleValueTypeCR extends ScaleValueTypeNumber {
  5679. /** @inheritdoc */
  5680. static defineSchema() {
  5681. return {
  5682. value: new foundry.data.fields.NumberField({required: true, min: 0})
  5683. // TODO: Add CR validator
  5684. };
  5685. }
  5686. /* -------------------------------------------- */
  5687. /** @inheritdoc */
  5688. static get metadata() {
  5689. return foundry.utils.mergeObject(super.metadata, {
  5690. label: "DND5E.AdvancementScaleValueTypeCR",
  5691. hint: "DND5E.AdvancementScaleValueTypeHintCR"
  5692. });
  5693. }
  5694. /* -------------------------------------------- */
  5695. /** @inheritdoc */
  5696. get display() {
  5697. switch ( this.value ) {
  5698. case 0.125: return "&frac18;";
  5699. case 0.25: return "&frac14;";
  5700. case 0.5: return "&frac12;";
  5701. default: return super.display;
  5702. }
  5703. }
  5704. }
  5705. /**
  5706. * Scale value data type that stores dice values.
  5707. *
  5708. * @property {number} number Number of dice.
  5709. * @property {number} faces Die faces.
  5710. */
  5711. class ScaleValueTypeDice extends ScaleValueType {
  5712. /** @inheritdoc */
  5713. static defineSchema() {
  5714. return {
  5715. number: new foundry.data.fields.NumberField({nullable: true, integer: true, positive: true}),
  5716. faces: new foundry.data.fields.NumberField({required: true, integer: true, positive: true})
  5717. };
  5718. }
  5719. /* -------------------------------------------- */
  5720. /** @inheritdoc */
  5721. static get metadata() {
  5722. return foundry.utils.mergeObject(super.metadata, {
  5723. label: "DND5E.AdvancementScaleValueTypeDice",
  5724. hint: "DND5E.AdvancementScaleValueTypeHintDice"
  5725. });
  5726. }
  5727. /* -------------------------------------------- */
  5728. /**
  5729. * List of die faces that can be chosen.
  5730. * @type {number[]}
  5731. */
  5732. static FACES = [2, 3, 4, 6, 8, 10, 12, 20, 100];
  5733. /* -------------------------------------------- */
  5734. /** @inheritdoc */
  5735. static convertFrom(original, options) {
  5736. const [number, faces] = (original.formula ?? "").split("d");
  5737. if ( !faces || !Number.isNumeric(number) || !Number.isNumeric(faces) ) return null;
  5738. return new this({number: Number(number) || null, faces: Number(faces)}, options);
  5739. }
  5740. /* -------------------------------------------- */
  5741. /** @inheritdoc */
  5742. get formula() {
  5743. if ( !this.faces ) return null;
  5744. return `${this.number ?? ""}${this.die}`;
  5745. }
  5746. /* -------------------------------------------- */
  5747. /**
  5748. * The die value to be rolled with the leading "d" (e.g. "d4").
  5749. * @type {string}
  5750. */
  5751. get die() {
  5752. if ( !this.faces ) return "";
  5753. return `d${this.faces}`;
  5754. }
  5755. /* -------------------------------------------- */
  5756. /** @inheritdoc */
  5757. static migrateData(source) {
  5758. if ( source.n ) source.number = source.n;
  5759. if ( source.die ) source.faces = source.die;
  5760. }
  5761. }
  5762. /**
  5763. * Scale value data type that stores distance values.
  5764. *
  5765. * @property {number} value Numeric value.
  5766. */
  5767. class ScaleValueTypeDistance extends ScaleValueTypeNumber {
  5768. /** @inheritdoc */
  5769. static get metadata() {
  5770. return foundry.utils.mergeObject(super.metadata, {
  5771. label: "DND5E.AdvancementScaleValueTypeDistance",
  5772. hint: "DND5E.AdvancementScaleValueTypeHintDistance"
  5773. });
  5774. }
  5775. /* -------------------------------------------- */
  5776. /** @inheritdoc */
  5777. get display() {
  5778. return `${this.value} ${CONFIG.DND5E.movementUnits[this.parent.configuration.distance?.units ?? "ft"]}`;
  5779. }
  5780. }
  5781. /**
  5782. * The available types of scaling value.
  5783. * @enum {ScaleValueType}
  5784. */
  5785. const TYPES = {
  5786. string: ScaleValueType,
  5787. number: ScaleValueTypeNumber,
  5788. cr: ScaleValueTypeCR,
  5789. dice: ScaleValueTypeDice,
  5790. distance: ScaleValueTypeDistance
  5791. };
  5792. var scaleValue = /*#__PURE__*/Object.freeze({
  5793. __proto__: null,
  5794. ScaleValueConfigurationData: ScaleValueConfigurationData,
  5795. ScaleValueEntryField: ScaleValueEntryField,
  5796. ScaleValueType: ScaleValueType,
  5797. ScaleValueTypeCR: ScaleValueTypeCR,
  5798. ScaleValueTypeDice: ScaleValueTypeDice,
  5799. ScaleValueTypeDistance: ScaleValueTypeDistance,
  5800. ScaleValueTypeNumber: ScaleValueTypeNumber,
  5801. TYPES: TYPES
  5802. });
  5803. /**
  5804. * Configuration application for scale values.
  5805. */
  5806. class ScaleValueConfig extends AdvancementConfig {
  5807. /** @inheritdoc */
  5808. static get defaultOptions() {
  5809. return foundry.utils.mergeObject(super.defaultOptions, {
  5810. classes: ["dnd5e", "advancement", "scale-value", "two-column"],
  5811. template: "systems/dnd5e/templates/advancement/scale-value-config.hbs",
  5812. width: 540
  5813. });
  5814. }
  5815. /* -------------------------------------------- */
  5816. /** @inheritdoc */
  5817. getData() {
  5818. const config = this.advancement.configuration;
  5819. const type = TYPES[config.type];
  5820. return foundry.utils.mergeObject(super.getData(), {
  5821. classIdentifier: this.item.identifier,
  5822. previewIdentifier: config.identifier || this.advancement.title?.slugify()
  5823. || this.advancement.constructor.metadata.title.slugify(),
  5824. type: type.metadata,
  5825. types: Object.fromEntries(
  5826. Object.entries(TYPES).map(([key, d]) => [key, game.i18n.localize(d.metadata.label)])
  5827. ),
  5828. faces: Object.fromEntries(TYPES.dice.FACES.map(die => [die, `d${die}`])),
  5829. levels: this._prepareLevelData(),
  5830. movementUnits: CONFIG.DND5E.movementUnits
  5831. });
  5832. }
  5833. /* -------------------------------------------- */
  5834. /**
  5835. * Prepare the data to display at each of the scale levels.
  5836. * @returns {object}
  5837. * @protected
  5838. */
  5839. _prepareLevelData() {
  5840. let lastValue = null;
  5841. return Array.fromRange(CONFIG.DND5E.maxLevel + 1).slice(1).reduce((obj, level) => {
  5842. obj[level] = { placeholder: this._formatPlaceholder(lastValue), value: null };
  5843. const value = this.advancement.configuration.scale[level];
  5844. if ( value ) {
  5845. this._mergeScaleValues(value, lastValue);
  5846. obj[level].className = "new-scale-value";
  5847. obj[level].value = value;
  5848. lastValue = value;
  5849. }
  5850. return obj;
  5851. }, {});
  5852. }
  5853. /* -------------------------------------------- */
  5854. /**
  5855. * Formats the placeholder for this scale value.
  5856. * @param {*} placeholder
  5857. * @returns {object}
  5858. * @protected
  5859. */
  5860. _formatPlaceholder(placeholder) {
  5861. if ( this.advancement.configuration.type === "dice" ) {
  5862. return { number: placeholder?.number ?? "", faces: placeholder?.faces ? `d${placeholder.faces}` : "" };
  5863. }
  5864. return { value: placeholder?.value ?? "" };
  5865. }
  5866. /* -------------------------------------------- */
  5867. /**
  5868. * For scale values with multiple properties, have missing properties inherit from earlier filled-in values.
  5869. * @param {*} value The primary value.
  5870. * @param {*} lastValue The previous value.
  5871. */
  5872. _mergeScaleValues(value, lastValue) {
  5873. for ( const k of Object.keys(lastValue ?? {}) ) {
  5874. if ( value[k] == null ) value[k] = lastValue[k];
  5875. }
  5876. }
  5877. /* -------------------------------------------- */
  5878. /** @inheritdoc */
  5879. static _cleanedObject(object) {
  5880. return Object.entries(object).reduce((obj, [key, value]) => {
  5881. if ( Object.keys(value ?? {}).some(k => value[k]) ) obj[key] = value;
  5882. else obj[`-=${key}`] = null;
  5883. return obj;
  5884. }, {});
  5885. }
  5886. /* -------------------------------------------- */
  5887. /** @inheritdoc */
  5888. prepareConfigurationUpdate(configuration) {
  5889. // Ensure multiple values in a row are not the same
  5890. let lastValue = null;
  5891. for ( const [lvl, value] of Object.entries(configuration.scale) ) {
  5892. if ( this.advancement.testEquality(lastValue, value) ) configuration.scale[lvl] = null;
  5893. else if ( Object.keys(value ?? {}).some(k => value[k]) ) {
  5894. this._mergeScaleValues(value, lastValue);
  5895. lastValue = value;
  5896. }
  5897. }
  5898. configuration.scale = this.constructor._cleanedObject(configuration.scale);
  5899. return configuration;
  5900. }
  5901. /* -------------------------------------------- */
  5902. /** @inheritdoc */
  5903. activateListeners(html) {
  5904. super.activateListeners(html);
  5905. this.form.querySelector("input[name='title']").addEventListener("input", this._onChangeTitle.bind(this));
  5906. this.form.querySelector(".identifier-hint-copy").addEventListener("click", this._onIdentifierHintCopy.bind(this));
  5907. }
  5908. /* -------------------------------------------- */
  5909. /**
  5910. * Copies the full scale identifier hint to the clipboard.
  5911. * @param {Event} event The triggering click event.
  5912. * @protected
  5913. */
  5914. _onIdentifierHintCopy(event) {
  5915. const data = this.getData();
  5916. game.clipboard.copyPlainText(`@scale.${data.classIdentifier}.${data.previewIdentifier}`);
  5917. game.tooltip.activate(event.target, {text: game.i18n.localize("DND5E.IdentifierCopied"), direction: "UP"});
  5918. }
  5919. /* -------------------------------------------- */
  5920. /**
  5921. * If no identifier is manually entered, slugify the custom title and display as placeholder.
  5922. * @param {Event} event Change event to the title input.
  5923. */
  5924. _onChangeTitle(event) {
  5925. const slug = (event.target.value || this.advancement.constructor.metadata.title).slugify();
  5926. this.form.querySelector("input[name='configuration.identifier']").placeholder = slug;
  5927. }
  5928. /* -------------------------------------------- */
  5929. /** @inheritdoc */
  5930. async _updateObject(event, formData) {
  5931. const updates = foundry.utils.expandObject(formData);
  5932. const typeChange = "configuration.type" in formData;
  5933. if ( typeChange && (updates.configuration.type !== this.advancement.configuration.type) ) {
  5934. // Clear existing scale value data to prevent error during type update
  5935. await this.advancement.update(Array.fromRange(CONFIG.DND5E.maxLevel, 1).reduce((obj, lvl) => {
  5936. obj[`configuration.scale.-=${lvl}`] = null;
  5937. return obj;
  5938. }, {}));
  5939. updates.configuration.scale ??= {};
  5940. const OriginalType = TYPES[this.advancement.configuration.type];
  5941. const NewType = TYPES[updates.configuration.type];
  5942. for ( const [lvl, data] of Object.entries(updates.configuration.scale) ) {
  5943. const original = new OriginalType(data, { parent: this.advancement });
  5944. updates.configuration.scale[lvl] = NewType.convertFrom(original)?.toObject();
  5945. }
  5946. }
  5947. return super._updateObject(event, foundry.utils.flattenObject(updates));
  5948. }
  5949. }
  5950. /**
  5951. * Inline application that displays any changes to a scale value.
  5952. */
  5953. class ScaleValueFlow extends AdvancementFlow {
  5954. /** @inheritdoc */
  5955. static get defaultOptions() {
  5956. return foundry.utils.mergeObject(super.defaultOptions, {
  5957. template: "systems/dnd5e/templates/advancement/scale-value-flow.hbs"
  5958. });
  5959. }
  5960. /* -------------------------------------------- */
  5961. /** @inheritdoc */
  5962. getData() {
  5963. return foundry.utils.mergeObject(super.getData(), {
  5964. initial: this.advancement.valueForLevel(this.level - 1)?.display,
  5965. final: this.advancement.valueForLevel(this.level).display
  5966. });
  5967. }
  5968. }
  5969. /**
  5970. * Advancement that represents a value that scales with class level. **Can only be added to classes or subclasses.**
  5971. */
  5972. class ScaleValueAdvancement extends Advancement {
  5973. /** @inheritdoc */
  5974. static get metadata() {
  5975. return foundry.utils.mergeObject(super.metadata, {
  5976. dataModels: {
  5977. configuration: ScaleValueConfigurationData
  5978. },
  5979. order: 60,
  5980. icon: "systems/dnd5e/icons/svg/scale-value.svg",
  5981. title: game.i18n.localize("DND5E.AdvancementScaleValueTitle"),
  5982. hint: game.i18n.localize("DND5E.AdvancementScaleValueHint"),
  5983. multiLevel: true,
  5984. validItemTypes: new Set(["class", "subclass"]),
  5985. apps: {
  5986. config: ScaleValueConfig,
  5987. flow: ScaleValueFlow
  5988. }
  5989. });
  5990. }
  5991. /* -------------------------------------------- */
  5992. /**
  5993. * The available types of scaling value.
  5994. * @enum {ScaleValueType}
  5995. */
  5996. static TYPES = TYPES;
  5997. /* -------------------------------------------- */
  5998. /* Instance Properties */
  5999. /* -------------------------------------------- */
  6000. /** @inheritdoc */
  6001. get levels() {
  6002. return Array.from(Object.keys(this.configuration.scale).map(l => Number(l)));
  6003. }
  6004. /* -------------------------------------------- */
  6005. /**
  6006. * Identifier for this scale value, either manual value or the slugified title.
  6007. * @type {string}
  6008. */
  6009. get identifier() {
  6010. return this.configuration.identifier || this.title.slugify();
  6011. }
  6012. /* -------------------------------------------- */
  6013. /* Display Methods */
  6014. /* -------------------------------------------- */
  6015. /** @inheritdoc */
  6016. titleForLevel(level, { configMode=false }={}) {
  6017. const value = this.valueForLevel(level)?.display;
  6018. if ( !value ) return this.title;
  6019. return `${this.title}: <strong>${value}</strong>`;
  6020. }
  6021. /* -------------------------------------------- */
  6022. /**
  6023. * Scale value for the given level.
  6024. * @param {number} level Level for which to get the scale value.
  6025. * @returns {ScaleValueType} Scale value at the given level or null if none exists.
  6026. */
  6027. valueForLevel(level) {
  6028. const key = Object.keys(this.configuration.scale).reverse().find(l => Number(l) <= level);
  6029. const data = this.configuration.scale[key];
  6030. const TypeClass = this.constructor.TYPES[this.configuration.type];
  6031. if ( !data || !TypeClass ) return null;
  6032. return new TypeClass(data, { parent: this });
  6033. }
  6034. /* -------------------------------------------- */
  6035. /**
  6036. * Compare two scaling values and determine if they are equal.
  6037. * @param {*} a
  6038. * @param {*} b
  6039. * @returns {boolean}
  6040. */
  6041. testEquality(a, b) {
  6042. const keys = Object.keys(a ?? {});
  6043. if ( keys.length !== Object.keys(b ?? {}).length ) return false;
  6044. for ( const k of keys ) {
  6045. if ( a[k] !== b[k] ) return false;
  6046. }
  6047. return true;
  6048. }
  6049. }
  6050. var _module$b = /*#__PURE__*/Object.freeze({
  6051. __proto__: null,
  6052. AbilityScoreImprovementAdvancement: AbilityScoreImprovementAdvancement,
  6053. Advancement: Advancement,
  6054. HitPointsAdvancement: HitPointsAdvancement,
  6055. ItemChoiceAdvancement: ItemChoiceAdvancement,
  6056. ItemGrantAdvancement: ItemGrantAdvancement,
  6057. ScaleValueAdvancement: ScaleValueAdvancement
  6058. });
  6059. // Namespace Configuration Values
  6060. const DND5E = {};
  6061. // ASCII Artwork
  6062. DND5E.ASCII = `_______________________________
  6063. ______ ______ _____ _____
  6064. | _ \\___ | _ \\ ___| ___|
  6065. | | | ( _ ) | | | |___ \\| |__
  6066. | | | / _ \\/\\ | | | \\ \\ __|
  6067. | |/ / (_> < |/ //\\__/ / |___
  6068. |___/ \\___/\\/___/ \\____/\\____/
  6069. _______________________________`;
  6070. /**
  6071. * Configuration data for abilities.
  6072. *
  6073. * @typedef {object} AbilityConfiguration
  6074. * @property {string} label Localized label.
  6075. * @property {string} abbreviation Localized abbreviation.
  6076. * @property {string} [type] Whether this is a "physical" or "mental" ability.
  6077. * @property {Object<string, number|string>} [defaults] Default values for this ability based on actor type.
  6078. * If a string is used, the system will attempt to fetch.
  6079. * the value of the specified ability.
  6080. */
  6081. /**
  6082. * The set of Ability Scores used within the system.
  6083. * @enum {AbilityConfiguration}
  6084. */
  6085. DND5E.abilities = {
  6086. str: {
  6087. label: "DND5E.AbilityStr",
  6088. abbreviation: "DND5E.AbilityStrAbbr",
  6089. type: "physical"
  6090. },
  6091. dex: {
  6092. label: "DND5E.AbilityDex",
  6093. abbreviation: "DND5E.AbilityDexAbbr",
  6094. type: "physical"
  6095. },
  6096. con: {
  6097. label: "DND5E.AbilityCon",
  6098. abbreviation: "DND5E.AbilityConAbbr",
  6099. type: "physical"
  6100. },
  6101. int: {
  6102. label: "DND5E.AbilityInt",
  6103. abbreviation: "DND5E.AbilityIntAbbr",
  6104. type: "mental",
  6105. defaults: { vehicle: 0 }
  6106. },
  6107. wis: {
  6108. label: "DND5E.AbilityWis",
  6109. abbreviation: "DND5E.AbilityWisAbbr",
  6110. type: "mental",
  6111. defaults: { vehicle: 0 }
  6112. },
  6113. cha: {
  6114. label: "DND5E.AbilityCha",
  6115. abbreviation: "DND5E.AbilityChaAbbr",
  6116. type: "mental",
  6117. defaults: { vehicle: 0 }
  6118. },
  6119. hon: {
  6120. label: "DND5E.AbilityHon",
  6121. abbreviation: "DND5E.AbilityHonAbbr",
  6122. type: "mental",
  6123. defaults: { npc: "cha", vehicle: 0 },
  6124. improvement: false
  6125. },
  6126. san: {
  6127. label: "DND5E.AbilitySan",
  6128. abbreviation: "DND5E.AbilitySanAbbr",
  6129. type: "mental",
  6130. defaults: { npc: "wis", vehicle: 0 },
  6131. improvement: false
  6132. }
  6133. };
  6134. preLocalize("abilities", { keys: ["label", "abbreviation"] });
  6135. patchConfig("abilities", "label", { since: 2.2, until: 2.4 });
  6136. Object.defineProperty(DND5E, "abilityAbbreviations", {
  6137. get() {
  6138. foundry.utils.logCompatibilityWarning(
  6139. "The `abilityAbbreviations` configuration object has been merged with `abilities`.",
  6140. { since: "DnD5e 2.2", until: "DnD5e 2.4" }
  6141. );
  6142. return Object.fromEntries(Object.entries(DND5E.abilities).map(([k, v]) => [k, v.abbreviation]));
  6143. }
  6144. });
  6145. /**
  6146. * Configure which ability score is used as the default modifier for initiative rolls.
  6147. * @type {string}
  6148. */
  6149. DND5E.initiativeAbility = "dex";
  6150. /**
  6151. * Configure which ability score is used when calculating hit points per level.
  6152. * @type {string}
  6153. */
  6154. DND5E.hitPointsAbility = "con";
  6155. /* -------------------------------------------- */
  6156. /**
  6157. * Configuration data for skills.
  6158. *
  6159. * @typedef {object} SkillConfiguration
  6160. * @property {string} label Localized label.
  6161. * @property {string} ability Key for the default ability used by this skill.
  6162. */
  6163. /**
  6164. * The set of skill which can be trained with their default ability scores.
  6165. * @enum {SkillConfiguration}
  6166. */
  6167. DND5E.skills = {
  6168. acr: { label: "DND5E.SkillAcr", ability: "dex" },
  6169. ani: { label: "DND5E.SkillAni", ability: "wis" },
  6170. arc: { label: "DND5E.SkillArc", ability: "int" },
  6171. ath: { label: "DND5E.SkillAth", ability: "str" },
  6172. dec: { label: "DND5E.SkillDec", ability: "cha" },
  6173. his: { label: "DND5E.SkillHis", ability: "int" },
  6174. ins: { label: "DND5E.SkillIns", ability: "wis" },
  6175. itm: { label: "DND5E.SkillItm", ability: "cha" },
  6176. inv: { label: "DND5E.SkillInv", ability: "int" },
  6177. med: { label: "DND5E.SkillMed", ability: "wis" },
  6178. nat: { label: "DND5E.SkillNat", ability: "int" },
  6179. prc: { label: "DND5E.SkillPrc", ability: "wis" },
  6180. prf: { label: "DND5E.SkillPrf", ability: "cha" },
  6181. per: { label: "DND5E.SkillPer", ability: "cha" },
  6182. rel: { label: "DND5E.SkillRel", ability: "int" },
  6183. slt: { label: "DND5E.SkillSlt", ability: "dex" },
  6184. ste: { label: "DND5E.SkillSte", ability: "dex" },
  6185. sur: { label: "DND5E.SkillSur", ability: "wis" }
  6186. };
  6187. preLocalize("skills", { key: "label", sort: true });
  6188. /* -------------------------------------------- */
  6189. /**
  6190. * Character alignment options.
  6191. * @enum {string}
  6192. */
  6193. DND5E.alignments = {
  6194. lg: "DND5E.AlignmentLG",
  6195. ng: "DND5E.AlignmentNG",
  6196. cg: "DND5E.AlignmentCG",
  6197. ln: "DND5E.AlignmentLN",
  6198. tn: "DND5E.AlignmentTN",
  6199. cn: "DND5E.AlignmentCN",
  6200. le: "DND5E.AlignmentLE",
  6201. ne: "DND5E.AlignmentNE",
  6202. ce: "DND5E.AlignmentCE"
  6203. };
  6204. preLocalize("alignments");
  6205. /* -------------------------------------------- */
  6206. /**
  6207. * An enumeration of item attunement types.
  6208. * @enum {number}
  6209. */
  6210. DND5E.attunementTypes = {
  6211. NONE: 0,
  6212. REQUIRED: 1,
  6213. ATTUNED: 2
  6214. };
  6215. /**
  6216. * An enumeration of item attunement states.
  6217. * @type {{"0": string, "1": string, "2": string}}
  6218. */
  6219. DND5E.attunements = {
  6220. 0: "DND5E.AttunementNone",
  6221. 1: "DND5E.AttunementRequired",
  6222. 2: "DND5E.AttunementAttuned"
  6223. };
  6224. preLocalize("attunements");
  6225. /* -------------------------------------------- */
  6226. /**
  6227. * General weapon categories.
  6228. * @enum {string}
  6229. */
  6230. DND5E.weaponProficiencies = {
  6231. sim: "DND5E.WeaponSimpleProficiency",
  6232. mar: "DND5E.WeaponMartialProficiency"
  6233. };
  6234. preLocalize("weaponProficiencies");
  6235. /**
  6236. * A mapping between `DND5E.weaponTypes` and `DND5E.weaponProficiencies` that
  6237. * is used to determine if character has proficiency when adding an item.
  6238. * @enum {(boolean|string)}
  6239. */
  6240. DND5E.weaponProficienciesMap = {
  6241. simpleM: "sim",
  6242. simpleR: "sim",
  6243. martialM: "mar",
  6244. martialR: "mar"
  6245. };
  6246. /**
  6247. * The basic weapon types in 5e. This enables specific weapon proficiencies or
  6248. * starting equipment provided by classes and backgrounds.
  6249. * @enum {string}
  6250. */
  6251. DND5E.weaponIds = {
  6252. battleaxe: "I0WocDSuNpGJayPb",
  6253. blowgun: "wNWK6yJMHG9ANqQV",
  6254. club: "nfIRTECQIG81CvM4",
  6255. dagger: "0E565kQUBmndJ1a2",
  6256. dart: "3rCO8MTIdPGSW6IJ",
  6257. flail: "UrH3sMdnUDckIHJ6",
  6258. glaive: "rOG1OM2ihgPjOvFW",
  6259. greataxe: "1Lxk6kmoRhG8qQ0u",
  6260. greatclub: "QRCsxkCwWNwswL9o",
  6261. greatsword: "xMkP8BmFzElcsMaR",
  6262. halberd: "DMejWAc8r8YvDPP1",
  6263. handaxe: "eO7Fbv5WBk5zvGOc",
  6264. handcrossbow: "qaSro7kFhxD6INbZ",
  6265. heavycrossbow: "RmP0mYRn2J7K26rX",
  6266. javelin: "DWLMnODrnHn8IbAG",
  6267. lance: "RnuxdHUAIgxccVwj",
  6268. lightcrossbow: "ddWvQRLmnnIS0eLF",
  6269. lighthammer: "XVK6TOL4sGItssAE",
  6270. longbow: "3cymOVja8jXbzrdT",
  6271. longsword: "10ZP2Bu3vnCuYMIB",
  6272. mace: "Ajyq6nGwF7FtLhDQ",
  6273. maul: "DizirD7eqjh8n95A",
  6274. morningstar: "dX8AxCh9o0A9CkT3",
  6275. net: "aEiM49V8vWpWw7rU",
  6276. pike: "tC0kcqZT9HHAO0PD",
  6277. quarterstaff: "g2dWN7PQiMRYWzyk",
  6278. rapier: "Tobce1hexTnDk4sV",
  6279. scimitar: "fbC0Mg1a73wdFbqO",
  6280. shortsword: "osLzOwQdPtrK3rQH",
  6281. sickle: "i4NeNZ30ycwPDHMx",
  6282. spear: "OG4nBBydvmfWYXIk",
  6283. shortbow: "GJv6WkD7D2J6rP6M",
  6284. sling: "3gynWO9sN4OLGMWD",
  6285. trident: "F65ANO66ckP8FDMa",
  6286. warpick: "2YdfjN1PIIrSHZii",
  6287. warhammer: "F0Df164Xv1gWcYt0",
  6288. whip: "QKTyxoO0YDnAsbYe"
  6289. };
  6290. /* -------------------------------------------- */
  6291. /**
  6292. * The basic ammunition types.
  6293. * @enum {string}
  6294. */
  6295. DND5E.ammoIds = {
  6296. arrow: "3c7JXOzsv55gqJS5",
  6297. blowgunNeedle: "gBQ8xqTA5f8wP5iu",
  6298. crossbowBolt: "SItCnYBqhzqBoaWG",
  6299. slingBullet: "z9SbsMIBZzuhZOqT"
  6300. };
  6301. /* -------------------------------------------- */
  6302. /**
  6303. * The categories into which Tool items can be grouped.
  6304. *
  6305. * @enum {string}
  6306. */
  6307. DND5E.toolTypes = {
  6308. art: "DND5E.ToolArtisans",
  6309. game: "DND5E.ToolGamingSet",
  6310. music: "DND5E.ToolMusicalInstrument"
  6311. };
  6312. preLocalize("toolTypes", { sort: true });
  6313. /**
  6314. * The categories of tool proficiencies that a character can gain.
  6315. *
  6316. * @enum {string}
  6317. */
  6318. DND5E.toolProficiencies = {
  6319. ...DND5E.toolTypes,
  6320. vehicle: "DND5E.ToolVehicle"
  6321. };
  6322. preLocalize("toolProficiencies", { sort: true });
  6323. /**
  6324. * The basic tool types in 5e. This enables specific tool proficiencies or
  6325. * starting equipment provided by classes and backgrounds.
  6326. * @enum {string}
  6327. */
  6328. DND5E.toolIds = {
  6329. alchemist: "SztwZhbhZeCqyAes",
  6330. bagpipes: "yxHi57T5mmVt0oDr",
  6331. brewer: "Y9S75go1hLMXUD48",
  6332. calligrapher: "jhjo20QoiD5exf09",
  6333. card: "YwlHI3BVJapz4a3E",
  6334. carpenter: "8NS6MSOdXtUqD7Ib",
  6335. cartographer: "fC0lFK8P4RuhpfaU",
  6336. chess: "23y8FvWKf9YLcnBL",
  6337. cobbler: "hM84pZnpCqKfi8XH",
  6338. cook: "Gflnp29aEv5Lc1ZM",
  6339. dice: "iBuTM09KD9IoM5L8",
  6340. disg: "IBhDAr7WkhWPYLVn",
  6341. drum: "69Dpr25pf4BjkHKb",
  6342. dulcimer: "NtdDkjmpdIMiX7I2",
  6343. flute: "eJOrPcAz9EcquyRQ",
  6344. forg: "cG3m4YlHfbQlLEOx",
  6345. glassblower: "rTbVrNcwApnuTz5E",
  6346. herb: "i89okN7GFTWHsvPy",
  6347. horn: "aa9KuBy4dst7WIW9",
  6348. jeweler: "YfBwELTgPFHmQdHh",
  6349. leatherworker: "PUMfwyVUbtyxgYbD",
  6350. lute: "qBydtUUIkv520DT7",
  6351. lyre: "EwG1EtmbgR3bM68U",
  6352. mason: "skUih6tBvcBbORzA",
  6353. navg: "YHCmjsiXxZ9UdUhU",
  6354. painter: "ccm5xlWhx74d6lsK",
  6355. panflute: "G5m5gYIx9VAUWC3J",
  6356. pois: "il2GNi8C0DvGLL9P",
  6357. potter: "hJS8yEVkqgJjwfWa",
  6358. shawm: "G3cqbejJpfB91VhP",
  6359. smith: "KndVe2insuctjIaj",
  6360. thief: "woWZ1sO5IUVGzo58",
  6361. tinker: "0d08g1i5WXnNrCNA",
  6362. viol: "baoe3U5BfMMMxhCU",
  6363. weaver: "ap9prThUB2y9lDyj",
  6364. woodcarver: "xKErqkLo4ASYr5EP"
  6365. };
  6366. /* -------------------------------------------- */
  6367. /**
  6368. * Time periods that accept a numeric value.
  6369. * @enum {string}
  6370. */
  6371. DND5E.scalarTimePeriods = {
  6372. turn: "DND5E.TimeTurn",
  6373. round: "DND5E.TimeRound",
  6374. minute: "DND5E.TimeMinute",
  6375. hour: "DND5E.TimeHour",
  6376. day: "DND5E.TimeDay",
  6377. month: "DND5E.TimeMonth",
  6378. year: "DND5E.TimeYear"
  6379. };
  6380. preLocalize("scalarTimePeriods");
  6381. /* -------------------------------------------- */
  6382. /**
  6383. * Time periods for spells that don't have a defined ending.
  6384. * @enum {string}
  6385. */
  6386. DND5E.permanentTimePeriods = {
  6387. disp: "DND5E.TimeDisp",
  6388. dstr: "DND5E.TimeDispTrig",
  6389. perm: "DND5E.TimePerm"
  6390. };
  6391. preLocalize("permanentTimePeriods");
  6392. /* -------------------------------------------- */
  6393. /**
  6394. * Time periods that don't accept a numeric value.
  6395. * @enum {string}
  6396. */
  6397. DND5E.specialTimePeriods = {
  6398. inst: "DND5E.TimeInst",
  6399. spec: "DND5E.Special"
  6400. };
  6401. preLocalize("specialTimePeriods");
  6402. /* -------------------------------------------- */
  6403. /**
  6404. * The various lengths of time over which effects can occur.
  6405. * @enum {string}
  6406. */
  6407. DND5E.timePeriods = {
  6408. ...DND5E.specialTimePeriods,
  6409. ...DND5E.permanentTimePeriods,
  6410. ...DND5E.scalarTimePeriods
  6411. };
  6412. preLocalize("timePeriods");
  6413. /* -------------------------------------------- */
  6414. /**
  6415. * Various ways in which an item or ability can be activated.
  6416. * @enum {string}
  6417. */
  6418. DND5E.abilityActivationTypes = {
  6419. action: "DND5E.Action",
  6420. bonus: "DND5E.BonusAction",
  6421. reaction: "DND5E.Reaction",
  6422. minute: DND5E.timePeriods.minute,
  6423. hour: DND5E.timePeriods.hour,
  6424. day: DND5E.timePeriods.day,
  6425. special: DND5E.timePeriods.spec,
  6426. legendary: "DND5E.LegendaryActionLabel",
  6427. mythic: "DND5E.MythicActionLabel",
  6428. lair: "DND5E.LairActionLabel",
  6429. crew: "DND5E.VehicleCrewAction"
  6430. };
  6431. preLocalize("abilityActivationTypes");
  6432. /* -------------------------------------------- */
  6433. /**
  6434. * Different things that an ability can consume upon use.
  6435. * @enum {string}
  6436. */
  6437. DND5E.abilityConsumptionTypes = {
  6438. ammo: "DND5E.ConsumeAmmunition",
  6439. attribute: "DND5E.ConsumeAttribute",
  6440. hitDice: "DND5E.ConsumeHitDice",
  6441. material: "DND5E.ConsumeMaterial",
  6442. charges: "DND5E.ConsumeCharges"
  6443. };
  6444. preLocalize("abilityConsumptionTypes", { sort: true });
  6445. /* -------------------------------------------- */
  6446. /**
  6447. * Creature sizes.
  6448. * @enum {string}
  6449. */
  6450. DND5E.actorSizes = {
  6451. tiny: "DND5E.SizeTiny",
  6452. sm: "DND5E.SizeSmall",
  6453. med: "DND5E.SizeMedium",
  6454. lg: "DND5E.SizeLarge",
  6455. huge: "DND5E.SizeHuge",
  6456. grg: "DND5E.SizeGargantuan"
  6457. };
  6458. preLocalize("actorSizes");
  6459. /**
  6460. * Default token image size for the values of `DND5E.actorSizes`.
  6461. * @enum {number}
  6462. */
  6463. DND5E.tokenSizes = {
  6464. tiny: 0.5,
  6465. sm: 1,
  6466. med: 1,
  6467. lg: 2,
  6468. huge: 3,
  6469. grg: 4
  6470. };
  6471. /**
  6472. * Colors used to visualize temporary and temporary maximum HP in token health bars.
  6473. * @enum {number}
  6474. */
  6475. DND5E.tokenHPColors = {
  6476. damage: 0xFF0000,
  6477. healing: 0x00FF00,
  6478. temp: 0x66CCFF,
  6479. tempmax: 0x440066,
  6480. negmax: 0x550000
  6481. };
  6482. /* -------------------------------------------- */
  6483. /**
  6484. * Default types of creatures.
  6485. * *Note: Not pre-localized to allow for easy fetching of pluralized forms.*
  6486. * @enum {string}
  6487. */
  6488. DND5E.creatureTypes = {
  6489. aberration: "DND5E.CreatureAberration",
  6490. beast: "DND5E.CreatureBeast",
  6491. celestial: "DND5E.CreatureCelestial",
  6492. construct: "DND5E.CreatureConstruct",
  6493. dragon: "DND5E.CreatureDragon",
  6494. elemental: "DND5E.CreatureElemental",
  6495. fey: "DND5E.CreatureFey",
  6496. fiend: "DND5E.CreatureFiend",
  6497. giant: "DND5E.CreatureGiant",
  6498. humanoid: "DND5E.CreatureHumanoid",
  6499. monstrosity: "DND5E.CreatureMonstrosity",
  6500. ooze: "DND5E.CreatureOoze",
  6501. plant: "DND5E.CreaturePlant",
  6502. undead: "DND5E.CreatureUndead"
  6503. };
  6504. /* -------------------------------------------- */
  6505. /**
  6506. * Classification types for item action types.
  6507. * @enum {string}
  6508. */
  6509. DND5E.itemActionTypes = {
  6510. mwak: "DND5E.ActionMWAK",
  6511. rwak: "DND5E.ActionRWAK",
  6512. msak: "DND5E.ActionMSAK",
  6513. rsak: "DND5E.ActionRSAK",
  6514. save: "DND5E.ActionSave",
  6515. heal: "DND5E.ActionHeal",
  6516. abil: "DND5E.ActionAbil",
  6517. util: "DND5E.ActionUtil",
  6518. other: "DND5E.ActionOther"
  6519. };
  6520. preLocalize("itemActionTypes");
  6521. /* -------------------------------------------- */
  6522. /**
  6523. * Different ways in which item capacity can be limited.
  6524. * @enum {string}
  6525. */
  6526. DND5E.itemCapacityTypes = {
  6527. items: "DND5E.ItemContainerCapacityItems",
  6528. weight: "DND5E.ItemContainerCapacityWeight"
  6529. };
  6530. preLocalize("itemCapacityTypes", { sort: true });
  6531. /* -------------------------------------------- */
  6532. /**
  6533. * List of various item rarities.
  6534. * @enum {string}
  6535. */
  6536. DND5E.itemRarity = {
  6537. common: "DND5E.ItemRarityCommon",
  6538. uncommon: "DND5E.ItemRarityUncommon",
  6539. rare: "DND5E.ItemRarityRare",
  6540. veryRare: "DND5E.ItemRarityVeryRare",
  6541. legendary: "DND5E.ItemRarityLegendary",
  6542. artifact: "DND5E.ItemRarityArtifact"
  6543. };
  6544. preLocalize("itemRarity");
  6545. /* -------------------------------------------- */
  6546. /**
  6547. * Enumerate the lengths of time over which an item can have limited use ability.
  6548. * @enum {string}
  6549. */
  6550. DND5E.limitedUsePeriods = {
  6551. sr: "DND5E.ShortRest",
  6552. lr: "DND5E.LongRest",
  6553. day: "DND5E.Day",
  6554. charges: "DND5E.Charges"
  6555. };
  6556. preLocalize("limitedUsePeriods");
  6557. /* -------------------------------------------- */
  6558. /**
  6559. * Specific equipment types that modify base AC.
  6560. * @enum {string}
  6561. */
  6562. DND5E.armorTypes = {
  6563. light: "DND5E.EquipmentLight",
  6564. medium: "DND5E.EquipmentMedium",
  6565. heavy: "DND5E.EquipmentHeavy",
  6566. natural: "DND5E.EquipmentNatural",
  6567. shield: "DND5E.EquipmentShield"
  6568. };
  6569. preLocalize("armorTypes");
  6570. /* -------------------------------------------- */
  6571. /**
  6572. * Equipment types that aren't armor.
  6573. * @enum {string}
  6574. */
  6575. DND5E.miscEquipmentTypes = {
  6576. clothing: "DND5E.EquipmentClothing",
  6577. trinket: "DND5E.EquipmentTrinket",
  6578. vehicle: "DND5E.EquipmentVehicle"
  6579. };
  6580. preLocalize("miscEquipmentTypes", { sort: true });
  6581. /* -------------------------------------------- */
  6582. /**
  6583. * The set of equipment types for armor, clothing, and other objects which can be worn by the character.
  6584. * @enum {string}
  6585. */
  6586. DND5E.equipmentTypes = {
  6587. ...DND5E.miscEquipmentTypes,
  6588. ...DND5E.armorTypes
  6589. };
  6590. preLocalize("equipmentTypes", { sort: true });
  6591. /* -------------------------------------------- */
  6592. /**
  6593. * The various types of vehicles in which characters can be proficient.
  6594. * @enum {string}
  6595. */
  6596. DND5E.vehicleTypes = {
  6597. air: "DND5E.VehicleTypeAir",
  6598. land: "DND5E.VehicleTypeLand",
  6599. space: "DND5E.VehicleTypeSpace",
  6600. water: "DND5E.VehicleTypeWater"
  6601. };
  6602. preLocalize("vehicleTypes", { sort: true });
  6603. /* -------------------------------------------- */
  6604. /**
  6605. * The set of Armor Proficiencies which a character may have.
  6606. * @type {object}
  6607. */
  6608. DND5E.armorProficiencies = {
  6609. lgt: DND5E.equipmentTypes.light,
  6610. med: DND5E.equipmentTypes.medium,
  6611. hvy: DND5E.equipmentTypes.heavy,
  6612. shl: "DND5E.EquipmentShieldProficiency"
  6613. };
  6614. preLocalize("armorProficiencies");
  6615. /**
  6616. * A mapping between `DND5E.equipmentTypes` and `DND5E.armorProficiencies` that
  6617. * is used to determine if character has proficiency when adding an item.
  6618. * @enum {(boolean|string)}
  6619. */
  6620. DND5E.armorProficienciesMap = {
  6621. natural: true,
  6622. clothing: true,
  6623. light: "lgt",
  6624. medium: "med",
  6625. heavy: "hvy",
  6626. shield: "shl"
  6627. };
  6628. /**
  6629. * The basic armor types in 5e. This enables specific armor proficiencies,
  6630. * automated AC calculation in NPCs, and starting equipment.
  6631. * @enum {string}
  6632. */
  6633. DND5E.armorIds = {
  6634. breastplate: "SK2HATQ4abKUlV8i",
  6635. chainmail: "rLMflzmxpe8JGTOA",
  6636. chainshirt: "p2zChy24ZJdVqMSH",
  6637. halfplate: "vsgmACFYINloIdPm",
  6638. hide: "n1V07puo0RQxPGuF",
  6639. leather: "WwdpHLXGX5r8uZu5",
  6640. padded: "GtKV1b5uqFQqpEni",
  6641. plate: "OjkIqlW2UpgFcjZa",
  6642. ringmail: "nsXZejlmgalj4he9",
  6643. scalemail: "XmnlF5fgIO3tg6TG",
  6644. splint: "cKpJmsJmU8YaiuqG",
  6645. studded: "TIV3B1vbrVHIhQAm"
  6646. };
  6647. /**
  6648. * The basic shield in 5e.
  6649. * @enum {string}
  6650. */
  6651. DND5E.shieldIds = {
  6652. shield: "sSs3hSzkKBMNBgTs"
  6653. };
  6654. /**
  6655. * Common armor class calculations.
  6656. * @enum {{ label: string, [formula]: string }}
  6657. */
  6658. DND5E.armorClasses = {
  6659. flat: {
  6660. label: "DND5E.ArmorClassFlat",
  6661. formula: "@attributes.ac.flat"
  6662. },
  6663. natural: {
  6664. label: "DND5E.ArmorClassNatural",
  6665. formula: "@attributes.ac.flat"
  6666. },
  6667. default: {
  6668. label: "DND5E.ArmorClassEquipment",
  6669. formula: "@attributes.ac.armor + @attributes.ac.dex"
  6670. },
  6671. mage: {
  6672. label: "DND5E.ArmorClassMage",
  6673. formula: "13 + @abilities.dex.mod"
  6674. },
  6675. draconic: {
  6676. label: "DND5E.ArmorClassDraconic",
  6677. formula: "13 + @abilities.dex.mod"
  6678. },
  6679. unarmoredMonk: {
  6680. label: "DND5E.ArmorClassUnarmoredMonk",
  6681. formula: "10 + @abilities.dex.mod + @abilities.wis.mod"
  6682. },
  6683. unarmoredBarb: {
  6684. label: "DND5E.ArmorClassUnarmoredBarbarian",
  6685. formula: "10 + @abilities.dex.mod + @abilities.con.mod"
  6686. },
  6687. custom: {
  6688. label: "DND5E.ArmorClassCustom"
  6689. }
  6690. };
  6691. preLocalize("armorClasses", { key: "label" });
  6692. /* -------------------------------------------- */
  6693. /**
  6694. * Enumerate the valid consumable types which are recognized by the system.
  6695. * @enum {string}
  6696. */
  6697. DND5E.consumableTypes = {
  6698. ammo: "DND5E.ConsumableAmmo",
  6699. potion: "DND5E.ConsumablePotion",
  6700. poison: "DND5E.ConsumablePoison",
  6701. food: "DND5E.ConsumableFood",
  6702. scroll: "DND5E.ConsumableScroll",
  6703. wand: "DND5E.ConsumableWand",
  6704. rod: "DND5E.ConsumableRod",
  6705. trinket: "DND5E.ConsumableTrinket"
  6706. };
  6707. preLocalize("consumableTypes", { sort: true });
  6708. /* -------------------------------------------- */
  6709. /**
  6710. * Types of containers.
  6711. * @enum {string}
  6712. */
  6713. DND5E.containerTypes = {
  6714. backpack: "H8YCd689ezlD26aT",
  6715. barrel: "7Yqbqg5EtVW16wfT",
  6716. basket: "Wv7HzD6dv1P0q78N",
  6717. boltcase: "eJtPBiZtr2pp6ynt",
  6718. bottle: "HZp69hhyNZUUCipF",
  6719. bucket: "mQVYcHmMSoCUnBnM",
  6720. case: "5mIeX824uMklU3xq",
  6721. chest: "2YbuclKfhDL0bU4u",
  6722. flask: "lHS63sC6bypENNlR",
  6723. jug: "0ZBWwjFz3nIAXMLW",
  6724. pot: "M8xM8BLK4tpUayEE",
  6725. pitcher: "nXWdGtzi8DXDLLsL",
  6726. pouch: "9bWTRRDym06PzSAf",
  6727. quiver: "4MtQKPn9qMWCFjDA",
  6728. sack: "CNdDj8dsXVpRVpXt",
  6729. saddlebags: "TmfaFUSZJAotndn9",
  6730. tankard: "uw6fINSmZ2j2o57A",
  6731. vial: "meJEfX3gZgtMX4x2"
  6732. };
  6733. /* -------------------------------------------- */
  6734. /**
  6735. * Configuration data for spellcasting foci.
  6736. *
  6737. * @typedef {object} SpellcastingFocusConfiguration
  6738. * @property {string} label Localized label for this category.
  6739. * @property {Object<string, string>} itemIds Item IDs or UUIDs.
  6740. */
  6741. /**
  6742. * Type of spellcasting foci.
  6743. * @enum {SpellcastingFocusConfiguration}
  6744. */
  6745. DND5E.focusTypes = {
  6746. arcane: {
  6747. label: "DND5E.Focus.Arcane",
  6748. itemIds: {
  6749. crystal: "uXOT4fYbgPY8DGdd",
  6750. orb: "tH5Rn0JVRG1zdmPa",
  6751. rod: "OojyyGfh91iViuMF",
  6752. staff: "BeKIrNIvNHRPQ4t5",
  6753. wand: "KA2P6I48iOWlnboO"
  6754. }
  6755. },
  6756. druidic: {
  6757. label: "DND5E.Focus.Druidic",
  6758. itemIds: {
  6759. mistletoe: "xDK9GQd2iqOGH8Sd",
  6760. totem: "PGL6aaM0wE5h0VN5",
  6761. woodenstaff: "FF1ktpb2YSiyv896",
  6762. yewwand: "t5yP0d7YaKwuKKiH"
  6763. }
  6764. },
  6765. holy: {
  6766. label: "DND5E.Focus.Holy",
  6767. itemIds: {
  6768. amulet: "paqlMjggWkBIAeCe",
  6769. emblem: "laVqttkGMW4B9654",
  6770. reliquary: "gP1URGq3kVIIFHJ7"
  6771. }
  6772. }
  6773. };
  6774. /* -------------------------------------------- */
  6775. /**
  6776. * Configuration data for an item with the "feature" type.
  6777. *
  6778. * @typedef {object} FeatureTypeConfiguration
  6779. * @property {string} label Localized label for this type.
  6780. * @property {Object<string, string>} [subtypes] Enum containing localized labels for subtypes.
  6781. */
  6782. /**
  6783. * Types of "features" items.
  6784. * @enum {FeatureTypeConfiguration}
  6785. */
  6786. DND5E.featureTypes = {
  6787. background: {
  6788. label: "DND5E.Feature.Background"
  6789. },
  6790. class: {
  6791. label: "DND5E.Feature.Class",
  6792. subtypes: {
  6793. arcaneShot: "DND5E.ClassFeature.ArcaneShot",
  6794. artificerInfusion: "DND5E.ClassFeature.ArtificerInfusion",
  6795. channelDivinity: "DND5E.ClassFeature.ChannelDivinity",
  6796. defensiveTactic: "DND5E.ClassFeature.DefensiveTactic",
  6797. eldritchInvocation: "DND5E.ClassFeature.EldritchInvocation",
  6798. elementalDiscipline: "DND5E.ClassFeature.ElementalDiscipline",
  6799. fightingStyle: "DND5E.ClassFeature.FightingStyle",
  6800. huntersPrey: "DND5E.ClassFeature.HuntersPrey",
  6801. ki: "DND5E.ClassFeature.Ki",
  6802. maneuver: "DND5E.ClassFeature.Maneuver",
  6803. metamagic: "DND5E.ClassFeature.Metamagic",
  6804. multiattack: "DND5E.ClassFeature.Multiattack",
  6805. pact: "DND5E.ClassFeature.PactBoon",
  6806. psionicPower: "DND5E.ClassFeature.PsionicPower",
  6807. rune: "DND5E.ClassFeature.Rune",
  6808. superiorHuntersDefense: "DND5E.ClassFeature.SuperiorHuntersDefense"
  6809. }
  6810. },
  6811. monster: {
  6812. label: "DND5E.Feature.Monster"
  6813. },
  6814. race: {
  6815. label: "DND5E.Feature.Race"
  6816. },
  6817. feat: {
  6818. label: "DND5E.Feature.Feat"
  6819. }
  6820. };
  6821. preLocalize("featureTypes", { key: "label" });
  6822. preLocalize("featureTypes.class.subtypes", { sort: true });
  6823. /* -------------------------------------------- */
  6824. /**
  6825. * @typedef {object} CurrencyConfiguration
  6826. * @property {string} label Localized label for the currency.
  6827. * @property {string} abbreviation Localized abbreviation for the currency.
  6828. * @property {number} conversion Number by which this currency should be multiplied to arrive at a standard value.
  6829. */
  6830. /**
  6831. * The valid currency denominations with localized labels, abbreviations, and conversions.
  6832. * The conversion number defines how many of that currency are equal to one GP.
  6833. * @enum {CurrencyConfiguration}
  6834. */
  6835. DND5E.currencies = {
  6836. pp: {
  6837. label: "DND5E.CurrencyPP",
  6838. abbreviation: "DND5E.CurrencyAbbrPP",
  6839. conversion: 0.1
  6840. },
  6841. gp: {
  6842. label: "DND5E.CurrencyGP",
  6843. abbreviation: "DND5E.CurrencyAbbrGP",
  6844. conversion: 1
  6845. },
  6846. ep: {
  6847. label: "DND5E.CurrencyEP",
  6848. abbreviation: "DND5E.CurrencyAbbrEP",
  6849. conversion: 2
  6850. },
  6851. sp: {
  6852. label: "DND5E.CurrencySP",
  6853. abbreviation: "DND5E.CurrencyAbbrSP",
  6854. conversion: 10
  6855. },
  6856. cp: {
  6857. label: "DND5E.CurrencyCP",
  6858. abbreviation: "DND5E.CurrencyAbbrCP",
  6859. conversion: 100
  6860. }
  6861. };
  6862. preLocalize("currencies", { keys: ["label", "abbreviation"] });
  6863. /* -------------------------------------------- */
  6864. /* Damage Types */
  6865. /* -------------------------------------------- */
  6866. /**
  6867. * Types of damage that are considered physical.
  6868. * @enum {string}
  6869. */
  6870. DND5E.physicalDamageTypes = {
  6871. bludgeoning: "DND5E.DamageBludgeoning",
  6872. piercing: "DND5E.DamagePiercing",
  6873. slashing: "DND5E.DamageSlashing"
  6874. };
  6875. preLocalize("physicalDamageTypes", { sort: true });
  6876. /* -------------------------------------------- */
  6877. /**
  6878. * Types of damage the can be caused by abilities.
  6879. * @enum {string}
  6880. */
  6881. DND5E.damageTypes = {
  6882. ...DND5E.physicalDamageTypes,
  6883. acid: "DND5E.DamageAcid",
  6884. cold: "DND5E.DamageCold",
  6885. fire: "DND5E.DamageFire",
  6886. force: "DND5E.DamageForce",
  6887. lightning: "DND5E.DamageLightning",
  6888. necrotic: "DND5E.DamageNecrotic",
  6889. poison: "DND5E.DamagePoison",
  6890. psychic: "DND5E.DamagePsychic",
  6891. radiant: "DND5E.DamageRadiant",
  6892. thunder: "DND5E.DamageThunder"
  6893. };
  6894. preLocalize("damageTypes", { sort: true });
  6895. /* -------------------------------------------- */
  6896. /**
  6897. * Types of damage to which an actor can possess resistance, immunity, or vulnerability.
  6898. * @enum {string}
  6899. * @deprecated
  6900. */
  6901. DND5E.damageResistanceTypes = {
  6902. ...DND5E.damageTypes,
  6903. physical: "DND5E.DamagePhysical"
  6904. };
  6905. preLocalize("damageResistanceTypes", { sort: true });
  6906. /* -------------------------------------------- */
  6907. /* Movement */
  6908. /* -------------------------------------------- */
  6909. /**
  6910. * Different types of healing that can be applied using abilities.
  6911. * @enum {string}
  6912. */
  6913. DND5E.healingTypes = {
  6914. healing: "DND5E.Healing",
  6915. temphp: "DND5E.HealingTemp"
  6916. };
  6917. preLocalize("healingTypes");
  6918. /* -------------------------------------------- */
  6919. /**
  6920. * The valid units of measure for movement distances in the game system.
  6921. * By default this uses the imperial units of feet and miles.
  6922. * @enum {string}
  6923. */
  6924. DND5E.movementTypes = {
  6925. burrow: "DND5E.MovementBurrow",
  6926. climb: "DND5E.MovementClimb",
  6927. fly: "DND5E.MovementFly",
  6928. swim: "DND5E.MovementSwim",
  6929. walk: "DND5E.MovementWalk"
  6930. };
  6931. preLocalize("movementTypes", { sort: true });
  6932. /* -------------------------------------------- */
  6933. /* Measurement */
  6934. /* -------------------------------------------- */
  6935. /**
  6936. * The valid units of measure for movement distances in the game system.
  6937. * By default this uses the imperial units of feet and miles.
  6938. * @enum {string}
  6939. */
  6940. DND5E.movementUnits = {
  6941. ft: "DND5E.DistFt",
  6942. mi: "DND5E.DistMi",
  6943. m: "DND5E.DistM",
  6944. km: "DND5E.DistKm"
  6945. };
  6946. preLocalize("movementUnits");
  6947. /* -------------------------------------------- */
  6948. /**
  6949. * The types of range that are used for measuring actions and effects.
  6950. * @enum {string}
  6951. */
  6952. DND5E.rangeTypes = {
  6953. self: "DND5E.DistSelf",
  6954. touch: "DND5E.DistTouch",
  6955. spec: "DND5E.Special",
  6956. any: "DND5E.DistAny"
  6957. };
  6958. preLocalize("rangeTypes");
  6959. /* -------------------------------------------- */
  6960. /**
  6961. * The valid units of measure for the range of an action or effect. A combination of `DND5E.movementUnits` and
  6962. * `DND5E.rangeUnits`.
  6963. * @enum {string}
  6964. */
  6965. DND5E.distanceUnits = {
  6966. ...DND5E.movementUnits,
  6967. ...DND5E.rangeTypes
  6968. };
  6969. preLocalize("distanceUnits");
  6970. /* -------------------------------------------- */
  6971. /**
  6972. * Configure aspects of encumbrance calculation so that it could be configured by modules.
  6973. * @enum {{ imperial: number, metric: number }}
  6974. */
  6975. DND5E.encumbrance = {
  6976. currencyPerWeight: {
  6977. imperial: 50,
  6978. metric: 110
  6979. },
  6980. strMultiplier: {
  6981. imperial: 15,
  6982. metric: 6.8
  6983. },
  6984. vehicleWeightMultiplier: {
  6985. imperial: 2000, // 2000 lbs in an imperial ton
  6986. metric: 1000 // 1000 kg in a metric ton
  6987. }
  6988. };
  6989. /* -------------------------------------------- */
  6990. /* Targeting */
  6991. /* -------------------------------------------- */
  6992. /**
  6993. * Targeting types that apply to one or more distinct targets.
  6994. * @enum {string}
  6995. */
  6996. DND5E.individualTargetTypes = {
  6997. self: "DND5E.TargetSelf",
  6998. ally: "DND5E.TargetAlly",
  6999. enemy: "DND5E.TargetEnemy",
  7000. creature: "DND5E.TargetCreature",
  7001. object: "DND5E.TargetObject",
  7002. space: "DND5E.TargetSpace",
  7003. creatureOrObject: "DND5E.TargetCreatureOrObject",
  7004. any: "DND5E.TargetAny",
  7005. willing: "DND5E.TargetWilling"
  7006. };
  7007. preLocalize("individualTargetTypes");
  7008. /* -------------------------------------------- */
  7009. /**
  7010. * Information needed to represent different area of effect target types.
  7011. *
  7012. * @typedef {object} AreaTargetDefinition
  7013. * @property {string} label Localized label for this type.
  7014. * @property {string} template Type of `MeasuredTemplate` create for this target type.
  7015. */
  7016. /**
  7017. * Targeting types that cover an area.
  7018. * @enum {AreaTargetDefinition}
  7019. */
  7020. DND5E.areaTargetTypes = {
  7021. radius: {
  7022. label: "DND5E.TargetRadius",
  7023. template: "circle"
  7024. },
  7025. sphere: {
  7026. label: "DND5E.TargetSphere",
  7027. template: "circle"
  7028. },
  7029. cylinder: {
  7030. label: "DND5E.TargetCylinder",
  7031. template: "circle"
  7032. },
  7033. cone: {
  7034. label: "DND5E.TargetCone",
  7035. template: "cone"
  7036. },
  7037. square: {
  7038. label: "DND5E.TargetSquare",
  7039. template: "rect"
  7040. },
  7041. cube: {
  7042. label: "DND5E.TargetCube",
  7043. template: "rect"
  7044. },
  7045. line: {
  7046. label: "DND5E.TargetLine",
  7047. template: "ray"
  7048. },
  7049. wall: {
  7050. label: "DND5E.TargetWall",
  7051. template: "ray"
  7052. }
  7053. };
  7054. preLocalize("areaTargetTypes", { key: "label", sort: true });
  7055. /* -------------------------------------------- */
  7056. /**
  7057. * The types of single or area targets which can be applied to abilities.
  7058. * @enum {string}
  7059. */
  7060. DND5E.targetTypes = {
  7061. ...DND5E.individualTargetTypes,
  7062. ...Object.fromEntries(Object.entries(DND5E.areaTargetTypes).map(([k, v]) => [k, v.label]))
  7063. };
  7064. preLocalize("targetTypes", { sort: true });
  7065. /* -------------------------------------------- */
  7066. /**
  7067. * Denominations of hit dice which can apply to classes.
  7068. * @type {string[]}
  7069. */
  7070. DND5E.hitDieTypes = ["d4", "d6", "d8", "d10", "d12"];
  7071. /* -------------------------------------------- */
  7072. /**
  7073. * The set of possible sensory perception types which an Actor may have.
  7074. * @enum {string}
  7075. */
  7076. DND5E.senses = {
  7077. blindsight: "DND5E.SenseBlindsight",
  7078. darkvision: "DND5E.SenseDarkvision",
  7079. tremorsense: "DND5E.SenseTremorsense",
  7080. truesight: "DND5E.SenseTruesight"
  7081. };
  7082. preLocalize("senses", { sort: true });
  7083. /* -------------------------------------------- */
  7084. /* Spellcasting */
  7085. /* -------------------------------------------- */
  7086. /**
  7087. * Define the standard slot progression by character level.
  7088. * The entries of this array represent the spell slot progression for a full spell-caster.
  7089. * @type {number[][]}
  7090. */
  7091. DND5E.SPELL_SLOT_TABLE = [
  7092. [2],
  7093. [3],
  7094. [4, 2],
  7095. [4, 3],
  7096. [4, 3, 2],
  7097. [4, 3, 3],
  7098. [4, 3, 3, 1],
  7099. [4, 3, 3, 2],
  7100. [4, 3, 3, 3, 1],
  7101. [4, 3, 3, 3, 2],
  7102. [4, 3, 3, 3, 2, 1],
  7103. [4, 3, 3, 3, 2, 1],
  7104. [4, 3, 3, 3, 2, 1, 1],
  7105. [4, 3, 3, 3, 2, 1, 1],
  7106. [4, 3, 3, 3, 2, 1, 1, 1],
  7107. [4, 3, 3, 3, 2, 1, 1, 1],
  7108. [4, 3, 3, 3, 2, 1, 1, 1, 1],
  7109. [4, 3, 3, 3, 3, 1, 1, 1, 1],
  7110. [4, 3, 3, 3, 3, 2, 1, 1, 1],
  7111. [4, 3, 3, 3, 3, 2, 2, 1, 1]
  7112. ];
  7113. /* -------------------------------------------- */
  7114. /**
  7115. * Configuration data for pact casting progression.
  7116. *
  7117. * @typedef {object} PactProgressionConfig
  7118. * @property {number} slots Number of spell slots granted.
  7119. * @property {number} level Level of spells that can be cast.
  7120. */
  7121. /**
  7122. * Define the pact slot & level progression by pact caster level.
  7123. * @enum {PactProgressionConfig}
  7124. */
  7125. DND5E.pactCastingProgression = {
  7126. 1: { slots: 1, level: 1 },
  7127. 2: { slots: 2, level: 1 },
  7128. 3: { slots: 2, level: 2 },
  7129. 5: { slots: 2, level: 3 },
  7130. 7: { slots: 2, level: 4 },
  7131. 9: { slots: 2, level: 5 },
  7132. 11: { slots: 3, level: 5 },
  7133. 17: { slots: 4, level: 5 }
  7134. };
  7135. /* -------------------------------------------- */
  7136. /**
  7137. * Various different ways a spell can be prepared.
  7138. */
  7139. DND5E.spellPreparationModes = {
  7140. prepared: "DND5E.SpellPrepPrepared",
  7141. pact: "DND5E.PactMagic",
  7142. always: "DND5E.SpellPrepAlways",
  7143. atwill: "DND5E.SpellPrepAtWill",
  7144. innate: "DND5E.SpellPrepInnate"
  7145. };
  7146. preLocalize("spellPreparationModes");
  7147. /* -------------------------------------------- */
  7148. /**
  7149. * Subset of `DND5E.spellPreparationModes` that consume spell slots.
  7150. * @type {boolean[]}
  7151. */
  7152. DND5E.spellUpcastModes = ["always", "pact", "prepared"];
  7153. /* -------------------------------------------- */
  7154. /**
  7155. * Configuration data for different types of spellcasting supported.
  7156. *
  7157. * @typedef {object} SpellcastingTypeConfiguration
  7158. * @property {string} label Localized label.
  7159. * @property {Object<string, SpellcastingProgressionConfiguration>} [progression] Any progression modes for this type.
  7160. */
  7161. /**
  7162. * Configuration data for a spellcasting progression mode.
  7163. *
  7164. * @typedef {object} SpellcastingProgressionConfiguration
  7165. * @property {string} label Localized label.
  7166. * @property {number} [divisor=1] Value by which the class levels are divided to determine spellcasting level.
  7167. * @property {boolean} [roundUp=false] Should fractional values should be rounded up by default?
  7168. */
  7169. /**
  7170. * Different spellcasting types and their progression.
  7171. * @type {SpellcastingTypeConfiguration}
  7172. */
  7173. DND5E.spellcastingTypes = {
  7174. leveled: {
  7175. label: "DND5E.SpellProgLeveled",
  7176. progression: {
  7177. full: {
  7178. label: "DND5E.SpellProgFull",
  7179. divisor: 1
  7180. },
  7181. half: {
  7182. label: "DND5E.SpellProgHalf",
  7183. divisor: 2
  7184. },
  7185. third: {
  7186. label: "DND5E.SpellProgThird",
  7187. divisor: 3
  7188. },
  7189. artificer: {
  7190. label: "DND5E.SpellProgArt",
  7191. divisor: 2,
  7192. roundUp: true
  7193. }
  7194. }
  7195. },
  7196. pact: {
  7197. label: "DND5E.SpellProgPact"
  7198. }
  7199. };
  7200. preLocalize("spellcastingTypes", { key: "label", sort: true });
  7201. preLocalize("spellcastingTypes.leveled.progression", { key: "label" });
  7202. /* -------------------------------------------- */
  7203. /**
  7204. * Ways in which a class can contribute to spellcasting levels.
  7205. * @enum {string}
  7206. */
  7207. DND5E.spellProgression = {
  7208. none: "DND5E.SpellNone",
  7209. full: "DND5E.SpellProgFull",
  7210. half: "DND5E.SpellProgHalf",
  7211. third: "DND5E.SpellProgThird",
  7212. pact: "DND5E.SpellProgPact",
  7213. artificer: "DND5E.SpellProgArt"
  7214. };
  7215. preLocalize("spellProgression", { key: "label" });
  7216. /* -------------------------------------------- */
  7217. /**
  7218. * Valid spell levels.
  7219. * @enum {string}
  7220. */
  7221. DND5E.spellLevels = {
  7222. 0: "DND5E.SpellLevel0",
  7223. 1: "DND5E.SpellLevel1",
  7224. 2: "DND5E.SpellLevel2",
  7225. 3: "DND5E.SpellLevel3",
  7226. 4: "DND5E.SpellLevel4",
  7227. 5: "DND5E.SpellLevel5",
  7228. 6: "DND5E.SpellLevel6",
  7229. 7: "DND5E.SpellLevel7",
  7230. 8: "DND5E.SpellLevel8",
  7231. 9: "DND5E.SpellLevel9"
  7232. };
  7233. preLocalize("spellLevels");
  7234. /* -------------------------------------------- */
  7235. /**
  7236. * The available choices for how spell damage scaling may be computed.
  7237. * @enum {string}
  7238. */
  7239. DND5E.spellScalingModes = {
  7240. none: "DND5E.SpellNone",
  7241. cantrip: "DND5E.SpellCantrip",
  7242. level: "DND5E.SpellLevel"
  7243. };
  7244. preLocalize("spellScalingModes", { sort: true });
  7245. /* -------------------------------------------- */
  7246. /**
  7247. * Types of components that can be required when casting a spell.
  7248. * @enum {object}
  7249. */
  7250. DND5E.spellComponents = {
  7251. vocal: {
  7252. label: "DND5E.ComponentVerbal",
  7253. abbr: "DND5E.ComponentVerbalAbbr"
  7254. },
  7255. somatic: {
  7256. label: "DND5E.ComponentSomatic",
  7257. abbr: "DND5E.ComponentSomaticAbbr"
  7258. },
  7259. material: {
  7260. label: "DND5E.ComponentMaterial",
  7261. abbr: "DND5E.ComponentMaterialAbbr"
  7262. }
  7263. };
  7264. preLocalize("spellComponents", {keys: ["label", "abbr"]});
  7265. /* -------------------------------------------- */
  7266. /**
  7267. * Supplementary rules keywords that inform a spell's use.
  7268. * @enum {object}
  7269. */
  7270. DND5E.spellTags = {
  7271. concentration: {
  7272. label: "DND5E.Concentration",
  7273. abbr: "DND5E.ConcentrationAbbr"
  7274. },
  7275. ritual: {
  7276. label: "DND5E.Ritual",
  7277. abbr: "DND5E.RitualAbbr"
  7278. }
  7279. };
  7280. preLocalize("spellTags", {keys: ["label", "abbr"]});
  7281. /* -------------------------------------------- */
  7282. /**
  7283. * Schools to which a spell can belong.
  7284. * @enum {string}
  7285. */
  7286. DND5E.spellSchools = {
  7287. abj: "DND5E.SchoolAbj",
  7288. con: "DND5E.SchoolCon",
  7289. div: "DND5E.SchoolDiv",
  7290. enc: "DND5E.SchoolEnc",
  7291. evo: "DND5E.SchoolEvo",
  7292. ill: "DND5E.SchoolIll",
  7293. nec: "DND5E.SchoolNec",
  7294. trs: "DND5E.SchoolTrs"
  7295. };
  7296. preLocalize("spellSchools", { sort: true });
  7297. /* -------------------------------------------- */
  7298. /**
  7299. * Spell scroll item ID within the `DND5E.sourcePacks` compendium for each level.
  7300. * @enum {string}
  7301. */
  7302. DND5E.spellScrollIds = {
  7303. 0: "rQ6sO7HDWzqMhSI3",
  7304. 1: "9GSfMg0VOA2b4uFN",
  7305. 2: "XdDp6CKh9qEvPTuS",
  7306. 3: "hqVKZie7x9w3Kqds",
  7307. 4: "DM7hzgL836ZyUFB1",
  7308. 5: "wa1VF8TXHmkrrR35",
  7309. 6: "tI3rWx4bxefNCexS",
  7310. 7: "mtyw4NS1s7j2EJaD",
  7311. 8: "aOrinPg7yuDZEuWr",
  7312. 9: "O4YbkJkLlnsgUszZ"
  7313. };
  7314. /* -------------------------------------------- */
  7315. /* Weapon Details */
  7316. /* -------------------------------------------- */
  7317. /**
  7318. * The set of types which a weapon item can take.
  7319. * @enum {string}
  7320. */
  7321. DND5E.weaponTypes = {
  7322. simpleM: "DND5E.WeaponSimpleM",
  7323. simpleR: "DND5E.WeaponSimpleR",
  7324. martialM: "DND5E.WeaponMartialM",
  7325. martialR: "DND5E.WeaponMartialR",
  7326. natural: "DND5E.WeaponNatural",
  7327. improv: "DND5E.WeaponImprov",
  7328. siege: "DND5E.WeaponSiege"
  7329. };
  7330. preLocalize("weaponTypes");
  7331. /* -------------------------------------------- */
  7332. /**
  7333. * A subset of weapon properties that determine the physical characteristics of the weapon.
  7334. * These properties are used for determining physical resistance bypasses.
  7335. * @enum {string}
  7336. */
  7337. DND5E.physicalWeaponProperties = {
  7338. ada: "DND5E.WeaponPropertiesAda",
  7339. mgc: "DND5E.WeaponPropertiesMgc",
  7340. sil: "DND5E.WeaponPropertiesSil"
  7341. };
  7342. preLocalize("physicalWeaponProperties", { sort: true });
  7343. /* -------------------------------------------- */
  7344. /**
  7345. * The set of weapon property flags which can exist on a weapon.
  7346. * @enum {string}
  7347. */
  7348. DND5E.weaponProperties = {
  7349. ...DND5E.physicalWeaponProperties,
  7350. amm: "DND5E.WeaponPropertiesAmm",
  7351. fin: "DND5E.WeaponPropertiesFin",
  7352. fir: "DND5E.WeaponPropertiesFir",
  7353. foc: "DND5E.WeaponPropertiesFoc",
  7354. hvy: "DND5E.WeaponPropertiesHvy",
  7355. lgt: "DND5E.WeaponPropertiesLgt",
  7356. lod: "DND5E.WeaponPropertiesLod",
  7357. rch: "DND5E.WeaponPropertiesRch",
  7358. rel: "DND5E.WeaponPropertiesRel",
  7359. ret: "DND5E.WeaponPropertiesRet",
  7360. spc: "DND5E.WeaponPropertiesSpc",
  7361. thr: "DND5E.WeaponPropertiesThr",
  7362. two: "DND5E.WeaponPropertiesTwo",
  7363. ver: "DND5E.WeaponPropertiesVer"
  7364. };
  7365. preLocalize("weaponProperties", { sort: true });
  7366. /* -------------------------------------------- */
  7367. /**
  7368. * Compendium packs used for localized items.
  7369. * @enum {string}
  7370. */
  7371. DND5E.sourcePacks = {
  7372. ITEMS: "dnd5e.items"
  7373. };
  7374. /* -------------------------------------------- */
  7375. /**
  7376. * Settings to configure how actors are merged when polymorphing is applied.
  7377. * @enum {string}
  7378. */
  7379. DND5E.polymorphSettings = {
  7380. keepPhysical: "DND5E.PolymorphKeepPhysical",
  7381. keepMental: "DND5E.PolymorphKeepMental",
  7382. keepSaves: "DND5E.PolymorphKeepSaves",
  7383. keepSkills: "DND5E.PolymorphKeepSkills",
  7384. mergeSaves: "DND5E.PolymorphMergeSaves",
  7385. mergeSkills: "DND5E.PolymorphMergeSkills",
  7386. keepClass: "DND5E.PolymorphKeepClass",
  7387. keepFeats: "DND5E.PolymorphKeepFeats",
  7388. keepSpells: "DND5E.PolymorphKeepSpells",
  7389. keepItems: "DND5E.PolymorphKeepItems",
  7390. keepBio: "DND5E.PolymorphKeepBio",
  7391. keepVision: "DND5E.PolymorphKeepVision",
  7392. keepSelf: "DND5E.PolymorphKeepSelf"
  7393. };
  7394. preLocalize("polymorphSettings", { sort: true });
  7395. /**
  7396. * Settings to configure how actors are effects are merged when polymorphing is applied.
  7397. * @enum {string}
  7398. */
  7399. DND5E.polymorphEffectSettings = {
  7400. keepAE: "DND5E.PolymorphKeepAE",
  7401. keepOtherOriginAE: "DND5E.PolymorphKeepOtherOriginAE",
  7402. keepOriginAE: "DND5E.PolymorphKeepOriginAE",
  7403. keepEquipmentAE: "DND5E.PolymorphKeepEquipmentAE",
  7404. keepFeatAE: "DND5E.PolymorphKeepFeatureAE",
  7405. keepSpellAE: "DND5E.PolymorphKeepSpellAE",
  7406. keepClassAE: "DND5E.PolymorphKeepClassAE",
  7407. keepBackgroundAE: "DND5E.PolymorphKeepBackgroundAE"
  7408. };
  7409. preLocalize("polymorphEffectSettings", { sort: true });
  7410. /**
  7411. * Settings to configure how actors are merged when preset polymorphing is applied.
  7412. * @enum {object}
  7413. */
  7414. DND5E.transformationPresets = {
  7415. wildshape: {
  7416. icon: '<i class="fas fa-paw"></i>',
  7417. label: "DND5E.PolymorphWildShape",
  7418. options: {
  7419. keepBio: true,
  7420. keepClass: true,
  7421. keepMental: true,
  7422. mergeSaves: true,
  7423. mergeSkills: true,
  7424. keepEquipmentAE: false
  7425. }
  7426. },
  7427. polymorph: {
  7428. icon: '<i class="fas fa-pastafarianism"></i>',
  7429. label: "DND5E.Polymorph",
  7430. options: {
  7431. keepEquipmentAE: false,
  7432. keepClassAE: false,
  7433. keepFeatAE: false,
  7434. keepBackgroundAE: false
  7435. }
  7436. },
  7437. polymorphSelf: {
  7438. icon: '<i class="fas fa-eye"></i>',
  7439. label: "DND5E.PolymorphSelf",
  7440. options: {
  7441. keepSelf: true
  7442. }
  7443. }
  7444. };
  7445. preLocalize("transformationPresets", { sort: true, keys: ["label"] });
  7446. /* -------------------------------------------- */
  7447. /**
  7448. * Skill, ability, and tool proficiency levels.
  7449. * The key for each level represents its proficiency multiplier.
  7450. * @enum {string}
  7451. */
  7452. DND5E.proficiencyLevels = {
  7453. 0: "DND5E.NotProficient",
  7454. 1: "DND5E.Proficient",
  7455. 0.5: "DND5E.HalfProficient",
  7456. 2: "DND5E.Expertise"
  7457. };
  7458. preLocalize("proficiencyLevels");
  7459. /* -------------------------------------------- */
  7460. /**
  7461. * Weapon and armor item proficiency levels.
  7462. * @enum {string}
  7463. */
  7464. DND5E.weaponAndArmorProficiencyLevels = {
  7465. 0: "DND5E.NotProficient",
  7466. 1: "DND5E.Proficient"
  7467. };
  7468. preLocalize("weaponAndArmorProficiencyLevels");
  7469. /* -------------------------------------------- */
  7470. /**
  7471. * The amount of cover provided by an object. In cases where multiple pieces
  7472. * of cover are in play, we take the highest value.
  7473. * @enum {string}
  7474. */
  7475. DND5E.cover = {
  7476. 0: "DND5E.None",
  7477. .5: "DND5E.CoverHalf",
  7478. .75: "DND5E.CoverThreeQuarters",
  7479. 1: "DND5E.CoverTotal"
  7480. };
  7481. preLocalize("cover");
  7482. /* -------------------------------------------- */
  7483. /**
  7484. * A selection of actor attributes that can be tracked on token resource bars.
  7485. * @type {string[]}
  7486. * @deprecated since v10
  7487. */
  7488. DND5E.trackableAttributes = [
  7489. "attributes.ac.value", "attributes.init.bonus", "attributes.movement", "attributes.senses", "attributes.spelldc",
  7490. "attributes.spellLevel", "details.cr", "details.spellLevel", "details.xp.value", "skills.*.passive",
  7491. "abilities.*.value"
  7492. ];
  7493. /* -------------------------------------------- */
  7494. /**
  7495. * A selection of actor and item attributes that are valid targets for item resource consumption.
  7496. * @type {string[]}
  7497. */
  7498. DND5E.consumableResources = [
  7499. // Configured during init.
  7500. ];
  7501. /* -------------------------------------------- */
  7502. /**
  7503. * Conditions that can affect an actor.
  7504. * @enum {string}
  7505. */
  7506. DND5E.conditionTypes = {
  7507. blinded: "DND5E.ConBlinded",
  7508. charmed: "DND5E.ConCharmed",
  7509. deafened: "DND5E.ConDeafened",
  7510. diseased: "DND5E.ConDiseased",
  7511. exhaustion: "DND5E.ConExhaustion",
  7512. frightened: "DND5E.ConFrightened",
  7513. grappled: "DND5E.ConGrappled",
  7514. incapacitated: "DND5E.ConIncapacitated",
  7515. invisible: "DND5E.ConInvisible",
  7516. paralyzed: "DND5E.ConParalyzed",
  7517. petrified: "DND5E.ConPetrified",
  7518. poisoned: "DND5E.ConPoisoned",
  7519. prone: "DND5E.ConProne",
  7520. restrained: "DND5E.ConRestrained",
  7521. stunned: "DND5E.ConStunned",
  7522. unconscious: "DND5E.ConUnconscious"
  7523. };
  7524. preLocalize("conditionTypes", { sort: true });
  7525. /**
  7526. * Languages a character can learn.
  7527. * @enum {string}
  7528. */
  7529. DND5E.languages = {
  7530. common: "DND5E.LanguagesCommon",
  7531. aarakocra: "DND5E.LanguagesAarakocra",
  7532. abyssal: "DND5E.LanguagesAbyssal",
  7533. aquan: "DND5E.LanguagesAquan",
  7534. auran: "DND5E.LanguagesAuran",
  7535. celestial: "DND5E.LanguagesCelestial",
  7536. deep: "DND5E.LanguagesDeepSpeech",
  7537. draconic: "DND5E.LanguagesDraconic",
  7538. druidic: "DND5E.LanguagesDruidic",
  7539. dwarvish: "DND5E.LanguagesDwarvish",
  7540. elvish: "DND5E.LanguagesElvish",
  7541. giant: "DND5E.LanguagesGiant",
  7542. gith: "DND5E.LanguagesGith",
  7543. gnomish: "DND5E.LanguagesGnomish",
  7544. goblin: "DND5E.LanguagesGoblin",
  7545. gnoll: "DND5E.LanguagesGnoll",
  7546. halfling: "DND5E.LanguagesHalfling",
  7547. ignan: "DND5E.LanguagesIgnan",
  7548. infernal: "DND5E.LanguagesInfernal",
  7549. orc: "DND5E.LanguagesOrc",
  7550. primordial: "DND5E.LanguagesPrimordial",
  7551. sylvan: "DND5E.LanguagesSylvan",
  7552. terran: "DND5E.LanguagesTerran",
  7553. cant: "DND5E.LanguagesThievesCant",
  7554. undercommon: "DND5E.LanguagesUndercommon"
  7555. };
  7556. preLocalize("languages", { sort: true });
  7557. /**
  7558. * Maximum allowed character level.
  7559. * @type {number}
  7560. */
  7561. DND5E.maxLevel = 20;
  7562. /**
  7563. * Maximum ability score value allowed by default.
  7564. * @type {number}
  7565. */
  7566. DND5E.maxAbilityScore = 20;
  7567. /**
  7568. * XP required to achieve each character level.
  7569. * @type {number[]}
  7570. */
  7571. DND5E.CHARACTER_EXP_LEVELS = [
  7572. 0, 300, 900, 2700, 6500, 14000, 23000, 34000, 48000, 64000, 85000, 100000,
  7573. 120000, 140000, 165000, 195000, 225000, 265000, 305000, 355000
  7574. ];
  7575. /**
  7576. * XP granted for each challenge rating.
  7577. * @type {number[]}
  7578. */
  7579. DND5E.CR_EXP_LEVELS = [
  7580. 10, 200, 450, 700, 1100, 1800, 2300, 2900, 3900, 5000, 5900, 7200, 8400, 10000, 11500, 13000, 15000, 18000,
  7581. 20000, 22000, 25000, 33000, 41000, 50000, 62000, 75000, 90000, 105000, 120000, 135000, 155000
  7582. ];
  7583. /**
  7584. * @typedef {object} CharacterFlagConfig
  7585. * @property {string} name
  7586. * @property {string} hint
  7587. * @property {string} section
  7588. * @property {typeof boolean|string|number} type
  7589. * @property {string} placeholder
  7590. * @property {string[]} [abilities]
  7591. * @property {Object<string, string>} [choices]
  7592. * @property {string[]} [skills]
  7593. */
  7594. /* -------------------------------------------- */
  7595. /**
  7596. * Trait configuration information.
  7597. *
  7598. * @typedef {object} TraitConfiguration
  7599. * @property {string} label Localization key for the trait name.
  7600. * @property {string} [actorKeyPath] If the trait doesn't directly map to an entry as `traits.[key]`, where is
  7601. * this trait's data stored on the actor?
  7602. * @property {string} [configKey] If the list of trait options doesn't match the name of the trait, where can
  7603. * the options be found within `CONFIG.DND5E`?
  7604. * @property {string} [labelKey] If config is an enum of objects, where can the label be found?
  7605. * @property {object} [subtypes] Configuration for traits that take some sort of base item.
  7606. * @property {string} [subtypes.keyPath] Path to subtype value on base items, should match a category key.
  7607. * @property {string[]} [subtypes.ids] Key for base item ID objects within `CONFIG.DND5E`.
  7608. * @property {object} [children] Mapping of category key to an object defining its children.
  7609. * @property {boolean} [sortCategories] Whether top-level categories should be sorted.
  7610. */
  7611. /**
  7612. * Configurable traits on actors.
  7613. * @enum {TraitConfiguration}
  7614. */
  7615. DND5E.traits = {
  7616. saves: {
  7617. label: "DND5E.ClassSaves",
  7618. configKey: "abilities",
  7619. labelKey: "label"
  7620. },
  7621. skills: {
  7622. label: "DND5E.TraitSkillProf",
  7623. labelKey: "label"
  7624. },
  7625. languages: {
  7626. label: "DND5E.Languages"
  7627. },
  7628. di: {
  7629. label: "DND5E.DamImm",
  7630. configKey: "damageTypes"
  7631. },
  7632. dr: {
  7633. label: "DND5E.DamRes",
  7634. configKey: "damageTypes"
  7635. },
  7636. dv: {
  7637. label: "DND5E.DamVuln",
  7638. configKey: "damageTypes"
  7639. },
  7640. ci: {
  7641. label: "DND5E.ConImm",
  7642. configKey: "conditionTypes"
  7643. },
  7644. weapon: {
  7645. label: "DND5E.TraitWeaponProf",
  7646. actorKeyPath: "traits.weaponProf",
  7647. configKey: "weaponProficiencies",
  7648. subtypes: { keyPath: "weaponType", ids: ["weaponIds"] }
  7649. },
  7650. armor: {
  7651. label: "DND5E.TraitArmorProf",
  7652. actorKeyPath: "traits.armorProf",
  7653. configKey: "armorProficiencies",
  7654. subtypes: { keyPath: "armor.type", ids: ["armorIds", "shieldIds"] }
  7655. },
  7656. tool: {
  7657. label: "DND5E.TraitToolProf",
  7658. actorKeyPath: "tools",
  7659. configKey: "toolProficiencies",
  7660. subtypes: { keyPath: "toolType", ids: ["toolIds"] },
  7661. children: { vehicle: "vehicleTypes" },
  7662. sortCategories: true
  7663. }
  7664. };
  7665. preLocalize("traits", { key: "label" });
  7666. /* -------------------------------------------- */
  7667. /**
  7668. * Special character flags.
  7669. * @enum {CharacterFlagConfig}
  7670. */
  7671. DND5E.characterFlags = {
  7672. diamondSoul: {
  7673. name: "DND5E.FlagsDiamondSoul",
  7674. hint: "DND5E.FlagsDiamondSoulHint",
  7675. section: "DND5E.Feats",
  7676. type: Boolean
  7677. },
  7678. elvenAccuracy: {
  7679. name: "DND5E.FlagsElvenAccuracy",
  7680. hint: "DND5E.FlagsElvenAccuracyHint",
  7681. section: "DND5E.RacialTraits",
  7682. abilities: ["dex", "int", "wis", "cha"],
  7683. type: Boolean
  7684. },
  7685. halflingLucky: {
  7686. name: "DND5E.FlagsHalflingLucky",
  7687. hint: "DND5E.FlagsHalflingLuckyHint",
  7688. section: "DND5E.RacialTraits",
  7689. type: Boolean
  7690. },
  7691. initiativeAdv: {
  7692. name: "DND5E.FlagsInitiativeAdv",
  7693. hint: "DND5E.FlagsInitiativeAdvHint",
  7694. section: "DND5E.Feats",
  7695. type: Boolean
  7696. },
  7697. initiativeAlert: {
  7698. name: "DND5E.FlagsAlert",
  7699. hint: "DND5E.FlagsAlertHint",
  7700. section: "DND5E.Feats",
  7701. type: Boolean
  7702. },
  7703. jackOfAllTrades: {
  7704. name: "DND5E.FlagsJOAT",
  7705. hint: "DND5E.FlagsJOATHint",
  7706. section: "DND5E.Feats",
  7707. type: Boolean
  7708. },
  7709. observantFeat: {
  7710. name: "DND5E.FlagsObservant",
  7711. hint: "DND5E.FlagsObservantHint",
  7712. skills: ["prc", "inv"],
  7713. section: "DND5E.Feats",
  7714. type: Boolean
  7715. },
  7716. tavernBrawlerFeat: {
  7717. name: "DND5E.FlagsTavernBrawler",
  7718. hint: "DND5E.FlagsTavernBrawlerHint",
  7719. section: "DND5E.Feats",
  7720. type: Boolean
  7721. },
  7722. powerfulBuild: {
  7723. name: "DND5E.FlagsPowerfulBuild",
  7724. hint: "DND5E.FlagsPowerfulBuildHint",
  7725. section: "DND5E.RacialTraits",
  7726. type: Boolean
  7727. },
  7728. reliableTalent: {
  7729. name: "DND5E.FlagsReliableTalent",
  7730. hint: "DND5E.FlagsReliableTalentHint",
  7731. section: "DND5E.Feats",
  7732. type: Boolean
  7733. },
  7734. remarkableAthlete: {
  7735. name: "DND5E.FlagsRemarkableAthlete",
  7736. hint: "DND5E.FlagsRemarkableAthleteHint",
  7737. abilities: ["str", "dex", "con"],
  7738. section: "DND5E.Feats",
  7739. type: Boolean
  7740. },
  7741. weaponCriticalThreshold: {
  7742. name: "DND5E.FlagsWeaponCritThreshold",
  7743. hint: "DND5E.FlagsWeaponCritThresholdHint",
  7744. section: "DND5E.Feats",
  7745. type: Number,
  7746. placeholder: 20
  7747. },
  7748. spellCriticalThreshold: {
  7749. name: "DND5E.FlagsSpellCritThreshold",
  7750. hint: "DND5E.FlagsSpellCritThresholdHint",
  7751. section: "DND5E.Feats",
  7752. type: Number,
  7753. placeholder: 20
  7754. },
  7755. meleeCriticalDamageDice: {
  7756. name: "DND5E.FlagsMeleeCriticalDice",
  7757. hint: "DND5E.FlagsMeleeCriticalDiceHint",
  7758. section: "DND5E.Feats",
  7759. type: Number,
  7760. placeholder: 0
  7761. }
  7762. };
  7763. preLocalize("characterFlags", { keys: ["name", "hint", "section"] });
  7764. /**
  7765. * Flags allowed on actors. Any flags not in the list may be deleted during a migration.
  7766. * @type {string[]}
  7767. */
  7768. DND5E.allowedActorFlags = ["isPolymorphed", "originalActor"].concat(Object.keys(DND5E.characterFlags));
  7769. /* -------------------------------------------- */
  7770. /**
  7771. * Advancement types that can be added to items.
  7772. * @enum {*}
  7773. */
  7774. DND5E.advancementTypes = {
  7775. AbilityScoreImprovement: AbilityScoreImprovementAdvancement,
  7776. HitPoints: HitPointsAdvancement,
  7777. ItemChoice: ItemChoiceAdvancement,
  7778. ItemGrant: ItemGrantAdvancement,
  7779. ScaleValue: ScaleValueAdvancement
  7780. };
  7781. /* -------------------------------------------- */
  7782. /**
  7783. * Patch an existing config enum to allow conversion from string values to object values without
  7784. * breaking existing modules that are expecting strings.
  7785. * @param {string} key Key within DND5E that has been replaced with an enum of objects.
  7786. * @param {string} fallbackKey Key within the new config object from which to get the fallback value.
  7787. * @param {object} [options] Additional options passed through to logCompatibilityWarning.
  7788. */
  7789. function patchConfig(key, fallbackKey, options) {
  7790. /** @override */
  7791. function toString() {
  7792. const message = `The value of CONFIG.DND5E.${key} has been changed to an object.`
  7793. +` The former value can be acccessed from .${fallbackKey}.`;
  7794. foundry.utils.logCompatibilityWarning(message, options);
  7795. return this[fallbackKey];
  7796. }
  7797. Object.values(DND5E[key]).forEach(o => o.toString = toString);
  7798. }
  7799. /**
  7800. * @typedef {object} ModuleArtInfo
  7801. * @property {string} actor The path to the actor's portrait image.
  7802. * @property {string|object} token The path to the token image, or a richer object specifying additional token
  7803. * adjustments.
  7804. */
  7805. /**
  7806. * A class responsible for managing module-provided art in compendia.
  7807. */
  7808. class ModuleArt {
  7809. constructor() {
  7810. /**
  7811. * The stored map of actor UUIDs to their art information.
  7812. * @type {Map<string, ModuleArtInfo>}
  7813. */
  7814. Object.defineProperty(this, "map", {value: new Map(), writable: false});
  7815. }
  7816. /* -------------------------------------------- */
  7817. /**
  7818. * Set to true to temporarily prevent actors from loading module art.
  7819. * @type {boolean}
  7820. */
  7821. suppressArt = false;
  7822. /* -------------------------------------------- */
  7823. /**
  7824. * Register any art mapping information included in active modules.
  7825. * @returns {Promise<void>}
  7826. */
  7827. async registerModuleArt() {
  7828. this.map.clear();
  7829. for ( const module of game.modules ) {
  7830. const flags = module.flags?.[module.id];
  7831. const artPath = this.constructor.getModuleArtPath(module);
  7832. if ( !artPath ) continue;
  7833. try {
  7834. const mapping = await foundry.utils.fetchJsonWithTimeout(artPath);
  7835. await this.#parseArtMapping(module.id, mapping, flags["dnd5e-art-credit"]);
  7836. } catch( e ) {
  7837. console.error(e);
  7838. }
  7839. }
  7840. // Load system mapping.
  7841. try {
  7842. const mapping = await foundry.utils.fetchJsonWithTimeout("systems/dnd5e/json/fa-token-mapping.json");
  7843. const credit = `
  7844. <em>
  7845. Token artwork by
  7846. <a href="https://www.forgotten-adventures.net/" target="_blank" rel="noopener">Forgotten Adventures</a>.
  7847. </em>
  7848. `;
  7849. await this.#parseArtMapping(game.system.id, mapping, credit);
  7850. } catch( e ) {
  7851. console.error(e);
  7852. }
  7853. }
  7854. /* -------------------------------------------- */
  7855. /**
  7856. * Parse a provided module art mapping and store it for reference later.
  7857. * @param {string} moduleId The module ID.
  7858. * @param {object} mapping A mapping containing pack names, a list of actor IDs, and paths to the art provided by
  7859. * the module for them.
  7860. * @param {string} [credit] An optional credit line to attach to the Actor's biography.
  7861. * @returns {Promise<void>}
  7862. */
  7863. async #parseArtMapping(moduleId, mapping, credit) {
  7864. let settings = game.settings.get("dnd5e", "moduleArtConfiguration")?.[moduleId];
  7865. settings ??= {portraits: true, tokens: true};
  7866. for ( const [packName, actors] of Object.entries(mapping) ) {
  7867. const pack = game.packs.get(packName);
  7868. if ( !pack ) continue;
  7869. for ( let [actorId, info] of Object.entries(actors) ) {
  7870. const entry = pack.index.get(actorId);
  7871. if ( !entry || !(settings.portraits || settings.tokens) ) continue;
  7872. if ( settings.portraits ) entry.img = info.actor;
  7873. else delete info.actor;
  7874. if ( !settings.tokens ) delete info.token;
  7875. if ( credit ) info.credit = credit;
  7876. const uuid = `Compendium.${packName}.${actorId}`;
  7877. info = foundry.utils.mergeObject(this.map.get(uuid) ?? {}, info, {inplace: false});
  7878. this.map.set(`Compendium.${packName}.${actorId}`, info);
  7879. }
  7880. }
  7881. }
  7882. /* -------------------------------------------- */
  7883. /**
  7884. * If a module provides art, return the path to is JSON mapping.
  7885. * @param {Module} module The module.
  7886. * @returns {string|null}
  7887. */
  7888. static getModuleArtPath(module) {
  7889. const flags = module.flags?.[module.id];
  7890. const artPath = flags?.["dnd5e-art"];
  7891. if ( !artPath || !module.active ) return null;
  7892. return artPath;
  7893. }
  7894. }
  7895. /**
  7896. * A class responsible for allowing GMs to configure art provided by installed modules.
  7897. */
  7898. class ModuleArtConfig extends FormApplication {
  7899. /** @inheritdoc */
  7900. constructor(object={}, options={}) {
  7901. object = foundry.utils.mergeObject(game.settings.get("dnd5e", "moduleArtConfiguration"), object, {inplace: false});
  7902. super(object, options);
  7903. }
  7904. /* -------------------------------------------- */
  7905. /** @inheritdoc */
  7906. static get defaultOptions() {
  7907. return foundry.utils.mergeObject(super.defaultOptions, {
  7908. title: game.i18n.localize("DND5E.ModuleArtConfigL"),
  7909. id: "module-art-config",
  7910. template: "systems/dnd5e/templates/apps/module-art-config.html",
  7911. popOut: true,
  7912. width: 600,
  7913. height: "auto"
  7914. });
  7915. }
  7916. /* -------------------------------------------- */
  7917. /** @inheritdoc */
  7918. getData(options={}) {
  7919. const context = super.getData(options);
  7920. context.config = [];
  7921. for ( const module of game.modules ) {
  7922. if ( !ModuleArt.getModuleArtPath(module) ) continue;
  7923. const settings = this.object[module.id] ?? {portraits: true, tokens: true};
  7924. context.config.push({label: module.title, id: module.id, ...settings});
  7925. }
  7926. context.config.sort((a, b) => a.label.localeCompare(b.label, game.i18n.lang));
  7927. context.config.unshift({label: game.system.title, id: game.system.id, ...this.object.dnd5e});
  7928. return context;
  7929. }
  7930. /* -------------------------------------------- */
  7931. /** @inheritdoc */
  7932. async _updateObject(event, formData) {
  7933. await game.settings.set("dnd5e", "moduleArtConfiguration", foundry.utils.expandObject(formData));
  7934. return SettingsConfig.reloadConfirm({world: true});
  7935. }
  7936. }
  7937. /**
  7938. * Register all of the system's settings.
  7939. */
  7940. function registerSystemSettings() {
  7941. // Internal System Migration Version
  7942. game.settings.register("dnd5e", "systemMigrationVersion", {
  7943. name: "System Migration Version",
  7944. scope: "world",
  7945. config: false,
  7946. type: String,
  7947. default: ""
  7948. });
  7949. // Rest Recovery Rules
  7950. game.settings.register("dnd5e", "restVariant", {
  7951. name: "SETTINGS.5eRestN",
  7952. hint: "SETTINGS.5eRestL",
  7953. scope: "world",
  7954. config: true,
  7955. default: "normal",
  7956. type: String,
  7957. choices: {
  7958. normal: "SETTINGS.5eRestPHB",
  7959. gritty: "SETTINGS.5eRestGritty",
  7960. epic: "SETTINGS.5eRestEpic"
  7961. }
  7962. });
  7963. // Diagonal Movement Rule
  7964. game.settings.register("dnd5e", "diagonalMovement", {
  7965. name: "SETTINGS.5eDiagN",
  7966. hint: "SETTINGS.5eDiagL",
  7967. scope: "world",
  7968. config: true,
  7969. default: "555",
  7970. type: String,
  7971. choices: {
  7972. 555: "SETTINGS.5eDiagPHB",
  7973. 5105: "SETTINGS.5eDiagDMG",
  7974. EUCL: "SETTINGS.5eDiagEuclidean"
  7975. },
  7976. onChange: rule => canvas.grid.diagonalRule = rule
  7977. });
  7978. // Proficiency modifier type
  7979. game.settings.register("dnd5e", "proficiencyModifier", {
  7980. name: "SETTINGS.5eProfN",
  7981. hint: "SETTINGS.5eProfL",
  7982. scope: "world",
  7983. config: true,
  7984. default: "bonus",
  7985. type: String,
  7986. choices: {
  7987. bonus: "SETTINGS.5eProfBonus",
  7988. dice: "SETTINGS.5eProfDice"
  7989. }
  7990. });
  7991. // Allow feats during Ability Score Improvements
  7992. game.settings.register("dnd5e", "allowFeats", {
  7993. name: "SETTINGS.5eFeatsN",
  7994. hint: "SETTINGS.5eFeatsL",
  7995. scope: "world",
  7996. config: true,
  7997. default: true,
  7998. type: Boolean
  7999. });
  8000. // Use Honor ability score
  8001. game.settings.register("dnd5e", "honorScore", {
  8002. name: "SETTINGS.5eHonorN",
  8003. hint: "SETTINGS.5eHonorL",
  8004. scope: "world",
  8005. config: true,
  8006. default: false,
  8007. type: Boolean,
  8008. requiresReload: true
  8009. });
  8010. // Use Sanity ability score
  8011. game.settings.register("dnd5e", "sanityScore", {
  8012. name: "SETTINGS.5eSanityN",
  8013. hint: "SETTINGS.5eSanityL",
  8014. scope: "world",
  8015. config: true,
  8016. default: false,
  8017. type: Boolean,
  8018. requiresReload: true
  8019. });
  8020. // Apply Dexterity as Initiative Tiebreaker
  8021. game.settings.register("dnd5e", "initiativeDexTiebreaker", {
  8022. name: "SETTINGS.5eInitTBN",
  8023. hint: "SETTINGS.5eInitTBL",
  8024. scope: "world",
  8025. config: true,
  8026. default: false,
  8027. type: Boolean
  8028. });
  8029. // Record Currency Weight
  8030. game.settings.register("dnd5e", "currencyWeight", {
  8031. name: "SETTINGS.5eCurWtN",
  8032. hint: "SETTINGS.5eCurWtL",
  8033. scope: "world",
  8034. config: true,
  8035. default: true,
  8036. type: Boolean
  8037. });
  8038. // Disable Experience Tracking
  8039. game.settings.register("dnd5e", "disableExperienceTracking", {
  8040. name: "SETTINGS.5eNoExpN",
  8041. hint: "SETTINGS.5eNoExpL",
  8042. scope: "world",
  8043. config: true,
  8044. default: false,
  8045. type: Boolean
  8046. });
  8047. // Disable Advancements
  8048. game.settings.register("dnd5e", "disableAdvancements", {
  8049. name: "SETTINGS.5eNoAdvancementsN",
  8050. hint: "SETTINGS.5eNoAdvancementsL",
  8051. scope: "world",
  8052. config: true,
  8053. default: false,
  8054. type: Boolean
  8055. });
  8056. // Collapse Item Cards (by default)
  8057. game.settings.register("dnd5e", "autoCollapseItemCards", {
  8058. name: "SETTINGS.5eAutoCollapseCardN",
  8059. hint: "SETTINGS.5eAutoCollapseCardL",
  8060. scope: "client",
  8061. config: true,
  8062. default: false,
  8063. type: Boolean,
  8064. onChange: s => {
  8065. ui.chat.render();
  8066. }
  8067. });
  8068. // Allow Polymorphing
  8069. game.settings.register("dnd5e", "allowPolymorphing", {
  8070. name: "SETTINGS.5eAllowPolymorphingN",
  8071. hint: "SETTINGS.5eAllowPolymorphingL",
  8072. scope: "world",
  8073. config: true,
  8074. default: false,
  8075. type: Boolean
  8076. });
  8077. // Polymorph Settings
  8078. game.settings.register("dnd5e", "polymorphSettings", {
  8079. scope: "client",
  8080. default: {
  8081. keepPhysical: false,
  8082. keepMental: false,
  8083. keepSaves: false,
  8084. keepSkills: false,
  8085. mergeSaves: false,
  8086. mergeSkills: false,
  8087. keepClass: false,
  8088. keepFeats: false,
  8089. keepSpells: false,
  8090. keepItems: false,
  8091. keepBio: false,
  8092. keepVision: true,
  8093. keepSelf: false,
  8094. keepAE: false,
  8095. keepOriginAE: true,
  8096. keepOtherOriginAE: true,
  8097. keepFeatAE: true,
  8098. keepSpellAE: true,
  8099. keepEquipmentAE: true,
  8100. keepClassAE: true,
  8101. keepBackgroundAE: true,
  8102. transformTokens: true
  8103. }
  8104. });
  8105. // Metric Unit Weights
  8106. game.settings.register("dnd5e", "metricWeightUnits", {
  8107. name: "SETTINGS.5eMetricN",
  8108. hint: "SETTINGS.5eMetricL",
  8109. scope: "world",
  8110. config: true,
  8111. type: Boolean,
  8112. default: false
  8113. });
  8114. // Critical Damage Modifiers
  8115. game.settings.register("dnd5e", "criticalDamageModifiers", {
  8116. name: "SETTINGS.5eCriticalModifiersN",
  8117. hint: "SETTINGS.5eCriticalModifiersL",
  8118. scope: "world",
  8119. config: true,
  8120. type: Boolean,
  8121. default: false
  8122. });
  8123. // Critical Damage Maximize
  8124. game.settings.register("dnd5e", "criticalDamageMaxDice", {
  8125. name: "SETTINGS.5eCriticalMaxDiceN",
  8126. hint: "SETTINGS.5eCriticalMaxDiceL",
  8127. scope: "world",
  8128. config: true,
  8129. type: Boolean,
  8130. default: false
  8131. });
  8132. // Strict validation
  8133. game.settings.register("dnd5e", "strictValidation", {
  8134. scope: "world",
  8135. config: false,
  8136. type: Boolean,
  8137. default: true
  8138. });
  8139. // Dynamic art.
  8140. game.settings.registerMenu("dnd5e", "moduleArtConfiguration", {
  8141. name: "DND5E.ModuleArtConfigN",
  8142. label: "DND5E.ModuleArtConfigL",
  8143. hint: "DND5E.ModuleArtConfigH",
  8144. icon: "fa-solid fa-palette",
  8145. type: ModuleArtConfig,
  8146. restricted: true
  8147. });
  8148. game.settings.register("dnd5e", "moduleArtConfiguration", {
  8149. name: "Module Art Configuration",
  8150. scope: "world",
  8151. config: false,
  8152. type: Object,
  8153. default: {
  8154. dnd5e: {
  8155. portraits: true,
  8156. tokens: true
  8157. }
  8158. }
  8159. });
  8160. }
  8161. /**
  8162. * Extend the base ActiveEffect class to implement system-specific logic.
  8163. */
  8164. class ActiveEffect5e extends ActiveEffect {
  8165. /**
  8166. * Is this active effect currently suppressed?
  8167. * @type {boolean}
  8168. */
  8169. isSuppressed = false;
  8170. /* --------------------------------------------- */
  8171. /** @inheritdoc */
  8172. apply(actor, change) {
  8173. if ( this.isSuppressed ) return null;
  8174. if ( change.key.startsWith("flags.dnd5e.") ) change = this._prepareFlagChange(actor, change);
  8175. return super.apply(actor, change);
  8176. }
  8177. /* -------------------------------------------- */
  8178. /** @inheritdoc */
  8179. _applyAdd(actor, change, current, delta, changes) {
  8180. if ( current instanceof Set ) {
  8181. if ( Array.isArray(delta) ) delta.forEach(item => current.add(item));
  8182. else current.add(delta);
  8183. return;
  8184. }
  8185. super._applyAdd(actor, change, current, delta, changes);
  8186. }
  8187. /* -------------------------------------------- */
  8188. /** @inheritdoc */
  8189. _applyOverride(actor, change, current, delta, changes) {
  8190. if ( current instanceof Set ) {
  8191. current.clear();
  8192. if ( Array.isArray(delta) ) delta.forEach(item => current.add(item));
  8193. else current.add(delta);
  8194. return;
  8195. }
  8196. return super._applyOverride(actor, change, current, delta, changes);
  8197. }
  8198. /* --------------------------------------------- */
  8199. /**
  8200. * Transform the data type of the change to match the type expected for flags.
  8201. * @param {Actor5e} actor The Actor to whom this effect should be applied.
  8202. * @param {EffectChangeData} change The change being applied.
  8203. * @returns {EffectChangeData} The change with altered types if necessary.
  8204. */
  8205. _prepareFlagChange(actor, change) {
  8206. const { key, value } = change;
  8207. const data = CONFIG.DND5E.characterFlags[key.replace("flags.dnd5e.", "")];
  8208. if ( !data ) return change;
  8209. // Set flag to initial value if it isn't present
  8210. const current = foundry.utils.getProperty(actor, key) ?? null;
  8211. if ( current === null ) {
  8212. let initialValue = null;
  8213. if ( data.placeholder ) initialValue = data.placeholder;
  8214. else if ( data.type === Boolean ) initialValue = false;
  8215. else if ( data.type === Number ) initialValue = 0;
  8216. foundry.utils.setProperty(actor, key, initialValue);
  8217. }
  8218. // Coerce change data into the correct type
  8219. if ( data.type === Boolean ) {
  8220. if ( value === "false" ) change.value = false;
  8221. else change.value = Boolean(value);
  8222. }
  8223. return change;
  8224. }
  8225. /* --------------------------------------------- */
  8226. /**
  8227. * Determine whether this Active Effect is suppressed or not.
  8228. */
  8229. determineSuppression() {
  8230. this.isSuppressed = false;
  8231. if ( this.disabled || (this.parent.documentName !== "Actor") ) return;
  8232. const parts = this.origin?.split(".") ?? [];
  8233. const [parentType, parentId, documentType, documentId, syntheticItem, syntheticItemId] = parts;
  8234. let item;
  8235. // Case 1: This is a linked or sidebar actor
  8236. if ( parentType === "Actor" ) {
  8237. if ( (parentId !== this.parent.id) || (documentType !== "Item") ) return;
  8238. item = this.parent.items.get(documentId);
  8239. }
  8240. // Case 2: This is a synthetic actor on the scene
  8241. else if ( parentType === "Scene" ) {
  8242. if ( (documentId !== this.parent.token?.id) || (syntheticItem !== "Item") ) return;
  8243. item = this.parent.items.get(syntheticItemId);
  8244. }
  8245. if ( !item ) return;
  8246. this.isSuppressed = item.areEffectsSuppressed;
  8247. }
  8248. /* --------------------------------------------- */
  8249. /**
  8250. * Manage Active Effect instances through the Actor Sheet via effect control buttons.
  8251. * @param {MouseEvent} event The left-click event on the effect control
  8252. * @param {Actor5e|Item5e} owner The owning document which manages this effect
  8253. * @returns {Promise|null} Promise that resolves when the changes are complete.
  8254. */
  8255. static onManageActiveEffect(event, owner) {
  8256. event.preventDefault();
  8257. const a = event.currentTarget;
  8258. const li = a.closest("li");
  8259. const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null;
  8260. switch ( a.dataset.action ) {
  8261. case "create":
  8262. return owner.createEmbeddedDocuments("ActiveEffect", [{
  8263. label: game.i18n.localize("DND5E.EffectNew"),
  8264. icon: "icons/svg/aura.svg",
  8265. origin: owner.uuid,
  8266. "duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined,
  8267. disabled: li.dataset.effectType === "inactive"
  8268. }]);
  8269. case "edit":
  8270. return effect.sheet.render(true);
  8271. case "delete":
  8272. return effect.delete();
  8273. case "toggle":
  8274. return effect.update({disabled: !effect.disabled});
  8275. }
  8276. }
  8277. /* --------------------------------------------- */
  8278. /**
  8279. * Prepare the data structure for Active Effects which are currently applied to an Actor or Item.
  8280. * @param {ActiveEffect5e[]} effects The array of Active Effect instances to prepare sheet data for
  8281. * @returns {object} Data for rendering
  8282. */
  8283. static prepareActiveEffectCategories(effects) {
  8284. // Define effect header categories
  8285. const categories = {
  8286. temporary: {
  8287. type: "temporary",
  8288. label: game.i18n.localize("DND5E.EffectTemporary"),
  8289. effects: []
  8290. },
  8291. passive: {
  8292. type: "passive",
  8293. label: game.i18n.localize("DND5E.EffectPassive"),
  8294. effects: []
  8295. },
  8296. inactive: {
  8297. type: "inactive",
  8298. label: game.i18n.localize("DND5E.EffectInactive"),
  8299. effects: []
  8300. },
  8301. suppressed: {
  8302. type: "suppressed",
  8303. label: game.i18n.localize("DND5E.EffectUnavailable"),
  8304. effects: [],
  8305. info: [game.i18n.localize("DND5E.EffectUnavailableInfo")]
  8306. }
  8307. };
  8308. // Iterate over active effects, classifying them into categories
  8309. for ( let e of effects ) {
  8310. if ( game.dnd5e.isV10 ) e._getSourceName(); // Trigger a lookup for the source name
  8311. if ( e.isSuppressed ) categories.suppressed.effects.push(e);
  8312. else if ( e.disabled ) categories.inactive.effects.push(e);
  8313. else if ( e.isTemporary ) categories.temporary.effects.push(e);
  8314. else categories.passive.effects.push(e);
  8315. }
  8316. categories.suppressed.hidden = !categories.suppressed.effects.length;
  8317. return categories;
  8318. }
  8319. }
  8320. /**
  8321. * A standardized helper function for simplifying the constant parts of a multipart roll formula.
  8322. *
  8323. * @param {string} formula The original roll formula.
  8324. * @param {object} [options] Formatting options.
  8325. * @param {boolean} [options.preserveFlavor=false] Preserve flavor text in the simplified formula.
  8326. *
  8327. * @returns {string} The resulting simplified formula.
  8328. */
  8329. function simplifyRollFormula(formula, { preserveFlavor=false } = {}) {
  8330. // Create a new roll and verify that the formula is valid before attempting simplification.
  8331. let roll;
  8332. try { roll = new Roll(formula); }
  8333. catch(err) { console.warn(`Unable to simplify formula '${formula}': ${err}`); }
  8334. Roll.validate(roll.formula);
  8335. // Optionally strip flavor annotations.
  8336. if ( !preserveFlavor ) roll.terms = Roll.parse(roll.formula.replace(RollTerm.FLAVOR_REGEXP, ""));
  8337. // Perform arithmetic simplification on the existing roll terms.
  8338. roll.terms = _simplifyOperatorTerms(roll.terms);
  8339. // If the formula contains multiplication or division we cannot easily simplify
  8340. if ( /[*/]/.test(roll.formula) ) {
  8341. if ( roll.isDeterministic && !/d\(/.test(roll.formula) && (!/\[/.test(roll.formula) || !preserveFlavor) ) {
  8342. return Roll.safeEval(roll.formula).toString();
  8343. }
  8344. else return roll.constructor.getFormula(roll.terms);
  8345. }
  8346. // Flatten the roll formula and eliminate string terms.
  8347. roll.terms = _expandParentheticalTerms(roll.terms);
  8348. roll.terms = Roll.simplifyTerms(roll.terms);
  8349. // Group terms by type and perform simplifications on various types of roll term.
  8350. let { poolTerms, diceTerms, mathTerms, numericTerms } = _groupTermsByType(roll.terms);
  8351. numericTerms = _simplifyNumericTerms(numericTerms ?? []);
  8352. diceTerms = _simplifyDiceTerms(diceTerms ?? []);
  8353. // Recombine the terms into a single term array and remove an initial + operator if present.
  8354. const simplifiedTerms = [diceTerms, poolTerms, mathTerms, numericTerms].flat().filter(Boolean);
  8355. if ( simplifiedTerms[0]?.operator === "+" ) simplifiedTerms.shift();
  8356. return roll.constructor.getFormula(simplifiedTerms);
  8357. }
  8358. /* -------------------------------------------- */
  8359. /**
  8360. * A helper function to perform arithmetic simplification and remove redundant operator terms.
  8361. * @param {RollTerm[]} terms An array of roll terms.
  8362. * @returns {RollTerm[]} A new array of roll terms with redundant operators removed.
  8363. */
  8364. function _simplifyOperatorTerms(terms) {
  8365. return terms.reduce((acc, term) => {
  8366. const prior = acc[acc.length - 1];
  8367. const ops = new Set([prior?.operator, term.operator]);
  8368. // If one of the terms is not an operator, add the current term as is.
  8369. if ( ops.has(undefined) ) acc.push(term);
  8370. // Replace consecutive "+ -" operators with a "-" operator.
  8371. else if ( (ops.has("+")) && (ops.has("-")) ) acc.splice(-1, 1, new OperatorTerm({ operator: "-" }));
  8372. // Replace double "-" operators with a "+" operator.
  8373. else if ( (ops.has("-")) && (ops.size === 1) ) acc.splice(-1, 1, new OperatorTerm({ operator: "+" }));
  8374. // Don't include "+" operators that directly follow "+", "*", or "/". Otherwise, add the term as is.
  8375. else if ( !ops.has("+") ) acc.push(term);
  8376. return acc;
  8377. }, []);
  8378. }
  8379. /* -------------------------------------------- */
  8380. /**
  8381. * A helper function for combining unannotated numeric terms in an array into a single numeric term.
  8382. * @param {object[]} terms An array of roll terms.
  8383. * @returns {object[]} A new array of terms with unannotated numeric terms combined into one.
  8384. */
  8385. function _simplifyNumericTerms(terms) {
  8386. const simplified = [];
  8387. const { annotated, unannotated } = _separateAnnotatedTerms(terms);
  8388. // Combine the unannotated numerical bonuses into a single new NumericTerm.
  8389. if ( unannotated.length ) {
  8390. const staticBonus = Roll.safeEval(Roll.getFormula(unannotated));
  8391. if ( staticBonus === 0 ) return [...annotated];
  8392. // If the staticBonus is greater than 0, add a "+" operator so the formula remains valid.
  8393. if ( staticBonus > 0 ) simplified.push(new OperatorTerm({ operator: "+"}));
  8394. simplified.push(new NumericTerm({ number: staticBonus} ));
  8395. }
  8396. return [...simplified, ...annotated];
  8397. }
  8398. /* -------------------------------------------- */
  8399. /**
  8400. * A helper function to group dice of the same size and sign into single dice terms.
  8401. * @param {object[]} terms An array of DiceTerms and associated OperatorTerms.
  8402. * @returns {object[]} A new array of simplified dice terms.
  8403. */
  8404. function _simplifyDiceTerms(terms) {
  8405. const { annotated, unannotated } = _separateAnnotatedTerms(terms);
  8406. // Split the unannotated terms into different die sizes and signs
  8407. const diceQuantities = unannotated.reduce((obj, curr, i) => {
  8408. if ( curr instanceof OperatorTerm ) return obj;
  8409. const key = `${unannotated[i - 1].operator}${curr.faces}`;
  8410. obj[key] = (obj[key] ?? 0) + curr.number;
  8411. return obj;
  8412. }, {});
  8413. // Add new die and operator terms to simplified for each die size and sign
  8414. const simplified = Object.entries(diceQuantities).flatMap(([key, number]) => ([
  8415. new OperatorTerm({ operator: key.charAt(0) }),
  8416. new Die({ number, faces: parseInt(key.slice(1)) })
  8417. ]));
  8418. return [...simplified, ...annotated];
  8419. }
  8420. /* -------------------------------------------- */
  8421. /**
  8422. * A helper function to extract the contents of parenthetical terms into their own terms.
  8423. * @param {object[]} terms An array of roll terms.
  8424. * @returns {object[]} A new array of terms with no parenthetical terms.
  8425. */
  8426. function _expandParentheticalTerms(terms) {
  8427. terms = terms.reduce((acc, term) => {
  8428. if ( term instanceof ParentheticalTerm ) {
  8429. if ( term.isDeterministic ) term = new NumericTerm({ number: Roll.safeEval(term.term) });
  8430. else {
  8431. const subterms = new Roll(term.term).terms;
  8432. term = _expandParentheticalTerms(subterms);
  8433. }
  8434. }
  8435. acc.push(term);
  8436. return acc;
  8437. }, []);
  8438. return _simplifyOperatorTerms(terms.flat());
  8439. }
  8440. /* -------------------------------------------- */
  8441. /**
  8442. * A helper function to group terms into PoolTerms, DiceTerms, MathTerms, and NumericTerms.
  8443. * MathTerms are included as NumericTerms if they are deterministic.
  8444. * @param {RollTerm[]} terms An array of roll terms.
  8445. * @returns {object} An object mapping term types to arrays containing roll terms of that type.
  8446. */
  8447. function _groupTermsByType(terms) {
  8448. // Add an initial operator so that terms can be rearranged arbitrarily.
  8449. if ( !(terms[0] instanceof OperatorTerm) ) terms.unshift(new OperatorTerm({ operator: "+" }));
  8450. return terms.reduce((obj, term, i) => {
  8451. let type;
  8452. if ( term instanceof DiceTerm ) type = DiceTerm;
  8453. else if ( (term instanceof MathTerm) && (term.isDeterministic) ) type = NumericTerm;
  8454. else type = term.constructor;
  8455. const key = `${type.name.charAt(0).toLowerCase()}${type.name.substring(1)}s`;
  8456. // Push the term and the preceding OperatorTerm.
  8457. (obj[key] = obj[key] ?? []).push(terms[i - 1], term);
  8458. return obj;
  8459. }, {});
  8460. }
  8461. /* -------------------------------------------- */
  8462. /**
  8463. * A helper function to separate annotated terms from unannotated terms.
  8464. * @param {object[]} terms An array of DiceTerms and associated OperatorTerms.
  8465. * @returns {Array | Array[]} A pair of term arrays, one containing annotated terms.
  8466. */
  8467. function _separateAnnotatedTerms(terms) {
  8468. return terms.reduce((obj, curr, i) => {
  8469. if ( curr instanceof OperatorTerm ) return obj;
  8470. obj[curr.flavor ? "annotated" : "unannotated"].push(terms[i - 1], curr);
  8471. return obj;
  8472. }, { annotated: [], unannotated: [] });
  8473. }
  8474. /**
  8475. * A specialized Dialog subclass for ability usage.
  8476. *
  8477. * @param {Item5e} item Item that is being used.
  8478. * @param {object} [dialogData={}] An object of dialog data which configures how the modal window is rendered.
  8479. * @param {object} [options={}] Dialog rendering options.
  8480. */
  8481. class AbilityUseDialog extends Dialog {
  8482. constructor(item, dialogData={}, options={}) {
  8483. super(dialogData, options);
  8484. this.options.classes = ["dnd5e", "dialog"];
  8485. /**
  8486. * Store a reference to the Item document being used
  8487. * @type {Item5e}
  8488. */
  8489. this.item = item;
  8490. }
  8491. /* -------------------------------------------- */
  8492. /* Rendering */
  8493. /* -------------------------------------------- */
  8494. /**
  8495. * A constructor function which displays the Spell Cast Dialog app for a given Actor and Item.
  8496. * Returns a Promise which resolves to the dialog FormData once the workflow has been completed.
  8497. * @param {Item5e} item Item being used.
  8498. * @returns {Promise} Promise that is resolved when the use dialog is acted upon.
  8499. */
  8500. static async create(item) {
  8501. if ( !item.isOwned ) throw new Error("You cannot display an ability usage dialog for an unowned item");
  8502. // Prepare data
  8503. const uses = item.system.uses ?? {};
  8504. const resource = item.system.consume ?? {};
  8505. const quantity = item.system.quantity ?? 0;
  8506. const recharge = item.system.recharge ?? {};
  8507. const recharges = !!recharge.value;
  8508. const sufficientUses = (quantity > 0 && !uses.value) || uses.value > 0;
  8509. // Prepare dialog form data
  8510. const data = {
  8511. item: item,
  8512. title: game.i18n.format("DND5E.AbilityUseHint", {type: game.i18n.localize(CONFIG.Item.typeLabels[item.type]), name: item.name}),
  8513. note: this._getAbilityUseNote(item, uses, recharge),
  8514. consumeSpellSlot: false,
  8515. consumeRecharge: recharges,
  8516. consumeResource: resource.target && (!item.hasAttack || (resource.type !== "ammo")),
  8517. consumeUses: uses.per && (uses.max > 0),
  8518. canUse: recharges ? recharge.charged : sufficientUses,
  8519. createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
  8520. errors: []
  8521. };
  8522. if ( item.type === "spell" ) this._getSpellData(item.actor.system, item.system, data);
  8523. // Render the ability usage template
  8524. const html = await renderTemplate("systems/dnd5e/templates/apps/ability-use.hbs", data);
  8525. // Create the Dialog and return data as a Promise
  8526. const icon = data.isSpell ? "fa-magic" : "fa-fist-raised";
  8527. const label = game.i18n.localize(`DND5E.AbilityUse${data.isSpell ? "Cast" : "Use"}`);
  8528. return new Promise(resolve => {
  8529. const dlg = new this(item, {
  8530. title: `${item.name}: ${game.i18n.localize("DND5E.AbilityUseConfig")}`,
  8531. content: html,
  8532. buttons: {
  8533. use: {
  8534. icon: `<i class="fas ${icon}"></i>`,
  8535. label: label,
  8536. callback: html => {
  8537. const fd = new FormDataExtended(html[0].querySelector("form"));
  8538. resolve(fd.object);
  8539. }
  8540. }
  8541. },
  8542. default: "use",
  8543. close: () => resolve(null)
  8544. });
  8545. dlg.render(true);
  8546. });
  8547. }
  8548. /* -------------------------------------------- */
  8549. /* Helpers */
  8550. /* -------------------------------------------- */
  8551. /**
  8552. * Get dialog data related to limited spell slots.
  8553. * @param {object} actorData System data from the actor using the spell.
  8554. * @param {object} itemData System data from the spell being used.
  8555. * @param {object} data Data for the dialog being presented.
  8556. * @returns {object} Modified dialog data.
  8557. * @private
  8558. */
  8559. static _getSpellData(actorData, itemData, data) {
  8560. // Determine whether the spell may be up-cast
  8561. const lvl = itemData.level;
  8562. const consumeSpellSlot = (lvl > 0) && CONFIG.DND5E.spellUpcastModes.includes(itemData.preparation.mode);
  8563. // If can't upcast, return early and don't bother calculating available spell slots
  8564. if ( !consumeSpellSlot ) {
  8565. return foundry.utils.mergeObject(data, { isSpell: true, consumeSpellSlot });
  8566. }
  8567. // Determine the levels which are feasible
  8568. let lmax = 0;
  8569. const spellLevels = Array.fromRange(Object.keys(CONFIG.DND5E.spellLevels).length).reduce((arr, i) => {
  8570. if ( i < lvl ) return arr;
  8571. const label = CONFIG.DND5E.spellLevels[i];
  8572. const l = actorData.spells[`spell${i}`] || {max: 0, override: null};
  8573. let max = parseInt(l.override || l.max || 0);
  8574. let slots = Math.clamped(parseInt(l.value || 0), 0, max);
  8575. if ( max > 0 ) lmax = i;
  8576. arr.push({
  8577. level: i,
  8578. label: i > 0 ? game.i18n.format("DND5E.SpellLevelSlot", {level: label, n: slots}) : label,
  8579. canCast: max > 0,
  8580. hasSlots: slots > 0
  8581. });
  8582. return arr;
  8583. }, []).filter(sl => sl.level <= lmax);
  8584. // If this character has pact slots, present them as an option for casting the spell.
  8585. const pact = actorData.spells.pact;
  8586. if ( pact.level >= lvl ) {
  8587. spellLevels.push({
  8588. level: "pact",
  8589. label: `${game.i18n.format("DND5E.SpellLevelPact", {level: pact.level, n: pact.value})}`,
  8590. canCast: true,
  8591. hasSlots: pact.value > 0
  8592. });
  8593. }
  8594. const canCast = spellLevels.some(l => l.hasSlots);
  8595. if ( !canCast ) data.errors.push(game.i18n.format("DND5E.SpellCastNoSlots", {
  8596. level: CONFIG.DND5E.spellLevels[lvl],
  8597. name: data.item.name
  8598. }));
  8599. // Merge spell casting data
  8600. return foundry.utils.mergeObject(data, { isSpell: true, consumeSpellSlot, spellLevels });
  8601. }
  8602. /* -------------------------------------------- */
  8603. /**
  8604. * Get the ability usage note that is displayed.
  8605. * @param {object} item Data for the item being used.
  8606. * @param {{value: number, max: number, per: string}} uses Object uses and recovery configuration.
  8607. * @param {{charged: boolean, value: string}} recharge Object recharge configuration.
  8608. * @returns {string} Localized string indicating available uses.
  8609. * @private
  8610. */
  8611. static _getAbilityUseNote(item, uses, recharge) {
  8612. // Zero quantity
  8613. const quantity = item.system.quantity;
  8614. if ( quantity <= 0 ) return game.i18n.localize("DND5E.AbilityUseUnavailableHint");
  8615. // Abilities which use Recharge
  8616. if ( recharge.value ) {
  8617. return game.i18n.format(recharge.charged ? "DND5E.AbilityUseChargedHint" : "DND5E.AbilityUseRechargeHint", {
  8618. type: game.i18n.localize(CONFIG.Item.typeLabels[item.type])
  8619. });
  8620. }
  8621. // Does not use any resource
  8622. if ( !uses.per || !uses.max ) return "";
  8623. // Consumables
  8624. if ( item.type === "consumable" ) {
  8625. let str = "DND5E.AbilityUseNormalHint";
  8626. if ( uses.value > 1 ) str = "DND5E.AbilityUseConsumableChargeHint";
  8627. else if ( item.system.quantity === 1 && uses.autoDestroy ) str = "DND5E.AbilityUseConsumableDestroyHint";
  8628. else if ( item.system.quantity > 1 ) str = "DND5E.AbilityUseConsumableQuantityHint";
  8629. return game.i18n.format(str, {
  8630. type: game.i18n.localize(`DND5E.Consumable${item.system.consumableType.capitalize()}`),
  8631. value: uses.value,
  8632. quantity: item.system.quantity,
  8633. max: uses.max,
  8634. per: CONFIG.DND5E.limitedUsePeriods[uses.per]
  8635. });
  8636. }
  8637. // Other Items
  8638. else {
  8639. return game.i18n.format("DND5E.AbilityUseNormalHint", {
  8640. type: game.i18n.localize(CONFIG.Item.typeLabels[item.type]),
  8641. value: uses.value,
  8642. max: uses.max,
  8643. per: CONFIG.DND5E.limitedUsePeriods[uses.per]
  8644. });
  8645. }
  8646. }
  8647. }
  8648. /**
  8649. * Override and extend the basic Item implementation.
  8650. */
  8651. class Item5e extends Item {
  8652. /**
  8653. * Caches an item linked to this one, such as a subclass associated with a class.
  8654. * @type {Item5e}
  8655. * @private
  8656. */
  8657. _classLink;
  8658. /* -------------------------------------------- */
  8659. /* Item Properties */
  8660. /* -------------------------------------------- */
  8661. /**
  8662. * Which ability score modifier is used by this item?
  8663. * @type {string|null}
  8664. * @see {@link ActionTemplate#abilityMod}
  8665. */
  8666. get abilityMod() {
  8667. return this.system.abilityMod ?? null;
  8668. }
  8669. /* --------------------------------------------- */
  8670. /**
  8671. * What is the critical hit threshold for this item, if applicable?
  8672. * @type {number|null}
  8673. * @see {@link ActionTemplate#criticalThreshold}
  8674. */
  8675. get criticalThreshold() {
  8676. return this.system.criticalThreshold ?? null;
  8677. }
  8678. /* --------------------------------------------- */
  8679. /**
  8680. * Does the Item implement an ability check as part of its usage?
  8681. * @type {boolean}
  8682. * @see {@link ActionTemplate#hasAbilityCheck}
  8683. */
  8684. get hasAbilityCheck() {
  8685. return this.system.hasAbilityCheck ?? false;
  8686. }
  8687. /* -------------------------------------------- */
  8688. /**
  8689. * Does this item support advancement and have advancements defined?
  8690. * @type {boolean}
  8691. */
  8692. get hasAdvancement() {
  8693. return !!this.system.advancement?.length;
  8694. }
  8695. /* -------------------------------------------- */
  8696. /**
  8697. * Does the Item have an area of effect target?
  8698. * @type {boolean}
  8699. * @see {@link ActivatedEffectTemplate#hasAreaTarget}
  8700. */
  8701. get hasAreaTarget() {
  8702. return this.system.hasAreaTarget ?? false;
  8703. }
  8704. /* -------------------------------------------- */
  8705. /**
  8706. * Does the Item implement an attack roll as part of its usage?
  8707. * @type {boolean}
  8708. * @see {@link ActionTemplate#hasAttack}
  8709. */
  8710. get hasAttack() {
  8711. return this.system.hasAttack ?? false;
  8712. }
  8713. /* -------------------------------------------- */
  8714. /**
  8715. * Does the Item implement a damage roll as part of its usage?
  8716. * @type {boolean}
  8717. * @see {@link ActionTemplate#hasDamage}
  8718. */
  8719. get hasDamage() {
  8720. return this.system.hasDamage ?? false;
  8721. }
  8722. /* -------------------------------------------- */
  8723. /**
  8724. * Does the Item target one or more distinct targets?
  8725. * @type {boolean}
  8726. * @see {@link ActivatedEffectTemplate#hasIndividualTarget}
  8727. */
  8728. get hasIndividualTarget() {
  8729. return this.system.hasIndividualTarget ?? false;
  8730. }
  8731. /* -------------------------------------------- */
  8732. /**
  8733. * Is this Item limited in its ability to be used by charges or by recharge?
  8734. * @type {boolean}
  8735. * @see {@link ActivatedEffectTemplate#hasLimitedUses}
  8736. * @see {@link FeatData#hasLimitedUses}
  8737. */
  8738. get hasLimitedUses() {
  8739. return this.system.hasLimitedUses ?? false;
  8740. }
  8741. /* -------------------------------------------- */
  8742. /**
  8743. * Does the Item implement a saving throw as part of its usage?
  8744. * @type {boolean}
  8745. * @see {@link ActionTemplate#hasSave}
  8746. */
  8747. get hasSave() {
  8748. return this.system.hasSave ?? false;
  8749. }
  8750. /* -------------------------------------------- */
  8751. /**
  8752. * Does the Item have a target?
  8753. * @type {boolean}
  8754. * @see {@link ActivatedEffectTemplate#hasTarget}
  8755. */
  8756. get hasTarget() {
  8757. return this.system.hasTarget ?? false;
  8758. }
  8759. /* -------------------------------------------- */
  8760. /**
  8761. * Return an item's identifier.
  8762. * @type {string}
  8763. */
  8764. get identifier() {
  8765. return this.system.identifier || this.name.slugify({strict: true});
  8766. }
  8767. /* -------------------------------------------- */
  8768. /**
  8769. * Is this item any of the armor subtypes?
  8770. * @type {boolean}
  8771. * @see {@link EquipmentTemplate#isArmor}
  8772. */
  8773. get isArmor() {
  8774. return this.system.isArmor ?? false;
  8775. }
  8776. /* -------------------------------------------- */
  8777. /**
  8778. * Does the item provide an amount of healing instead of conventional damage?
  8779. * @type {boolean}
  8780. * @see {@link ActionTemplate#isHealing}
  8781. */
  8782. get isHealing() {
  8783. return this.system.isHealing ?? false;
  8784. }
  8785. /* -------------------------------------------- */
  8786. /**
  8787. * Is this item a separate large object like a siege engine or vehicle component that is
  8788. * usually mounted on fixtures rather than equipped, and has its own AC and HP?
  8789. * @type {boolean}
  8790. * @see {@link EquipmentData#isMountable}
  8791. * @see {@link WeaponData#isMountable}
  8792. */
  8793. get isMountable() {
  8794. return this.system.isMountable ?? false;
  8795. }
  8796. /* -------------------------------------------- */
  8797. /**
  8798. * Is this class item the original class for the containing actor? If the item is not a class or it is not
  8799. * embedded in an actor then this will return `null`.
  8800. * @type {boolean|null}
  8801. */
  8802. get isOriginalClass() {
  8803. if ( this.type !== "class" || !this.isEmbedded ) return null;
  8804. return this.id === this.parent.system.details.originalClass;
  8805. }
  8806. /* -------------------------------------------- */
  8807. /**
  8808. * Does the Item implement a versatile damage roll as part of its usage?
  8809. * @type {boolean}
  8810. * @see {@link ActionTemplate#isVersatile}
  8811. */
  8812. get isVersatile() {
  8813. return this.system.isVersatile ?? false;
  8814. }
  8815. /* -------------------------------------------- */
  8816. /**
  8817. * Class associated with this subclass. Always returns null on non-subclass or non-embedded items.
  8818. * @type {Item5e|null}
  8819. */
  8820. get class() {
  8821. if ( !this.isEmbedded || (this.type !== "subclass") ) return null;
  8822. const cid = this.system.classIdentifier;
  8823. return this._classLink ??= this.parent.items.find(i => (i.type === "class") && (i.identifier === cid));
  8824. }
  8825. /* -------------------------------------------- */
  8826. /**
  8827. * Subclass associated with this class. Always returns null on non-class or non-embedded items.
  8828. * @type {Item5e|null}
  8829. */
  8830. get subclass() {
  8831. if ( !this.isEmbedded || (this.type !== "class") ) return null;
  8832. const items = this.parent.items;
  8833. const cid = this.identifier;
  8834. return this._classLink ??= items.find(i => (i.type === "subclass") && (i.system.classIdentifier === cid));
  8835. }
  8836. /* -------------------------------------------- */
  8837. /**
  8838. * Retrieve scale values for current level from advancement data.
  8839. * @type {object}
  8840. */
  8841. get scaleValues() {
  8842. if ( !["class", "subclass"].includes(this.type) || !this.advancement.byType.ScaleValue ) return {};
  8843. const level = this.type === "class" ? this.system.levels : this.class?.system.levels ?? 0;
  8844. return this.advancement.byType.ScaleValue.reduce((obj, advancement) => {
  8845. obj[advancement.identifier] = advancement.valueForLevel(level);
  8846. return obj;
  8847. }, {});
  8848. }
  8849. /* -------------------------------------------- */
  8850. /**
  8851. * Spellcasting details for a class or subclass.
  8852. *
  8853. * @typedef {object} SpellcastingDescription
  8854. * @property {string} type Spellcasting type as defined in ``CONFIG.DND5E.spellcastingTypes`.
  8855. * @property {string|null} progression Progression within the specified spellcasting type if supported.
  8856. * @property {string} ability Ability used when casting spells from this class or subclass.
  8857. * @property {number|null} levels Number of levels of this class or subclass's class if embedded.
  8858. */
  8859. /**
  8860. * Retrieve the spellcasting for a class or subclass. For classes, this will return the spellcasting
  8861. * of the subclass if it overrides the class. For subclasses, this will return the class's spellcasting
  8862. * if no spellcasting is defined on the subclass.
  8863. * @type {SpellcastingDescription|null} Spellcasting object containing progression & ability.
  8864. */
  8865. get spellcasting() {
  8866. const spellcasting = this.system.spellcasting;
  8867. if ( !spellcasting ) return null;
  8868. const isSubclass = this.type === "subclass";
  8869. const classSC = isSubclass ? this.class?.system.spellcasting : spellcasting;
  8870. const subclassSC = isSubclass ? spellcasting : this.subclass?.system.spellcasting;
  8871. const finalSC = foundry.utils.deepClone(
  8872. ( subclassSC && (subclassSC.progression !== "none") ) ? subclassSC : classSC
  8873. );
  8874. if ( !finalSC ) return null;
  8875. finalSC.levels = this.isEmbedded ? (this.system.levels ?? this.class?.system.levels) : null;
  8876. // Temp method for determining spellcasting type until this data is available directly using advancement
  8877. if ( CONFIG.DND5E.spellcastingTypes[finalSC.progression] ) finalSC.type = finalSC.progression;
  8878. else finalSC.type = Object.entries(CONFIG.DND5E.spellcastingTypes).find(([type, data]) => {
  8879. return !!data.progression?.[finalSC.progression];
  8880. })?.[0];
  8881. return finalSC;
  8882. }
  8883. /* -------------------------------------------- */
  8884. /**
  8885. * Should this item's active effects be suppressed.
  8886. * @type {boolean}
  8887. */
  8888. get areEffectsSuppressed() {
  8889. const requireEquipped = (this.type !== "consumable")
  8890. || ["rod", "trinket", "wand"].includes(this.system.consumableType);
  8891. if ( requireEquipped && (this.system.equipped === false) ) return true;
  8892. return this.system.attunement === CONFIG.DND5E.attunementTypes.REQUIRED;
  8893. }
  8894. /* -------------------------------------------- */
  8895. /* Data Preparation */
  8896. /* -------------------------------------------- */
  8897. /** @inheritDoc */
  8898. prepareDerivedData() {
  8899. super.prepareDerivedData();
  8900. this.labels = {};
  8901. // Clear out linked item cache
  8902. this._classLink = undefined;
  8903. // Advancement
  8904. this._prepareAdvancement();
  8905. // Specialized preparation per Item type
  8906. switch ( this.type ) {
  8907. case "equipment":
  8908. this._prepareEquipment(); break;
  8909. case "feat":
  8910. this._prepareFeat(); break;
  8911. case "spell":
  8912. this._prepareSpell(); break;
  8913. }
  8914. // Activated Items
  8915. this._prepareActivation();
  8916. this._prepareAction();
  8917. // Un-owned items can have their final preparation done here, otherwise this needs to happen in the owning Actor
  8918. if ( !this.isOwned ) this.prepareFinalAttributes();
  8919. }
  8920. /* -------------------------------------------- */
  8921. /**
  8922. * Prepare derived data for an equipment-type item and define labels.
  8923. * @protected
  8924. */
  8925. _prepareEquipment() {
  8926. this.labels.armor = this.system.armor.value ? `${this.system.armor.value} ${game.i18n.localize("DND5E.AC")}` : "";
  8927. }
  8928. /* -------------------------------------------- */
  8929. /**
  8930. * Prepare derived data for a feat-type item and define labels.
  8931. * @protected
  8932. */
  8933. _prepareFeat() {
  8934. const act = this.system.activation;
  8935. const types = CONFIG.DND5E.abilityActivationTypes;
  8936. if ( act?.type === types.legendary ) this.labels.featType = game.i18n.localize("DND5E.LegendaryActionLabel");
  8937. else if ( act?.type === types.lair ) this.labels.featType = game.i18n.localize("DND5E.LairActionLabel");
  8938. else if ( act?.type ) {
  8939. this.labels.featType = game.i18n.localize(this.system.damage.length ? "DND5E.Attack" : "DND5E.Action");
  8940. }
  8941. else this.labels.featType = game.i18n.localize("DND5E.Passive");
  8942. }
  8943. /* -------------------------------------------- */
  8944. /**
  8945. * Prepare derived data for a spell-type item and define labels.
  8946. * @protected
  8947. */
  8948. _prepareSpell() {
  8949. const tags = Object.fromEntries(Object.entries(CONFIG.DND5E.spellTags).map(([k, v]) => {
  8950. v.tag = true;
  8951. return [k, v];
  8952. }));
  8953. const attributes = {...CONFIG.DND5E.spellComponents, ...tags};
  8954. this.system.preparation.mode ||= "prepared";
  8955. this.labels.level = CONFIG.DND5E.spellLevels[this.system.level];
  8956. this.labels.school = CONFIG.DND5E.spellSchools[this.system.school];
  8957. this.labels.components = Object.entries(this.system.components).reduce((obj, [c, active]) => {
  8958. const config = attributes[c];
  8959. if ( !config || (active !== true) ) return obj;
  8960. obj.all.push({abbr: config.abbr, tag: config.tag});
  8961. if ( config.tag ) obj.tags.push(config.label);
  8962. else obj.vsm.push(config.abbr);
  8963. return obj;
  8964. }, {all: [], vsm: [], tags: []});
  8965. this.labels.components.vsm = new Intl.ListFormat(game.i18n.lang, { style: "narrow", type: "conjunction" })
  8966. .format(this.labels.components.vsm);
  8967. this.labels.materials = this.system?.materials?.value ?? null;
  8968. }
  8969. /* -------------------------------------------- */
  8970. /**
  8971. * Prepare derived data for activated items and define labels.
  8972. * @protected
  8973. */
  8974. _prepareActivation() {
  8975. if ( !("activation" in this.system) ) return;
  8976. const C = CONFIG.DND5E;
  8977. // Ability Activation Label
  8978. const act = this.system.activation ?? {};
  8979. if ( ["none", ""].includes(act.type) ) act.type = null; // Backwards compatibility
  8980. this.labels.activation = act.type ? [act.cost, C.abilityActivationTypes[act.type]].filterJoin(" ") : "";
  8981. // Target Label
  8982. let tgt = this.system.target ?? {};
  8983. if ( ["none", ""].includes(tgt.type) ) tgt.type = null; // Backwards compatibility
  8984. if ( [null, "self"].includes(tgt.type) ) tgt.value = tgt.units = null;
  8985. else if ( tgt.units === "touch" ) tgt.value = null;
  8986. this.labels.target = tgt.type
  8987. ? [tgt.value, C.distanceUnits[tgt.units], C.targetTypes[tgt.type]].filterJoin(" ") : "";
  8988. // Range Label
  8989. let rng = this.system.range ?? {};
  8990. if ( ["none", ""].includes(rng.units) ) rng.units = null; // Backwards compatibility
  8991. if ( [null, "touch", "self"].includes(rng.units) ) rng.value = rng.long = null;
  8992. this.labels.range = rng.units
  8993. ? [rng.value, rng.long ? `/ ${rng.long}` : null, C.distanceUnits[rng.units]].filterJoin(" ") : "";
  8994. // Recharge Label
  8995. let chg = this.system.recharge ?? {};
  8996. const chgSuffix = `${chg.value}${parseInt(chg.value) < 6 ? "+" : ""}`;
  8997. this.labels.recharge = `${game.i18n.localize("DND5E.Recharge")} [${chgSuffix}]`;
  8998. }
  8999. /* -------------------------------------------- */
  9000. /**
  9001. * Prepare derived data and labels for items which have an action which deals damage.
  9002. * @protected
  9003. */
  9004. _prepareAction() {
  9005. if ( !("actionType" in this.system) ) return;
  9006. let dmg = this.system.damage || {};
  9007. if ( dmg.parts ) {
  9008. const types = CONFIG.DND5E.damageTypes;
  9009. this.labels.damage = dmg.parts.map(d => d[0]).join(" + ").replace(/\+ -/g, "- ");
  9010. this.labels.damageTypes = dmg.parts.map(d => types[d[1]]).join(", ");
  9011. }
  9012. }
  9013. /* -------------------------------------------- */
  9014. /**
  9015. * Prepare advancement objects from stored advancement data.
  9016. * @protected
  9017. */
  9018. _prepareAdvancement() {
  9019. const minAdvancementLevel = ["class", "subclass"].includes(this.type) ? 1 : 0;
  9020. this.advancement = {
  9021. byId: {},
  9022. byLevel: Object.fromEntries(
  9023. Array.fromRange(CONFIG.DND5E.maxLevel + 1).slice(minAdvancementLevel).map(l => [l, []])
  9024. ),
  9025. byType: {},
  9026. needingConfiguration: []
  9027. };
  9028. for ( const advancement of this.system.advancement ?? [] ) {
  9029. if ( !(advancement instanceof Advancement) ) continue;
  9030. this.advancement.byId[advancement.id] = advancement;
  9031. this.advancement.byType[advancement.type] ??= [];
  9032. this.advancement.byType[advancement.type].push(advancement);
  9033. advancement.levels.forEach(l => this.advancement.byLevel[l].push(advancement));
  9034. if ( !advancement.levels.length ) this.advancement.needingConfiguration.push(advancement);
  9035. }
  9036. Object.entries(this.advancement.byLevel).forEach(([lvl, data]) => data.sort((a, b) => {
  9037. return a.sortingValueForLevel(lvl).localeCompare(b.sortingValueForLevel(lvl));
  9038. }));
  9039. }
  9040. /* -------------------------------------------- */
  9041. /**
  9042. * Determine an item's proficiency level based on its parent actor's proficiencies.
  9043. * @protected
  9044. */
  9045. _prepareProficiency() {
  9046. if ( !["spell", "weapon", "equipment", "tool", "feat", "consumable"].includes(this.type) ) return;
  9047. if ( !this.actor?.system.attributes?.prof ) {
  9048. this.system.prof = new Proficiency(0, 0);
  9049. return;
  9050. }
  9051. this.system.prof = new Proficiency(this.actor.system.attributes.prof, this.system.proficiencyMultiplier ?? 0);
  9052. }
  9053. /* -------------------------------------------- */
  9054. /**
  9055. * Compute item attributes which might depend on prepared actor data. If this item is embedded this method will
  9056. * be called after the actor's data is prepared.
  9057. * Otherwise, it will be called at the end of `Item5e#prepareDerivedData`.
  9058. */
  9059. prepareFinalAttributes() {
  9060. // Proficiency
  9061. this._prepareProficiency();
  9062. // Class data
  9063. if ( this.type === "class" ) this.system.isOriginalClass = this.isOriginalClass;
  9064. // Action usage
  9065. if ( "actionType" in this.system ) {
  9066. this.labels.abilityCheck = game.i18n.format("DND5E.AbilityPromptTitle", {
  9067. ability: CONFIG.DND5E.abilities[this.system.ability]?.label ?? ""
  9068. });
  9069. // Saving throws
  9070. this.getSaveDC();
  9071. // To Hit
  9072. this.getAttackToHit();
  9073. // Limited Uses
  9074. this.prepareMaxUses();
  9075. // Duration
  9076. this.prepareDurationValue();
  9077. // Damage Label
  9078. this.getDerivedDamageLabel();
  9079. }
  9080. }
  9081. /* -------------------------------------------- */
  9082. /**
  9083. * Populate a label with the compiled and simplified damage formula based on owned item
  9084. * actor data. This is only used for display purposes and is not related to `Item5e#rollDamage`.
  9085. * @returns {{damageType: string, formula: string, label: string}[]}
  9086. */
  9087. getDerivedDamageLabel() {
  9088. if ( !this.hasDamage || !this.isOwned ) return [];
  9089. const rollData = this.getRollData();
  9090. const damageLabels = { ...CONFIG.DND5E.damageTypes, ...CONFIG.DND5E.healingTypes };
  9091. const derivedDamage = this.system.damage?.parts?.map(damagePart => {
  9092. let formula;
  9093. try {
  9094. const roll = new Roll(damagePart[0], rollData);
  9095. formula = simplifyRollFormula(roll.formula, { preserveFlavor: true });
  9096. }
  9097. catch(err) {
  9098. console.warn(`Unable to simplify formula for ${this.name}: ${err}`);
  9099. }
  9100. const damageType = damagePart[1];
  9101. return { formula, damageType, label: `${formula} ${damageLabels[damageType] ?? ""}` };
  9102. });
  9103. return this.labels.derivedDamage = derivedDamage;
  9104. }
  9105. /* -------------------------------------------- */
  9106. /**
  9107. * Update the derived spell DC for an item that requires a saving throw.
  9108. * @returns {number|null}
  9109. */
  9110. getSaveDC() {
  9111. if ( !this.hasSave ) return null;
  9112. const save = this.system.save;
  9113. // Actor spell-DC based scaling
  9114. if ( save.scaling === "spell" ) {
  9115. save.dc = this.isOwned ? this.actor.system.attributes.spelldc : null;
  9116. }
  9117. // Ability-score based scaling
  9118. else if ( save.scaling !== "flat" ) {
  9119. save.dc = this.isOwned ? this.actor.system.abilities[save.scaling].dc : null;
  9120. }
  9121. // Update labels
  9122. const abl = CONFIG.DND5E.abilities[save.ability]?.label ?? "";
  9123. this.labels.save = game.i18n.format("DND5E.SaveDC", {dc: save.dc || "", ability: abl});
  9124. return save.dc;
  9125. }
  9126. /* -------------------------------------------- */
  9127. /**
  9128. * Update a label to the Item detailing its total to hit bonus from the following sources:
  9129. * - item document's innate attack bonus
  9130. * - item's actor's proficiency bonus if applicable
  9131. * - item's actor's global bonuses to the given item type
  9132. * - item's ammunition if applicable
  9133. * @returns {{rollData: object, parts: string[]}|null} Data used in the item's Attack roll.
  9134. */
  9135. getAttackToHit() {
  9136. if ( !this.hasAttack ) return null;
  9137. const rollData = this.getRollData();
  9138. const parts = [];
  9139. // Include the item's innate attack bonus as the initial value and label
  9140. const ab = this.system.attackBonus;
  9141. if ( ab ) {
  9142. parts.push(ab);
  9143. this.labels.toHit = !/^[+-]/.test(ab) ? `+ ${ab}` : ab;
  9144. }
  9145. // Take no further action for un-owned items
  9146. if ( !this.isOwned ) return {rollData, parts};
  9147. // Ability score modifier
  9148. if ( this.system.ability !== "none" ) parts.push("@mod");
  9149. // Add proficiency bonus.
  9150. if ( this.system.prof?.hasProficiency ) {
  9151. parts.push("@prof");
  9152. rollData.prof = this.system.prof.term;
  9153. }
  9154. // Actor-level global bonus to attack rolls
  9155. const actorBonus = this.actor.system.bonuses?.[this.system.actionType] || {};
  9156. if ( actorBonus.attack ) parts.push(actorBonus.attack);
  9157. // One-time bonus provided by consumed ammunition
  9158. if ( (this.system.consume?.type === "ammo") && this.actor.items ) {
  9159. const ammoItem = this.actor.items.get(this.system.consume.target);
  9160. if ( ammoItem ) {
  9161. const ammoItemQuantity = ammoItem.system.quantity;
  9162. const ammoCanBeConsumed = ammoItemQuantity && (ammoItemQuantity - (this.system.consume.amount ?? 0) >= 0);
  9163. const ammoItemAttackBonus = ammoItem.system.attackBonus;
  9164. const ammoIsTypeConsumable = (ammoItem.type === "consumable") && (ammoItem.system.consumableType === "ammo");
  9165. if ( ammoCanBeConsumed && ammoItemAttackBonus && ammoIsTypeConsumable ) {
  9166. parts.push("@ammo");
  9167. rollData.ammo = ammoItemAttackBonus;
  9168. }
  9169. }
  9170. }
  9171. // Condense the resulting attack bonus formula into a simplified label
  9172. const roll = new Roll(parts.join("+"), rollData);
  9173. const formula = simplifyRollFormula(roll.formula) || "0";
  9174. this.labels.toHit = !/^[+-]/.test(formula) ? `+ ${formula}` : formula;
  9175. return {rollData, parts};
  9176. }
  9177. /* -------------------------------------------- */
  9178. /**
  9179. * Populates the max uses of an item.
  9180. * If the item is an owned item and the `max` is not numeric, calculate based on actor data.
  9181. */
  9182. prepareMaxUses() {
  9183. const uses = this.system.uses;
  9184. if ( !uses?.max ) return;
  9185. let max = uses.max;
  9186. if ( this.isOwned && !Number.isNumeric(max) ) {
  9187. const property = game.i18n.localize("DND5E.UsesMax");
  9188. try {
  9189. const rollData = this.getRollData({ deterministic: true });
  9190. max = Roll.safeEval(this.replaceFormulaData(max, rollData, { property }));
  9191. } catch(e) {
  9192. const message = game.i18n.format("DND5E.FormulaMalformedError", { property, name: this.name });
  9193. this.actor._preparationWarnings.push({ message, link: this.uuid, type: "error" });
  9194. console.error(message, e);
  9195. return;
  9196. }
  9197. }
  9198. uses.max = Number(max);
  9199. }
  9200. /* -------------------------------------------- */
  9201. /**
  9202. * Populate the duration value of an item. If the item is an owned item and the
  9203. * duration value is not numeric, calculate based on actor data.
  9204. */
  9205. prepareDurationValue() {
  9206. const duration = this.system.duration;
  9207. if ( !duration?.value ) return;
  9208. let value = duration.value;
  9209. // If this is an owned item and the value is not numeric, we need to calculate it
  9210. if ( this.isOwned && !Number.isNumeric(value) ) {
  9211. const property = game.i18n.localize("DND5E.Duration");
  9212. try {
  9213. const rollData = this.getRollData({ deterministic: true });
  9214. value = Roll.safeEval(this.replaceFormulaData(value, rollData, { property }));
  9215. } catch(e) {
  9216. const message = game.i18n.format("DND5E.FormulaMalformedError", { property, name: this.name });
  9217. this.actor._preparationWarnings.push({ message, link: this.uuid, type: "error" });
  9218. console.error(message, e);
  9219. return;
  9220. }
  9221. }
  9222. duration.value = Number(value);
  9223. // Now that duration value is a number, set the label
  9224. if ( ["inst", "perm"].includes(duration.units) ) duration.value = null;
  9225. this.labels.duration = [duration.value, CONFIG.DND5E.timePeriods[duration.units]].filterJoin(" ");
  9226. }
  9227. /* -------------------------------------------- */
  9228. /**
  9229. * Replace referenced data attributes in the roll formula with values from the provided data.
  9230. * If the attribute is not found in the provided data, display a warning on the actor.
  9231. * @param {string} formula The original formula within which to replace.
  9232. * @param {object} data The data object which provides replacements.
  9233. * @param {object} options
  9234. * @param {string} options.property Name of the property to which this formula belongs.
  9235. * @returns {string} Formula with replaced data.
  9236. */
  9237. replaceFormulaData(formula, data, { property }) {
  9238. const dataRgx = new RegExp(/@([a-z.0-9_-]+)/gi);
  9239. const missingReferences = new Set();
  9240. formula = formula.replace(dataRgx, (match, term) => {
  9241. let value = foundry.utils.getProperty(data, term);
  9242. if ( value == null ) {
  9243. missingReferences.add(match);
  9244. return "0";
  9245. }
  9246. return String(value).trim();
  9247. });
  9248. if ( (missingReferences.size > 0) && this.actor ) {
  9249. const listFormatter = new Intl.ListFormat(game.i18n.lang, { style: "long", type: "conjunction" });
  9250. const message = game.i18n.format("DND5E.FormulaMissingReferenceWarn", {
  9251. property, name: this.name, references: listFormatter.format(missingReferences)
  9252. });
  9253. this.actor._preparationWarnings.push({ message, link: this.uuid, type: "warning" });
  9254. }
  9255. return formula;
  9256. }
  9257. /* -------------------------------------------- */
  9258. /**
  9259. * Configuration data for an item usage being prepared.
  9260. *
  9261. * @typedef {object} ItemUseConfiguration
  9262. * @property {boolean} createMeasuredTemplate Trigger a template creation
  9263. * @property {boolean} consumeQuantity Should the item's quantity be consumed?
  9264. * @property {boolean} consumeRecharge Should a recharge be consumed?
  9265. * @property {boolean} consumeResource Should a linked (non-ammo) resource be consumed?
  9266. * @property {number|string|null} consumeSpellLevel Specific spell level to consume, or "pact" for pact level.
  9267. * @property {boolean} consumeSpellSlot Should any spell slot be consumed?
  9268. * @property {boolean} consumeUsage Should limited uses be consumed?
  9269. * @property {boolean} needsConfiguration Is user-configuration needed?
  9270. */
  9271. /**
  9272. * Additional options used for configuring item usage.
  9273. *
  9274. * @typedef {object} ItemUseOptions
  9275. * @property {boolean} configureDialog Display a configuration dialog for the item usage, if applicable?
  9276. * @property {string} rollMode The roll display mode with which to display (or not) the card.
  9277. * @property {boolean} createMessage Whether to automatically create a chat message (if true) or simply return
  9278. * the prepared chat message data (if false).
  9279. * @property {object} flags Additional flags added to the chat message.
  9280. * @property {Event} event The browser event which triggered the item usage, if any.
  9281. */
  9282. /**
  9283. * Trigger an item usage, optionally creating a chat message with followup actions.
  9284. * @param {ItemUseOptions} [options] Options used for configuring item usage.
  9285. * @returns {Promise<ChatMessage|object|void>} Chat message if options.createMessage is true, message data if it is
  9286. * false, and nothing if the roll wasn't performed.
  9287. * @deprecated since 2.0 in favor of `Item5e#use`, targeted for removal in 2.4
  9288. */
  9289. async roll(options={}) {
  9290. foundry.utils.logCompatibilityWarning(
  9291. "Item5e#roll has been renamed Item5e#use. Support for the old name will be removed in future versions.",
  9292. { since: "DnD5e 2.0", until: "DnD5e 2.4" }
  9293. );
  9294. return this.use(undefined, options);
  9295. }
  9296. /**
  9297. * Trigger an item usage, optionally creating a chat message with followup actions.
  9298. * @param {ItemUseConfiguration} [config] Initial configuration data for the usage.
  9299. * @param {ItemUseOptions} [options] Options used for configuring item usage.
  9300. * @returns {Promise<ChatMessage|object|void>} Chat message if options.createMessage is true, message data if it is
  9301. * false, and nothing if the roll wasn't performed.
  9302. */
  9303. async use(config={}, options={}) {
  9304. let item = this;
  9305. const is = item.system;
  9306. const as = item.actor.system;
  9307. // Ensure the options object is ready
  9308. options = foundry.utils.mergeObject({
  9309. configureDialog: true,
  9310. createMessage: true,
  9311. "flags.dnd5e.use": {type: this.type, itemId: this.id, itemUuid: this.uuid}
  9312. }, options);
  9313. // Reference aspects of the item data necessary for usage
  9314. const resource = is.consume || {}; // Resource consumption
  9315. const isSpell = item.type === "spell"; // Does the item require a spell slot?
  9316. const requireSpellSlot = isSpell && (is.level > 0) && CONFIG.DND5E.spellUpcastModes.includes(is.preparation.mode);
  9317. // Define follow-up actions resulting from the item usage
  9318. config = foundry.utils.mergeObject({
  9319. createMeasuredTemplate: item.hasAreaTarget,
  9320. consumeQuantity: is.uses?.autoDestroy ?? false,
  9321. consumeRecharge: !!is.recharge?.value,
  9322. consumeResource: !!resource.target && (!item.hasAttack || (resource.type !== "ammo")),
  9323. consumeSpellLevel: requireSpellSlot ? is.preparation.mode === "pact" ? "pact" : is.level : null,
  9324. consumeSpellSlot: requireSpellSlot,
  9325. consumeUsage: !!is.uses?.per && (is.uses?.max > 0)
  9326. }, config);
  9327. // Display a configuration dialog to customize the usage
  9328. if ( config.needsConfiguration === undefined ) config.needsConfiguration = config.createMeasuredTemplate
  9329. || config.consumeRecharge || config.consumeResource || config.consumeSpellSlot || config.consumeUsage;
  9330. /**
  9331. * A hook event that fires before an item usage is configured.
  9332. * @function dnd5e.preUseItem
  9333. * @memberof hookEvents
  9334. * @param {Item5e} item Item being used.
  9335. * @param {ItemUseConfiguration} config Configuration data for the item usage being prepared.
  9336. * @param {ItemUseOptions} options Additional options used for configuring item usage.
  9337. * @returns {boolean} Explicitly return `false` to prevent item from being used.
  9338. */
  9339. if ( Hooks.call("dnd5e.preUseItem", item, config, options) === false ) return;
  9340. // Display configuration dialog
  9341. if ( (options.configureDialog !== false) && config.needsConfiguration ) {
  9342. const configuration = await AbilityUseDialog.create(item);
  9343. if ( !configuration ) return;
  9344. foundry.utils.mergeObject(config, configuration);
  9345. }
  9346. // Handle spell upcasting
  9347. if ( isSpell && (config.consumeSpellSlot || config.consumeSpellLevel) ) {
  9348. const upcastLevel = config.consumeSpellLevel === "pact" ? as.spells.pact.level
  9349. : parseInt(config.consumeSpellLevel);
  9350. if ( upcastLevel && (upcastLevel !== is.level) ) {
  9351. item = item.clone({"system.level": upcastLevel}, {keepId: true});
  9352. item.prepareData();
  9353. item.prepareFinalAttributes();
  9354. }
  9355. }
  9356. if ( isSpell ) foundry.utils.mergeObject(options.flags, {"dnd5e.use.spellLevel": item.system.level});
  9357. /**
  9358. * A hook event that fires before an item's resource consumption has been calculated.
  9359. * @function dnd5e.preItemUsageConsumption
  9360. * @memberof hookEvents
  9361. * @param {Item5e} item Item being used.
  9362. * @param {ItemUseConfiguration} config Configuration data for the item usage being prepared.
  9363. * @param {ItemUseOptions} options Additional options used for configuring item usage.
  9364. * @returns {boolean} Explicitly return `false` to prevent item from being used.
  9365. */
  9366. if ( Hooks.call("dnd5e.preItemUsageConsumption", item, config, options) === false ) return;
  9367. // Determine whether the item can be used by testing for resource consumption
  9368. const usage = item._getUsageUpdates(config);
  9369. if ( !usage ) return;
  9370. /**
  9371. * A hook event that fires after an item's resource consumption has been calculated but before any
  9372. * changes have been made.
  9373. * @function dnd5e.itemUsageConsumption
  9374. * @memberof hookEvents
  9375. * @param {Item5e} item Item being used.
  9376. * @param {ItemUseConfiguration} config Configuration data for the item usage being prepared.
  9377. * @param {ItemUseOptions} options Additional options used for configuring item usage.
  9378. * @param {object} usage
  9379. * @param {object} usage.actorUpdates Updates that will be applied to the actor.
  9380. * @param {object} usage.itemUpdates Updates that will be applied to the item being used.
  9381. * @param {object[]} usage.resourceUpdates Updates that will be applied to other items on the actor.
  9382. * @returns {boolean} Explicitly return `false` to prevent item from being used.
  9383. */
  9384. if ( Hooks.call("dnd5e.itemUsageConsumption", item, config, options, usage) === false ) return;
  9385. // Commit pending data updates
  9386. const { actorUpdates, itemUpdates, resourceUpdates } = usage;
  9387. if ( !foundry.utils.isEmpty(itemUpdates) ) await item.update(itemUpdates);
  9388. if ( config.consumeQuantity && (item.system.quantity === 0) ) await item.delete();
  9389. if ( !foundry.utils.isEmpty(actorUpdates) ) await this.actor.update(actorUpdates);
  9390. if ( resourceUpdates.length ) await this.actor.updateEmbeddedDocuments("Item", resourceUpdates);
  9391. // Prepare card data & display it if options.createMessage is true
  9392. const cardData = await item.displayCard(options);
  9393. // Initiate measured template creation
  9394. let templates;
  9395. if ( config.createMeasuredTemplate ) {
  9396. try {
  9397. templates = await (dnd5e.canvas.AbilityTemplate.fromItem(item))?.drawPreview();
  9398. } catch(err) {
  9399. Hooks.onError("Item5e#use", err, {
  9400. msg: game.i18n.localize("DND5E.PlaceTemplateError"),
  9401. log: "error",
  9402. notify: "error"
  9403. });
  9404. }
  9405. }
  9406. /**
  9407. * A hook event that fires when an item is used, after the measured template has been created if one is needed.
  9408. * @function dnd5e.useItem
  9409. * @memberof hookEvents
  9410. * @param {Item5e} item Item being used.
  9411. * @param {ItemUseConfiguration} config Configuration data for the roll.
  9412. * @param {ItemUseOptions} options Additional options for configuring item usage.
  9413. * @param {MeasuredTemplateDocument[]|null} templates The measured templates if they were created.
  9414. */
  9415. Hooks.callAll("dnd5e.useItem", item, config, options, templates ?? null);
  9416. return cardData;
  9417. }
  9418. /* -------------------------------------------- */
  9419. /**
  9420. * Verify that the consumed resources used by an Item are available and prepare the updates that should
  9421. * be performed. If required resources are not available, display an error and return false.
  9422. * @param {ItemUseConfiguration} config Configuration data for an item usage being prepared.
  9423. * @returns {object|boolean} A set of data changes to apply when the item is used, or false.
  9424. * @protected
  9425. */
  9426. _getUsageUpdates({
  9427. consumeQuantity, consumeRecharge, consumeResource, consumeSpellSlot,
  9428. consumeSpellLevel, consumeUsage}) {
  9429. const actorUpdates = {};
  9430. const itemUpdates = {};
  9431. const resourceUpdates = [];
  9432. // Consume Recharge
  9433. if ( consumeRecharge ) {
  9434. const recharge = this.system.recharge || {};
  9435. if ( recharge.charged === false ) {
  9436. ui.notifications.warn(game.i18n.format("DND5E.ItemNoUses", {name: this.name}));
  9437. return false;
  9438. }
  9439. itemUpdates["system.recharge.charged"] = false;
  9440. }
  9441. // Consume Limited Resource
  9442. if ( consumeResource ) {
  9443. const canConsume = this._handleConsumeResource(itemUpdates, actorUpdates, resourceUpdates);
  9444. if ( canConsume === false ) return false;
  9445. }
  9446. // Consume Spell Slots
  9447. if ( consumeSpellSlot && consumeSpellLevel ) {
  9448. if ( Number.isNumeric(consumeSpellLevel) ) consumeSpellLevel = `spell${consumeSpellLevel}`;
  9449. const level = this.actor?.system.spells[consumeSpellLevel];
  9450. const spells = Number(level?.value ?? 0);
  9451. if ( spells === 0 ) {
  9452. const labelKey = consumeSpellLevel === "pact" ? "DND5E.SpellProgPact" : `DND5E.SpellLevel${this.system.level}`;
  9453. const label = game.i18n.localize(labelKey);
  9454. ui.notifications.warn(game.i18n.format("DND5E.SpellCastNoSlots", {name: this.name, level: label}));
  9455. return false;
  9456. }
  9457. actorUpdates[`system.spells.${consumeSpellLevel}.value`] = Math.max(spells - 1, 0);
  9458. }
  9459. // Consume Limited Usage
  9460. if ( consumeUsage ) {
  9461. const uses = this.system.uses || {};
  9462. const available = Number(uses.value ?? 0);
  9463. let used = false;
  9464. const remaining = Math.max(available - 1, 0);
  9465. if ( available >= 1 ) {
  9466. used = true;
  9467. itemUpdates["system.uses.value"] = remaining;
  9468. }
  9469. // Reduce quantity if not reducing usages or if usages hit zero, and we are set to consumeQuantity
  9470. if ( consumeQuantity && (!used || (remaining === 0)) ) {
  9471. const q = Number(this.system.quantity ?? 1);
  9472. if ( q >= 1 ) {
  9473. used = true;
  9474. itemUpdates["system.quantity"] = Math.max(q - 1, 0);
  9475. itemUpdates["system.uses.value"] = uses.max ?? 1;
  9476. }
  9477. }
  9478. // If the item was not used, return a warning
  9479. if ( !used ) {
  9480. ui.notifications.warn(game.i18n.format("DND5E.ItemNoUses", {name: this.name}));
  9481. return false;
  9482. }
  9483. }
  9484. // Return the configured usage
  9485. return {itemUpdates, actorUpdates, resourceUpdates};
  9486. }
  9487. /* -------------------------------------------- */
  9488. /**
  9489. * Handle update actions required when consuming an external resource
  9490. * @param {object} itemUpdates An object of data updates applied to this item
  9491. * @param {object} actorUpdates An object of data updates applied to the item owner (Actor)
  9492. * @param {object[]} resourceUpdates An array of updates to apply to other items owned by the actor
  9493. * @returns {boolean|void} Return false to block further progress, or return nothing to continue
  9494. * @protected
  9495. */
  9496. _handleConsumeResource(itemUpdates, actorUpdates, resourceUpdates) {
  9497. const consume = this.system.consume || {};
  9498. if ( !consume.type ) return;
  9499. // No consumed target
  9500. const typeLabel = CONFIG.DND5E.abilityConsumptionTypes[consume.type];
  9501. if ( !consume.target ) {
  9502. ui.notifications.warn(game.i18n.format("DND5E.ConsumeWarningNoResource", {name: this.name, type: typeLabel}));
  9503. return false;
  9504. }
  9505. // Identify the consumed resource and its current quantity
  9506. let resource = null;
  9507. let amount = Number(consume.amount ?? 1);
  9508. let quantity = 0;
  9509. switch ( consume.type ) {
  9510. case "attribute":
  9511. resource = foundry.utils.getProperty(this.actor.system, consume.target);
  9512. quantity = resource || 0;
  9513. break;
  9514. case "ammo":
  9515. case "material":
  9516. resource = this.actor.items.get(consume.target);
  9517. quantity = resource ? resource.system.quantity : 0;
  9518. break;
  9519. case "hitDice":
  9520. const denom = !["smallest", "largest"].includes(consume.target) ? consume.target : false;
  9521. resource = Object.values(this.actor.classes).filter(cls => !denom || (cls.system.hitDice === denom));
  9522. quantity = resource.reduce((count, cls) => count + cls.system.levels - cls.system.hitDiceUsed, 0);
  9523. break;
  9524. case "charges":
  9525. resource = this.actor.items.get(consume.target);
  9526. if ( !resource ) break;
  9527. const uses = resource.system.uses;
  9528. if ( uses.per && uses.max ) quantity = uses.value;
  9529. else if ( resource.system.recharge?.value ) {
  9530. quantity = resource.system.recharge.charged ? 1 : 0;
  9531. amount = 1;
  9532. }
  9533. break;
  9534. }
  9535. // Verify that a consumed resource is available
  9536. if ( resource === undefined ) {
  9537. ui.notifications.warn(game.i18n.format("DND5E.ConsumeWarningNoSource", {name: this.name, type: typeLabel}));
  9538. return false;
  9539. }
  9540. // Verify that the required quantity is available
  9541. let remaining = quantity - amount;
  9542. if ( remaining < 0 ) {
  9543. ui.notifications.warn(game.i18n.format("DND5E.ConsumeWarningNoQuantity", {name: this.name, type: typeLabel}));
  9544. return false;
  9545. }
  9546. // Define updates to provided data objects
  9547. switch ( consume.type ) {
  9548. case "attribute":
  9549. actorUpdates[`system.${consume.target}`] = remaining;
  9550. break;
  9551. case "ammo":
  9552. case "material":
  9553. resourceUpdates.push({_id: consume.target, "system.quantity": remaining});
  9554. break;
  9555. case "hitDice":
  9556. if ( ["smallest", "largest"].includes(consume.target) ) resource = resource.sort((lhs, rhs) => {
  9557. let sort = lhs.system.hitDice.localeCompare(rhs.system.hitDice, "en", {numeric: true});
  9558. if ( consume.target === "largest" ) sort *= -1;
  9559. return sort;
  9560. });
  9561. let toConsume = consume.amount;
  9562. for ( const cls of resource ) {
  9563. const available = (toConsume > 0 ? cls.system.levels : 0) - cls.system.hitDiceUsed;
  9564. const delta = toConsume > 0 ? Math.min(toConsume, available) : Math.max(toConsume, available);
  9565. if ( delta !== 0 ) {
  9566. resourceUpdates.push({_id: cls.id, "system.hitDiceUsed": cls.system.hitDiceUsed + delta});
  9567. toConsume -= delta;
  9568. if ( toConsume === 0 ) break;
  9569. }
  9570. }
  9571. break;
  9572. case "charges":
  9573. const uses = resource.system.uses || {};
  9574. const recharge = resource.system.recharge || {};
  9575. const update = {_id: consume.target};
  9576. if ( uses.per && uses.max ) update["system.uses.value"] = remaining;
  9577. else if ( recharge.value ) update["system.recharge.charged"] = false;
  9578. resourceUpdates.push(update);
  9579. break;
  9580. }
  9581. }
  9582. /* -------------------------------------------- */
  9583. /**
  9584. * Display the chat card for an Item as a Chat Message
  9585. * @param {ItemUseOptions} [options] Options which configure the display of the item chat card.
  9586. * @returns {ChatMessage|object} Chat message if `createMessage` is true, otherwise an object containing
  9587. * message data.
  9588. */
  9589. async displayCard(options={}) {
  9590. // Render the chat card template
  9591. const token = this.actor.token;
  9592. const templateData = {
  9593. actor: this.actor,
  9594. tokenId: token?.uuid || null,
  9595. item: this,
  9596. data: await this.getChatData(),
  9597. labels: this.labels,
  9598. hasAttack: this.hasAttack,
  9599. isHealing: this.isHealing,
  9600. hasDamage: this.hasDamage,
  9601. isVersatile: this.isVersatile,
  9602. isSpell: this.type === "spell",
  9603. hasSave: this.hasSave,
  9604. hasAreaTarget: this.hasAreaTarget,
  9605. isTool: this.type === "tool",
  9606. hasAbilityCheck: this.hasAbilityCheck
  9607. };
  9608. const html = await renderTemplate("systems/dnd5e/templates/chat/item-card.hbs", templateData);
  9609. // Create the ChatMessage data object
  9610. const chatData = {
  9611. user: game.user.id,
  9612. type: CONST.CHAT_MESSAGE_TYPES.OTHER,
  9613. content: html,
  9614. flavor: this.system.chatFlavor || this.name,
  9615. speaker: ChatMessage.getSpeaker({actor: this.actor, token}),
  9616. flags: {"core.canPopout": true}
  9617. };
  9618. // If the Item was destroyed in the process of displaying its card - embed the item data in the chat message
  9619. if ( (this.type === "consumable") && !this.actor.items.has(this.id) ) {
  9620. chatData.flags["dnd5e.itemData"] = templateData.item.toObject();
  9621. }
  9622. // Merge in the flags from options
  9623. chatData.flags = foundry.utils.mergeObject(chatData.flags, options.flags);
  9624. /**
  9625. * A hook event that fires before an item chat card is created.
  9626. * @function dnd5e.preDisplayCard
  9627. * @memberof hookEvents
  9628. * @param {Item5e} item Item for which the chat card is being displayed.
  9629. * @param {object} chatData Data used to create the chat message.
  9630. * @param {ItemUseOptions} options Options which configure the display of the item chat card.
  9631. */
  9632. Hooks.callAll("dnd5e.preDisplayCard", this, chatData, options);
  9633. // Apply the roll mode to adjust message visibility
  9634. ChatMessage.applyRollMode(chatData, options.rollMode ?? game.settings.get("core", "rollMode"));
  9635. // Create the Chat Message or return its data
  9636. const card = (options.createMessage !== false) ? await ChatMessage.create(chatData) : chatData;
  9637. /**
  9638. * A hook event that fires after an item chat card is created.
  9639. * @function dnd5e.displayCard
  9640. * @memberof hookEvents
  9641. * @param {Item5e} item Item for which the chat card is being displayed.
  9642. * @param {ChatMessage|object} card The created ChatMessage instance or ChatMessageData depending on whether
  9643. * options.createMessage was set to `true`.
  9644. */
  9645. Hooks.callAll("dnd5e.displayCard", this, card);
  9646. return card;
  9647. }
  9648. /* -------------------------------------------- */
  9649. /* Chat Cards */
  9650. /* -------------------------------------------- */
  9651. /**
  9652. * Prepare an object of chat data used to display a card for the Item in the chat log.
  9653. * @param {object} htmlOptions Options used by the TextEditor.enrichHTML function.
  9654. * @returns {object} An object of chat data to render.
  9655. */
  9656. async getChatData(htmlOptions={}) {
  9657. const data = this.toObject().system;
  9658. // Rich text description
  9659. data.description.value = await TextEditor.enrichHTML(data.description.value, {
  9660. async: true,
  9661. relativeTo: this,
  9662. rollData: this.getRollData(),
  9663. ...htmlOptions
  9664. });
  9665. // Type specific properties
  9666. data.properties = [
  9667. ...this.system.chatProperties ?? [],
  9668. ...this.system.equippableItemChatProperties ?? [],
  9669. ...this.system.activatedEffectChatProperties ?? []
  9670. ].filter(p => p);
  9671. return data;
  9672. }
  9673. /* -------------------------------------------- */
  9674. /* Item Rolls - Attack, Damage, Saves, Checks */
  9675. /* -------------------------------------------- */
  9676. /**
  9677. * Place an attack roll using an item (weapon, feat, spell, or equipment)
  9678. * Rely upon the d20Roll logic for the core implementation
  9679. *
  9680. * @param {D20RollConfiguration} options Roll options which are configured and provided to the d20Roll function
  9681. * @returns {Promise<D20Roll|null>} A Promise which resolves to the created Roll instance
  9682. */
  9683. async rollAttack(options={}) {
  9684. const flags = this.actor.flags.dnd5e ?? {};
  9685. if ( !this.hasAttack ) throw new Error("You may not place an Attack Roll with this Item.");
  9686. let title = `${this.name} - ${game.i18n.localize("DND5E.AttackRoll")}`;
  9687. // Get the parts and rollData for this item's attack
  9688. const {parts, rollData} = this.getAttackToHit();
  9689. if ( options.spellLevel ) rollData.item.level = options.spellLevel;
  9690. // Handle ammunition consumption
  9691. delete this._ammo;
  9692. let ammo = null;
  9693. let ammoUpdate = [];
  9694. const consume = this.system.consume;
  9695. if ( consume?.type === "ammo" ) {
  9696. ammo = this.actor.items.get(consume.target);
  9697. if ( ammo?.system ) {
  9698. const q = ammo.system.quantity;
  9699. const consumeAmount = consume.amount ?? 0;
  9700. if ( q && (q - consumeAmount >= 0) ) {
  9701. this._ammo = ammo;
  9702. title += ` [${ammo.name}]`;
  9703. }
  9704. }
  9705. // Get pending ammunition update
  9706. const usage = this._getUsageUpdates({consumeResource: true});
  9707. if ( usage === false ) return null;
  9708. ammoUpdate = usage.resourceUpdates ?? [];
  9709. }
  9710. // Flags
  9711. const elvenAccuracy = (flags.elvenAccuracy
  9712. && CONFIG.DND5E.characterFlags.elvenAccuracy.abilities.includes(this.abilityMod)) || undefined;
  9713. // Compose roll options
  9714. const rollConfig = foundry.utils.mergeObject({
  9715. actor: this.actor,
  9716. data: rollData,
  9717. critical: this.criticalThreshold,
  9718. title,
  9719. flavor: title,
  9720. elvenAccuracy,
  9721. halflingLucky: flags.halflingLucky,
  9722. dialogOptions: {
  9723. width: 400,
  9724. top: options.event ? options.event.clientY - 80 : null,
  9725. left: window.innerWidth - 710
  9726. },
  9727. messageData: {
  9728. "flags.dnd5e.roll": {type: "attack", itemId: this.id, itemUuid: this.uuid},
  9729. speaker: ChatMessage.getSpeaker({actor: this.actor})
  9730. }
  9731. }, options);
  9732. rollConfig.parts = parts.concat(options.parts ?? []);
  9733. /**
  9734. * A hook event that fires before an attack is rolled for an Item.
  9735. * @function dnd5e.preRollAttack
  9736. * @memberof hookEvents
  9737. * @param {Item5e} item Item for which the roll is being performed.
  9738. * @param {D20RollConfiguration} config Configuration data for the pending roll.
  9739. * @returns {boolean} Explicitly return false to prevent the roll from being performed.
  9740. */
  9741. if ( Hooks.call("dnd5e.preRollAttack", this, rollConfig) === false ) return;
  9742. const roll = await d20Roll(rollConfig);
  9743. if ( roll === null ) return null;
  9744. /**
  9745. * A hook event that fires after an attack has been rolled for an Item.
  9746. * @function dnd5e.rollAttack
  9747. * @memberof hookEvents
  9748. * @param {Item5e} item Item for which the roll was performed.
  9749. * @param {D20Roll} roll The resulting roll.
  9750. * @param {object[]} ammoUpdate Updates that will be applied to ammo Items as a result of this attack.
  9751. */
  9752. Hooks.callAll("dnd5e.rollAttack", this, roll, ammoUpdate);
  9753. // Commit ammunition consumption on attack rolls resource consumption if the attack roll was made
  9754. if ( ammoUpdate.length ) await this.actor?.updateEmbeddedDocuments("Item", ammoUpdate);
  9755. return roll;
  9756. }
  9757. /* -------------------------------------------- */
  9758. /**
  9759. * Place a damage roll using an item (weapon, feat, spell, or equipment)
  9760. * Rely upon the damageRoll logic for the core implementation.
  9761. * @param {object} [config]
  9762. * @param {MouseEvent} [config.event] An event which triggered this roll, if any
  9763. * @param {boolean} [config.critical] Should damage be rolled as a critical hit?
  9764. * @param {number} [config.spellLevel] If the item is a spell, override the level for damage scaling
  9765. * @param {boolean} [config.versatile] If the item is a weapon, roll damage using the versatile formula
  9766. * @param {DamageRollConfiguration} [config.options] Additional options passed to the damageRoll function
  9767. * @returns {Promise<DamageRoll>} A Promise which resolves to the created Roll instance, or null if the action
  9768. * cannot be performed.
  9769. */
  9770. async rollDamage({critical, event=null, spellLevel=null, versatile=false, options={}}={}) {
  9771. if ( !this.hasDamage ) throw new Error("You may not make a Damage Roll with this Item.");
  9772. const messageData = {
  9773. "flags.dnd5e.roll": {type: "damage", itemId: this.id, itemUuid: this.uuid},
  9774. speaker: ChatMessage.getSpeaker({actor: this.actor})
  9775. };
  9776. // Get roll data
  9777. const dmg = this.system.damage;
  9778. const parts = dmg.parts.map(d => d[0]);
  9779. const rollData = this.getRollData();
  9780. if ( spellLevel ) rollData.item.level = spellLevel;
  9781. // Configure the damage roll
  9782. const actionFlavor = game.i18n.localize(this.system.actionType === "heal" ? "DND5E.Healing" : "DND5E.DamageRoll");
  9783. const title = `${this.name} - ${actionFlavor}`;
  9784. const rollConfig = {
  9785. actor: this.actor,
  9786. critical,
  9787. data: rollData,
  9788. event,
  9789. title: title,
  9790. flavor: this.labels.damageTypes.length ? `${title} (${this.labels.damageTypes})` : title,
  9791. dialogOptions: {
  9792. width: 400,
  9793. top: event ? event.clientY - 80 : null,
  9794. left: window.innerWidth - 710
  9795. },
  9796. messageData
  9797. };
  9798. // Adjust damage from versatile usage
  9799. if ( versatile && dmg.versatile ) {
  9800. parts[0] = dmg.versatile;
  9801. messageData["flags.dnd5e.roll"].versatile = true;
  9802. }
  9803. // Scale damage from up-casting spells
  9804. const scaling = this.system.scaling;
  9805. if ( (this.type === "spell") ) {
  9806. if ( scaling.mode === "cantrip" ) {
  9807. let level;
  9808. if ( this.actor.type === "character" ) level = this.actor.system.details.level;
  9809. else if ( this.system.preparation.mode === "innate" ) level = Math.ceil(this.actor.system.details.cr);
  9810. else level = this.actor.system.details.spellLevel;
  9811. this._scaleCantripDamage(parts, scaling.formula, level, rollData);
  9812. }
  9813. else if ( spellLevel && (scaling.mode === "level") && scaling.formula ) {
  9814. this._scaleSpellDamage(parts, this.system.level, spellLevel, scaling.formula, rollData);
  9815. }
  9816. }
  9817. // Add damage bonus formula
  9818. const actorBonus = foundry.utils.getProperty(this.actor.system, `bonuses.${this.system.actionType}`) || {};
  9819. if ( actorBonus.damage && (parseInt(actorBonus.damage) !== 0) ) {
  9820. parts.push(actorBonus.damage);
  9821. }
  9822. // Only add the ammunition damage if the ammunition is a consumable with type 'ammo'
  9823. if ( this._ammo && (this._ammo.type === "consumable") && (this._ammo.system.consumableType === "ammo") ) {
  9824. parts.push("@ammo");
  9825. rollData.ammo = this._ammo.system.damage.parts.map(p => p[0]).join("+");
  9826. rollConfig.flavor += ` [${this._ammo.name}]`;
  9827. delete this._ammo;
  9828. }
  9829. // Factor in extra critical damage dice from the Barbarian's "Brutal Critical"
  9830. if ( this.system.actionType === "mwak" ) {
  9831. rollConfig.criticalBonusDice = this.actor.getFlag("dnd5e", "meleeCriticalDamageDice") ?? 0;
  9832. }
  9833. // Factor in extra weapon-specific critical damage
  9834. if ( this.system.critical?.damage ) rollConfig.criticalBonusDamage = this.system.critical.damage;
  9835. foundry.utils.mergeObject(rollConfig, options);
  9836. rollConfig.parts = parts.concat(options.parts ?? []);
  9837. /**
  9838. * A hook event that fires before a damage is rolled for an Item.
  9839. * @function dnd5e.preRollDamage
  9840. * @memberof hookEvents
  9841. * @param {Item5e} item Item for which the roll is being performed.
  9842. * @param {DamageRollConfiguration} config Configuration data for the pending roll.
  9843. * @returns {boolean} Explicitly return false to prevent the roll from being performed.
  9844. */
  9845. if ( Hooks.call("dnd5e.preRollDamage", this, rollConfig) === false ) return;
  9846. const roll = await damageRoll(rollConfig);
  9847. /**
  9848. * A hook event that fires after a damage has been rolled for an Item.
  9849. * @function dnd5e.rollDamage
  9850. * @memberof hookEvents
  9851. * @param {Item5e} item Item for which the roll was performed.
  9852. * @param {DamageRoll} roll The resulting roll.
  9853. */
  9854. if ( roll ) Hooks.callAll("dnd5e.rollDamage", this, roll);
  9855. // Call the roll helper utility
  9856. return roll;
  9857. }
  9858. /* -------------------------------------------- */
  9859. /**
  9860. * Adjust a cantrip damage formula to scale it for higher level characters and monsters.
  9861. * @param {string[]} parts The original parts of the damage formula.
  9862. * @param {string} scale The scaling formula.
  9863. * @param {number} level Level at which the spell is being cast.
  9864. * @param {object} rollData A data object that should be applied to the scaled damage roll.
  9865. * @returns {string[]} The parts of the damage formula with the scaling applied.
  9866. * @private
  9867. */
  9868. _scaleCantripDamage(parts, scale, level, rollData) {
  9869. const add = Math.floor((level + 1) / 6);
  9870. if ( add === 0 ) return [];
  9871. return this._scaleDamage(parts, scale || parts.join(" + "), add, rollData);
  9872. }
  9873. /* -------------------------------------------- */
  9874. /**
  9875. * Adjust the spell damage formula to scale it for spell level up-casting.
  9876. * @param {string[]} parts The original parts of the damage formula.
  9877. * @param {number} baseLevel Default level for the spell.
  9878. * @param {number} spellLevel Level at which the spell is being cast.
  9879. * @param {string} formula The scaling formula.
  9880. * @param {object} rollData A data object that should be applied to the scaled damage roll.
  9881. * @returns {string[]} The parts of the damage formula with the scaling applied.
  9882. * @private
  9883. */
  9884. _scaleSpellDamage(parts, baseLevel, spellLevel, formula, rollData) {
  9885. const upcastLevels = Math.max(spellLevel - baseLevel, 0);
  9886. if ( upcastLevels === 0 ) return parts;
  9887. return this._scaleDamage(parts, formula, upcastLevels, rollData);
  9888. }
  9889. /* -------------------------------------------- */
  9890. /**
  9891. * Scale an array of damage parts according to a provided scaling formula and scaling multiplier.
  9892. * @param {string[]} parts The original parts of the damage formula.
  9893. * @param {string} scaling The scaling formula.
  9894. * @param {number} times A number of times to apply the scaling formula.
  9895. * @param {object} rollData A data object that should be applied to the scaled damage roll
  9896. * @returns {string[]} The parts of the damage formula with the scaling applied.
  9897. * @private
  9898. */
  9899. _scaleDamage(parts, scaling, times, rollData) {
  9900. if ( times <= 0 ) return parts;
  9901. const p0 = new Roll(parts[0], rollData);
  9902. const s = new Roll(scaling, rollData).alter(times);
  9903. // Attempt to simplify by combining like dice terms
  9904. let simplified = false;
  9905. if ( (s.terms[0] instanceof Die) && (s.terms.length === 1) ) {
  9906. const d0 = p0.terms[0];
  9907. const s0 = s.terms[0];
  9908. if ( (d0 instanceof Die) && (d0.faces === s0.faces) && d0.modifiers.equals(s0.modifiers) ) {
  9909. d0.number += s0.number;
  9910. parts[0] = p0.formula;
  9911. simplified = true;
  9912. }
  9913. }
  9914. // Otherwise, add to the first part
  9915. if ( !simplified ) parts[0] = `${parts[0]} + ${s.formula}`;
  9916. return parts;
  9917. }
  9918. /* -------------------------------------------- */
  9919. /**
  9920. * Prepare data needed to roll an attack using an item (weapon, feat, spell, or equipment)
  9921. * and then pass it off to `d20Roll`.
  9922. * @param {object} [options]
  9923. * @param {boolean} [options.spellLevel] Level at which a spell is cast.
  9924. * @returns {Promise<Roll>} A Promise which resolves to the created Roll instance.
  9925. */
  9926. async rollFormula({spellLevel}={}) {
  9927. if ( !this.system.formula ) throw new Error("This Item does not have a formula to roll!");
  9928. const rollConfig = {
  9929. formula: this.system.formula,
  9930. data: this.getRollData(),
  9931. chatMessage: true
  9932. };
  9933. if ( spellLevel ) rollConfig.data.item.level = spellLevel;
  9934. /**
  9935. * A hook event that fires before a formula is rolled for an Item.
  9936. * @function dnd5e.preRollFormula
  9937. * @memberof hookEvents
  9938. * @param {Item5e} item Item for which the roll is being performed.
  9939. * @param {object} config Configuration data for the pending roll.
  9940. * @param {string} config.formula Formula that will be rolled.
  9941. * @param {object} config.data Data used when evaluating the roll.
  9942. * @param {boolean} config.chatMessage Should a chat message be created for this roll?
  9943. * @returns {boolean} Explicitly return false to prevent the roll from being performed.
  9944. */
  9945. if ( Hooks.call("dnd5e.preRollFormula", this, rollConfig) === false ) return;
  9946. const roll = await new Roll(rollConfig.formula, rollConfig.data).roll({async: true});
  9947. if ( rollConfig.chatMessage ) {
  9948. roll.toMessage({
  9949. speaker: ChatMessage.getSpeaker({actor: this.actor}),
  9950. flavor: `${this.name} - ${game.i18n.localize("DND5E.OtherFormula")}`,
  9951. rollMode: game.settings.get("core", "rollMode"),
  9952. messageData: {"flags.dnd5e.roll": {type: "other", itemId: this.id, itemUuid: this.uuid}}
  9953. });
  9954. }
  9955. /**
  9956. * A hook event that fires after a formula has been rolled for an Item.
  9957. * @function dnd5e.rollFormula
  9958. * @memberof hookEvents
  9959. * @param {Item5e} item Item for which the roll was performed.
  9960. * @param {Roll} roll The resulting roll.
  9961. */
  9962. Hooks.callAll("dnd5e.rollFormula", this, roll);
  9963. return roll;
  9964. }
  9965. /* -------------------------------------------- */
  9966. /**
  9967. * Perform an ability recharge test for an item which uses the d6 recharge mechanic.
  9968. * @returns {Promise<Roll>} A Promise which resolves to the created Roll instance
  9969. */
  9970. async rollRecharge() {
  9971. const recharge = this.system.recharge ?? {};
  9972. if ( !recharge.value ) return;
  9973. const rollConfig = {
  9974. formula: "1d6",
  9975. data: this.getRollData(),
  9976. target: parseInt(recharge.value),
  9977. chatMessage: true
  9978. };
  9979. /**
  9980. * A hook event that fires before the Item is rolled to recharge.
  9981. * @function dnd5e.preRollRecharge
  9982. * @memberof hookEvents
  9983. * @param {Item5e} item Item for which the roll is being performed.
  9984. * @param {object} config Configuration data for the pending roll.
  9985. * @param {string} config.formula Formula that will be used to roll the recharge.
  9986. * @param {object} config.data Data used when evaluating the roll.
  9987. * @param {number} config.target Total required to be considered recharged.
  9988. * @param {boolean} config.chatMessage Should a chat message be created for this roll?
  9989. * @returns {boolean} Explicitly return false to prevent the roll from being performed.
  9990. */
  9991. if ( Hooks.call("dnd5e.preRollRecharge", this, rollConfig) === false ) return;
  9992. const roll = await new Roll(rollConfig.formula, rollConfig.data).roll({async: true});
  9993. const success = roll.total >= rollConfig.target;
  9994. if ( rollConfig.chatMessage ) {
  9995. const resultMessage = game.i18n.localize(`DND5E.ItemRecharge${success ? "Success" : "Failure"}`);
  9996. roll.toMessage({
  9997. flavor: `${game.i18n.format("DND5E.ItemRechargeCheck", {name: this.name})} - ${resultMessage}`,
  9998. speaker: ChatMessage.getSpeaker({actor: this.actor, token: this.actor.token})
  9999. });
  10000. }
  10001. /**
  10002. * A hook event that fires after the Item has rolled to recharge, but before any changes have been performed.
  10003. * @function dnd5e.rollRecharge
  10004. * @memberof hookEvents
  10005. * @param {Item5e} item Item for which the roll was performed.
  10006. * @param {Roll} roll The resulting roll.
  10007. * @returns {boolean} Explicitly return false to prevent the item from being recharged.
  10008. */
  10009. if ( Hooks.call("dnd5e.rollRecharge", this, roll) === false ) return roll;
  10010. // Update the Item data
  10011. if ( success ) this.update({"system.recharge.charged": true});
  10012. return roll;
  10013. }
  10014. /* -------------------------------------------- */
  10015. /**
  10016. * Prepare data needed to roll a tool check and then pass it off to `d20Roll`.
  10017. * @param {D20RollConfiguration} [options] Roll configuration options provided to the d20Roll function.
  10018. * @returns {Promise<Roll>} A Promise which resolves to the created Roll instance.
  10019. */
  10020. async rollToolCheck(options={}) {
  10021. if ( this.type !== "tool" ) throw new Error("Wrong item type!");
  10022. return this.actor?.rollToolCheck(this.system.baseItem, {
  10023. ability: this.system.ability,
  10024. bonus: this.system.bonus,
  10025. prof: this.system.prof,
  10026. ...options
  10027. });
  10028. }
  10029. /* -------------------------------------------- */
  10030. /**
  10031. * @inheritdoc
  10032. * @param {object} [options]
  10033. * @param {boolean} [options.deterministic] Whether to force deterministic values for data properties that could be
  10034. * either a die term or a flat term.
  10035. */
  10036. getRollData({ deterministic=false }={}) {
  10037. if ( !this.actor ) return null;
  10038. const actorRollData = this.actor.getRollData({ deterministic });
  10039. const rollData = {
  10040. ...actorRollData,
  10041. item: this.toObject().system
  10042. };
  10043. // Include an ability score modifier if one exists
  10044. const abl = this.abilityMod;
  10045. if ( abl && ("abilities" in rollData) ) {
  10046. const ability = rollData.abilities[abl];
  10047. if ( !ability ) {
  10048. console.warn(`Item ${this.name} in Actor ${this.actor.name} has an invalid item ability modifier of ${abl} defined`);
  10049. }
  10050. rollData.mod = ability?.mod ?? 0;
  10051. }
  10052. return rollData;
  10053. }
  10054. /* -------------------------------------------- */
  10055. /* Chat Message Helpers */
  10056. /* -------------------------------------------- */
  10057. /**
  10058. * Apply listeners to chat messages.
  10059. * @param {HTML} html Rendered chat message.
  10060. */
  10061. static chatListeners(html) {
  10062. html.on("click", ".card-buttons button", this._onChatCardAction.bind(this));
  10063. html.on("click", ".item-name", this._onChatCardToggleContent.bind(this));
  10064. }
  10065. /* -------------------------------------------- */
  10066. /**
  10067. * Handle execution of a chat card action via a click event on one of the card buttons
  10068. * @param {Event} event The originating click event
  10069. * @returns {Promise} A promise which resolves once the handler workflow is complete
  10070. * @private
  10071. */
  10072. static async _onChatCardAction(event) {
  10073. event.preventDefault();
  10074. // Extract card data
  10075. const button = event.currentTarget;
  10076. button.disabled = true;
  10077. const card = button.closest(".chat-card");
  10078. const messageId = card.closest(".message").dataset.messageId;
  10079. const message = game.messages.get(messageId);
  10080. const action = button.dataset.action;
  10081. // Recover the actor for the chat card
  10082. const actor = await this._getChatCardActor(card);
  10083. if ( !actor ) return;
  10084. // Validate permission to proceed with the roll
  10085. const isTargetted = action === "save";
  10086. if ( !( isTargetted || game.user.isGM || actor.isOwner ) ) return;
  10087. // Get the Item from stored flag data or by the item ID on the Actor
  10088. const storedData = message.getFlag("dnd5e", "itemData");
  10089. const item = storedData ? new this(storedData, {parent: actor}) : actor.items.get(card.dataset.itemId);
  10090. if ( !item ) {
  10091. const err = game.i18n.format("DND5E.ActionWarningNoItem", {item: card.dataset.itemId, name: actor.name});
  10092. return ui.notifications.error(err);
  10093. }
  10094. const spellLevel = parseInt(card.dataset.spellLevel) || null;
  10095. // Handle different actions
  10096. let targets;
  10097. switch ( action ) {
  10098. case "attack":
  10099. await item.rollAttack({
  10100. event: event,
  10101. spellLevel: spellLevel
  10102. });
  10103. break;
  10104. case "damage":
  10105. case "versatile":
  10106. await item.rollDamage({
  10107. event: event,
  10108. spellLevel: spellLevel,
  10109. versatile: action === "versatile"
  10110. });
  10111. break;
  10112. case "formula":
  10113. await item.rollFormula({event, spellLevel}); break;
  10114. case "save":
  10115. targets = this._getChatCardTargets(card);
  10116. for ( let token of targets ) {
  10117. const speaker = ChatMessage.getSpeaker({scene: canvas.scene, token: token.document});
  10118. await token.actor.rollAbilitySave(button.dataset.ability, { event, speaker });
  10119. }
  10120. break;
  10121. case "toolCheck":
  10122. await item.rollToolCheck({event}); break;
  10123. case "placeTemplate":
  10124. try {
  10125. await dnd5e.canvas.AbilityTemplate.fromItem(item)?.drawPreview();
  10126. } catch(err) {
  10127. Hooks.onError("Item5e._onChatCardAction", err, {
  10128. msg: game.i18n.localize("DND5E.PlaceTemplateError"),
  10129. log: "error",
  10130. notify: "error"
  10131. });
  10132. }
  10133. break;
  10134. case "abilityCheck":
  10135. targets = this._getChatCardTargets(card);
  10136. for ( let token of targets ) {
  10137. const speaker = ChatMessage.getSpeaker({scene: canvas.scene, token: token.document});
  10138. await token.actor.rollAbilityTest(button.dataset.ability, { event, speaker });
  10139. }
  10140. break;
  10141. }
  10142. // Re-enable the button
  10143. button.disabled = false;
  10144. }
  10145. /* -------------------------------------------- */
  10146. /**
  10147. * Handle toggling the visibility of chat card content when the name is clicked
  10148. * @param {Event} event The originating click event
  10149. * @private
  10150. */
  10151. static _onChatCardToggleContent(event) {
  10152. event.preventDefault();
  10153. const header = event.currentTarget;
  10154. const card = header.closest(".chat-card");
  10155. const content = card.querySelector(".card-content");
  10156. content.style.display = content.style.display === "none" ? "block" : "none";
  10157. }
  10158. /* -------------------------------------------- */
  10159. /**
  10160. * Get the Actor which is the author of a chat card
  10161. * @param {HTMLElement} card The chat card being used
  10162. * @returns {Actor|null} The Actor document or null
  10163. * @private
  10164. */
  10165. static async _getChatCardActor(card) {
  10166. // Case 1 - a synthetic actor from a Token
  10167. if ( card.dataset.tokenId ) {
  10168. const token = await fromUuid(card.dataset.tokenId);
  10169. if ( !token ) return null;
  10170. return token.actor;
  10171. }
  10172. // Case 2 - use Actor ID directory
  10173. const actorId = card.dataset.actorId;
  10174. return game.actors.get(actorId) || null;
  10175. }
  10176. /* -------------------------------------------- */
  10177. /**
  10178. * Get the Actor which is the author of a chat card
  10179. * @param {HTMLElement} card The chat card being used
  10180. * @returns {Actor[]} An Array of Actor documents, if any
  10181. * @private
  10182. */
  10183. static _getChatCardTargets(card) {
  10184. let targets = canvas.tokens.controlled.filter(t => !!t.actor);
  10185. if ( !targets.length && game.user.character ) targets = targets.concat(game.user.character.getActiveTokens());
  10186. if ( !targets.length ) ui.notifications.warn(game.i18n.localize("DND5E.ActionWarningNoToken"));
  10187. return targets;
  10188. }
  10189. /* -------------------------------------------- */
  10190. /* Advancements */
  10191. /* -------------------------------------------- */
  10192. /**
  10193. * Create a new advancement of the specified type.
  10194. * @param {string} type Type of advancement to create.
  10195. * @param {object} [data] Data to use when creating the advancement.
  10196. * @param {object} [options]
  10197. * @param {boolean} [options.showConfig=true] Should the new advancement's configuration application be shown?
  10198. * @param {boolean} [options.source=false] Should a source-only update be performed?
  10199. * @returns {Promise<AdvancementConfig>|Item5e} Promise for advancement config for new advancement if local
  10200. * is `false`, or item with newly added advancement.
  10201. */
  10202. createAdvancement(type, data={}, { showConfig=true, source=false }={}) {
  10203. if ( !this.system.advancement ) return this;
  10204. const Advancement = CONFIG.DND5E.advancementTypes[type];
  10205. if ( !Advancement ) throw new Error(`${type} not found in CONFIG.DND5E.advancementTypes`);
  10206. if ( !Advancement.metadata.validItemTypes.has(this.type) || !Advancement.availableForItem(this) ) {
  10207. throw new Error(`${type} advancement cannot be added to ${this.name}`);
  10208. }
  10209. const advancement = new Advancement(data, {parent: this});
  10210. const advancementCollection = this.toObject().system.advancement;
  10211. advancementCollection.push(advancement.toObject());
  10212. if ( source ) return this.updateSource({"system.advancement": advancementCollection});
  10213. return this.update({"system.advancement": advancementCollection}).then(() => {
  10214. if ( !showConfig ) return this;
  10215. const config = new Advancement.metadata.apps.config(this.advancement.byId[advancement.id]);
  10216. return config.render(true);
  10217. });
  10218. }
  10219. /* -------------------------------------------- */
  10220. /**
  10221. * Update an advancement belonging to this item.
  10222. * @param {string} id ID of the advancement to update.
  10223. * @param {object} updates Updates to apply to this advancement.
  10224. * @param {object} [options={}]
  10225. * @param {boolean} [options.source=false] Should a source-only update be performed?
  10226. * @returns {Promise<Item5e>|Item5e} This item with the changes applied, promised if source is `false`.
  10227. */
  10228. updateAdvancement(id, updates, { source=false }={}) {
  10229. if ( !this.system.advancement ) return this;
  10230. const idx = this.system.advancement.findIndex(a => a._id === id);
  10231. if ( idx === -1 ) throw new Error(`Advancement of ID ${id} could not be found to update`);
  10232. const advancement = this.advancement.byId[id];
  10233. advancement.updateSource(updates);
  10234. if ( source ) {
  10235. advancement.render();
  10236. return this;
  10237. }
  10238. const advancementCollection = this.toObject().system.advancement;
  10239. advancementCollection[idx] = advancement.toObject();
  10240. return this.update({"system.advancement": advancementCollection}).then(r => {
  10241. advancement.render();
  10242. return r;
  10243. });
  10244. }
  10245. /* -------------------------------------------- */
  10246. /**
  10247. * Remove an advancement from this item.
  10248. * @param {string} id ID of the advancement to remove.
  10249. * @param {object} [options={}]
  10250. * @param {boolean} [options.source=false] Should a source-only update be performed?
  10251. * @returns {Promise<Item5e>|Item5e} This item with the changes applied.
  10252. */
  10253. deleteAdvancement(id, { source=false }={}) {
  10254. if ( !this.system.advancement ) return this;
  10255. const advancementCollection = this.toObject().system.advancement.filter(a => a._id !== id);
  10256. if ( source ) return this.updateSource({"system.advancement": advancementCollection});
  10257. return this.update({"system.advancement": advancementCollection});
  10258. }
  10259. /* -------------------------------------------- */
  10260. /**
  10261. * Duplicate an advancement, resetting its value to default and giving it a new ID.
  10262. * @param {string} id ID of the advancement to duplicate.
  10263. * @param {object} [options]
  10264. * @param {boolean} [options.showConfig=true] Should the new advancement's configuration application be shown?
  10265. * @param {boolean} [options.source=false] Should a source-only update be performed?
  10266. * @returns {Promise<AdvancementConfig>|Item5e} Promise for advancement config for duplicate advancement if source
  10267. * is `false`, or item with newly duplicated advancement.
  10268. */
  10269. duplicateAdvancement(id, options) {
  10270. const original = this.advancement.byId[id];
  10271. if ( !original ) return this;
  10272. const duplicate = original.toObject();
  10273. delete duplicate._id;
  10274. if ( original.constructor.metadata.dataModels?.value ) {
  10275. duplicate.value = (new original.constructor.metadata.dataModels.value()).toObject();
  10276. } else {
  10277. duplicate.value = original.constructor.metadata.defaults?.value ?? {};
  10278. }
  10279. return this.createAdvancement(original.constructor.typeName, duplicate, options);
  10280. }
  10281. /* -------------------------------------------- */
  10282. /** @inheritdoc */
  10283. getEmbeddedDocument(embeddedName, id, options) {
  10284. if ( embeddedName !== "Advancement" ) return super.getEmbeddedDocument(embeddedName, id, options);
  10285. const advancement = this.advancement.byId[id];
  10286. if ( options?.strict && (advancement === undefined) ) {
  10287. throw new Error(`The key ${id} does not exist in the ${embeddedName} Collection`);
  10288. }
  10289. return advancement;
  10290. }
  10291. /* -------------------------------------------- */
  10292. /* Event Handlers */
  10293. /* -------------------------------------------- */
  10294. /** @inheritdoc */
  10295. async _preCreate(data, options, user) {
  10296. await super._preCreate(data, options, user);
  10297. // Create class identifier based on name
  10298. if ( ["class", "subclass"].includes(this.type) && !this.system.identifier ) {
  10299. await this.updateSource({ "system.identifier": data.name.slugify({strict: true}) });
  10300. }
  10301. if ( !this.isEmbedded || (this.parent.type === "vehicle") ) return;
  10302. const isNPC = this.parent.type === "npc";
  10303. let updates;
  10304. switch (data.type) {
  10305. case "equipment":
  10306. updates = this._onCreateOwnedEquipment(data, isNPC);
  10307. break;
  10308. case "spell":
  10309. updates = this._onCreateOwnedSpell(data, isNPC);
  10310. break;
  10311. case "weapon":
  10312. updates = this._onCreateOwnedWeapon(data, isNPC);
  10313. break;
  10314. case "feat":
  10315. updates = this._onCreateOwnedFeature(data, isNPC);
  10316. break;
  10317. }
  10318. if ( updates ) return this.updateSource(updates);
  10319. }
  10320. /* -------------------------------------------- */
  10321. /** @inheritdoc */
  10322. async _onCreate(data, options, userId) {
  10323. super._onCreate(data, options, userId);
  10324. if ( (userId !== game.user.id) || !this.parent ) return;
  10325. // Assign a new original class
  10326. if ( (this.parent.type === "character") && (this.type === "class") ) {
  10327. const pc = this.parent.items.get(this.parent.system.details.originalClass);
  10328. if ( !pc ) await this.parent._assignPrimaryClass();
  10329. }
  10330. }
  10331. /* -------------------------------------------- */
  10332. /** @inheritdoc */
  10333. async _preUpdate(changed, options, user) {
  10334. await super._preUpdate(changed, options, user);
  10335. if ( (this.type !== "class") || !("levels" in (changed.system || {})) ) return;
  10336. // Check to make sure the updated class level isn't below zero
  10337. if ( changed.system.levels <= 0 ) {
  10338. ui.notifications.warn(game.i18n.localize("DND5E.MaxClassLevelMinimumWarn"));
  10339. changed.system.levels = 1;
  10340. }
  10341. // Check to make sure the updated class level doesn't exceed level cap
  10342. if ( changed.system.levels > CONFIG.DND5E.maxLevel ) {
  10343. ui.notifications.warn(game.i18n.format("DND5E.MaxClassLevelExceededWarn", {max: CONFIG.DND5E.maxLevel}));
  10344. changed.system.levels = CONFIG.DND5E.maxLevel;
  10345. }
  10346. if ( !this.isEmbedded || (this.parent.type !== "character") ) return;
  10347. // Check to ensure the updated character doesn't exceed level cap
  10348. const newCharacterLevel = this.actor.system.details.level + (changed.system.levels - this.system.levels);
  10349. if ( newCharacterLevel > CONFIG.DND5E.maxLevel ) {
  10350. ui.notifications.warn(game.i18n.format("DND5E.MaxCharacterLevelExceededWarn", {max: CONFIG.DND5E.maxLevel}));
  10351. changed.system.levels -= newCharacterLevel - CONFIG.DND5E.maxLevel;
  10352. }
  10353. }
  10354. /* -------------------------------------------- */
  10355. /** @inheritdoc */
  10356. _onDelete(options, userId) {
  10357. super._onDelete(options, userId);
  10358. if ( (userId !== game.user.id) || !this.parent ) return;
  10359. // Assign a new original class
  10360. if ( (this.type === "class") && (this.id === this.parent.system.details.originalClass) ) {
  10361. this.parent._assignPrimaryClass();
  10362. }
  10363. }
  10364. /* -------------------------------------------- */
  10365. /**
  10366. * Pre-creation logic for the automatic configuration of owned equipment type Items.
  10367. *
  10368. * @param {object} data Data for the newly created item.
  10369. * @param {boolean} isNPC Is this actor an NPC?
  10370. * @returns {object} Updates to apply to the item data.
  10371. * @private
  10372. */
  10373. _onCreateOwnedEquipment(data, isNPC) {
  10374. const updates = {};
  10375. if ( foundry.utils.getProperty(data, "system.equipped") === undefined ) {
  10376. updates["system.equipped"] = isNPC; // NPCs automatically equip equipment
  10377. }
  10378. return updates;
  10379. }
  10380. /* -------------------------------------------- */
  10381. /**
  10382. * Pre-creation logic for the automatic configuration of owned spell type Items.
  10383. *
  10384. * @param {object} data Data for the newly created item.
  10385. * @param {boolean} isNPC Is this actor an NPC?
  10386. * @returns {object} Updates to apply to the item data.
  10387. * @private
  10388. */
  10389. _onCreateOwnedSpell(data, isNPC) {
  10390. const updates = {};
  10391. if ( foundry.utils.getProperty(data, "system.preparation.prepared") === undefined ) {
  10392. updates["system.preparation.prepared"] = isNPC; // NPCs automatically prepare spells
  10393. }
  10394. return updates;
  10395. }
  10396. /* -------------------------------------------- */
  10397. /**
  10398. * Pre-creation logic for the automatic configuration of owned weapon type Items.
  10399. * @param {object} data Data for the newly created item.
  10400. * @param {boolean} isNPC Is this actor an NPC?
  10401. * @returns {object} Updates to apply to the item data.
  10402. * @private
  10403. */
  10404. _onCreateOwnedWeapon(data, isNPC) {
  10405. if ( !isNPC ) return;
  10406. // NPCs automatically equip items.
  10407. const updates = {};
  10408. if ( !foundry.utils.hasProperty(data, "system.equipped") ) updates["system.equipped"] = true;
  10409. return updates;
  10410. }
  10411. /**
  10412. * Pre-creation logic for the automatic configuration of owned feature type Items.
  10413. * @param {object} data Data for the newly created item.
  10414. * @param {boolean} isNPC Is this actor an NPC?
  10415. * @returns {object} Updates to apply to the item data.
  10416. * @private
  10417. */
  10418. _onCreateOwnedFeature(data, isNPC) {
  10419. const updates = {};
  10420. if ( isNPC && !foundry.utils.getProperty(data, "system.type.value") ) {
  10421. updates["system.type.value"] = "monster"; // Set features on NPCs to be 'monster features'.
  10422. }
  10423. return updates;
  10424. }
  10425. /* -------------------------------------------- */
  10426. /* Factory Methods */
  10427. /* -------------------------------------------- */
  10428. /**
  10429. * Create a consumable spell scroll Item from a spell Item.
  10430. * @param {Item5e|object} spell The spell or item data to be made into a scroll
  10431. * @param {object} [options] Additional options that modify the created scroll
  10432. * @returns {Item5e} The created scroll consumable item
  10433. */
  10434. static async createScrollFromSpell(spell, options={}) {
  10435. // Get spell data
  10436. const itemData = (spell instanceof Item5e) ? spell.toObject() : spell;
  10437. let {
  10438. actionType, description, source, activation, duration, target,
  10439. range, damage, formula, save, level, attackBonus, ability, components
  10440. } = itemData.system;
  10441. // Get scroll data
  10442. const scrollUuid = `Compendium.${CONFIG.DND5E.sourcePacks.ITEMS}.${CONFIG.DND5E.spellScrollIds[level]}`;
  10443. const scrollItem = await fromUuid(scrollUuid);
  10444. const scrollData = scrollItem.toObject();
  10445. delete scrollData._id;
  10446. // Split the scroll description into an intro paragraph and the remaining details
  10447. const scrollDescription = scrollData.system.description.value;
  10448. const pdel = "</p>";
  10449. const scrollIntroEnd = scrollDescription.indexOf(pdel);
  10450. const scrollIntro = scrollDescription.slice(0, scrollIntroEnd + pdel.length);
  10451. const scrollDetails = scrollDescription.slice(scrollIntroEnd + pdel.length);
  10452. // Create a composite description from the scroll description and the spell details
  10453. const desc = scrollIntro
  10454. + `<hr><h3>${itemData.name} (${game.i18n.format("DND5E.LevelNumber", {level})})</h3>`
  10455. + (components.concentration ? `<p><em>${game.i18n.localize("DND5E.ScrollRequiresConcentration")}</em></p>` : "")
  10456. + `<hr>${description.value}<hr>`
  10457. + `<h3>${game.i18n.localize("DND5E.ScrollDetails")}</h3><hr>${scrollDetails}`;
  10458. // Used a fixed attack modifier and saving throw according to the level of spell scroll.
  10459. if ( ["mwak", "rwak", "msak", "rsak"].includes(actionType) ) {
  10460. attackBonus = scrollData.system.attackBonus;
  10461. ability = "none";
  10462. }
  10463. if ( save.ability ) {
  10464. save.scaling = "flat";
  10465. save.dc = scrollData.system.save.dc;
  10466. }
  10467. // Create the spell scroll data
  10468. const spellScrollData = foundry.utils.mergeObject(scrollData, {
  10469. name: `${game.i18n.localize("DND5E.SpellScroll")}: ${itemData.name}`,
  10470. img: itemData.img,
  10471. system: {
  10472. description: {value: desc.trim()}, source, actionType, activation, duration, target,
  10473. range, damage, formula, save, level, attackBonus, ability
  10474. }
  10475. });
  10476. foundry.utils.mergeObject(spellScrollData, options);
  10477. /**
  10478. * A hook event that fires after the item data for a scroll is created but before the item is returned.
  10479. * @function dnd5e.createScrollFromSpell
  10480. * @memberof hookEvents
  10481. * @param {Item5e|object} spell The spell or item data to be made into a scroll.
  10482. * @param {object} spellScrollData The final item data used to make the scroll.
  10483. */
  10484. Hooks.callAll("dnd5e.createScrollFromSpell", spell, spellScrollData);
  10485. return new this(spellScrollData);
  10486. }
  10487. /* -------------------------------------------- */
  10488. /* Deprecations */
  10489. /* -------------------------------------------- */
  10490. /**
  10491. * Retrieve an item's critical hit threshold. Uses the smallest value from among the following sources:
  10492. * - item document
  10493. * - item document's actor (if it has one)
  10494. * - item document's ammunition (if it has any)
  10495. * - the constant '20'
  10496. * @returns {number|null} The minimum value that must be rolled to be considered a critical hit.
  10497. * @deprecated since dnd5e 2.2, targeted for removal in 2.4
  10498. */
  10499. getCriticalThreshold() {
  10500. foundry.utils.logCompatibilityWarning(
  10501. "Item5e#getCriticalThreshold has been replaced with the Item5e#criticalThreshold getter.",
  10502. { since: "DnD5e 2.2", until: "DnD5e 2.4" }
  10503. );
  10504. return this.criticalThreshold;
  10505. }
  10506. }
  10507. /**
  10508. * An abstract class containing common functionality between actor sheet configuration apps.
  10509. * @extends {DocumentSheet}
  10510. * @abstract
  10511. */
  10512. class BaseConfigSheet extends DocumentSheet {
  10513. /** @inheritdoc */
  10514. activateListeners(html) {
  10515. super.activateListeners(html);
  10516. if ( this.isEditable ) {
  10517. for ( const override of this._getActorOverrides() ) {
  10518. html.find(`input[name="${override}"],select[name="${override}"]`).each((i, el) => {
  10519. el.disabled = true;
  10520. el.dataset.tooltip = "DND5E.ActiveEffectOverrideWarning";
  10521. });
  10522. }
  10523. }
  10524. }
  10525. /* -------------------------------------------- */
  10526. /**
  10527. * Retrieve the list of fields that are currently modified by Active Effects on the Actor.
  10528. * @returns {string[]}
  10529. * @protected
  10530. */
  10531. _getActorOverrides() {
  10532. return Object.keys(foundry.utils.flattenObject(this.object.overrides || {}));
  10533. }
  10534. }
  10535. /**
  10536. * A simple form to set save throw configuration for a given ability score.
  10537. *
  10538. * @param {Actor5e} actor The Actor instance being displayed within the sheet.
  10539. * @param {ApplicationOptions} options Additional application configuration options.
  10540. * @param {string} abilityId The ability key as defined in CONFIG.DND5E.abilities.
  10541. */
  10542. class ActorAbilityConfig extends BaseConfigSheet {
  10543. constructor(actor, options, abilityId) {
  10544. super(actor, options);
  10545. this._abilityId = abilityId;
  10546. }
  10547. /* -------------------------------------------- */
  10548. /** @override */
  10549. static get defaultOptions() {
  10550. return foundry.utils.mergeObject(super.defaultOptions, {
  10551. classes: ["dnd5e"],
  10552. template: "systems/dnd5e/templates/apps/ability-config.hbs",
  10553. width: 500,
  10554. height: "auto"
  10555. });
  10556. }
  10557. /* -------------------------------------------- */
  10558. /** @override */
  10559. get title() {
  10560. return `${game.i18n.format("DND5E.AbilityConfigureTitle", {
  10561. ability: CONFIG.DND5E.abilities[this._abilityId].label})}: ${this.document.name}`;
  10562. }
  10563. /* -------------------------------------------- */
  10564. /** @override */
  10565. getData(options) {
  10566. const src = this.document.toObject();
  10567. const ability = CONFIG.DND5E.abilities[this._abilityId].label;
  10568. return {
  10569. ability: src.system.abilities[this._abilityId] ?? this.document.system.abilities[this._abilityId] ?? {},
  10570. labelSaves: game.i18n.format("DND5E.AbilitySaveConfigure", {ability}),
  10571. labelChecks: game.i18n.format("DND5E.AbilityCheckConfigure", {ability}),
  10572. abilityId: this._abilityId,
  10573. proficiencyLevels: {
  10574. 0: CONFIG.DND5E.proficiencyLevels[0],
  10575. 1: CONFIG.DND5E.proficiencyLevels[1]
  10576. },
  10577. bonusGlobalSave: src.system.bonuses?.abilities?.save,
  10578. bonusGlobalCheck: src.system.bonuses?.abilities?.check
  10579. };
  10580. }
  10581. }
  10582. /**
  10583. * Interface for managing a character's armor calculation.
  10584. */
  10585. class ActorArmorConfig extends BaseConfigSheet {
  10586. constructor(...args) {
  10587. super(...args);
  10588. /**
  10589. * Cloned copy of the actor for previewing changes.
  10590. * @type {Actor5e}
  10591. */
  10592. this.clone = this.document.clone();
  10593. }
  10594. /* -------------------------------------------- */
  10595. /** @inheritdoc */
  10596. static get defaultOptions() {
  10597. return foundry.utils.mergeObject(super.defaultOptions, {
  10598. classes: ["dnd5e", "actor-armor-config"],
  10599. template: "systems/dnd5e/templates/apps/actor-armor.hbs",
  10600. width: 320,
  10601. height: "auto",
  10602. sheetConfig: false
  10603. });
  10604. }
  10605. /* -------------------------------------------- */
  10606. /** @inheritdoc */
  10607. get title() {
  10608. return `${game.i18n.localize("DND5E.ArmorConfig")}: ${this.document.name}`;
  10609. }
  10610. /* -------------------------------------------- */
  10611. /** @inheritdoc */
  10612. async getData() {
  10613. const ac = this.clone.system.attributes.ac;
  10614. const isFlat = ["flat", "natural"].includes(ac.calc);
  10615. // Get configuration data for the calculation mode, reset to flat if configuration is unavailable
  10616. let cfg = CONFIG.DND5E.armorClasses[ac.calc];
  10617. if ( !cfg ) {
  10618. ac.calc = "flat";
  10619. cfg = CONFIG.DND5E.armorClasses.flat;
  10620. this.clone.updateSource({ "system.attributes.ac.calc": "flat" });
  10621. }
  10622. return {
  10623. ac, isFlat,
  10624. calculations: CONFIG.DND5E.armorClasses,
  10625. valueDisabled: !isFlat,
  10626. formula: ac.calc === "custom" ? ac.formula : cfg.formula,
  10627. formulaDisabled: ac.calc !== "custom"
  10628. };
  10629. }
  10630. /* -------------------------------------------- */
  10631. /** @inheritdoc */
  10632. _getActorOverrides() {
  10633. return Object.keys(foundry.utils.flattenObject(this.object.overrides?.system?.attributes || {}));
  10634. }
  10635. /* -------------------------------------------- */
  10636. /** @inheritdoc */
  10637. async _updateObject(event, formData) {
  10638. const ac = foundry.utils.expandObject(formData).ac;
  10639. return this.document.update({"system.attributes.ac": ac});
  10640. }
  10641. /* -------------------------------------------- */
  10642. /* Event Listeners and Handlers */
  10643. /* -------------------------------------------- */
  10644. /** @inheritdoc */
  10645. async _onChangeInput(event) {
  10646. await super._onChangeInput(event);
  10647. // Update clone with new data & re-render
  10648. this.clone.updateSource({ [`system.attributes.${event.currentTarget.name}`]: event.currentTarget.value });
  10649. this.render();
  10650. }
  10651. }
  10652. /**
  10653. * A simple form to set actor hit dice amounts.
  10654. */
  10655. class ActorHitDiceConfig extends BaseConfigSheet {
  10656. /** @inheritDoc */
  10657. static get defaultOptions() {
  10658. return foundry.utils.mergeObject(super.defaultOptions, {
  10659. classes: ["dnd5e", "hd-config", "dialog"],
  10660. template: "systems/dnd5e/templates/apps/hit-dice-config.hbs",
  10661. width: 360,
  10662. height: "auto"
  10663. });
  10664. }
  10665. /* -------------------------------------------- */
  10666. /** @inheritDoc */
  10667. get title() {
  10668. return `${game.i18n.localize("DND5E.HitDiceConfig")}: ${this.object.name}`;
  10669. }
  10670. /* -------------------------------------------- */
  10671. /** @inheritDoc */
  10672. getData(options) {
  10673. return {
  10674. classes: this.object.items.reduce((classes, item) => {
  10675. if (item.type === "class") {
  10676. classes.push({
  10677. classItemId: item.id,
  10678. name: item.name,
  10679. diceDenom: item.system.hitDice,
  10680. currentHitDice: item.system.levels - item.system.hitDiceUsed,
  10681. maxHitDice: item.system.levels,
  10682. canRoll: (item.system.levels - item.system.hitDiceUsed) > 0
  10683. });
  10684. }
  10685. return classes;
  10686. }, []).sort((a, b) => parseInt(b.diceDenom.slice(1)) - parseInt(a.diceDenom.slice(1)))
  10687. };
  10688. }
  10689. /* -------------------------------------------- */
  10690. /** @inheritDoc */
  10691. activateListeners(html) {
  10692. super.activateListeners(html);
  10693. // Hook up -/+ buttons to adjust the current value in the form
  10694. html.find("button.increment,button.decrement").click(event => {
  10695. const button = event.currentTarget;
  10696. const current = button.parentElement.querySelector(".current");
  10697. const max = button.parentElement.querySelector(".max");
  10698. const direction = button.classList.contains("increment") ? 1 : -1;
  10699. current.value = Math.clamped(parseInt(current.value) + direction, 0, parseInt(max.value));
  10700. });
  10701. html.find("button.roll-hd").click(this._onRollHitDie.bind(this));
  10702. }
  10703. /* -------------------------------------------- */
  10704. /** @inheritDoc */
  10705. async _updateObject(event, formData) {
  10706. const actorItems = this.object.items;
  10707. const classUpdates = Object.entries(formData).map(([id, hd]) => ({
  10708. _id: id,
  10709. "system.hitDiceUsed": actorItems.get(id).system.levels - hd
  10710. }));
  10711. return this.object.updateEmbeddedDocuments("Item", classUpdates);
  10712. }
  10713. /* -------------------------------------------- */
  10714. /**
  10715. * Rolls the hit die corresponding with the class row containing the event's target button.
  10716. * @param {MouseEvent} event Triggering click event.
  10717. * @protected
  10718. */
  10719. async _onRollHitDie(event) {
  10720. event.preventDefault();
  10721. const button = event.currentTarget;
  10722. await this.object.rollHitDie(button.dataset.hdDenom);
  10723. // Re-render dialog to reflect changed hit dice quantities
  10724. this.render();
  10725. }
  10726. }
  10727. /**
  10728. * A form for configuring actor hit points and bonuses.
  10729. */
  10730. class ActorHitPointsConfig extends BaseConfigSheet {
  10731. constructor(...args) {
  10732. super(...args);
  10733. /**
  10734. * Cloned copy of the actor for previewing changes.
  10735. * @type {Actor5e}
  10736. */
  10737. this.clone = this.object.clone();
  10738. }
  10739. /* -------------------------------------------- */
  10740. /** @override */
  10741. static get defaultOptions() {
  10742. return foundry.utils.mergeObject(super.defaultOptions, {
  10743. classes: ["dnd5e", "actor-hit-points-config"],
  10744. template: "systems/dnd5e/templates/apps/hit-points-config.hbs",
  10745. width: 320,
  10746. height: "auto",
  10747. sheetConfig: false
  10748. });
  10749. }
  10750. /* -------------------------------------------- */
  10751. /** @inheritdoc */
  10752. get title() {
  10753. return `${game.i18n.localize("DND5E.HitPointsConfig")}: ${this.document.name}`;
  10754. }
  10755. /* -------------------------------------------- */
  10756. /** @inheritdoc */
  10757. getData(options) {
  10758. return {
  10759. hp: this.clone.system.attributes.hp,
  10760. source: this.clone.toObject().system.attributes.hp,
  10761. isCharacter: this.document.type === "character"
  10762. };
  10763. }
  10764. /* -------------------------------------------- */
  10765. /** @inheritdoc */
  10766. _getActorOverrides() {
  10767. return Object.keys(foundry.utils.flattenObject(this.object.overrides?.system?.attributes || {}));
  10768. }
  10769. /* -------------------------------------------- */
  10770. /** @inheritdoc */
  10771. async _updateObject(event, formData) {
  10772. const hp = foundry.utils.expandObject(formData).hp;
  10773. this.clone.updateSource({"system.attributes.hp": hp});
  10774. const maxDelta = this.clone.system.attributes.hp.max - this.document.system.attributes.hp.max;
  10775. hp.value = Math.max(this.document.system.attributes.hp.value + maxDelta, 0);
  10776. return this.document.update({"system.attributes.hp": hp});
  10777. }
  10778. /* -------------------------------------------- */
  10779. /* Event Listeners and Handlers */
  10780. /* -------------------------------------------- */
  10781. /** @inheritDoc */
  10782. activateListeners(html) {
  10783. super.activateListeners(html);
  10784. html.find(".roll-hit-points").click(this._onRollHPFormula.bind(this));
  10785. }
  10786. /* -------------------------------------------- */
  10787. /** @inheritdoc */
  10788. async _onChangeInput(event) {
  10789. await super._onChangeInput(event);
  10790. const t = event.currentTarget;
  10791. // Update clone with new data & re-render
  10792. this.clone.updateSource({ [`system.attributes.${t.name}`]: t.value || null });
  10793. if ( t.name !== "hp.formula" ) this.render();
  10794. }
  10795. /* -------------------------------------------- */
  10796. /**
  10797. * Handle rolling NPC health values using the provided formula.
  10798. * @param {Event} event The original click event.
  10799. * @protected
  10800. */
  10801. async _onRollHPFormula(event) {
  10802. event.preventDefault();
  10803. try {
  10804. const roll = await this.clone.rollNPCHitPoints();
  10805. this.clone.updateSource({"system.attributes.hp.max": roll.total});
  10806. this.render();
  10807. } catch(error) {
  10808. ui.notifications.error(game.i18n.localize("DND5E.HPFormulaError"));
  10809. throw error;
  10810. }
  10811. }
  10812. }
  10813. /**
  10814. * A simple sub-application of the ActorSheet which is used to configure properties related to initiative.
  10815. */
  10816. class ActorInitiativeConfig extends BaseConfigSheet {
  10817. /** @override */
  10818. static get defaultOptions() {
  10819. return foundry.utils.mergeObject(super.defaultOptions, {
  10820. classes: ["dnd5e"],
  10821. template: "systems/dnd5e/templates/apps/initiative-config.hbs",
  10822. width: 360,
  10823. height: "auto"
  10824. });
  10825. }
  10826. /* -------------------------------------------- */
  10827. /** @override */
  10828. get title() {
  10829. return `${game.i18n.localize("DND5E.InitiativeConfig")}: ${this.document.name}`;
  10830. }
  10831. /* -------------------------------------------- */
  10832. /** @override */
  10833. getData(options={}) {
  10834. const source = this.document.toObject();
  10835. const init = source.system.attributes.init || {};
  10836. const flags = source.flags.dnd5e || {};
  10837. return {
  10838. ability: init.ability,
  10839. abilities: CONFIG.DND5E.abilities,
  10840. bonus: init.bonus,
  10841. initiativeAlert: flags.initiativeAlert,
  10842. initiativeAdv: flags.initiativeAdv
  10843. };
  10844. }
  10845. /* -------------------------------------------- */
  10846. /** @inheritDoc */
  10847. _getSubmitData(updateData={}) {
  10848. const formData = super._getSubmitData(updateData);
  10849. formData.flags = {dnd5e: {}};
  10850. for ( const flag of ["initiativeAlert", "initiativeAdv"] ) {
  10851. const k = `flags.dnd5e.${flag}`;
  10852. if ( formData[k] ) formData.flags.dnd5e[flag] = true;
  10853. else formData.flags.dnd5e[`-=${flag}`] = null;
  10854. delete formData[k];
  10855. }
  10856. return formData;
  10857. }
  10858. }
  10859. /**
  10860. * A simple form to set actor movement speeds.
  10861. */
  10862. class ActorMovementConfig extends BaseConfigSheet {
  10863. /** @override */
  10864. static get defaultOptions() {
  10865. return foundry.utils.mergeObject(super.defaultOptions, {
  10866. classes: ["dnd5e"],
  10867. template: "systems/dnd5e/templates/apps/movement-config.hbs",
  10868. width: 300,
  10869. height: "auto"
  10870. });
  10871. }
  10872. /* -------------------------------------------- */
  10873. /** @override */
  10874. get title() {
  10875. return `${game.i18n.localize("DND5E.MovementConfig")}: ${this.document.name}`;
  10876. }
  10877. /* -------------------------------------------- */
  10878. /** @override */
  10879. getData(options={}) {
  10880. const source = this.document.toObject();
  10881. // Current movement values
  10882. const movement = source.system.attributes?.movement || {};
  10883. for ( let [k, v] of Object.entries(movement) ) {
  10884. if ( ["units", "hover"].includes(k) ) continue;
  10885. movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : 0;
  10886. }
  10887. // Allowed speeds
  10888. const speeds = source.type === "group" ? {
  10889. land: "DND5E.MovementLand",
  10890. water: "DND5E.MovementWater",
  10891. air: "DND5E.MovementAir"
  10892. } : {
  10893. walk: "DND5E.MovementWalk",
  10894. burrow: "DND5E.MovementBurrow",
  10895. climb: "DND5E.MovementClimb",
  10896. fly: "DND5E.MovementFly",
  10897. swim: "DND5E.MovementSwim"
  10898. };
  10899. // Return rendering context
  10900. return {
  10901. speeds,
  10902. movement,
  10903. selectUnits: source.type !== "group",
  10904. canHover: source.type !== "group",
  10905. units: CONFIG.DND5E.movementUnits
  10906. };
  10907. }
  10908. }
  10909. /**
  10910. * A simple form to configure Actor senses.
  10911. */
  10912. class ActorSensesConfig extends BaseConfigSheet {
  10913. /** @inheritdoc */
  10914. static get defaultOptions() {
  10915. return foundry.utils.mergeObject(super.defaultOptions, {
  10916. classes: ["dnd5e"],
  10917. template: "systems/dnd5e/templates/apps/senses-config.hbs",
  10918. width: 300,
  10919. height: "auto"
  10920. });
  10921. }
  10922. /* -------------------------------------------- */
  10923. /** @inheritdoc */
  10924. get title() {
  10925. return `${game.i18n.localize("DND5E.SensesConfig")}: ${this.document.name}`;
  10926. }
  10927. /* -------------------------------------------- */
  10928. /** @inheritdoc */
  10929. getData(options) {
  10930. const source = this.document.toObject().system.attributes?.senses || {};
  10931. const data = {
  10932. senses: {},
  10933. special: source.special ?? "",
  10934. units: source.units, movementUnits: CONFIG.DND5E.movementUnits
  10935. };
  10936. for ( let [name, label] of Object.entries(CONFIG.DND5E.senses) ) {
  10937. const v = Number(source[name]);
  10938. data.senses[name] = {
  10939. label: game.i18n.localize(label),
  10940. value: Number.isNumeric(v) ? v.toNearest(0.1) : 0
  10941. };
  10942. }
  10943. return data;
  10944. }
  10945. }
  10946. /**
  10947. * An application class which provides advanced configuration for special character flags which modify an Actor.
  10948. */
  10949. class ActorSheetFlags extends BaseConfigSheet {
  10950. /** @inheritDoc */
  10951. static get defaultOptions() {
  10952. return foundry.utils.mergeObject(super.defaultOptions, {
  10953. id: "actor-flags",
  10954. classes: ["dnd5e"],
  10955. template: "systems/dnd5e/templates/apps/actor-flags.hbs",
  10956. width: 500,
  10957. closeOnSubmit: true
  10958. });
  10959. }
  10960. /* -------------------------------------------- */
  10961. /** @inheritDoc */
  10962. get title() {
  10963. return `${game.i18n.localize("DND5E.FlagsTitle")}: ${this.object.name}`;
  10964. }
  10965. /* -------------------------------------------- */
  10966. /** @inheritDoc */
  10967. getData() {
  10968. const data = {};
  10969. data.actor = this.object;
  10970. data.classes = this._getClasses();
  10971. data.flags = this._getFlags();
  10972. data.bonuses = this._getBonuses();
  10973. return data;
  10974. }
  10975. /* -------------------------------------------- */
  10976. /**
  10977. * Prepare an object of sorted classes.
  10978. * @returns {object}
  10979. * @private
  10980. */
  10981. _getClasses() {
  10982. const classes = this.object.items.filter(i => i.type === "class");
  10983. return classes.sort((a, b) => a.name.localeCompare(b.name)).reduce((obj, i) => {
  10984. obj[i.id] = i.name;
  10985. return obj;
  10986. }, {});
  10987. }
  10988. /* -------------------------------------------- */
  10989. /**
  10990. * Prepare an object of flags data which groups flags by section
  10991. * Add some additional data for rendering
  10992. * @returns {object}
  10993. * @private
  10994. */
  10995. _getFlags() {
  10996. const flags = {};
  10997. const baseData = this.document.toJSON();
  10998. for ( let [k, v] of Object.entries(CONFIG.DND5E.characterFlags) ) {
  10999. if ( !flags.hasOwnProperty(v.section) ) flags[v.section] = {};
  11000. let flag = foundry.utils.deepClone(v);
  11001. flag.type = v.type.name;
  11002. flag.isCheckbox = v.type === Boolean;
  11003. flag.isSelect = v.hasOwnProperty("choices");
  11004. flag.value = foundry.utils.getProperty(baseData.flags, `dnd5e.${k}`);
  11005. flags[v.section][`flags.dnd5e.${k}`] = flag;
  11006. }
  11007. return flags;
  11008. }
  11009. /* -------------------------------------------- */
  11010. /**
  11011. * Get the bonuses fields and their localization strings
  11012. * @returns {Array<object>}
  11013. * @private
  11014. */
  11015. _getBonuses() {
  11016. const src = this.object.toObject();
  11017. const bonuses = [
  11018. {name: "system.bonuses.mwak.attack", label: "DND5E.BonusMWAttack"},
  11019. {name: "system.bonuses.mwak.damage", label: "DND5E.BonusMWDamage"},
  11020. {name: "system.bonuses.rwak.attack", label: "DND5E.BonusRWAttack"},
  11021. {name: "system.bonuses.rwak.damage", label: "DND5E.BonusRWDamage"},
  11022. {name: "system.bonuses.msak.attack", label: "DND5E.BonusMSAttack"},
  11023. {name: "system.bonuses.msak.damage", label: "DND5E.BonusMSDamage"},
  11024. {name: "system.bonuses.rsak.attack", label: "DND5E.BonusRSAttack"},
  11025. {name: "system.bonuses.rsak.damage", label: "DND5E.BonusRSDamage"},
  11026. {name: "system.bonuses.abilities.check", label: "DND5E.BonusAbilityCheck"},
  11027. {name: "system.bonuses.abilities.save", label: "DND5E.BonusAbilitySave"},
  11028. {name: "system.bonuses.abilities.skill", label: "DND5E.BonusAbilitySkill"},
  11029. {name: "system.bonuses.spell.dc", label: "DND5E.BonusSpellDC"}
  11030. ];
  11031. for ( let b of bonuses ) {
  11032. b.value = foundry.utils.getProperty(src, b.name) || "";
  11033. }
  11034. return bonuses;
  11035. }
  11036. /* -------------------------------------------- */
  11037. /** @inheritDoc */
  11038. async _updateObject(event, formData) {
  11039. const actor = this.object;
  11040. let updateData = foundry.utils.expandObject(formData);
  11041. const src = actor.toObject();
  11042. // Unset any flags which are "false"
  11043. const flags = updateData.flags.dnd5e;
  11044. for ( let [k, v] of Object.entries(flags) ) {
  11045. if ( [undefined, null, "", false, 0].includes(v) ) {
  11046. delete flags[k];
  11047. if ( foundry.utils.hasProperty(src.flags, `dnd5e.${k}`) ) flags[`-=${k}`] = null;
  11048. }
  11049. }
  11050. // Clear any bonuses which are whitespace only
  11051. for ( let b of Object.values(updateData.system.bonuses ) ) {
  11052. for ( let [k, v] of Object.entries(b) ) {
  11053. b[k] = v.trim();
  11054. }
  11055. }
  11056. // Diff the data against any applied overrides and apply
  11057. await actor.update(updateData, {diff: false});
  11058. }
  11059. }
  11060. /**
  11061. * A specialized form used to select from a checklist of attributes, traits, or properties
  11062. */
  11063. class ActorTypeConfig extends FormApplication {
  11064. /** @inheritDoc */
  11065. static get defaultOptions() {
  11066. return foundry.utils.mergeObject(super.defaultOptions, {
  11067. classes: ["dnd5e", "actor-type", "trait-selector"],
  11068. template: "systems/dnd5e/templates/apps/actor-type.hbs",
  11069. width: 280,
  11070. height: "auto",
  11071. choices: {},
  11072. allowCustom: true,
  11073. minimum: 0,
  11074. maximum: null
  11075. });
  11076. }
  11077. /* -------------------------------------------- */
  11078. /** @inheritDoc */
  11079. get title() {
  11080. return `${game.i18n.localize("DND5E.CreatureTypeTitle")}: ${this.object.name}`;
  11081. }
  11082. /* -------------------------------------------- */
  11083. /** @override */
  11084. get id() {
  11085. return `actor-type-${this.object.id}`;
  11086. }
  11087. /* -------------------------------------------- */
  11088. /** @override */
  11089. getData(options={}) {
  11090. // Get current value or new default
  11091. let attr = foundry.utils.getProperty(this.object.system, "details.type");
  11092. if ( foundry.utils.getType(attr) !== "Object" ) attr = {
  11093. value: (attr in CONFIG.DND5E.creatureTypes) ? attr : "humanoid",
  11094. subtype: "",
  11095. swarm: "",
  11096. custom: ""
  11097. };
  11098. // Populate choices
  11099. const types = {};
  11100. for ( let [k, v] of Object.entries(CONFIG.DND5E.creatureTypes) ) {
  11101. types[k] = {
  11102. label: game.i18n.localize(v),
  11103. chosen: attr.value === k
  11104. };
  11105. }
  11106. // Return data for rendering
  11107. return {
  11108. types: types,
  11109. custom: {
  11110. value: attr.custom,
  11111. label: game.i18n.localize("DND5E.CreatureTypeSelectorCustom"),
  11112. chosen: attr.value === "custom"
  11113. },
  11114. subtype: attr.subtype,
  11115. swarm: attr.swarm,
  11116. sizes: Array.from(Object.entries(CONFIG.DND5E.actorSizes)).reverse().reduce((obj, e) => {
  11117. obj[e[0]] = e[1];
  11118. return obj;
  11119. }, {}),
  11120. preview: Actor5e.formatCreatureType(attr) || "–"
  11121. };
  11122. }
  11123. /* -------------------------------------------- */
  11124. /** @override */
  11125. async _updateObject(event, formData) {
  11126. const typeObject = foundry.utils.expandObject(formData);
  11127. return this.object.update({"system.details.type": typeObject});
  11128. }
  11129. /* -------------------------------------------- */
  11130. /* Event Listeners and Handlers */
  11131. /* -------------------------------------------- */
  11132. /** @inheritdoc */
  11133. activateListeners(html) {
  11134. super.activateListeners(html);
  11135. html.find("input[name='custom']").focusin(this._onCustomFieldFocused.bind(this));
  11136. }
  11137. /* -------------------------------------------- */
  11138. /** @inheritdoc */
  11139. _onChangeInput(event) {
  11140. super._onChangeInput(event);
  11141. const typeObject = foundry.utils.expandObject(this._getSubmitData());
  11142. this.form.preview.value = Actor5e.formatCreatureType(typeObject) || "—";
  11143. }
  11144. /* -------------------------------------------- */
  11145. /**
  11146. * Select the custom radio button when the custom text field is focused.
  11147. * @param {FocusEvent} event The original focusin event
  11148. * @private
  11149. */
  11150. _onCustomFieldFocused(event) {
  11151. this.form.querySelector("input[name='value'][value='custom']").checked = true;
  11152. this._onChangeInput(event);
  11153. }
  11154. }
  11155. /**
  11156. * Dialog to confirm the deletion of an embedded item with advancement or decreasing a class level.
  11157. */
  11158. class AdvancementConfirmationDialog extends Dialog {
  11159. /** @inheritdoc */
  11160. static get defaultOptions() {
  11161. return foundry.utils.mergeObject(super.defaultOptions, {
  11162. template: "systems/dnd5e/templates/advancement/advancement-confirmation-dialog.hbs",
  11163. jQuery: false
  11164. });
  11165. }
  11166. /* -------------------------------------------- */
  11167. /**
  11168. * A helper function that displays the dialog prompting for an item deletion.
  11169. * @param {Item5e} item Item to be deleted.
  11170. * @returns {Promise<boolean|null>} Resolves with whether advancements should be unapplied. Rejects with null.
  11171. */
  11172. static forDelete(item) {
  11173. return this.createDialog(
  11174. item,
  11175. game.i18n.localize("DND5E.AdvancementDeleteConfirmationTitle"),
  11176. game.i18n.localize("DND5E.AdvancementDeleteConfirmationMessage"),
  11177. {
  11178. icon: '<i class="fas fa-trash"></i>',
  11179. label: game.i18n.localize("Delete")
  11180. }
  11181. );
  11182. }
  11183. /* -------------------------------------------- */
  11184. /**
  11185. * A helper function that displays the dialog prompting for leveling down.
  11186. * @param {Item5e} item The class whose level is being changed.
  11187. * @returns {Promise<boolean|null>} Resolves with whether advancements should be unapplied. Rejects with null.
  11188. */
  11189. static forLevelDown(item) {
  11190. return this.createDialog(
  11191. item,
  11192. game.i18n.localize("DND5E.AdvancementLevelDownConfirmationTitle"),
  11193. game.i18n.localize("DND5E.AdvancementLevelDownConfirmationMessage"),
  11194. {
  11195. icon: '<i class="fas fa-sort-numeric-down-alt"></i>',
  11196. label: game.i18n.localize("DND5E.LevelActionDecrease")
  11197. }
  11198. );
  11199. }
  11200. /* -------------------------------------------- */
  11201. /**
  11202. * A helper constructor function which displays the confirmation dialog.
  11203. * @param {Item5e} item Item to be changed.
  11204. * @param {string} title Localized dialog title.
  11205. * @param {string} message Localized dialog message.
  11206. * @param {object} continueButton Object containing label and icon for the action button.
  11207. * @returns {Promise<boolean|null>} Resolves with whether advancements should be unapplied. Rejects with null.
  11208. */
  11209. static createDialog(item, title, message, continueButton) {
  11210. return new Promise((resolve, reject) => {
  11211. const dialog = new this({
  11212. title: `${title}: ${item.name}`,
  11213. content: message,
  11214. buttons: {
  11215. continue: foundry.utils.mergeObject(continueButton, {
  11216. callback: html => {
  11217. const checkbox = html.querySelector('input[name="apply-advancement"]');
  11218. resolve(checkbox.checked);
  11219. }
  11220. }),
  11221. cancel: {
  11222. icon: '<i class="fas fa-times"></i>',
  11223. label: game.i18n.localize("Cancel"),
  11224. callback: html => reject(null)
  11225. }
  11226. },
  11227. default: "continue",
  11228. close: () => reject(null)
  11229. });
  11230. dialog.render(true);
  11231. });
  11232. }
  11233. }
  11234. /**
  11235. * Internal type used to manage each step within the advancement process.
  11236. *
  11237. * @typedef {object} AdvancementStep
  11238. * @property {string} type Step type from "forward", "reverse", "restore", or "delete".
  11239. * @property {AdvancementFlow} [flow] Flow object for the advancement being applied by this step.
  11240. * @property {Item5e} [item] For "delete" steps only, the item to be removed.
  11241. * @property {object} [class] Contains data on class if step was triggered by class level change.
  11242. * @property {Item5e} [class.item] Class item that caused this advancement step.
  11243. * @property {number} [class.level] Level the class should be during this step.
  11244. * @property {boolean} [automatic=false] Should the manager attempt to apply this step without user interaction?
  11245. */
  11246. /**
  11247. * Application for controlling the advancement workflow and displaying the interface.
  11248. *
  11249. * @param {Actor5e} actor Actor on which this advancement is being performed.
  11250. * @param {object} [options={}] Additional application options.
  11251. */
  11252. class AdvancementManager extends Application {
  11253. constructor(actor, options={}) {
  11254. super(options);
  11255. /**
  11256. * The original actor to which changes will be applied when the process is complete.
  11257. * @type {Actor5e}
  11258. */
  11259. this.actor = actor;
  11260. /**
  11261. * A clone of the original actor to which the changes can be applied during the advancement process.
  11262. * @type {Actor5e}
  11263. */
  11264. this.clone = actor.clone();
  11265. /**
  11266. * Individual steps that will be applied in order.
  11267. * @type {object}
  11268. */
  11269. this.steps = [];
  11270. /**
  11271. * Step being currently displayed.
  11272. * @type {number|null}
  11273. * @private
  11274. */
  11275. this._stepIndex = null;
  11276. /**
  11277. * Is the prompt currently advancing through un-rendered steps?
  11278. * @type {boolean}
  11279. * @private
  11280. */
  11281. this._advancing = false;
  11282. }
  11283. /* -------------------------------------------- */
  11284. /** @inheritdoc */
  11285. static get defaultOptions() {
  11286. return foundry.utils.mergeObject(super.defaultOptions, {
  11287. classes: ["dnd5e", "advancement", "flow"],
  11288. template: "systems/dnd5e/templates/advancement/advancement-manager.hbs",
  11289. width: 460,
  11290. height: "auto"
  11291. });
  11292. }
  11293. /* -------------------------------------------- */
  11294. /** @inheritdoc */
  11295. get title() {
  11296. const visibleSteps = this.steps.filter(s => !s.automatic);
  11297. const visibleIndex = visibleSteps.indexOf(this.step);
  11298. const step = visibleIndex < 0 ? "" : game.i18n.format("DND5E.AdvancementManagerSteps", {
  11299. current: visibleIndex + 1,
  11300. total: visibleSteps.length
  11301. });
  11302. return `${game.i18n.localize("DND5E.AdvancementManagerTitle")} ${step}`;
  11303. }
  11304. /* -------------------------------------------- */
  11305. /** @inheritdoc */
  11306. get id() {
  11307. return `actor-${this.actor.id}-advancement`;
  11308. }
  11309. /* -------------------------------------------- */
  11310. /**
  11311. * Get the step that is currently in progress.
  11312. * @type {object|null}
  11313. */
  11314. get step() {
  11315. return this.steps[this._stepIndex] ?? null;
  11316. }
  11317. /* -------------------------------------------- */
  11318. /**
  11319. * Get the step before the current one.
  11320. * @type {object|null}
  11321. */
  11322. get previousStep() {
  11323. return this.steps[this._stepIndex - 1] ?? null;
  11324. }
  11325. /* -------------------------------------------- */
  11326. /**
  11327. * Get the step after the current one.
  11328. * @type {object|null}
  11329. */
  11330. get nextStep() {
  11331. const nextIndex = this._stepIndex === null ? 0 : this._stepIndex + 1;
  11332. return this.steps[nextIndex] ?? null;
  11333. }
  11334. /* -------------------------------------------- */
  11335. /* Factory Methods */
  11336. /* -------------------------------------------- */
  11337. /**
  11338. * Construct a manager for a newly added advancement from drag-drop.
  11339. * @param {Actor5e} actor Actor from which the advancement should be updated.
  11340. * @param {string} itemId ID of the item to which the advancements are being dropped.
  11341. * @param {Advancement[]} advancements Dropped advancements to add.
  11342. * @param {object} options Rendering options passed to the application.
  11343. * @returns {AdvancementManager} Prepared manager. Steps count can be used to determine if advancements are needed.
  11344. */
  11345. static forNewAdvancement(actor, itemId, advancements, options) {
  11346. const manager = new this(actor, options);
  11347. const clonedItem = manager.clone.items.get(itemId);
  11348. if ( !clonedItem || !advancements.length ) return manager;
  11349. const currentLevel = this.currentLevel(clonedItem, manager.clone);
  11350. const minimumLevel = advancements.reduce((min, a) => Math.min(a.levels[0] ?? Infinity, min), Infinity);
  11351. if ( minimumLevel > currentLevel ) return manager;
  11352. const oldFlows = Array.fromRange(currentLevel + 1).slice(minimumLevel)
  11353. .flatMap(l => this.flowsForLevel(clonedItem, l));
  11354. // Revert advancements through minimum level
  11355. oldFlows.reverse().forEach(flow => manager.steps.push({ type: "reverse", flow, automatic: true }));
  11356. // Add new advancements
  11357. const advancementArray = clonedItem.toObject().system.advancement;
  11358. advancementArray.push(...advancements.map(a => {
  11359. const obj = a.toObject();
  11360. if ( obj.constructor.dataModels?.value ) a.value = (new a.constructor.metadata.dataModels.value()).toObject();
  11361. else obj.value = foundry.utils.deepClone(a.constructor.metadata.defaults?.value ?? {});
  11362. return obj;
  11363. }));
  11364. clonedItem.updateSource({"system.advancement": advancementArray});
  11365. const newFlows = Array.fromRange(currentLevel + 1).slice(minimumLevel)
  11366. .flatMap(l => this.flowsForLevel(clonedItem, l));
  11367. // Restore existing advancements and apply new advancements
  11368. newFlows.forEach(flow => {
  11369. const matchingFlow = oldFlows.find(f => (f.advancement.id === flow.advancement.id) && (f.level === flow.level));
  11370. if ( matchingFlow ) manager.steps.push({ type: "restore", flow: matchingFlow, automatic: true });
  11371. else manager.steps.push({ type: "forward", flow });
  11372. });
  11373. return manager;
  11374. }
  11375. /* -------------------------------------------- */
  11376. /**
  11377. * Construct a manager for a newly added item.
  11378. * @param {Actor5e} actor Actor to which the item is being added.
  11379. * @param {object} itemData Data for the item being added.
  11380. * @param {object} options Rendering options passed to the application.
  11381. * @returns {AdvancementManager} Prepared manager. Steps count can be used to determine if advancements are needed.
  11382. */
  11383. static forNewItem(actor, itemData, options={}) {
  11384. const manager = new this(actor, options);
  11385. // Prepare data for adding to clone
  11386. const dataClone = foundry.utils.deepClone(itemData);
  11387. dataClone._id = foundry.utils.randomID();
  11388. if ( itemData.type === "class" ) {
  11389. dataClone.system.levels = 0;
  11390. if ( !manager.clone.system.details.originalClass ) {
  11391. manager.clone.updateSource({"system.details.originalClass": dataClone._id});
  11392. }
  11393. }
  11394. // Add item to clone & get new instance from clone
  11395. manager.clone.updateSource({items: [dataClone]});
  11396. const clonedItem = manager.clone.items.get(dataClone._id);
  11397. // For class items, prepare level change data
  11398. if ( itemData.type === "class" ) {
  11399. return manager.createLevelChangeSteps(clonedItem, itemData.system?.levels ?? 1);
  11400. }
  11401. // All other items, just create some flows up to current character level (or class level for subclasses)
  11402. let targetLevel = manager.clone.system.details.level;
  11403. if ( clonedItem.type === "subclass" ) targetLevel = clonedItem.class?.system.levels ?? 0;
  11404. Array.fromRange(targetLevel + 1)
  11405. .flatMap(l => this.flowsForLevel(clonedItem, l))
  11406. .forEach(flow => manager.steps.push({ type: "forward", flow }));
  11407. return manager;
  11408. }
  11409. /* -------------------------------------------- */
  11410. /**
  11411. * Construct a manager for modifying choices on an item at a specific level.
  11412. * @param {Actor5e} actor Actor from which the choices should be modified.
  11413. * @param {object} itemId ID of the item whose choices are to be changed.
  11414. * @param {number} level Level at which the choices are being changed.
  11415. * @param {object} options Rendering options passed to the application.
  11416. * @returns {AdvancementManager} Prepared manager. Steps count can be used to determine if advancements are needed.
  11417. */
  11418. static forModifyChoices(actor, itemId, level, options) {
  11419. const manager = new this(actor, options);
  11420. const clonedItem = manager.clone.items.get(itemId);
  11421. if ( !clonedItem ) return manager;
  11422. const flows = Array.fromRange(this.currentLevel(clonedItem, manager.clone) + 1).slice(level)
  11423. .flatMap(l => this.flowsForLevel(clonedItem, l));
  11424. // Revert advancements through changed level
  11425. flows.reverse().forEach(flow => manager.steps.push({ type: "reverse", flow, automatic: true }));
  11426. // Create forward advancements for level being changed
  11427. flows.reverse().filter(f => f.level === level).forEach(flow => manager.steps.push({ type: "forward", flow }));
  11428. // Create restore advancements for other levels
  11429. flows.filter(f => f.level > level).forEach(flow => manager.steps.push({ type: "restore", flow, automatic: true }));
  11430. return manager;
  11431. }
  11432. /* -------------------------------------------- */
  11433. /**
  11434. * Construct a manager for an advancement that needs to be deleted.
  11435. * @param {Actor5e} actor Actor from which the advancement should be unapplied.
  11436. * @param {string} itemId ID of the item from which the advancement should be deleted.
  11437. * @param {string} advancementId ID of the advancement to delete.
  11438. * @param {object} options Rendering options passed to the application.
  11439. * @returns {AdvancementManager} Prepared manager. Steps count can be used to determine if advancements are needed.
  11440. */
  11441. static forDeletedAdvancement(actor, itemId, advancementId, options) {
  11442. const manager = new this(actor, options);
  11443. const clonedItem = manager.clone.items.get(itemId);
  11444. const advancement = clonedItem?.advancement.byId[advancementId];
  11445. if ( !advancement ) return manager;
  11446. const minimumLevel = advancement.levels[0];
  11447. const currentLevel = this.currentLevel(clonedItem, manager.clone);
  11448. // If minimum level is greater than current level, no changes to remove
  11449. if ( (minimumLevel > currentLevel) || !advancement.appliesToClass ) return manager;
  11450. advancement.levels
  11451. .reverse()
  11452. .filter(l => l <= currentLevel)
  11453. .map(l => new advancement.constructor.metadata.apps.flow(clonedItem, advancementId, l))
  11454. .forEach(flow => manager.steps.push({ type: "reverse", flow, automatic: true }));
  11455. if ( manager.steps.length ) manager.steps.push({ type: "delete", advancement, automatic: true });
  11456. return manager;
  11457. }
  11458. /* -------------------------------------------- */
  11459. /**
  11460. * Construct a manager for an item that needs to be deleted.
  11461. * @param {Actor5e} actor Actor from which the item should be deleted.
  11462. * @param {string} itemId ID of the item to be deleted.
  11463. * @param {object} options Rendering options passed to the application.
  11464. * @returns {AdvancementManager} Prepared manager. Steps count can be used to determine if advancements are needed.
  11465. */
  11466. static forDeletedItem(actor, itemId, options) {
  11467. const manager = new this(actor, options);
  11468. const clonedItem = manager.clone.items.get(itemId);
  11469. if ( !clonedItem ) return manager;
  11470. // For class items, prepare level change data
  11471. if ( clonedItem.type === "class" ) {
  11472. return manager.createLevelChangeSteps(clonedItem, clonedItem.system.levels * -1);
  11473. }
  11474. // All other items, just create some flows down from current character level
  11475. Array.fromRange(manager.clone.system.details.level + 1)
  11476. .flatMap(l => this.flowsForLevel(clonedItem, l))
  11477. .reverse()
  11478. .forEach(flow => manager.steps.push({ type: "reverse", flow, automatic: true }));
  11479. // Add a final step to remove the item only if there are advancements to apply
  11480. if ( manager.steps.length ) manager.steps.push({ type: "delete", item: clonedItem, automatic: true });
  11481. return manager;
  11482. }
  11483. /* -------------------------------------------- */
  11484. /**
  11485. * Construct a manager for a change in a class's levels.
  11486. * @param {Actor5e} actor Actor whose level has changed.
  11487. * @param {string} classId ID of the class being changed.
  11488. * @param {number} levelDelta Levels by which to increase or decrease the class.
  11489. * @param {object} options Rendering options passed to the application.
  11490. * @returns {AdvancementManager} Prepared manager. Steps count can be used to determine if advancements are needed.
  11491. */
  11492. static forLevelChange(actor, classId, levelDelta, options={}) {
  11493. const manager = new this(actor, options);
  11494. const clonedItem = manager.clone.items.get(classId);
  11495. if ( !clonedItem ) return manager;
  11496. return manager.createLevelChangeSteps(clonedItem, levelDelta);
  11497. }
  11498. /* -------------------------------------------- */
  11499. /**
  11500. * Create steps based on the provided level change data.
  11501. * @param {string} classItem Class being changed.
  11502. * @param {number} levelDelta Levels by which to increase or decrease the class.
  11503. * @returns {AdvancementManager} Manager with new steps.
  11504. * @private
  11505. */
  11506. createLevelChangeSteps(classItem, levelDelta) {
  11507. const pushSteps = (flows, data) => this.steps.push(...flows.map(flow => ({ flow, ...data })));
  11508. const getItemFlows = characterLevel => this.clone.items.contents.flatMap(i => {
  11509. if ( ["class", "subclass"].includes(i.type) ) return [];
  11510. return this.constructor.flowsForLevel(i, characterLevel);
  11511. });
  11512. // Level increased
  11513. for ( let offset = 1; offset <= levelDelta; offset++ ) {
  11514. const classLevel = classItem.system.levels + offset;
  11515. const characterLevel = this.actor.system.details.level + offset;
  11516. const stepData = { type: "forward", class: {item: classItem, level: classLevel} };
  11517. pushSteps(this.constructor.flowsForLevel(classItem, classLevel), stepData);
  11518. pushSteps(this.constructor.flowsForLevel(classItem.subclass, classLevel), stepData);
  11519. pushSteps(getItemFlows(characterLevel), stepData);
  11520. }
  11521. // Level decreased
  11522. for ( let offset = 0; offset > levelDelta; offset-- ) {
  11523. const classLevel = classItem.system.levels + offset;
  11524. const characterLevel = this.actor.system.details.level + offset;
  11525. const stepData = { type: "reverse", class: {item: classItem, level: classLevel}, automatic: true };
  11526. pushSteps(getItemFlows(characterLevel).reverse(), stepData);
  11527. pushSteps(this.constructor.flowsForLevel(classItem.subclass, classLevel).reverse(), stepData);
  11528. pushSteps(this.constructor.flowsForLevel(classItem, classLevel).reverse(), stepData);
  11529. if ( classLevel === 1 ) this.steps.push({ type: "delete", item: classItem, automatic: true });
  11530. }
  11531. // Ensure the class level ends up at the appropriate point
  11532. this.steps.push({
  11533. type: "forward", automatic: true,
  11534. class: {item: classItem, level: classItem.system.levels += levelDelta}
  11535. });
  11536. return this;
  11537. }
  11538. /* -------------------------------------------- */
  11539. /**
  11540. * Creates advancement flows for all advancements at a specific level.
  11541. * @param {Item5e} item Item that has advancement.
  11542. * @param {number} level Level in question.
  11543. * @returns {AdvancementFlow[]} Created flow applications.
  11544. * @protected
  11545. */
  11546. static flowsForLevel(item, level) {
  11547. return (item?.advancement.byLevel[level] ?? [])
  11548. .filter(a => a.appliesToClass)
  11549. .map(a => new a.constructor.metadata.apps.flow(item, a.id, level));
  11550. }
  11551. /* -------------------------------------------- */
  11552. /**
  11553. * Determine the proper working level either from the provided item or from the cloned actor.
  11554. * @param {Item5e} item Item being advanced. If class or subclass, its level will be used.
  11555. * @param {Actor5e} actor Actor being advanced.
  11556. * @returns {number} Working level.
  11557. */
  11558. static currentLevel(item, actor) {
  11559. return item.system.levels ?? item.class?.system.levels ?? actor.system.details.level;
  11560. }
  11561. /* -------------------------------------------- */
  11562. /* Form Rendering */
  11563. /* -------------------------------------------- */
  11564. /** @inheritdoc */
  11565. getData() {
  11566. if ( !this.step ) return {};
  11567. // Prepare information for subheading
  11568. const item = this.step.flow.item;
  11569. let level = this.step.flow.level;
  11570. if ( (this.step.class) && ["class", "subclass"].includes(item.type) ) level = this.step.class.level;
  11571. const visibleSteps = this.steps.filter(s => !s.automatic);
  11572. const visibleIndex = visibleSteps.indexOf(this.step);
  11573. return {
  11574. actor: this.clone,
  11575. flowId: this.step.flow.id,
  11576. header: item.name,
  11577. subheader: level ? game.i18n.format("DND5E.AdvancementLevelHeader", { level }) : "",
  11578. steps: {
  11579. current: visibleIndex + 1,
  11580. total: visibleSteps.length,
  11581. hasPrevious: visibleIndex > 0,
  11582. hasNext: visibleIndex < visibleSteps.length - 1
  11583. }
  11584. };
  11585. }
  11586. /* -------------------------------------------- */
  11587. /** @inheritdoc */
  11588. render(...args) {
  11589. if ( this.steps.length && (this._stepIndex === null) ) this._stepIndex = 0;
  11590. // Ensure the level on the class item matches the specified level
  11591. if ( this.step?.class ) {
  11592. let level = this.step.class.level;
  11593. if ( this.step.type === "reverse" ) level -= 1;
  11594. this.step.class.item.updateSource({"system.levels": level});
  11595. this.clone.reset();
  11596. }
  11597. /**
  11598. * A hook event that fires when an AdvancementManager is about to be processed.
  11599. * @function dnd5e.preAdvancementManagerRender
  11600. * @memberof hookEvents
  11601. * @param {AdvancementManager} advancementManager The advancement manager about to be rendered
  11602. */
  11603. const allowed = Hooks.call("dnd5e.preAdvancementManagerRender", this);
  11604. // Abort if not allowed
  11605. if ( allowed === false ) return this;
  11606. if ( this.step?.automatic ) {
  11607. if ( this._advancing ) return this;
  11608. this._forward();
  11609. return this;
  11610. }
  11611. return super.render(...args);
  11612. }
  11613. /* -------------------------------------------- */
  11614. /** @inheritdoc */
  11615. async _render(force, options) {
  11616. await super._render(force, options);
  11617. if ( (this._state !== Application.RENDER_STATES.RENDERED) || !this.step ) return;
  11618. // Render the step
  11619. this.step.flow._element = null;
  11620. await this.step.flow._render(force, options);
  11621. this.setPosition();
  11622. }
  11623. /* -------------------------------------------- */
  11624. /** @inheritdoc */
  11625. activateListeners(html) {
  11626. super.activateListeners(html);
  11627. html.find("button[data-action]").click(event => {
  11628. const buttons = html.find("button");
  11629. buttons.attr("disabled", true);
  11630. html.find(".error").removeClass("error");
  11631. try {
  11632. switch ( event.currentTarget.dataset.action ) {
  11633. case "restart":
  11634. if ( !this.previousStep ) return;
  11635. return this._restart(event);
  11636. case "previous":
  11637. if ( !this.previousStep ) return;
  11638. return this._backward(event);
  11639. case "next":
  11640. case "complete":
  11641. return this._forward(event);
  11642. }
  11643. } finally {
  11644. buttons.attr("disabled", false);
  11645. }
  11646. });
  11647. }
  11648. /* -------------------------------------------- */
  11649. /** @inheritdoc */
  11650. async close(options={}) {
  11651. if ( !options.skipConfirmation ) {
  11652. return new Dialog({
  11653. title: `${game.i18n.localize("DND5E.AdvancementManagerCloseTitle")}: ${this.actor.name}`,
  11654. content: game.i18n.localize("DND5E.AdvancementManagerCloseMessage"),
  11655. buttons: {
  11656. close: {
  11657. icon: '<i class="fas fa-times"></i>',
  11658. label: game.i18n.localize("DND5E.AdvancementManagerCloseButtonStop"),
  11659. callback: () => super.close(options)
  11660. },
  11661. continue: {
  11662. icon: '<i class="fas fa-chevron-right"></i>',
  11663. label: game.i18n.localize("DND5E.AdvancementManagerCloseButtonContinue")
  11664. }
  11665. },
  11666. default: "close"
  11667. }).render(true);
  11668. }
  11669. await super.close(options);
  11670. }
  11671. /* -------------------------------------------- */
  11672. /* Process */
  11673. /* -------------------------------------------- */
  11674. /**
  11675. * Advance through the steps until one requiring user interaction is encountered.
  11676. * @param {Event} [event] Triggering click event if one occurred.
  11677. * @returns {Promise}
  11678. * @private
  11679. */
  11680. async _forward(event) {
  11681. this._advancing = true;
  11682. try {
  11683. do {
  11684. const flow = this.step.flow;
  11685. const type = this.step.type;
  11686. // Apply changes based on step type
  11687. if ( (type === "delete") && this.step.item ) this.clone.items.delete(this.step.item.id);
  11688. else if ( (type === "delete") && this.step.advancement ) {
  11689. this.step.advancement.item.deleteAdvancement(this.step.advancement.id, { source: true });
  11690. }
  11691. else if ( type === "restore" ) await flow.advancement.restore(flow.level, flow.retainedData);
  11692. else if ( type === "reverse" ) await flow.retainData(await flow.advancement.reverse(flow.level));
  11693. else if ( flow ) await flow._updateObject(event, flow._getSubmitData());
  11694. this._stepIndex++;
  11695. // Ensure the level on the class item matches the specified level
  11696. if ( this.step?.class ) {
  11697. let level = this.step.class.level;
  11698. if ( this.step.type === "reverse" ) level -= 1;
  11699. this.step.class.item.updateSource({"system.levels": level});
  11700. }
  11701. this.clone.reset();
  11702. } while ( this.step?.automatic );
  11703. } catch(error) {
  11704. if ( !(error instanceof Advancement.ERROR) ) throw error;
  11705. ui.notifications.error(error.message);
  11706. this.step.automatic = false;
  11707. if ( this.step.type === "restore" ) this.step.type = "forward";
  11708. } finally {
  11709. this._advancing = false;
  11710. }
  11711. if ( this.step ) this.render(true);
  11712. else this._complete();
  11713. }
  11714. /* -------------------------------------------- */
  11715. /**
  11716. * Reverse through the steps until one requiring user interaction is encountered.
  11717. * @param {Event} [event] Triggering click event if one occurred.
  11718. * @param {object} [options] Additional options to configure behavior.
  11719. * @param {boolean} [options.render=true] Whether to render the Application after the step has been reversed. Used
  11720. * by the restart workflow.
  11721. * @returns {Promise}
  11722. * @private
  11723. */
  11724. async _backward(event, { render=true }={}) {
  11725. this._advancing = true;
  11726. try {
  11727. do {
  11728. this._stepIndex--;
  11729. if ( !this.step ) break;
  11730. const flow = this.step.flow;
  11731. const type = this.step.type;
  11732. // Reverse step based on step type
  11733. if ( (type === "delete") && this.step.item ) this.clone.updateSource({items: [this.step.item]});
  11734. else if ( (type === "delete") && this.step.advancement ) this.advancement.item.createAdvancement(
  11735. this.advancement.typeName, this.advancement._source, { source: true }
  11736. );
  11737. else if ( type === "reverse" ) await flow.advancement.restore(flow.level, flow.retainedData);
  11738. else if ( flow ) await flow.retainData(await flow.advancement.reverse(flow.level));
  11739. this.clone.reset();
  11740. } while ( this.step?.automatic );
  11741. } catch(error) {
  11742. if ( !(error instanceof Advancement.ERROR) ) throw error;
  11743. ui.notifications.error(error.message);
  11744. this.step.automatic = false;
  11745. } finally {
  11746. this._advancing = false;
  11747. }
  11748. if ( !render ) return;
  11749. if ( this.step ) this.render(true);
  11750. else this.close({ skipConfirmation: true });
  11751. }
  11752. /* -------------------------------------------- */
  11753. /**
  11754. * Reset back to the manager's initial state.
  11755. * @param {MouseEvent} [event] The triggering click event if one occurred.
  11756. * @returns {Promise}
  11757. * @private
  11758. */
  11759. async _restart(event) {
  11760. const restart = await Dialog.confirm({
  11761. title: game.i18n.localize("DND5E.AdvancementManagerRestartConfirmTitle"),
  11762. content: game.i18n.localize("DND5E.AdvancementManagerRestartConfirm")
  11763. });
  11764. if ( !restart ) return;
  11765. // While there is still a renderable step.
  11766. while ( this.steps.slice(0, this._stepIndex).some(s => !s.automatic) ) {
  11767. await this._backward(event, {render: false});
  11768. }
  11769. this.render(true);
  11770. }
  11771. /* -------------------------------------------- */
  11772. /**
  11773. * Apply changes to actual actor after all choices have been made.
  11774. * @param {Event} event Button click that triggered the change.
  11775. * @returns {Promise}
  11776. * @private
  11777. */
  11778. async _complete(event) {
  11779. const updates = this.clone.toObject();
  11780. const items = updates.items;
  11781. delete updates.items;
  11782. // Gather changes to embedded items
  11783. const { toCreate, toUpdate, toDelete } = items.reduce((obj, item) => {
  11784. if ( !this.actor.items.get(item._id) ) {
  11785. obj.toCreate.push(item);
  11786. } else {
  11787. obj.toUpdate.push(item);
  11788. obj.toDelete.findSplice(id => id === item._id);
  11789. }
  11790. return obj;
  11791. }, { toCreate: [], toUpdate: [], toDelete: this.actor.items.map(i => i.id) });
  11792. /**
  11793. * A hook event that fires at the final stage of a character's advancement process, before actor and item updates
  11794. * are applied.
  11795. * @function dnd5e.preAdvancementManagerComplete
  11796. * @memberof hookEvents
  11797. * @param {AdvancementManager} advancementManager The advancement manager.
  11798. * @param {object} actorUpdates Updates to the actor.
  11799. * @param {object[]} toCreate Items that will be created on the actor.
  11800. * @param {object[]} toUpdate Items that will be updated on the actor.
  11801. * @param {string[]} toDelete IDs of items that will be deleted on the actor.
  11802. */
  11803. if ( Hooks.call("dnd5e.preAdvancementManagerComplete", this, updates, toCreate, toUpdate, toDelete) === false ) {
  11804. console.log("AdvancementManager completion was prevented by the 'preAdvancementManagerComplete' hook.");
  11805. return this.close({ skipConfirmation: true });
  11806. }
  11807. // Apply changes from clone to original actor
  11808. await Promise.all([
  11809. this.actor.update(updates, { isAdvancement: true }),
  11810. this.actor.createEmbeddedDocuments("Item", toCreate, { keepId: true, isAdvancement: true }),
  11811. this.actor.updateEmbeddedDocuments("Item", toUpdate, { isAdvancement: true }),
  11812. this.actor.deleteEmbeddedDocuments("Item", toDelete, { isAdvancement: true })
  11813. ]);
  11814. /**
  11815. * A hook event that fires when an AdvancementManager is done modifying an actor.
  11816. * @function dnd5e.advancementManagerComplete
  11817. * @memberof hookEvents
  11818. * @param {AdvancementManager} advancementManager The advancement manager that just completed
  11819. */
  11820. Hooks.callAll("dnd5e.advancementManagerComplete", this);
  11821. // Close prompt
  11822. return this.close({ skipConfirmation: true });
  11823. }
  11824. }
  11825. /**
  11826. * Description for a single part of a property attribution.
  11827. * @typedef {object} AttributionDescription
  11828. * @property {string} label Descriptive label that will be displayed. If the label is in the form
  11829. * of an @ property, the system will try to turn it into a human-readable label.
  11830. * @property {number} mode Application mode for this step as defined in
  11831. * [CONST.ACTIVE_EFFECT_MODES](https://foundryvtt.com/api/module-constants.html#.ACTIVE_EFFECT_MODES).
  11832. * @property {number} value Value of this step.
  11833. */
  11834. /**
  11835. * Interface for viewing what factors went into determining a specific property.
  11836. *
  11837. * @param {Document} object The Document that owns the property being attributed.
  11838. * @param {AttributionDescription[]} attributions An array of all the attribution data.
  11839. * @param {string} property Dot separated path to the property.
  11840. * @param {object} [options={}] Application rendering options.
  11841. */
  11842. class PropertyAttribution extends Application {
  11843. constructor(object, attributions, property, options={}) {
  11844. super(options);
  11845. this.object = object;
  11846. this.attributions = attributions;
  11847. this.property = property;
  11848. }
  11849. /* -------------------------------------------- */
  11850. /** @inheritDoc */
  11851. static get defaultOptions() {
  11852. return foundry.utils.mergeObject(super.defaultOptions, {
  11853. id: "property-attribution",
  11854. classes: ["dnd5e", "property-attribution"],
  11855. template: "systems/dnd5e/templates/apps/property-attribution.hbs",
  11856. width: 320,
  11857. height: "auto"
  11858. });
  11859. }
  11860. /* -------------------------------------------- */
  11861. /**
  11862. * Render this view as a tooltip rather than a whole window.
  11863. * @param {HTMLElement} element The element to which the tooltip should be attached.
  11864. */
  11865. async renderTooltip(element) {
  11866. const data = this.getData(this.options);
  11867. const text = (await this._renderInner(data))[0].outerHTML;
  11868. game.tooltip.activate(element, { text, cssClass: "property-attribution" });
  11869. }
  11870. /* -------------------------------------------- */
  11871. /** @inheritDoc */
  11872. getData() {
  11873. const property = foundry.utils.getProperty(this.object.system, this.property);
  11874. let total;
  11875. if ( Number.isNumeric(property)) total = property;
  11876. else if ( typeof property === "object" && Number.isNumeric(property.value) ) total = property.value;
  11877. const sources = foundry.utils.duplicate(this.attributions);
  11878. return {
  11879. caption: this.options.title,
  11880. sources: sources.map(entry => {
  11881. if ( entry.label.startsWith("@") ) entry.label = this.getPropertyLabel(entry.label.slice(1));
  11882. if ( (entry.mode === CONST.ACTIVE_EFFECT_MODES.ADD) && (entry.value < 0) ) {
  11883. entry.negative = true;
  11884. entry.value = entry.value * -1;
  11885. }
  11886. return entry;
  11887. }),
  11888. total: total
  11889. };
  11890. }
  11891. /* -------------------------------------------- */
  11892. /**
  11893. * Produce a human-readable and localized name for the provided property.
  11894. * @param {string} property Dot separated path to the property.
  11895. * @returns {string} Property name for display.
  11896. */
  11897. getPropertyLabel(property) {
  11898. const parts = property.split(".");
  11899. if ( parts[0] === "abilities" && parts[1] ) {
  11900. return CONFIG.DND5E.abilities[parts[1]]?.label ?? property;
  11901. } else if ( (property === "attributes.ac.dex") && CONFIG.DND5E.abilities.dex ) {
  11902. return CONFIG.DND5E.abilities.dex.label;
  11903. } else if ( (parts[0] === "prof") || (property === "attributes.prof") ) {
  11904. return game.i18n.localize("DND5E.Proficiency");
  11905. }
  11906. return property;
  11907. }
  11908. }
  11909. /**
  11910. * A specialized application used to modify actor traits.
  11911. *
  11912. * @param {Actor5e} actor Actor for whose traits are being edited.
  11913. * @param {string} trait Trait key as defined in CONFIG.traits.
  11914. * @param {object} [options={}]
  11915. * @param {boolean} [options.allowCustom=true] Support user custom trait entries.
  11916. */
  11917. let TraitSelector$1 = class TraitSelector extends BaseConfigSheet {
  11918. constructor(actor, trait, options={}) {
  11919. if ( !CONFIG.DND5E.traits[trait] ) throw new Error(
  11920. `Cannot instantiate TraitSelector with a trait not defined in CONFIG.DND5E.traits: ${trait}.`
  11921. );
  11922. if ( ["saves", "skills"].includes(trait) ) throw new Error(
  11923. `TraitSelector does not support selection of ${trait}. That should be handled through `
  11924. + "that type's more specialized configuration application."
  11925. );
  11926. super(actor, options);
  11927. /**
  11928. * Trait key as defined in CONFIG.traits.
  11929. * @type {string}
  11930. */
  11931. this.trait = trait;
  11932. }
  11933. /* -------------------------------------------- */
  11934. /** @inheritdoc */
  11935. static get defaultOptions() {
  11936. return foundry.utils.mergeObject(super.defaultOptions, {
  11937. id: "trait-selector",
  11938. classes: ["dnd5e", "trait-selector", "subconfig"],
  11939. template: "systems/dnd5e/templates/apps/trait-selector.hbs",
  11940. width: 320,
  11941. height: "auto",
  11942. sheetConfig: false,
  11943. allowCustom: true
  11944. });
  11945. }
  11946. /* -------------------------------------------- */
  11947. /** @inheritdoc */
  11948. get id() {
  11949. return `${this.constructor.name}-${this.trait}-Actor-${this.document.id}`;
  11950. }
  11951. /* -------------------------------------------- */
  11952. /** @inheritdoc */
  11953. get title() {
  11954. return `${this.document.name}: ${traitLabel(this.trait)}`;
  11955. }
  11956. /* -------------------------------------------- */
  11957. /** @inheritdoc */
  11958. async getData() {
  11959. const path = `system.${actorKeyPath(this.trait)}`;
  11960. const data = foundry.utils.getProperty(this.document, path);
  11961. if ( !data ) return super.getData();
  11962. return {
  11963. ...super.getData(),
  11964. choices: await choices(this.trait, data.value),
  11965. custom: data.custom,
  11966. customPath: "custom" in data ? `${path}.custom` : null,
  11967. bypasses: "bypasses" in data ? Object.entries(CONFIG.DND5E.physicalWeaponProperties).reduce((obj, [k, v]) => {
  11968. obj[k] = { label: v, chosen: data.bypasses.has(k) };
  11969. return obj;
  11970. }, {}) : null,
  11971. bypassesPath: "bypasses" in data ? `${path}.bypasses` : null
  11972. };
  11973. }
  11974. /* -------------------------------------------- */
  11975. /** @inheritdoc */
  11976. activateListeners(html) {
  11977. super.activateListeners(html);
  11978. for ( const checkbox of html[0].querySelectorAll("input[type='checkbox']") ) {
  11979. if ( checkbox.checked ) this._onToggleCategory(checkbox);
  11980. }
  11981. }
  11982. /* -------------------------------------------- */
  11983. /** @inheritdoc */
  11984. _getActorOverrides() {
  11985. const overrides = super._getActorOverrides();
  11986. const path = `system.${actorKeyPath(this.trait)}.value`;
  11987. const src = new Set(foundry.utils.getProperty(this.document._source, path));
  11988. const current = foundry.utils.getProperty(this.document, path);
  11989. const delta = current.difference(src);
  11990. for ( const choice of delta ) {
  11991. overrides.push(`choices.${choice}`);
  11992. }
  11993. return overrides;
  11994. }
  11995. /* -------------------------------------------- */
  11996. /** @inheritdoc */
  11997. async _onChangeInput(event) {
  11998. super._onChangeInput(event);
  11999. if ( event.target.name?.startsWith("choices") ) this._onToggleCategory(event.target);
  12000. }
  12001. /* -------------------------------------------- */
  12002. /**
  12003. * Enable/disable all children when a category is checked.
  12004. * @param {HTMLElement} checkbox Checkbox that was changed.
  12005. * @protected
  12006. */
  12007. _onToggleCategory(checkbox) {
  12008. const children = checkbox.closest("li")?.querySelector("ol");
  12009. if ( !children ) return;
  12010. for ( const child of children.querySelectorAll("input[type='checkbox']") ) {
  12011. child.checked = child.disabled = checkbox.checked;
  12012. }
  12013. }
  12014. /* -------------------------------------------- */
  12015. /**
  12016. * Filter a list of choices that begin with the provided key for update.
  12017. * @param {string} prefix They initial form prefix under which the choices are grouped.
  12018. * @param {string} path Path in actor data where the final choices will be saved.
  12019. * @param {object} formData Form data being prepared. *Will be mutated.*
  12020. * @protected
  12021. */
  12022. _prepareChoices(prefix, path, formData) {
  12023. const chosen = [];
  12024. for ( const key of Object.keys(formData).filter(k => k.startsWith(`${prefix}.`)) ) {
  12025. if ( formData[key] ) chosen.push(key.replace(`${prefix}.`, ""));
  12026. delete formData[key];
  12027. }
  12028. formData[path] = chosen;
  12029. }
  12030. /* -------------------------------------------- */
  12031. /** @override */
  12032. async _updateObject(event, formData) {
  12033. const path = `system.${actorKeyPath(this.trait)}`;
  12034. const data = foundry.utils.getProperty(this.document, path);
  12035. this._prepareChoices("choices", `${path}.value`, formData);
  12036. if ( "bypasses" in data ) this._prepareChoices("bypasses", `${path}.bypasses`, formData);
  12037. return this.object.update(formData);
  12038. }
  12039. };
  12040. /**
  12041. * @typedef {FormApplicationOptions} ProficiencyConfigOptions
  12042. * @property {string} key The ID of the skill or tool being configured.
  12043. * @property {string} property The property on the actor being configured, either 'skills', or 'tools'.
  12044. */
  12045. /**
  12046. * An application responsible for configuring proficiencies and bonuses in tools and skills.
  12047. *
  12048. * @param {Actor5e} actor The Actor being configured.
  12049. * @param {ProficiencyConfigOptions} options Additional configuration options.
  12050. */
  12051. class ProficiencyConfig extends BaseConfigSheet {
  12052. /** @inheritdoc */
  12053. static get defaultOptions() {
  12054. return foundry.utils.mergeObject(super.defaultOptions, {
  12055. classes: ["dnd5e"],
  12056. template: "systems/dnd5e/templates/apps/proficiency-config.hbs",
  12057. width: 500,
  12058. height: "auto"
  12059. });
  12060. }
  12061. /* -------------------------------------------- */
  12062. /**
  12063. * Are we configuring a tool?
  12064. * @returns {boolean}
  12065. */
  12066. get isTool() {
  12067. return this.options.property === "tools";
  12068. }
  12069. /* -------------------------------------------- */
  12070. /**
  12071. * Are we configuring a skill?
  12072. * @returns {boolean}
  12073. */
  12074. get isSkill() {
  12075. return this.options.property === "skills";
  12076. }
  12077. /* -------------------------------------------- */
  12078. /** @inheritdoc */
  12079. get title() {
  12080. const label = this.isSkill ? CONFIG.DND5E.skills[this.options.key].label : keyLabel("tool", this.options.key);
  12081. return `${game.i18n.format("DND5E.ProficiencyConfigureTitle", {label})}: ${this.document.name}`;
  12082. }
  12083. /* -------------------------------------------- */
  12084. /** @inheritdoc */
  12085. get id() {
  12086. return `ProficiencyConfig-${this.document.documentName}-${this.document.id}-${this.options.key}`;
  12087. }
  12088. /* -------------------------------------------- */
  12089. /** @inheritdoc */
  12090. getData(options={}) {
  12091. return {
  12092. abilities: CONFIG.DND5E.abilities,
  12093. proficiencyLevels: CONFIG.DND5E.proficiencyLevels,
  12094. entry: this.document.system[this.options.property]?.[this.options.key],
  12095. isTool: this.isTool,
  12096. isSkill: this.isSkill,
  12097. key: this.options.key,
  12098. property: this.options.property
  12099. };
  12100. }
  12101. /* -------------------------------------------- */
  12102. /** @inheritdoc */
  12103. async _updateObject(event, formData) {
  12104. if ( this.isTool ) return super._updateObject(event, formData);
  12105. const passive = formData[`system.skills.${this.options.key}.bonuses.passive`];
  12106. const passiveRoll = new Roll(passive);
  12107. if ( !passiveRoll.isDeterministic ) {
  12108. const message = game.i18n.format("DND5E.FormulaCannotContainDiceError", {
  12109. name: game.i18n.localize("DND5E.SkillBonusPassive")
  12110. });
  12111. ui.notifications.error(message);
  12112. throw new Error(message);
  12113. }
  12114. return super._updateObject(event, formData);
  12115. }
  12116. }
  12117. /**
  12118. * A specialized version of the TraitSelector used for selecting tool and vehicle proficiencies.
  12119. * @extends {TraitSelector}
  12120. */
  12121. class ToolSelector extends TraitSelector$1 {
  12122. /** @inheritdoc */
  12123. async getData() {
  12124. return {
  12125. ...super.getData(),
  12126. choices: await choices(this.trait, Object.keys(this.document.system.tools))
  12127. };
  12128. }
  12129. /* -------------------------------------------- */
  12130. /** @inheritdoc */
  12131. _getActorOverrides() {
  12132. return Object.keys(foundry.utils.flattenObject(this.document.overrides));
  12133. }
  12134. /* -------------------------------------------- */
  12135. /** @inheritdoc */
  12136. async _updateObject(event, formData) {
  12137. return this.document.update(Object.entries(formData).reduce((obj, [k, v]) => {
  12138. const [, key] = k.split(".");
  12139. const tool = this.document.system.tools[key];
  12140. if ( tool && !v ) obj[`system.tools.-=${key}`] = null;
  12141. else if ( !tool && v ) obj[`system.tools.${key}`] = {value: 1};
  12142. return obj;
  12143. }, {}));
  12144. }
  12145. }
  12146. /**
  12147. * Extend the basic ActorSheet class to suppose system-specific logic and functionality.
  12148. * @abstract
  12149. */
  12150. class ActorSheet5e extends ActorSheet {
  12151. /**
  12152. * Track the set of item filters which are applied
  12153. * @type {Object<string, Set>}
  12154. * @protected
  12155. */
  12156. _filters = {
  12157. inventory: new Set(),
  12158. spellbook: new Set(),
  12159. features: new Set(),
  12160. effects: new Set()
  12161. };
  12162. /* -------------------------------------------- */
  12163. /**
  12164. * IDs for items on the sheet that have been expanded.
  12165. * @type {Set<string>}
  12166. * @protected
  12167. */
  12168. _expanded = new Set();
  12169. /* -------------------------------------------- */
  12170. /** @override */
  12171. static get defaultOptions() {
  12172. return foundry.utils.mergeObject(super.defaultOptions, {
  12173. scrollY: [
  12174. ".inventory .inventory-list",
  12175. ".features .inventory-list",
  12176. ".spellbook .inventory-list",
  12177. ".effects .inventory-list",
  12178. ".center-pane"
  12179. ],
  12180. tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}],
  12181. width: 720,
  12182. height: Math.max(680, Math.max(
  12183. 237 + (Object.keys(CONFIG.DND5E.abilities).length * 70),
  12184. 240 + (Object.keys(CONFIG.DND5E.skills).length * 24)
  12185. ))
  12186. });
  12187. }
  12188. /* -------------------------------------------- */
  12189. /**
  12190. * A set of item types that should be prevented from being dropped on this type of actor sheet.
  12191. * @type {Set<string>}
  12192. */
  12193. static unsupportedItemTypes = new Set();
  12194. /* -------------------------------------------- */
  12195. /** @override */
  12196. get template() {
  12197. if ( !game.user.isGM && this.actor.limited ) return "systems/dnd5e/templates/actors/limited-sheet.hbs";
  12198. return `systems/dnd5e/templates/actors/${this.actor.type}-sheet.hbs`;
  12199. }
  12200. /* -------------------------------------------- */
  12201. /* Context Preparation */
  12202. /* -------------------------------------------- */
  12203. /** @override */
  12204. async getData(options) {
  12205. // The Actor's data
  12206. const source = this.actor.toObject();
  12207. // Basic data
  12208. const context = {
  12209. actor: this.actor,
  12210. source: source.system,
  12211. system: this.actor.system,
  12212. items: Array.from(this.actor.items),
  12213. itemContext: {},
  12214. abilities: foundry.utils.deepClone(this.actor.system.abilities),
  12215. skills: foundry.utils.deepClone(this.actor.system.skills ?? {}),
  12216. tools: foundry.utils.deepClone(this.actor.system.tools ?? {}),
  12217. labels: this._getLabels(),
  12218. movement: this._getMovementSpeed(this.actor.system),
  12219. senses: this._getSenses(this.actor.system),
  12220. effects: ActiveEffect5e.prepareActiveEffectCategories(this.actor.effects),
  12221. warnings: foundry.utils.deepClone(this.actor._preparationWarnings),
  12222. filters: this._filters,
  12223. owner: this.actor.isOwner,
  12224. limited: this.actor.limited,
  12225. options: this.options,
  12226. editable: this.isEditable,
  12227. cssClass: this.actor.isOwner ? "editable" : "locked",
  12228. isCharacter: this.actor.type === "character",
  12229. isNPC: this.actor.type === "npc",
  12230. isVehicle: this.actor.type === "vehicle",
  12231. config: CONFIG.DND5E,
  12232. rollableClass: this.isEditable ? "rollable" : "",
  12233. rollData: this.actor.getRollData(),
  12234. overrides: {
  12235. attunement: foundry.utils.hasProperty(this.actor.overrides, "system.attributes.attunement.max")
  12236. }
  12237. };
  12238. // Sort Owned Items
  12239. context.items.sort((a, b) => (a.sort || 0) - (b.sort || 0));
  12240. // Temporary HP
  12241. const hp = {...context.system.attributes.hp};
  12242. if ( hp.temp === 0 ) delete hp.temp;
  12243. if ( hp.tempmax === 0 ) delete hp.tempmax;
  12244. context.hp = hp;
  12245. // Ability Scores
  12246. for ( const [a, abl] of Object.entries(context.abilities) ) {
  12247. abl.icon = this._getProficiencyIcon(abl.proficient);
  12248. abl.hover = CONFIG.DND5E.proficiencyLevels[abl.proficient];
  12249. abl.label = CONFIG.DND5E.abilities[a]?.label;
  12250. abl.baseProf = source.system.abilities[a]?.proficient ?? 0;
  12251. }
  12252. // Skills & tools.
  12253. ["skills", "tools"].forEach(prop => {
  12254. for ( const [key, entry] of Object.entries(context[prop]) ) {
  12255. entry.abbreviation = CONFIG.DND5E.abilities[entry.ability]?.abbreviation;
  12256. entry.icon = this._getProficiencyIcon(entry.value);
  12257. entry.hover = CONFIG.DND5E.proficiencyLevels[entry.value];
  12258. entry.label = prop === "skills" ? CONFIG.DND5E.skills[key]?.label : keyLabel("tool", key);
  12259. entry.baseValue = source.system[prop]?.[key]?.value ?? 0;
  12260. }
  12261. });
  12262. // Update traits
  12263. context.traits = this._prepareTraits(context.system);
  12264. // Prepare owned items
  12265. this._prepareItems(context);
  12266. context.expandedData = {};
  12267. for ( const id of this._expanded ) {
  12268. const item = this.actor.items.get(id);
  12269. if ( item ) context.expandedData[id] = await item.getChatData({secrets: this.actor.isOwner});
  12270. }
  12271. // Biography HTML enrichment
  12272. context.biographyHTML = await TextEditor.enrichHTML(context.system.details.biography.value, {
  12273. secrets: this.actor.isOwner,
  12274. rollData: context.rollData,
  12275. async: true,
  12276. relativeTo: this.actor
  12277. });
  12278. return context;
  12279. }
  12280. /* -------------------------------------------- */
  12281. /**
  12282. * Prepare labels object for the context.
  12283. * @returns {object} Object containing various labels.
  12284. * @protected
  12285. */
  12286. _getLabels() {
  12287. const labels = {...this.actor.labels};
  12288. // Currency Labels
  12289. labels.currencies = Object.entries(CONFIG.DND5E.currencies).reduce((obj, [k, c]) => {
  12290. obj[k] = c.label;
  12291. return obj;
  12292. }, {});
  12293. // Proficiency
  12294. labels.proficiency = game.settings.get("dnd5e", "proficiencyModifier") === "dice"
  12295. ? `d${this.actor.system.attributes.prof * 2}`
  12296. : `+${this.actor.system.attributes.prof}`;
  12297. return labels;
  12298. }
  12299. /* -------------------------------------------- */
  12300. /**
  12301. * Prepare the display of movement speed data for the Actor.
  12302. * @param {object} systemData System data for the Actor being prepared.
  12303. * @param {boolean} [largestPrimary=false] Show the largest movement speed as "primary", otherwise show "walk".
  12304. * @returns {{primary: string, special: string}}
  12305. * @protected
  12306. */
  12307. _getMovementSpeed(systemData, largestPrimary=false) {
  12308. const movement = systemData.attributes.movement ?? {};
  12309. // Prepare an array of available movement speeds
  12310. let speeds = [
  12311. [movement.burrow, `${game.i18n.localize("DND5E.MovementBurrow")} ${movement.burrow}`],
  12312. [movement.climb, `${game.i18n.localize("DND5E.MovementClimb")} ${movement.climb}`],
  12313. [movement.fly, `${game.i18n.localize("DND5E.MovementFly")} ${movement.fly}${movement.hover ? ` (${game.i18n.localize("DND5E.MovementHover")})` : ""}`],
  12314. [movement.swim, `${game.i18n.localize("DND5E.MovementSwim")} ${movement.swim}`]
  12315. ];
  12316. if ( largestPrimary ) {
  12317. speeds.push([movement.walk, `${game.i18n.localize("DND5E.MovementWalk")} ${movement.walk}`]);
  12318. }
  12319. // Filter and sort speeds on their values
  12320. speeds = speeds.filter(s => s[0]).sort((a, b) => b[0] - a[0]);
  12321. // Case 1: Largest as primary
  12322. if ( largestPrimary ) {
  12323. let primary = speeds.shift();
  12324. return {
  12325. primary: `${primary ? primary[1] : "0"} ${movement.units}`,
  12326. special: speeds.map(s => s[1]).join(", ")
  12327. };
  12328. }
  12329. // Case 2: Walk as primary
  12330. else {
  12331. return {
  12332. primary: `${movement.walk || 0} ${movement.units}`,
  12333. special: speeds.length ? speeds.map(s => s[1]).join(", ") : ""
  12334. };
  12335. }
  12336. }
  12337. /* -------------------------------------------- */
  12338. /**
  12339. * Prepare senses object for display.
  12340. * @param {object} systemData System data for the Actor being prepared.
  12341. * @returns {object} Senses grouped by key with localized and formatted string.
  12342. * @protected
  12343. */
  12344. _getSenses(systemData) {
  12345. const senses = systemData.attributes.senses ?? {};
  12346. const tags = {};
  12347. for ( let [k, label] of Object.entries(CONFIG.DND5E.senses) ) {
  12348. const v = senses[k] ?? 0;
  12349. if ( v === 0 ) continue;
  12350. tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
  12351. }
  12352. if ( senses.special ) tags.special = senses.special;
  12353. return tags;
  12354. }
  12355. /* -------------------------------------------- */
  12356. /** @inheritdoc */
  12357. async activateEditor(name, options={}, initialContent="") {
  12358. options.relativeLinks = true;
  12359. return super.activateEditor(name, options, initialContent);
  12360. }
  12361. /* --------------------------------------------- */
  12362. /* Property Attribution */
  12363. /* --------------------------------------------- */
  12364. /**
  12365. * Break down all of the Active Effects affecting a given target property.
  12366. * @param {string} target The data property being targeted.
  12367. * @returns {AttributionDescription[]} Any active effects that modify that property.
  12368. * @protected
  12369. */
  12370. _prepareActiveEffectAttributions(target) {
  12371. return this.actor.effects.reduce((arr, e) => {
  12372. let source = e.sourceName;
  12373. if ( e.origin === this.actor.uuid ) source = e.label;
  12374. if ( !source || e.disabled || e.isSuppressed ) return arr;
  12375. const value = e.changes.reduce((n, change) => {
  12376. if ( (change.key !== target) || !Number.isNumeric(change.value) ) return n;
  12377. if ( change.mode !== CONST.ACTIVE_EFFECT_MODES.ADD ) return n;
  12378. return n + Number(change.value);
  12379. }, 0);
  12380. if ( !value ) return arr;
  12381. arr.push({value, label: source, mode: CONST.ACTIVE_EFFECT_MODES.ADD});
  12382. return arr;
  12383. }, []);
  12384. }
  12385. /* -------------------------------------------- */
  12386. /**
  12387. * Produce a list of armor class attribution objects.
  12388. * @param {object} rollData Data provided by Actor5e#getRollData
  12389. * @returns {AttributionDescription[]} List of attribution descriptions.
  12390. * @protected
  12391. */
  12392. _prepareArmorClassAttribution(rollData) {
  12393. const ac = rollData.attributes.ac;
  12394. const cfg = CONFIG.DND5E.armorClasses[ac.calc];
  12395. const attribution = [];
  12396. // Base AC Attribution
  12397. switch ( ac.calc ) {
  12398. // Flat AC
  12399. case "flat":
  12400. return [{
  12401. label: game.i18n.localize("DND5E.ArmorClassFlat"),
  12402. mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE,
  12403. value: ac.flat
  12404. }];
  12405. // Natural armor
  12406. case "natural":
  12407. attribution.push({
  12408. label: game.i18n.localize("DND5E.ArmorClassNatural"),
  12409. mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE,
  12410. value: ac.flat
  12411. });
  12412. break;
  12413. default:
  12414. const formula = ac.calc === "custom" ? ac.formula : cfg.formula;
  12415. let base = ac.base;
  12416. const dataRgx = new RegExp(/@([a-z.0-9_-]+)/gi);
  12417. for ( const [match, term] of formula.matchAll(dataRgx) ) {
  12418. const value = String(foundry.utils.getProperty(rollData, term));
  12419. if ( (term === "attributes.ac.armor") || (value === "0") ) continue;
  12420. if ( Number.isNumeric(value) ) base -= Number(value);
  12421. attribution.push({
  12422. label: match,
  12423. mode: CONST.ACTIVE_EFFECT_MODES.ADD,
  12424. value
  12425. });
  12426. }
  12427. const armorInFormula = formula.includes("@attributes.ac.armor");
  12428. let label = game.i18n.localize("DND5E.PropertyBase");
  12429. if ( armorInFormula ) label = this.actor.armor?.name ?? game.i18n.localize("DND5E.ArmorClassUnarmored");
  12430. attribution.unshift({
  12431. label,
  12432. mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE,
  12433. value: base
  12434. });
  12435. break;
  12436. }
  12437. // Shield
  12438. if ( ac.shield !== 0 ) attribution.push({
  12439. label: this.actor.shield?.name ?? game.i18n.localize("DND5E.EquipmentShield"),
  12440. mode: CONST.ACTIVE_EFFECT_MODES.ADD,
  12441. value: ac.shield
  12442. });
  12443. // Bonus
  12444. if ( ac.bonus !== 0 ) attribution.push(...this._prepareActiveEffectAttributions("system.attributes.ac.bonus"));
  12445. // Cover
  12446. if ( ac.cover !== 0 ) attribution.push({
  12447. label: game.i18n.localize("DND5E.Cover"),
  12448. mode: CONST.ACTIVE_EFFECT_MODES.ADD,
  12449. value: ac.cover
  12450. });
  12451. return attribution;
  12452. }
  12453. /* -------------------------------------------- */
  12454. /**
  12455. * Prepare the data structure for traits data like languages, resistances & vulnerabilities, and proficiencies.
  12456. * @param {object} systemData System data for the Actor being prepared.
  12457. * @returns {object} Prepared trait data.
  12458. * @protected
  12459. */
  12460. _prepareTraits(systemData) {
  12461. const traits = {};
  12462. for ( const [trait$1, traitConfig] of Object.entries(CONFIG.DND5E.traits) ) {
  12463. const key = traitConfig.actorKeyPath ?? `traits.${trait$1}`;
  12464. const data = foundry.utils.deepClone(foundry.utils.getProperty(systemData, key));
  12465. const choices = CONFIG.DND5E[traitConfig.configKey];
  12466. if ( !data ) continue;
  12467. foundry.utils.setProperty(traits, key, data);
  12468. let values = data.value;
  12469. if ( !values ) values = [];
  12470. else if ( values instanceof Set ) values = Array.from(values);
  12471. else if ( !Array.isArray(values) ) values = [values];
  12472. // Split physical damage types from others if bypasses is set
  12473. const physical = [];
  12474. if ( data.bypasses?.size ) {
  12475. values = values.filter(t => {
  12476. if ( !CONFIG.DND5E.physicalDamageTypes[t] ) return true;
  12477. physical.push(t);
  12478. return false;
  12479. });
  12480. }
  12481. data.selected = values.reduce((obj, key) => {
  12482. obj[key] = keyLabel(trait$1, key) ?? key;
  12483. return obj;
  12484. }, {});
  12485. // Display bypassed damage types
  12486. if ( physical.length ) {
  12487. const damageTypesFormatter = new Intl.ListFormat(game.i18n.lang, { style: "long", type: "conjunction" });
  12488. const bypassFormatter = new Intl.ListFormat(game.i18n.lang, { style: "long", type: "disjunction" });
  12489. data.selected.physical = game.i18n.format("DND5E.DamagePhysicalBypasses", {
  12490. damageTypes: damageTypesFormatter.format(physical.map(t => choices[t])),
  12491. bypassTypes: bypassFormatter.format(data.bypasses.map(t => CONFIG.DND5E.physicalWeaponProperties[t]))
  12492. });
  12493. }
  12494. // Add custom entries
  12495. if ( data.custom ) data.custom.split(";").forEach((c, i) => data.selected[`custom${i+1}`] = c.trim());
  12496. data.cssClass = !foundry.utils.isEmpty(data.selected) ? "" : "inactive";
  12497. }
  12498. return traits;
  12499. }
  12500. /* -------------------------------------------- */
  12501. /**
  12502. * Prepare the data structure for items which appear on the actor sheet.
  12503. * Each subclass overrides this method to implement type-specific logic.
  12504. * @protected
  12505. */
  12506. _prepareItems() {}
  12507. /* -------------------------------------------- */
  12508. /**
  12509. * Insert a spell into the spellbook object when rendering the character sheet.
  12510. * @param {object} context Sheet rendering context data being prepared for render.
  12511. * @param {object[]} spells Spells to be included in the spellbook.
  12512. * @returns {object[]} Spellbook sections in the proper order.
  12513. * @protected
  12514. */
  12515. _prepareSpellbook(context, spells) {
  12516. const owner = this.actor.isOwner;
  12517. const levels = context.actor.system.spells;
  12518. const spellbook = {};
  12519. // Define section and label mappings
  12520. const sections = {atwill: -20, innate: -10, pact: 0.5 };
  12521. const useLabels = {"-20": "-", "-10": "-", 0: "&infin;"};
  12522. // Format a spellbook entry for a certain indexed level
  12523. const registerSection = (sl, i, label, {prepMode="prepared", value, max, override}={}) => {
  12524. const aeOverride = foundry.utils.hasProperty(this.actor.overrides, `system.spells.spell${i}.override`);
  12525. spellbook[i] = {
  12526. order: i,
  12527. label: label,
  12528. usesSlots: i > 0,
  12529. canCreate: owner,
  12530. canPrepare: (context.actor.type === "character") && (i >= 1),
  12531. spells: [],
  12532. uses: useLabels[i] || value || 0,
  12533. slots: useLabels[i] || max || 0,
  12534. override: override || 0,
  12535. dataset: {type: "spell", level: prepMode in sections ? 1 : i, "preparation.mode": prepMode},
  12536. prop: sl,
  12537. editable: context.editable && !aeOverride
  12538. };
  12539. };
  12540. // Determine the maximum spell level which has a slot
  12541. const maxLevel = Array.fromRange(Object.keys(CONFIG.DND5E.spellLevels).length - 1, 1).reduce((max, i) => {
  12542. const level = levels[`spell${i}`];
  12543. if ( level && (level.max || level.override ) && ( i > max ) ) max = i;
  12544. return max;
  12545. }, 0);
  12546. // Level-based spellcasters have cantrips and leveled slots
  12547. if ( maxLevel > 0 ) {
  12548. registerSection("spell0", 0, CONFIG.DND5E.spellLevels[0]);
  12549. for (let lvl = 1; lvl <= maxLevel; lvl++) {
  12550. const sl = `spell${lvl}`;
  12551. registerSection(sl, lvl, CONFIG.DND5E.spellLevels[lvl], levels[sl]);
  12552. }
  12553. }
  12554. // Pact magic users have cantrips and a pact magic section
  12555. if ( levels.pact && levels.pact.max ) {
  12556. if ( !spellbook["0"] ) registerSection("spell0", 0, CONFIG.DND5E.spellLevels[0]);
  12557. const l = levels.pact;
  12558. const config = CONFIG.DND5E.spellPreparationModes.pact;
  12559. const level = game.i18n.localize(`DND5E.SpellLevel${levels.pact.level}`);
  12560. const label = `${config} — ${level}`;
  12561. registerSection("pact", sections.pact, label, {
  12562. prepMode: "pact",
  12563. value: l.value,
  12564. max: l.max,
  12565. override: l.override
  12566. });
  12567. }
  12568. // Iterate over every spell item, adding spells to the spellbook by section
  12569. spells.forEach(spell => {
  12570. const mode = spell.system.preparation.mode || "prepared";
  12571. let s = spell.system.level || 0;
  12572. const sl = `spell${s}`;
  12573. // Specialized spellcasting modes (if they exist)
  12574. if ( mode in sections ) {
  12575. s = sections[mode];
  12576. if ( !spellbook[s] ) {
  12577. const l = levels[mode] || {};
  12578. const config = CONFIG.DND5E.spellPreparationModes[mode];
  12579. registerSection(mode, s, config, {
  12580. prepMode: mode,
  12581. value: l.value,
  12582. max: l.max,
  12583. override: l.override
  12584. });
  12585. }
  12586. }
  12587. // Sections for higher-level spells which the caster "should not" have, but spell items exist for
  12588. else if ( !spellbook[s] ) {
  12589. registerSection(sl, s, CONFIG.DND5E.spellLevels[s], {levels: levels[sl]});
  12590. }
  12591. // Add the spell to the relevant heading
  12592. spellbook[s].spells.push(spell);
  12593. });
  12594. // Sort the spellbook by section level
  12595. const sorted = Object.values(spellbook);
  12596. sorted.sort((a, b) => a.order - b.order);
  12597. return sorted;
  12598. }
  12599. /* -------------------------------------------- */
  12600. /**
  12601. * Determine whether an Owned Item will be shown based on the current set of filters.
  12602. * @param {object[]} items Copies of item data to be filtered.
  12603. * @param {Set<string>} filters Filters applied to the item list.
  12604. * @returns {object[]} Subset of input items limited by the provided filters.
  12605. * @protected
  12606. */
  12607. _filterItems(items, filters) {
  12608. return items.filter(item => {
  12609. // Action usage
  12610. for ( let f of ["action", "bonus", "reaction"] ) {
  12611. if ( filters.has(f) && (item.system.activation?.type !== f) ) return false;
  12612. }
  12613. // Spell-specific filters
  12614. if ( filters.has("ritual") && (item.system.components.ritual !== true) ) return false;
  12615. if ( filters.has("concentration") && (item.system.components.concentration !== true) ) return false;
  12616. if ( filters.has("prepared") ) {
  12617. if ( (item.system.level === 0) || ["innate", "always"].includes(item.system.preparation.mode) ) return true;
  12618. if ( this.actor.type === "npc" ) return true;
  12619. return item.system.preparation.prepared;
  12620. }
  12621. // Equipment-specific filters
  12622. if ( filters.has("equipped") && (item.system.equipped !== true) ) return false;
  12623. return true;
  12624. });
  12625. }
  12626. /* -------------------------------------------- */
  12627. /**
  12628. * Get the font-awesome icon used to display a certain level of skill proficiency.
  12629. * @param {number} level A proficiency mode defined in `CONFIG.DND5E.proficiencyLevels`.
  12630. * @returns {string} HTML string for the chosen icon.
  12631. * @private
  12632. */
  12633. _getProficiencyIcon(level) {
  12634. const icons = {
  12635. 0: '<i class="far fa-circle"></i>',
  12636. 0.5: '<i class="fas fa-adjust"></i>',
  12637. 1: '<i class="fas fa-check"></i>',
  12638. 2: '<i class="fas fa-check-double"></i>'
  12639. };
  12640. return icons[level] || icons[0];
  12641. }
  12642. /* -------------------------------------------- */
  12643. /* Event Listeners and Handlers */
  12644. /* -------------------------------------------- */
  12645. /** @inheritdoc */
  12646. activateListeners(html) {
  12647. // Activate Item Filters
  12648. const filterLists = html.find(".filter-list");
  12649. filterLists.each(this._initializeFilterItemList.bind(this));
  12650. filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
  12651. // Item summaries
  12652. html.find(".item .item-name.rollable h4").click(event => this._onItemSummary(event));
  12653. // View Item Sheets
  12654. html.find(".item-edit").click(this._onItemEdit.bind(this));
  12655. // Property attributions
  12656. html.find("[data-attribution]").mouseover(this._onPropertyAttribution.bind(this));
  12657. html.find(".attributable").mouseover(this._onPropertyAttribution.bind(this));
  12658. // Preparation Warnings
  12659. html.find(".warnings").click(this._onWarningLink.bind(this));
  12660. // Editable Only Listeners
  12661. if ( this.isEditable ) {
  12662. // Input focus and update
  12663. const inputs = html.find("input");
  12664. inputs.focus(ev => ev.currentTarget.select());
  12665. inputs.addBack().find('[type="text"][data-dtype="Number"]').change(this._onChangeInputDelta.bind(this));
  12666. // Ability Proficiency
  12667. html.find(".ability-proficiency").click(this._onToggleAbilityProficiency.bind(this));
  12668. // Toggle Skill Proficiency
  12669. html.find(".skill-proficiency").on("click contextmenu", event => this._onCycleProficiency(event, "skill"));
  12670. // Toggle Tool Proficiency
  12671. html.find(".tool-proficiency").on("click contextmenu", event => this._onCycleProficiency(event, "tool"));
  12672. // Trait Selector
  12673. html.find(".trait-selector").click(this._onTraitSelector.bind(this));
  12674. // Configure Special Flags
  12675. html.find(".config-button").click(this._onConfigMenu.bind(this));
  12676. // Owned Item management
  12677. html.find(".item-create").click(this._onItemCreate.bind(this));
  12678. html.find(".item-delete").click(this._onItemDelete.bind(this));
  12679. html.find(".item-uses input").click(ev => ev.target.select()).change(this._onUsesChange.bind(this));
  12680. html.find(".item-quantity input").click(ev => ev.target.select()).change(this._onQuantityChange.bind(this));
  12681. html.find(".slot-max-override").click(this._onSpellSlotOverride.bind(this));
  12682. html.find(".attunement-max-override").click(this._onAttunementOverride.bind(this));
  12683. // Active Effect management
  12684. html.find(".effect-control").click(ev => ActiveEffect5e.onManageActiveEffect(ev, this.actor));
  12685. this._disableOverriddenFields(html);
  12686. }
  12687. // Owner Only Listeners
  12688. if ( this.actor.isOwner ) {
  12689. // Ability Checks
  12690. html.find(".ability-name").click(this._onRollAbilityTest.bind(this));
  12691. // Roll Skill Checks
  12692. html.find(".skill-name").click(this._onRollSkillCheck.bind(this));
  12693. // Roll Tool Checks.
  12694. html.find(".tool-name").on("click", this._onRollToolCheck.bind(this));
  12695. // Item Rolling
  12696. html.find(".rollable .item-image").click(event => this._onItemUse(event));
  12697. html.find(".item .item-recharge").click(event => this._onItemRecharge(event));
  12698. // Item Context Menu
  12699. new ContextMenu(html, ".item-list .item", [], {onOpen: this._onItemContext.bind(this)});
  12700. }
  12701. // Otherwise, remove rollable classes
  12702. else {
  12703. html.find(".rollable").each((i, el) => el.classList.remove("rollable"));
  12704. }
  12705. // Handle default listeners last so system listeners are triggered first
  12706. super.activateListeners(html);
  12707. }
  12708. /* -------------------------------------------- */
  12709. /**
  12710. * Disable any fields that are overridden by active effects and display an informative tooltip.
  12711. * @param {jQuery} html The sheet's rendered HTML.
  12712. * @protected
  12713. */
  12714. _disableOverriddenFields(html) {
  12715. const proficiencyToggles = {
  12716. ability: /system\.abilities\.([^.]+)\.proficient/,
  12717. skill: /system\.skills\.([^.]+)\.value/,
  12718. tool: /system\.tools\.([^.]+)\.value/
  12719. };
  12720. for ( const override of Object.keys(foundry.utils.flattenObject(this.actor.overrides)) ) {
  12721. html.find(`input[name="${override}"],select[name="${override}"]`).each((i, el) => {
  12722. el.disabled = true;
  12723. el.dataset.tooltip = "DND5E.ActiveEffectOverrideWarning";
  12724. });
  12725. for ( const [key, regex] of Object.entries(proficiencyToggles) ) {
  12726. const [, match] = override.match(regex) || [];
  12727. if ( match ) {
  12728. const toggle = html.find(`li[data-${key}="${match}"] .proficiency-toggle`);
  12729. toggle.addClass("disabled");
  12730. toggle.attr("data-tooltip", "DND5E.ActiveEffectOverrideWarning");
  12731. }
  12732. }
  12733. const [, spell] = override.match(/system\.spells\.(spell\d)\.override/) || [];
  12734. if ( spell ) {
  12735. html.find(`.spell-max[data-level="${spell}"]`).attr("data-tooltip", "DND5E.ActiveEffectOverrideWarning");
  12736. }
  12737. }
  12738. }
  12739. /* -------------------------------------------- */
  12740. /**
  12741. * Handle activation of a context menu for an embedded Item or ActiveEffect document.
  12742. * Dynamically populate the array of context menu options.
  12743. * @param {HTMLElement} element The HTML element for which the context menu is activated
  12744. * @protected
  12745. */
  12746. _onItemContext(element) {
  12747. // Active Effects
  12748. if ( element.classList.contains("effect") ) {
  12749. const effect = this.actor.effects.get(element.dataset.effectId);
  12750. if ( !effect ) return;
  12751. ui.context.menuItems = this._getActiveEffectContextOptions(effect);
  12752. Hooks.call("dnd5e.getActiveEffectContextOptions", effect, ui.context.menuItems);
  12753. }
  12754. // Items
  12755. else {
  12756. const item = this.actor.items.get(element.dataset.itemId);
  12757. if ( !item ) return;
  12758. ui.context.menuItems = this._getItemContextOptions(item);
  12759. Hooks.call("dnd5e.getItemContextOptions", item, ui.context.menuItems);
  12760. }
  12761. }
  12762. /* -------------------------------------------- */
  12763. /**
  12764. * Prepare an array of context menu options which are available for owned ActiveEffect documents.
  12765. * @param {ActiveEffect5e} effect The ActiveEffect for which the context menu is activated
  12766. * @returns {ContextMenuEntry[]} An array of context menu options offered for the ActiveEffect
  12767. * @protected
  12768. */
  12769. _getActiveEffectContextOptions(effect) {
  12770. return [
  12771. {
  12772. name: "DND5E.ContextMenuActionEdit",
  12773. icon: "<i class='fas fa-edit fa-fw'></i>",
  12774. callback: () => effect.sheet.render(true)
  12775. },
  12776. {
  12777. name: "DND5E.ContextMenuActionDuplicate",
  12778. icon: "<i class='fas fa-copy fa-fw'></i>",
  12779. callback: () => effect.clone({label: game.i18n.format("DOCUMENT.CopyOf", {name: effect.label})}, {save: true})
  12780. },
  12781. {
  12782. name: "DND5E.ContextMenuActionDelete",
  12783. icon: "<i class='fas fa-trash fa-fw'></i>",
  12784. callback: () => effect.deleteDialog()
  12785. },
  12786. {
  12787. name: effect.disabled ? "DND5E.ContextMenuActionEnable" : "DND5E.ContextMenuActionDisable",
  12788. icon: effect.disabled ? "<i class='fas fa-check fa-fw'></i>" : "<i class='fas fa-times fa-fw'></i>",
  12789. callback: () => effect.update({disabled: !effect.disabled})
  12790. }
  12791. ];
  12792. }
  12793. /* -------------------------------------------- */
  12794. /**
  12795. * Prepare an array of context menu options which are available for owned Item documents.
  12796. * @param {Item5e} item The Item for which the context menu is activated
  12797. * @returns {ContextMenuEntry[]} An array of context menu options offered for the Item
  12798. * @protected
  12799. */
  12800. _getItemContextOptions(item) {
  12801. // Standard Options
  12802. const options = [
  12803. {
  12804. name: "DND5E.ContextMenuActionEdit",
  12805. icon: "<i class='fas fa-edit fa-fw'></i>",
  12806. callback: () => item.sheet.render(true)
  12807. },
  12808. {
  12809. name: "DND5E.ContextMenuActionDuplicate",
  12810. icon: "<i class='fas fa-copy fa-fw'></i>",
  12811. condition: () => !["race", "background", "class", "subclass"].includes(item.type),
  12812. callback: () => item.clone({name: game.i18n.format("DOCUMENT.CopyOf", {name: item.name})}, {save: true})
  12813. },
  12814. {
  12815. name: "DND5E.ContextMenuActionDelete",
  12816. icon: "<i class='fas fa-trash fa-fw'></i>",
  12817. callback: () => item.deleteDialog()
  12818. }
  12819. ];
  12820. // Toggle Attunement State
  12821. if ( ("attunement" in item.system) && (item.system.attunement !== CONFIG.DND5E.attunementTypes.NONE) ) {
  12822. const isAttuned = item.system.attunement === CONFIG.DND5E.attunementTypes.ATTUNED;
  12823. options.push({
  12824. name: isAttuned ? "DND5E.ContextMenuActionUnattune" : "DND5E.ContextMenuActionAttune",
  12825. icon: "<i class='fas fa-sun fa-fw'></i>",
  12826. callback: () => item.update({
  12827. "system.attunement": CONFIG.DND5E.attunementTypes[isAttuned ? "REQUIRED" : "ATTUNED"]
  12828. })
  12829. });
  12830. }
  12831. // Toggle Equipped State
  12832. if ( "equipped" in item.system ) options.push({
  12833. name: item.system.equipped ? "DND5E.ContextMenuActionUnequip" : "DND5E.ContextMenuActionEquip",
  12834. icon: "<i class='fas fa-shield-alt fa-fw'></i>",
  12835. callback: () => item.update({"system.equipped": !item.system.equipped})
  12836. });
  12837. // Toggle Prepared State
  12838. if ( ("preparation" in item.system) && (item.system.preparation?.mode === "prepared") ) options.push({
  12839. name: item.system?.preparation?.prepared ? "DND5E.ContextMenuActionUnprepare" : "DND5E.ContextMenuActionPrepare",
  12840. icon: "<i class='fas fa-sun fa-fw'></i>",
  12841. callback: () => item.update({"system.preparation.prepared": !item.system.preparation?.prepared})
  12842. });
  12843. return options;
  12844. }
  12845. /* -------------------------------------------- */
  12846. /**
  12847. * Initialize Item list filters by activating the set of filters which are currently applied
  12848. * @param {number} i Index of the filter in the list.
  12849. * @param {HTML} ul HTML object for the list item surrounding the filter.
  12850. * @private
  12851. */
  12852. _initializeFilterItemList(i, ul) {
  12853. const set = this._filters[ul.dataset.filter];
  12854. const filters = ul.querySelectorAll(".filter-item");
  12855. for ( let li of filters ) {
  12856. if ( set.has(li.dataset.filter) ) li.classList.add("active");
  12857. }
  12858. }
  12859. /* -------------------------------------------- */
  12860. /* Event Listeners and Handlers */
  12861. /* -------------------------------------------- */
  12862. /**
  12863. * Handle input changes to numeric form fields, allowing them to accept delta-typed inputs.
  12864. * @param {Event} event Triggering event.
  12865. * @protected
  12866. */
  12867. _onChangeInputDelta(event) {
  12868. const input = event.target;
  12869. const value = input.value;
  12870. if ( ["+", "-"].includes(value[0]) ) {
  12871. const delta = parseFloat(value);
  12872. const item = this.actor.items.get(input.closest("[data-item-id]")?.dataset.itemId);
  12873. if ( item ) input.value = Number(foundry.utils.getProperty(item, input.dataset.name)) + delta;
  12874. else input.value = Number(foundry.utils.getProperty(this.actor, input.name)) + delta;
  12875. } else if ( value[0] === "=" ) input.value = value.slice(1);
  12876. }
  12877. /* -------------------------------------------- */
  12878. /**
  12879. * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options.
  12880. * @param {Event} event The click event which originated the selection.
  12881. * @private
  12882. */
  12883. _onConfigMenu(event) {
  12884. event.preventDefault();
  12885. event.stopPropagation();
  12886. const button = event.currentTarget;
  12887. let app;
  12888. switch ( button.dataset.action ) {
  12889. case "armor":
  12890. app = new ActorArmorConfig(this.actor);
  12891. break;
  12892. case "hit-dice":
  12893. app = new ActorHitDiceConfig(this.actor);
  12894. break;
  12895. case "hit-points":
  12896. app = new ActorHitPointsConfig(this.actor);
  12897. break;
  12898. case "initiative":
  12899. app = new ActorInitiativeConfig(this.actor);
  12900. break;
  12901. case "movement":
  12902. app = new ActorMovementConfig(this.actor);
  12903. break;
  12904. case "flags":
  12905. app = new ActorSheetFlags(this.actor);
  12906. break;
  12907. case "senses":
  12908. app = new ActorSensesConfig(this.actor);
  12909. break;
  12910. case "type":
  12911. app = new ActorTypeConfig(this.actor);
  12912. break;
  12913. case "ability": {
  12914. const ability = event.currentTarget.closest("[data-ability]").dataset.ability;
  12915. app = new ActorAbilityConfig(this.actor, null, ability);
  12916. break;
  12917. }
  12918. case "skill": {
  12919. const skill = event.currentTarget.closest("[data-key]").dataset.key;
  12920. app = new ProficiencyConfig(this.actor, {property: "skills", key: skill});
  12921. break;
  12922. }
  12923. case "tool": {
  12924. const tool = event.currentTarget.closest("[data-key]").dataset.key;
  12925. app = new ProficiencyConfig(this.actor, {property: "tools", key: tool});
  12926. break;
  12927. }
  12928. }
  12929. app?.render(true);
  12930. }
  12931. /* -------------------------------------------- */
  12932. /**
  12933. * Handle cycling proficiency in a skill or tool.
  12934. * @param {Event} event A click or contextmenu event which triggered this action.
  12935. * @returns {Promise|void} Updated data for this actor after changes are applied.
  12936. * @protected
  12937. */
  12938. _onCycleProficiency(event) {
  12939. if ( event.currentTarget.classList.contains("disabled") ) return;
  12940. event.preventDefault();
  12941. const parent = event.currentTarget.closest(".proficiency-row");
  12942. const field = parent.querySelector('[name$=".value"]');
  12943. const {property, key} = parent.dataset;
  12944. const value = this.actor._source.system[property]?.[key]?.value ?? 0;
  12945. // Cycle to the next or previous skill level.
  12946. const levels = [0, 1, .5, 2];
  12947. const idx = levels.indexOf(value);
  12948. const next = idx + (event.type === "contextmenu" ? 3 : 1);
  12949. field.value = levels[next % levels.length];
  12950. // Update the field value and save the form.
  12951. return this._onSubmit(event);
  12952. }
  12953. /* -------------------------------------------- */
  12954. /** @override */
  12955. async _onDropActor(event, data) {
  12956. const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get("dnd5e", "allowPolymorphing"));
  12957. if ( !canPolymorph ) return false;
  12958. // Get the target actor
  12959. const cls = getDocumentClass("Actor");
  12960. const sourceActor = await cls.fromDropData(data);
  12961. if ( !sourceActor ) return;
  12962. // Define a function to record polymorph settings for future use
  12963. const rememberOptions = html => {
  12964. const options = {};
  12965. html.find("input").each((i, el) => {
  12966. options[el.name] = el.checked;
  12967. });
  12968. const settings = foundry.utils.mergeObject(game.settings.get("dnd5e", "polymorphSettings") ?? {}, options);
  12969. game.settings.set("dnd5e", "polymorphSettings", settings);
  12970. return settings;
  12971. };
  12972. // Create and render the Dialog
  12973. return new Dialog({
  12974. title: game.i18n.localize("DND5E.PolymorphPromptTitle"),
  12975. content: {
  12976. options: game.settings.get("dnd5e", "polymorphSettings"),
  12977. settings: CONFIG.DND5E.polymorphSettings,
  12978. effectSettings: CONFIG.DND5E.polymorphEffectSettings,
  12979. isToken: this.actor.isToken
  12980. },
  12981. default: "accept",
  12982. buttons: {
  12983. accept: {
  12984. icon: '<i class="fas fa-check"></i>',
  12985. label: game.i18n.localize("DND5E.PolymorphAcceptSettings"),
  12986. callback: html => this.actor.transformInto(sourceActor, rememberOptions(html))
  12987. },
  12988. wildshape: {
  12989. icon: CONFIG.DND5E.transformationPresets.wildshape.icon,
  12990. label: CONFIG.DND5E.transformationPresets.wildshape.label,
  12991. callback: html => this.actor.transformInto(sourceActor, foundry.utils.mergeObject(
  12992. CONFIG.DND5E.transformationPresets.wildshape.options,
  12993. { transformTokens: rememberOptions(html).transformTokens }
  12994. ))
  12995. },
  12996. polymorph: {
  12997. icon: CONFIG.DND5E.transformationPresets.polymorph.icon,
  12998. label: CONFIG.DND5E.transformationPresets.polymorph.label,
  12999. callback: html => this.actor.transformInto(sourceActor, foundry.utils.mergeObject(
  13000. CONFIG.DND5E.transformationPresets.polymorph.options,
  13001. { transformTokens: rememberOptions(html).transformTokens }
  13002. ))
  13003. },
  13004. self: {
  13005. icon: CONFIG.DND5E.transformationPresets.polymorphSelf.icon,
  13006. label: CONFIG.DND5E.transformationPresets.polymorphSelf.label,
  13007. callback: html => this.actor.transformInto(sourceActor, foundry.utils.mergeObject(
  13008. CONFIG.DND5E.transformationPresets.polymorphSelf.options,
  13009. { transformTokens: rememberOptions(html).transformTokens }
  13010. ))
  13011. },
  13012. cancel: {
  13013. icon: '<i class="fas fa-times"></i>',
  13014. label: game.i18n.localize("Cancel")
  13015. }
  13016. }
  13017. }, {
  13018. classes: ["dialog", "dnd5e", "polymorph"],
  13019. width: 900,
  13020. template: "systems/dnd5e/templates/apps/polymorph-prompt.hbs"
  13021. }).render(true);
  13022. }
  13023. /* -------------------------------------------- */
  13024. /** @override */
  13025. async _onDropItemCreate(itemData) {
  13026. let items = itemData instanceof Array ? itemData : [itemData];
  13027. const itemsWithoutAdvancement = items.filter(i => !i.system.advancement?.length);
  13028. const multipleAdvancements = (items.length - itemsWithoutAdvancement.length) > 1;
  13029. if ( multipleAdvancements && !game.settings.get("dnd5e", "disableAdvancements") ) {
  13030. ui.notifications.warn(game.i18n.format("DND5E.WarnCantAddMultipleAdvancements"));
  13031. items = itemsWithoutAdvancement;
  13032. }
  13033. const toCreate = [];
  13034. for ( const item of items ) {
  13035. const result = await this._onDropSingleItem(item);
  13036. if ( result ) toCreate.push(result);
  13037. }
  13038. // Create the owned items as normal
  13039. return this.actor.createEmbeddedDocuments("Item", toCreate);
  13040. }
  13041. /* -------------------------------------------- */
  13042. /**
  13043. * Handles dropping of a single item onto this character sheet.
  13044. * @param {object} itemData The item data to create.
  13045. * @returns {Promise<object|boolean>} The item data to create after processing, or false if the item should not be
  13046. * created or creation has been otherwise handled.
  13047. * @protected
  13048. */
  13049. async _onDropSingleItem(itemData) {
  13050. // Check to make sure items of this type are allowed on this actor
  13051. if ( this.constructor.unsupportedItemTypes.has(itemData.type) ) {
  13052. ui.notifications.warn(game.i18n.format("DND5E.ActorWarningInvalidItem", {
  13053. itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]),
  13054. actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type])
  13055. }));
  13056. return false;
  13057. }
  13058. // Create a Consumable spell scroll on the Inventory tab
  13059. if ( (itemData.type === "spell")
  13060. && (this._tabs[0].active === "inventory" || this.actor.type === "vehicle") ) {
  13061. const scroll = await Item5e.createScrollFromSpell(itemData);
  13062. return scroll.toObject();
  13063. }
  13064. // Clean up data
  13065. this._onDropResetData(itemData);
  13066. // Stack identical consumables
  13067. const stacked = this._onDropStackConsumables(itemData);
  13068. if ( stacked ) return false;
  13069. // Bypass normal creation flow for any items with advancement
  13070. if ( itemData.system.advancement?.length && !game.settings.get("dnd5e", "disableAdvancements") ) {
  13071. const manager = AdvancementManager.forNewItem(this.actor, itemData);
  13072. if ( manager.steps.length ) {
  13073. manager.render(true);
  13074. return false;
  13075. }
  13076. }
  13077. return itemData;
  13078. }
  13079. /* -------------------------------------------- */
  13080. /**
  13081. * Reset certain pieces of data stored on items when they are dropped onto the actor.
  13082. * @param {object} itemData The item data requested for creation. **Will be mutated.**
  13083. */
  13084. _onDropResetData(itemData) {
  13085. if ( !itemData.system ) return;
  13086. ["equipped", "proficient", "prepared"].forEach(k => delete itemData.system[k]);
  13087. if ( "attunement" in itemData.system ) {
  13088. itemData.system.attunement = Math.min(itemData.system.attunement, CONFIG.DND5E.attunementTypes.REQUIRED);
  13089. }
  13090. }
  13091. /* -------------------------------------------- */
  13092. /**
  13093. * Stack identical consumables when a new one is dropped rather than creating a duplicate item.
  13094. * @param {object} itemData The item data requested for creation.
  13095. * @returns {Promise<Item5e>|null} If a duplicate was found, returns the adjusted item stack.
  13096. */
  13097. _onDropStackConsumables(itemData) {
  13098. const droppedSourceId = itemData.flags.core?.sourceId;
  13099. if ( itemData.type !== "consumable" || !droppedSourceId ) return null;
  13100. const similarItem = this.actor.items.find(i => {
  13101. const sourceId = i.getFlag("core", "sourceId");
  13102. return sourceId && (sourceId === droppedSourceId) && (i.type === "consumable") && (i.name === itemData.name);
  13103. });
  13104. if ( !similarItem ) return null;
  13105. return similarItem.update({
  13106. "system.quantity": similarItem.system.quantity + Math.max(itemData.system.quantity, 1)
  13107. });
  13108. }
  13109. /* -------------------------------------------- */
  13110. /**
  13111. * Handle enabling editing for a spell slot override value.
  13112. * @param {MouseEvent} event The originating click event.
  13113. * @protected
  13114. */
  13115. async _onSpellSlotOverride(event) {
  13116. const span = event.currentTarget.parentElement;
  13117. const level = span.dataset.level;
  13118. const override = this.actor.system.spells[level].override || span.dataset.slots;
  13119. const input = document.createElement("INPUT");
  13120. input.type = "text";
  13121. input.name = `system.spells.${level}.override`;
  13122. input.value = override;
  13123. input.placeholder = span.dataset.slots;
  13124. input.dataset.dtype = "Number";
  13125. input.addEventListener("focus", event => event.currentTarget.select());
  13126. // Replace the HTML
  13127. const parent = span.parentElement;
  13128. parent.removeChild(span);
  13129. parent.appendChild(input);
  13130. }
  13131. /* -------------------------------------------- */
  13132. /**
  13133. * Handle enabling editing for attunement maximum.
  13134. * @param {MouseEvent} event The originating click event.
  13135. * @private
  13136. */
  13137. async _onAttunementOverride(event) {
  13138. const span = event.currentTarget.parentElement;
  13139. const input = document.createElement("INPUT");
  13140. input.type = "text";
  13141. input.name = "system.attributes.attunement.max";
  13142. input.value = this.actor.system.attributes.attunement.max;
  13143. input.placeholder = 3;
  13144. input.dataset.dtype = "Number";
  13145. input.addEventListener("focus", event => event.currentTarget.select());
  13146. // Replace the HTML
  13147. const parent = span.parentElement;
  13148. parent.removeChild(span);
  13149. parent.appendChild(input);
  13150. }
  13151. /* -------------------------------------------- */
  13152. /**
  13153. * Change the uses amount of an Owned Item within the Actor.
  13154. * @param {Event} event The triggering click event.
  13155. * @returns {Promise<Item5e>} Updated item.
  13156. * @protected
  13157. */
  13158. async _onUsesChange(event) {
  13159. event.preventDefault();
  13160. const itemId = event.currentTarget.closest(".item").dataset.itemId;
  13161. const item = this.actor.items.get(itemId);
  13162. const uses = Math.clamped(0, parseInt(event.target.value), item.system.uses.max);
  13163. event.target.value = uses;
  13164. return item.update({"system.uses.value": uses});
  13165. }
  13166. /* -------------------------------------------- */
  13167. /**
  13168. * Change the quantity of an Owned Item within the actor.
  13169. * @param {Event} event The triggering click event.
  13170. * @returns {Promise<Item5e>} Updated item.
  13171. * @protected
  13172. */
  13173. async _onQuantityChange(event) {
  13174. event.preventDefault();
  13175. const itemId = event.currentTarget.closest(".item").dataset.itemId;
  13176. const item = this.actor.items.get(itemId);
  13177. const quantity = Math.max(0, parseInt(event.target.value));
  13178. event.target.value = quantity;
  13179. return item.update({"system.quantity": quantity});
  13180. }
  13181. /* -------------------------------------------- */
  13182. /**
  13183. * Handle using an item from the Actor sheet, obtaining the Item instance, and dispatching to its use method.
  13184. * @param {Event} event The triggering click event.
  13185. * @returns {Promise} Results of the usage.
  13186. * @protected
  13187. */
  13188. _onItemUse(event) {
  13189. event.preventDefault();
  13190. const itemId = event.currentTarget.closest(".item").dataset.itemId;
  13191. const item = this.actor.items.get(itemId);
  13192. return item.use({}, {event});
  13193. }
  13194. /* -------------------------------------------- */
  13195. /**
  13196. * Handle attempting to recharge an item usage by rolling a recharge check.
  13197. * @param {Event} event The originating click event.
  13198. * @returns {Promise<Roll>} The resulting recharge roll.
  13199. * @private
  13200. */
  13201. _onItemRecharge(event) {
  13202. event.preventDefault();
  13203. const itemId = event.currentTarget.closest(".item").dataset.itemId;
  13204. const item = this.actor.items.get(itemId);
  13205. return item.rollRecharge();
  13206. }
  13207. /* -------------------------------------------- */
  13208. /**
  13209. * Handle toggling and items expanded description.
  13210. * @param {Event} event Triggering event.
  13211. * @private
  13212. */
  13213. async _onItemSummary(event) {
  13214. event.preventDefault();
  13215. const li = $(event.currentTarget).parents(".item");
  13216. const item = this.actor.items.get(li.data("item-id"));
  13217. const chatData = await item.getChatData({secrets: this.actor.isOwner});
  13218. // Toggle summary
  13219. if ( li.hasClass("expanded") ) {
  13220. const summary = li.children(".item-summary");
  13221. summary.slideUp(200, () => summary.remove());
  13222. this._expanded.delete(item.id);
  13223. } else {
  13224. const summary = $(await renderTemplate("systems/dnd5e/templates/items/parts/item-summary.hbs", chatData));
  13225. li.append(summary.hide());
  13226. summary.slideDown(200);
  13227. this._expanded.add(item.id);
  13228. }
  13229. li.toggleClass("expanded");
  13230. }
  13231. /* -------------------------------------------- */
  13232. /**
  13233. * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset.
  13234. * @param {Event} event The originating click event.
  13235. * @returns {Promise<Item5e[]>} The newly created item.
  13236. * @private
  13237. */
  13238. _onItemCreate(event) {
  13239. event.preventDefault();
  13240. const header = event.currentTarget;
  13241. const type = header.dataset.type;
  13242. // Check to make sure the newly created class doesn't take player over level cap
  13243. if ( type === "class" && (this.actor.system.details.level + 1 > CONFIG.DND5E.maxLevel) ) {
  13244. const err = game.i18n.format("DND5E.MaxCharacterLevelExceededWarn", {max: CONFIG.DND5E.maxLevel});
  13245. return ui.notifications.error(err);
  13246. }
  13247. const itemData = {
  13248. name: game.i18n.format("DND5E.ItemNew", {type: game.i18n.localize(CONFIG.Item.typeLabels[type])}),
  13249. type: type,
  13250. system: foundry.utils.expandObject({ ...header.dataset })
  13251. };
  13252. delete itemData.system.type;
  13253. return this.actor.createEmbeddedDocuments("Item", [itemData]);
  13254. }
  13255. /* -------------------------------------------- */
  13256. /**
  13257. * Handle editing an existing Owned Item for the Actor.
  13258. * @param {Event} event The originating click event.
  13259. * @returns {ItemSheet5e} The rendered item sheet.
  13260. * @private
  13261. */
  13262. _onItemEdit(event) {
  13263. event.preventDefault();
  13264. const li = event.currentTarget.closest(".item");
  13265. const item = this.actor.items.get(li.dataset.itemId);
  13266. return item.sheet.render(true);
  13267. }
  13268. /* -------------------------------------------- */
  13269. /**
  13270. * Handle deleting an existing Owned Item for the Actor.
  13271. * @param {Event} event The originating click event.
  13272. * @returns {Promise<Item5e|AdvancementManager>|undefined} The deleted item if something was deleted or the
  13273. * advancement manager if advancements need removing.
  13274. * @private
  13275. */
  13276. async _onItemDelete(event) {
  13277. event.preventDefault();
  13278. const li = event.currentTarget.closest(".item");
  13279. const item = this.actor.items.get(li.dataset.itemId);
  13280. if ( !item ) return;
  13281. // If item has advancement, handle it separately
  13282. if ( !game.settings.get("dnd5e", "disableAdvancements") ) {
  13283. const manager = AdvancementManager.forDeletedItem(this.actor, item.id);
  13284. if ( manager.steps.length ) {
  13285. if ( ["class", "subclass"].includes(item.type) ) {
  13286. try {
  13287. const shouldRemoveAdvancements = await AdvancementConfirmationDialog.forDelete(item);
  13288. if ( shouldRemoveAdvancements ) return manager.render(true);
  13289. } catch(err) {
  13290. return;
  13291. }
  13292. } else {
  13293. return manager.render(true);
  13294. }
  13295. }
  13296. }
  13297. return item.deleteDialog();
  13298. }
  13299. /* -------------------------------------------- */
  13300. /**
  13301. * Handle displaying the property attribution tooltip when a property is hovered over.
  13302. * @param {Event} event The originating mouse event.
  13303. * @private
  13304. */
  13305. async _onPropertyAttribution(event) {
  13306. const element = event.target;
  13307. let property = element.dataset.attribution;
  13308. if ( !property ) {
  13309. property = element.dataset.property;
  13310. if ( !property ) return;
  13311. foundry.utils.logCompatibilityWarning(
  13312. "Defining attributable properties on sheets with the `.attributable` class and `data-property` value"
  13313. + " has been deprecated in favor of a single `data-attribution` value.",
  13314. { since: "DnD5e 2.1.3", until: "DnD5e 2.4" }
  13315. );
  13316. }
  13317. const rollData = this.actor.getRollData({ deterministic: true });
  13318. const title = game.i18n.localize(element.dataset.attributionCaption);
  13319. let attributions;
  13320. switch ( property ) {
  13321. case "attributes.ac":
  13322. attributions = this._prepareArmorClassAttribution(rollData); break;
  13323. }
  13324. if ( !attributions ) return;
  13325. new PropertyAttribution(this.actor, attributions, property, {title}).renderTooltip(element);
  13326. }
  13327. /* -------------------------------------------- */
  13328. /**
  13329. * Handle rolling an Ability test or saving throw.
  13330. * @param {Event} event The originating click event.
  13331. * @private
  13332. */
  13333. _onRollAbilityTest(event) {
  13334. event.preventDefault();
  13335. let ability = event.currentTarget.parentElement.dataset.ability;
  13336. this.actor.rollAbility(ability, {event: event});
  13337. }
  13338. /* -------------------------------------------- */
  13339. /**
  13340. * Handle rolling a Skill check.
  13341. * @param {Event} event The originating click event.
  13342. * @returns {Promise<Roll>} The resulting roll.
  13343. * @private
  13344. */
  13345. _onRollSkillCheck(event) {
  13346. event.preventDefault();
  13347. const skill = event.currentTarget.closest("[data-key]").dataset.key;
  13348. return this.actor.rollSkill(skill, {event: event});
  13349. }
  13350. /* -------------------------------------------- */
  13351. _onRollToolCheck(event) {
  13352. event.preventDefault();
  13353. const tool = event.currentTarget.closest("[data-key]").dataset.key;
  13354. return this.actor.rollToolCheck(tool, {event});
  13355. }
  13356. /* -------------------------------------------- */
  13357. /**
  13358. * Handle toggling Ability score proficiency level.
  13359. * @param {Event} event The originating click event.
  13360. * @returns {Promise<Actor5e>|void} Updated actor instance.
  13361. * @private
  13362. */
  13363. _onToggleAbilityProficiency(event) {
  13364. if ( event.currentTarget.classList.contains("disabled") ) return;
  13365. event.preventDefault();
  13366. const field = event.currentTarget.previousElementSibling;
  13367. return this.actor.update({[field.name]: 1 - parseInt(field.value)});
  13368. }
  13369. /* -------------------------------------------- */
  13370. /**
  13371. * Handle toggling of filters to display a different set of owned items.
  13372. * @param {Event} event The click event which triggered the toggle.
  13373. * @returns {ActorSheet5e} This actor sheet with toggled filters.
  13374. * @private
  13375. */
  13376. _onToggleFilter(event) {
  13377. event.preventDefault();
  13378. const li = event.currentTarget;
  13379. const set = this._filters[li.parentElement.dataset.filter];
  13380. const filter = li.dataset.filter;
  13381. if ( set.has(filter) ) set.delete(filter);
  13382. else set.add(filter);
  13383. return this.render();
  13384. }
  13385. /* -------------------------------------------- */
  13386. /**
  13387. * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options.
  13388. * @param {Event} event The click event which originated the selection.
  13389. * @returns {TraitSelector} Newly displayed application.
  13390. * @private
  13391. */
  13392. _onTraitSelector(event) {
  13393. event.preventDefault();
  13394. const trait = event.currentTarget.dataset.trait;
  13395. if ( trait === "tool" ) return new ToolSelector(this.actor, trait).render(true);
  13396. return new TraitSelector$1(this.actor, trait).render(true);
  13397. }
  13398. /* -------------------------------------------- */
  13399. /**
  13400. * Handle links within preparation warnings.
  13401. * @param {Event} event The click event on the warning.
  13402. * @protected
  13403. */
  13404. async _onWarningLink(event) {
  13405. event.preventDefault();
  13406. const a = event.target;
  13407. if ( !a || !a.dataset.target ) return;
  13408. switch ( a.dataset.target ) {
  13409. case "armor":
  13410. (new ActorArmorConfig(this.actor)).render(true);
  13411. return;
  13412. default:
  13413. const item = await fromUuid(a.dataset.target);
  13414. item?.sheet.render(true);
  13415. }
  13416. }
  13417. /* -------------------------------------------- */
  13418. /** @override */
  13419. _getHeaderButtons() {
  13420. let buttons = super._getHeaderButtons();
  13421. if ( this.actor.isPolymorphed ) {
  13422. buttons.unshift({
  13423. label: "DND5E.PolymorphRestoreTransformation",
  13424. class: "restore-transformation",
  13425. icon: "fas fa-backward",
  13426. onclick: () => this.actor.revertOriginalForm()
  13427. });
  13428. }
  13429. return buttons;
  13430. }
  13431. }
  13432. /**
  13433. * An Actor sheet for player character type actors.
  13434. */
  13435. class ActorSheet5eCharacter extends ActorSheet5e {
  13436. /** @inheritDoc */
  13437. static get defaultOptions() {
  13438. return foundry.utils.mergeObject(super.defaultOptions, {
  13439. classes: ["dnd5e", "sheet", "actor", "character"]
  13440. });
  13441. }
  13442. /* -------------------------------------------- */
  13443. /* Context Preparation */
  13444. /* -------------------------------------------- */
  13445. /** @inheritDoc */
  13446. async getData(options={}) {
  13447. const context = await super.getData(options);
  13448. // Resources
  13449. context.resources = ["primary", "secondary", "tertiary"].reduce((arr, r) => {
  13450. const res = context.actor.system.resources[r] || {};
  13451. res.name = r;
  13452. res.placeholder = game.i18n.localize(`DND5E.Resource${r.titleCase()}`);
  13453. if (res && res.value === 0) delete res.value;
  13454. if (res && res.max === 0) delete res.max;
  13455. return arr.concat([res]);
  13456. }, []);
  13457. const classes = this.actor.itemTypes.class;
  13458. return foundry.utils.mergeObject(context, {
  13459. disableExperience: game.settings.get("dnd5e", "disableExperienceTracking"),
  13460. classLabels: classes.map(c => c.name).join(", "),
  13461. multiclassLabels: classes.map(c => [c.subclass?.name ?? "", c.name, c.system.levels].filterJoin(" ")).join(", "),
  13462. weightUnit: game.i18n.localize(`DND5E.Abbreviation${
  13463. game.settings.get("dnd5e", "metricWeightUnits") ? "Kg" : "Lbs"}`),
  13464. encumbrance: context.system.attributes.encumbrance
  13465. });
  13466. }
  13467. /* -------------------------------------------- */
  13468. /** @override */
  13469. _prepareItems(context) {
  13470. // Categorize items as inventory, spellbook, features, and classes
  13471. const inventory = {};
  13472. for ( const type of ["weapon", "equipment", "consumable", "tool", "backpack", "loot"] ) {
  13473. inventory[type] = {label: `${CONFIG.Item.typeLabels[type]}Pl`, items: [], dataset: {type}};
  13474. }
  13475. // Partition items by category
  13476. let {items, spells, feats, backgrounds, classes, subclasses} = context.items.reduce((obj, item) => {
  13477. const {quantity, uses, recharge, target} = item.system;
  13478. // Item details
  13479. const ctx = context.itemContext[item.id] ??= {};
  13480. ctx.isStack = Number.isNumeric(quantity) && (quantity !== 1);
  13481. ctx.attunement = {
  13482. [CONFIG.DND5E.attunementTypes.REQUIRED]: {
  13483. icon: "fa-sun",
  13484. cls: "not-attuned",
  13485. title: "DND5E.AttunementRequired"
  13486. },
  13487. [CONFIG.DND5E.attunementTypes.ATTUNED]: {
  13488. icon: "fa-sun",
  13489. cls: "attuned",
  13490. title: "DND5E.AttunementAttuned"
  13491. }
  13492. }[item.system.attunement];
  13493. // Prepare data needed to display expanded sections
  13494. ctx.isExpanded = this._expanded.has(item.id);
  13495. // Item usage
  13496. ctx.hasUses = uses && (uses.max > 0);
  13497. ctx.isOnCooldown = recharge && !!recharge.value && (recharge.charged === false);
  13498. ctx.isDepleted = ctx.isOnCooldown && (uses.per && (uses.value > 0));
  13499. ctx.hasTarget = !!target && !(["none", ""].includes(target.type));
  13500. // Item toggle state
  13501. this._prepareItemToggleState(item, ctx);
  13502. // Classify items into types
  13503. if ( item.type === "spell" ) obj.spells.push(item);
  13504. else if ( item.type === "feat" ) obj.feats.push(item);
  13505. else if ( item.type === "background" ) obj.backgrounds.push(item);
  13506. else if ( item.type === "class" ) obj.classes.push(item);
  13507. else if ( item.type === "subclass" ) obj.subclasses.push(item);
  13508. else if ( Object.keys(inventory).includes(item.type) ) obj.items.push(item);
  13509. return obj;
  13510. }, { items: [], spells: [], feats: [], backgrounds: [], classes: [], subclasses: [] });
  13511. // Apply active item filters
  13512. items = this._filterItems(items, this._filters.inventory);
  13513. spells = this._filterItems(spells, this._filters.spellbook);
  13514. feats = this._filterItems(feats, this._filters.features);
  13515. // Organize items
  13516. for ( let i of items ) {
  13517. const ctx = context.itemContext[i.id] ??= {};
  13518. ctx.totalWeight = (i.system.quantity * i.system.weight).toNearest(0.1);
  13519. inventory[i.type].items.push(i);
  13520. }
  13521. // Organize Spellbook and count the number of prepared spells (excluding always, at will, etc...)
  13522. const spellbook = this._prepareSpellbook(context, spells);
  13523. const nPrepared = spells.filter(spell => {
  13524. const prep = spell.system.preparation;
  13525. return (spell.system.level > 0) && (prep.mode === "prepared") && prep.prepared;
  13526. }).length;
  13527. // Sort classes and interleave matching subclasses, put unmatched subclasses into features so they don't disappear
  13528. classes.sort((a, b) => b.system.levels - a.system.levels);
  13529. const maxLevelDelta = CONFIG.DND5E.maxLevel - this.actor.system.details.level;
  13530. classes = classes.reduce((arr, cls) => {
  13531. const ctx = context.itemContext[cls.id] ??= {};
  13532. ctx.availableLevels = Array.fromRange(CONFIG.DND5E.maxLevel + 1).slice(1).map(level => {
  13533. const delta = level - cls.system.levels;
  13534. return { level, delta, disabled: delta > maxLevelDelta };
  13535. });
  13536. arr.push(cls);
  13537. const identifier = cls.system.identifier || cls.name.slugify({strict: true});
  13538. const subclass = subclasses.findSplice(s => s.system.classIdentifier === identifier);
  13539. if ( subclass ) arr.push(subclass);
  13540. return arr;
  13541. }, []);
  13542. for ( const subclass of subclasses ) {
  13543. feats.push(subclass);
  13544. const message = game.i18n.format("DND5E.SubclassMismatchWarn", {
  13545. name: subclass.name, class: subclass.system.classIdentifier
  13546. });
  13547. context.warnings.push({ message, type: "warning" });
  13548. }
  13549. // Organize Features
  13550. const features = {
  13551. background: {
  13552. label: CONFIG.Item.typeLabels.background, items: backgrounds,
  13553. hasActions: false, dataset: {type: "background"} },
  13554. classes: {
  13555. label: `${CONFIG.Item.typeLabels.class}Pl`, items: classes,
  13556. hasActions: false, dataset: {type: "class"}, isClass: true },
  13557. active: {
  13558. label: "DND5E.FeatureActive", items: [],
  13559. hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
  13560. passive: {
  13561. label: "DND5E.FeaturePassive", items: [],
  13562. hasActions: false, dataset: {type: "feat"} }
  13563. };
  13564. for ( const feat of feats ) {
  13565. if ( feat.system.activation?.type ) features.active.items.push(feat);
  13566. else features.passive.items.push(feat);
  13567. }
  13568. // Assign and return
  13569. context.inventoryFilters = true;
  13570. context.inventory = Object.values(inventory);
  13571. context.spellbook = spellbook;
  13572. context.preparedSpells = nPrepared;
  13573. context.features = Object.values(features);
  13574. context.labels.background = backgrounds[0]?.name;
  13575. }
  13576. /* -------------------------------------------- */
  13577. /**
  13578. * A helper method to establish the displayed preparation state for an item.
  13579. * @param {Item5e} item Item being prepared for display.
  13580. * @param {object} context Context data for display.
  13581. * @protected
  13582. */
  13583. _prepareItemToggleState(item, context) {
  13584. if ( item.type === "spell" ) {
  13585. const prep = item.system.preparation || {};
  13586. const isAlways = prep.mode === "always";
  13587. const isPrepared = !!prep.prepared;
  13588. context.toggleClass = isPrepared ? "active" : "";
  13589. if ( isAlways ) context.toggleClass = "fixed";
  13590. if ( isAlways ) context.toggleTitle = CONFIG.DND5E.spellPreparationModes.always;
  13591. else if ( isPrepared ) context.toggleTitle = CONFIG.DND5E.spellPreparationModes.prepared;
  13592. else context.toggleTitle = game.i18n.localize("DND5E.SpellUnprepared");
  13593. }
  13594. else {
  13595. const isActive = !!item.system.equipped;
  13596. context.toggleClass = isActive ? "active" : "";
  13597. context.toggleTitle = game.i18n.localize(isActive ? "DND5E.Equipped" : "DND5E.Unequipped");
  13598. context.canToggle = "equipped" in item.system;
  13599. }
  13600. }
  13601. /* -------------------------------------------- */
  13602. /* Event Listeners and Handlers
  13603. /* -------------------------------------------- */
  13604. /** @inheritDoc */
  13605. activateListeners(html) {
  13606. super.activateListeners(html);
  13607. if ( !this.isEditable ) return;
  13608. html.find(".level-selector").change(this._onLevelChange.bind(this));
  13609. html.find(".item-toggle").click(this._onToggleItem.bind(this));
  13610. html.find(".short-rest").click(this._onShortRest.bind(this));
  13611. html.find(".long-rest").click(this._onLongRest.bind(this));
  13612. html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
  13613. }
  13614. /* -------------------------------------------- */
  13615. /**
  13616. * Handle mouse click events for character sheet actions.
  13617. * @param {MouseEvent} event The originating click event.
  13618. * @returns {Promise} Dialog or roll result.
  13619. * @private
  13620. */
  13621. _onSheetAction(event) {
  13622. event.preventDefault();
  13623. const button = event.currentTarget;
  13624. switch ( button.dataset.action ) {
  13625. case "convertCurrency":
  13626. return Dialog.confirm({
  13627. title: `${game.i18n.localize("DND5E.CurrencyConvert")}`,
  13628. content: `<p>${game.i18n.localize("DND5E.CurrencyConvertHint")}</p>`,
  13629. yes: () => this.actor.convertCurrency()
  13630. });
  13631. case "rollDeathSave":
  13632. return this.actor.rollDeathSave({event: event});
  13633. case "rollInitiative":
  13634. return this.actor.rollInitiativeDialog({event});
  13635. }
  13636. }
  13637. /* -------------------------------------------- */
  13638. /**
  13639. * Respond to a new level being selected from the level selector.
  13640. * @param {Event} event The originating change.
  13641. * @returns {Promise<AdvancementManager|Item5e>} Manager if advancements needed, otherwise updated class item.
  13642. * @private
  13643. */
  13644. async _onLevelChange(event) {
  13645. event.preventDefault();
  13646. const delta = Number(event.target.value);
  13647. const classId = event.target.closest(".item")?.dataset.itemId;
  13648. if ( !delta || !classId ) return;
  13649. const classItem = this.actor.items.get(classId);
  13650. if ( !game.settings.get("dnd5e", "disableAdvancements") ) {
  13651. const manager = AdvancementManager.forLevelChange(this.actor, classId, delta);
  13652. if ( manager.steps.length ) {
  13653. if ( delta > 0 ) return manager.render(true);
  13654. try {
  13655. const shouldRemoveAdvancements = await AdvancementConfirmationDialog.forLevelDown(classItem);
  13656. if ( shouldRemoveAdvancements ) return manager.render(true);
  13657. }
  13658. catch(err) {
  13659. return;
  13660. }
  13661. }
  13662. }
  13663. return classItem.update({"system.levels": classItem.system.levels + delta});
  13664. }
  13665. /* -------------------------------------------- */
  13666. /**
  13667. * Handle toggling the state of an Owned Item within the Actor.
  13668. * @param {Event} event The triggering click event.
  13669. * @returns {Promise<Item5e>} Item with the updates applied.
  13670. * @private
  13671. */
  13672. _onToggleItem(event) {
  13673. event.preventDefault();
  13674. const itemId = event.currentTarget.closest(".item").dataset.itemId;
  13675. const item = this.actor.items.get(itemId);
  13676. const attr = item.type === "spell" ? "system.preparation.prepared" : "system.equipped";
  13677. return item.update({[attr]: !foundry.utils.getProperty(item, attr)});
  13678. }
  13679. /* -------------------------------------------- */
  13680. /**
  13681. * Take a short rest, calling the relevant function on the Actor instance.
  13682. * @param {Event} event The triggering click event.
  13683. * @returns {Promise<RestResult>} Result of the rest action.
  13684. * @private
  13685. */
  13686. async _onShortRest(event) {
  13687. event.preventDefault();
  13688. await this._onSubmit(event);
  13689. return this.actor.shortRest();
  13690. }
  13691. /* -------------------------------------------- */
  13692. /**
  13693. * Take a long rest, calling the relevant function on the Actor instance.
  13694. * @param {Event} event The triggering click event.
  13695. * @returns {Promise<RestResult>} Result of the rest action.
  13696. * @private
  13697. */
  13698. async _onLongRest(event) {
  13699. event.preventDefault();
  13700. await this._onSubmit(event);
  13701. return this.actor.longRest();
  13702. }
  13703. /* -------------------------------------------- */
  13704. /** @override */
  13705. async _onDropSingleItem(itemData) {
  13706. // Increment the number of class levels a character instead of creating a new item
  13707. if ( itemData.type === "class" ) {
  13708. const charLevel = this.actor.system.details.level;
  13709. itemData.system.levels = Math.min(itemData.system.levels, CONFIG.DND5E.maxLevel - charLevel);
  13710. if ( itemData.system.levels <= 0 ) {
  13711. const err = game.i18n.format("DND5E.MaxCharacterLevelExceededWarn", { max: CONFIG.DND5E.maxLevel });
  13712. ui.notifications.error(err);
  13713. return false;
  13714. }
  13715. const cls = this.actor.itemTypes.class.find(c => c.identifier === itemData.system.identifier);
  13716. if ( cls ) {
  13717. const priorLevel = cls.system.levels;
  13718. if ( !game.settings.get("dnd5e", "disableAdvancements") ) {
  13719. const manager = AdvancementManager.forLevelChange(this.actor, cls.id, itemData.system.levels);
  13720. if ( manager.steps.length ) {
  13721. manager.render(true);
  13722. return false;
  13723. }
  13724. }
  13725. cls.update({"system.levels": priorLevel + itemData.system.levels});
  13726. return false;
  13727. }
  13728. }
  13729. // If a subclass is dropped, ensure it doesn't match another subclass with the same identifier
  13730. else if ( itemData.type === "subclass" ) {
  13731. const other = this.actor.itemTypes.subclass.find(i => i.identifier === itemData.system.identifier);
  13732. if ( other ) {
  13733. const err = game.i18n.format("DND5E.SubclassDuplicateError", {identifier: other.identifier});
  13734. ui.notifications.error(err);
  13735. return false;
  13736. }
  13737. const cls = this.actor.itemTypes.class.find(i => i.identifier === itemData.system.classIdentifier);
  13738. if ( cls && cls.subclass ) {
  13739. const err = game.i18n.format("DND5E.SubclassAssignmentError", {class: cls.name, subclass: cls.subclass.name});
  13740. ui.notifications.error(err);
  13741. return false;
  13742. }
  13743. }
  13744. return super._onDropSingleItem(itemData);
  13745. }
  13746. }
  13747. /**
  13748. * An Actor sheet for NPC type characters.
  13749. */
  13750. class ActorSheet5eNPC extends ActorSheet5e {
  13751. /** @inheritDoc */
  13752. static get defaultOptions() {
  13753. return foundry.utils.mergeObject(super.defaultOptions, {
  13754. classes: ["dnd5e", "sheet", "actor", "npc"],
  13755. width: 600
  13756. });
  13757. }
  13758. /* -------------------------------------------- */
  13759. /** @override */
  13760. static unsupportedItemTypes = new Set(["background", "class", "subclass"]);
  13761. /* -------------------------------------------- */
  13762. /* Context Preparation */
  13763. /* -------------------------------------------- */
  13764. /** @inheritDoc */
  13765. async getData(options) {
  13766. const context = await super.getData(options);
  13767. // Challenge Rating
  13768. const cr = parseFloat(context.system.details.cr ?? 0);
  13769. const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
  13770. return foundry.utils.mergeObject(context, {
  13771. labels: {
  13772. cr: cr >= 1 ? String(cr) : crLabels[cr] ?? 1,
  13773. type: this.actor.constructor.formatCreatureType(context.system.details.type),
  13774. armorType: this.getArmorLabel()
  13775. }
  13776. });
  13777. }
  13778. /* -------------------------------------------- */
  13779. /** @override */
  13780. _prepareItems(context) {
  13781. // Categorize Items as Features and Spells
  13782. const features = {
  13783. weapons: { label: game.i18n.localize("DND5E.AttackPl"), items: [], hasActions: true,
  13784. dataset: {type: "weapon", "weapon-type": "natural"} },
  13785. actions: { label: game.i18n.localize("DND5E.ActionPl"), items: [], hasActions: true,
  13786. dataset: {type: "feat", "activation.type": "action"} },
  13787. passive: { label: game.i18n.localize("DND5E.Features"), items: [], dataset: {type: "feat"} },
  13788. equipment: { label: game.i18n.localize("DND5E.Inventory"), items: [], dataset: {type: "loot"}}
  13789. };
  13790. // Start by classifying items into groups for rendering
  13791. let [spells, other] = context.items.reduce((arr, item) => {
  13792. const {quantity, uses, recharge, target} = item.system;
  13793. const ctx = context.itemContext[item.id] ??= {};
  13794. ctx.isStack = Number.isNumeric(quantity) && (quantity !== 1);
  13795. ctx.isExpanded = this._expanded.has(item.id);
  13796. ctx.hasUses = uses && (uses.max > 0);
  13797. ctx.isOnCooldown = recharge && !!recharge.value && (recharge.charged === false);
  13798. ctx.isDepleted = item.isOnCooldown && (uses.per && (uses.value > 0));
  13799. ctx.hasTarget = !!target && !(["none", ""].includes(target.type));
  13800. ctx.canToggle = false;
  13801. if ( item.type === "spell" ) arr[0].push(item);
  13802. else arr[1].push(item);
  13803. return arr;
  13804. }, [[], []]);
  13805. // Apply item filters
  13806. spells = this._filterItems(spells, this._filters.spellbook);
  13807. other = this._filterItems(other, this._filters.features);
  13808. // Organize Spellbook
  13809. const spellbook = this._prepareSpellbook(context, spells);
  13810. // Organize Features
  13811. for ( let item of other ) {
  13812. if ( item.type === "weapon" ) features.weapons.items.push(item);
  13813. else if ( item.type === "feat" ) {
  13814. if ( item.system.activation.type ) features.actions.items.push(item);
  13815. else features.passive.items.push(item);
  13816. }
  13817. else features.equipment.items.push(item);
  13818. }
  13819. // Assign and return
  13820. context.inventoryFilters = true;
  13821. context.features = Object.values(features);
  13822. context.spellbook = spellbook;
  13823. }
  13824. /* -------------------------------------------- */
  13825. /**
  13826. * Format NPC armor information into a localized string.
  13827. * @returns {string} Formatted armor label.
  13828. */
  13829. getArmorLabel() {
  13830. const ac = this.actor.system.attributes.ac;
  13831. const label = [];
  13832. if ( ac.calc === "default" ) label.push(this.actor.armor?.name || game.i18n.localize("DND5E.ArmorClassUnarmored"));
  13833. else label.push(game.i18n.localize(CONFIG.DND5E.armorClasses[ac.calc].label));
  13834. if ( this.actor.shield ) label.push(this.actor.shield.name);
  13835. return label.filterJoin(", ");
  13836. }
  13837. /* -------------------------------------------- */
  13838. /* Object Updates */
  13839. /* -------------------------------------------- */
  13840. /** @inheritDoc */
  13841. async _updateObject(event, formData) {
  13842. // Format NPC Challenge Rating
  13843. const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
  13844. let crv = "system.details.cr";
  13845. let cr = formData[crv];
  13846. cr = crs[cr] || parseFloat(cr);
  13847. if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
  13848. // Parent ActorSheet update steps
  13849. return super._updateObject(event, formData);
  13850. }
  13851. }
  13852. /**
  13853. * An Actor sheet for Vehicle type actors.
  13854. */
  13855. class ActorSheet5eVehicle extends ActorSheet5e {
  13856. /** @inheritDoc */
  13857. static get defaultOptions() {
  13858. return foundry.utils.mergeObject(super.defaultOptions, {
  13859. classes: ["dnd5e", "sheet", "actor", "vehicle"]
  13860. });
  13861. }
  13862. /* -------------------------------------------- */
  13863. /** @override */
  13864. static unsupportedItemTypes = new Set(["background", "class", "subclass"]);
  13865. /* -------------------------------------------- */
  13866. /**
  13867. * Creates a new cargo entry for a vehicle Actor.
  13868. * @type {object}
  13869. */
  13870. static get newCargo() {
  13871. return {name: "", quantity: 1};
  13872. }
  13873. /* -------------------------------------------- */
  13874. /* Context Preparation */
  13875. /* -------------------------------------------- */
  13876. /**
  13877. * Compute the total weight of the vehicle's cargo.
  13878. * @param {number} totalWeight The cumulative item weight from inventory items
  13879. * @param {object} actorData The data object for the Actor being rendered
  13880. * @returns {{max: number, value: number, pct: number}}
  13881. * @private
  13882. */
  13883. _computeEncumbrance(totalWeight, actorData) {
  13884. // Compute currency weight
  13885. const totalCoins = Object.values(actorData.system.currency).reduce((acc, denom) => acc + denom, 0);
  13886. const currencyPerWeight = game.settings.get("dnd5e", "metricWeightUnits")
  13887. ? CONFIG.DND5E.encumbrance.currencyPerWeight.metric
  13888. : CONFIG.DND5E.encumbrance.currencyPerWeight.imperial;
  13889. totalWeight += totalCoins / currencyPerWeight;
  13890. // Vehicle weights are an order of magnitude greater.
  13891. totalWeight /= game.settings.get("dnd5e", "metricWeightUnits")
  13892. ? CONFIG.DND5E.encumbrance.vehicleWeightMultiplier.metric
  13893. : CONFIG.DND5E.encumbrance.vehicleWeightMultiplier.imperial;
  13894. // Compute overall encumbrance
  13895. const max = actorData.system.attributes.capacity.cargo;
  13896. const pct = Math.clamped((totalWeight * 100) / max, 0, 100);
  13897. return {value: totalWeight.toNearest(0.1), max, pct};
  13898. }
  13899. /* -------------------------------------------- */
  13900. /** @override */
  13901. _getMovementSpeed(actorData, largestPrimary=true) {
  13902. return super._getMovementSpeed(actorData, largestPrimary);
  13903. }
  13904. /* -------------------------------------------- */
  13905. /**
  13906. * Prepare items that are mounted to a vehicle and require one or more crew to operate.
  13907. * @param {object} item Copy of the item data being prepared for display.
  13908. * @param {object} context Display context for the item.
  13909. * @protected
  13910. */
  13911. _prepareCrewedItem(item, context) {
  13912. // Determine crewed status
  13913. const isCrewed = item.system.crewed;
  13914. context.toggleClass = isCrewed ? "active" : "";
  13915. context.toggleTitle = game.i18n.localize(`DND5E.${isCrewed ? "Crewed" : "Uncrewed"}`);
  13916. // Handle crew actions
  13917. if ( item.type === "feat" && item.system.activation.type === "crew" ) {
  13918. context.cover = game.i18n.localize(`DND5E.${item.system.cover ? "CoverTotal" : "None"}`);
  13919. if ( item.system.cover === .5 ) context.cover = "½";
  13920. else if ( item.system.cover === .75 ) context.cover = "¾";
  13921. else if ( item.system.cover === null ) context.cover = "—";
  13922. }
  13923. // Prepare vehicle weapons
  13924. if ( (item.type === "equipment") || (item.type === "weapon") ) {
  13925. context.threshold = item.system.hp.dt ? item.system.hp.dt : "—";
  13926. }
  13927. }
  13928. /* -------------------------------------------- */
  13929. /** @override */
  13930. _prepareItems(context) {
  13931. const cargoColumns = [{
  13932. label: game.i18n.localize("DND5E.Quantity"),
  13933. css: "item-qty",
  13934. property: "quantity",
  13935. editable: "Number"
  13936. }];
  13937. const equipmentColumns = [{
  13938. label: game.i18n.localize("DND5E.Quantity"),
  13939. css: "item-qty",
  13940. property: "system.quantity",
  13941. editable: "Number"
  13942. }, {
  13943. label: game.i18n.localize("DND5E.AC"),
  13944. css: "item-ac",
  13945. property: "system.armor.value"
  13946. }, {
  13947. label: game.i18n.localize("DND5E.HP"),
  13948. css: "item-hp",
  13949. property: "system.hp.value",
  13950. editable: "Number"
  13951. }, {
  13952. label: game.i18n.localize("DND5E.Threshold"),
  13953. css: "item-threshold",
  13954. property: "threshold"
  13955. }];
  13956. const features = {
  13957. actions: {
  13958. label: game.i18n.localize("DND5E.ActionPl"),
  13959. items: [],
  13960. hasActions: true,
  13961. crewable: true,
  13962. dataset: {type: "feat", "activation.type": "crew"},
  13963. columns: [{
  13964. label: game.i18n.localize("DND5E.Cover"),
  13965. css: "item-cover",
  13966. property: "cover"
  13967. }]
  13968. },
  13969. equipment: {
  13970. label: game.i18n.localize(CONFIG.Item.typeLabels.equipment),
  13971. items: [],
  13972. crewable: true,
  13973. dataset: {type: "equipment", "armor.type": "vehicle"},
  13974. columns: equipmentColumns
  13975. },
  13976. passive: {
  13977. label: game.i18n.localize("DND5E.Features"),
  13978. items: [],
  13979. dataset: {type: "feat"}
  13980. },
  13981. reactions: {
  13982. label: game.i18n.localize("DND5E.ReactionPl"),
  13983. items: [],
  13984. dataset: {type: "feat", "activation.type": "reaction"}
  13985. },
  13986. weapons: {
  13987. label: game.i18n.localize(`${CONFIG.Item.typeLabels.weapon}Pl`),
  13988. items: [],
  13989. crewable: true,
  13990. dataset: {type: "weapon", "weapon-type": "siege"},
  13991. columns: equipmentColumns
  13992. }
  13993. };
  13994. context.items.forEach(item => {
  13995. const {uses, recharge} = item.system;
  13996. const ctx = context.itemContext[item.id] ??= {};
  13997. ctx.canToggle = false;
  13998. ctx.isExpanded = this._expanded.has(item.id);
  13999. ctx.hasUses = uses && (uses.max > 0);
  14000. ctx.isOnCooldown = recharge && !!recharge.value && (recharge.charged === false);
  14001. ctx.isDepleted = item.isOnCooldown && (uses.per && (uses.value > 0));
  14002. });
  14003. const cargo = {
  14004. crew: {
  14005. label: game.i18n.localize("DND5E.VehicleCrew"),
  14006. items: context.actor.system.cargo.crew,
  14007. css: "cargo-row crew",
  14008. editableName: true,
  14009. dataset: {type: "crew"},
  14010. columns: cargoColumns
  14011. },
  14012. passengers: {
  14013. label: game.i18n.localize("DND5E.VehiclePassengers"),
  14014. items: context.actor.system.cargo.passengers,
  14015. css: "cargo-row passengers",
  14016. editableName: true,
  14017. dataset: {type: "passengers"},
  14018. columns: cargoColumns
  14019. },
  14020. cargo: {
  14021. label: game.i18n.localize("DND5E.VehicleCargo"),
  14022. items: [],
  14023. dataset: {type: "loot"},
  14024. columns: [{
  14025. label: game.i18n.localize("DND5E.Quantity"),
  14026. css: "item-qty",
  14027. property: "system.quantity",
  14028. editable: "Number"
  14029. }, {
  14030. label: game.i18n.localize("DND5E.Price"),
  14031. css: "item-price",
  14032. property: "system.price.value",
  14033. editable: "Number"
  14034. }, {
  14035. label: game.i18n.localize("DND5E.Weight"),
  14036. css: "item-weight",
  14037. property: "system.weight",
  14038. editable: "Number"
  14039. }]
  14040. }
  14041. };
  14042. // Classify items owned by the vehicle and compute total cargo weight
  14043. let totalWeight = 0;
  14044. for ( const item of context.items ) {
  14045. const ctx = context.itemContext[item.id] ??= {};
  14046. this._prepareCrewedItem(item, ctx);
  14047. // Handle cargo explicitly
  14048. const isCargo = item.flags.dnd5e?.vehicleCargo === true;
  14049. if ( isCargo ) {
  14050. totalWeight += (item.system.weight || 0) * item.system.quantity;
  14051. cargo.cargo.items.push(item);
  14052. continue;
  14053. }
  14054. // Handle non-cargo item types
  14055. switch ( item.type ) {
  14056. case "weapon":
  14057. features.weapons.items.push(item);
  14058. break;
  14059. case "equipment":
  14060. features.equipment.items.push(item);
  14061. break;
  14062. case "feat":
  14063. const act = item.system.activation;
  14064. if ( !act.type || (act.type === "none") ) features.passive.items.push(item);
  14065. else if (act.type === "reaction") features.reactions.items.push(item);
  14066. else features.actions.items.push(item);
  14067. break;
  14068. default:
  14069. totalWeight += (item.system.weight || 0) * item.system.quantity;
  14070. cargo.cargo.items.push(item);
  14071. }
  14072. }
  14073. // Update the rendering context data
  14074. context.inventoryFilters = false;
  14075. context.features = Object.values(features);
  14076. context.cargo = Object.values(cargo);
  14077. context.encumbrance = this._computeEncumbrance(totalWeight, context);
  14078. }
  14079. /* -------------------------------------------- */
  14080. /* Event Listeners and Handlers */
  14081. /* -------------------------------------------- */
  14082. /** @override */
  14083. activateListeners(html) {
  14084. super.activateListeners(html);
  14085. if ( !this.isEditable ) return;
  14086. html.find(".item-toggle").click(this._onToggleItem.bind(this));
  14087. html.find(".item-hp input")
  14088. .click(evt => evt.target.select())
  14089. .change(this._onHPChange.bind(this));
  14090. html.find(".item:not(.cargo-row) input[data-property]")
  14091. .click(evt => evt.target.select())
  14092. .change(this._onEditInSheet.bind(this));
  14093. html.find(".cargo-row input")
  14094. .click(evt => evt.target.select())
  14095. .change(this._onCargoRowChange.bind(this));
  14096. html.find(".item:not(.cargo-row) .item-qty input")
  14097. .click(evt => evt.target.select())
  14098. .change(this._onQtyChange.bind(this));
  14099. if (this.actor.system.attributes.actions.stations) {
  14100. html.find(".counter.actions, .counter.action-thresholds").hide();
  14101. }
  14102. }
  14103. /* -------------------------------------------- */
  14104. /**
  14105. * Handle saving a cargo row (i.e. crew or passenger) in-sheet.
  14106. * @param {Event} event Triggering event.
  14107. * @returns {Promise<Actor5e>|null} Actor after update if any changes were made.
  14108. * @private
  14109. */
  14110. _onCargoRowChange(event) {
  14111. event.preventDefault();
  14112. const target = event.currentTarget;
  14113. const row = target.closest(".item");
  14114. const idx = Number(row.dataset.itemIndex);
  14115. const property = row.classList.contains("crew") ? "crew" : "passengers";
  14116. // Get the cargo entry
  14117. const cargo = foundry.utils.deepClone(this.actor.system.cargo[property]);
  14118. const entry = cargo[idx];
  14119. if ( !entry ) return null;
  14120. // Update the cargo value
  14121. const key = target.dataset.property ?? "name";
  14122. const type = target.dataset.dtype;
  14123. let value = target.value;
  14124. if (type === "Number") value = Number(value);
  14125. entry[key] = value;
  14126. // Perform the Actor update
  14127. return this.actor.update({[`system.cargo.${property}`]: cargo});
  14128. }
  14129. /* -------------------------------------------- */
  14130. /**
  14131. * Handle editing certain values like quantity, price, and weight in-sheet.
  14132. * @param {Event} event Triggering event.
  14133. * @returns {Promise<Item5e>} Item with updates applied.
  14134. * @private
  14135. */
  14136. _onEditInSheet(event) {
  14137. event.preventDefault();
  14138. const itemID = event.currentTarget.closest(".item").dataset.itemId;
  14139. const item = this.actor.items.get(itemID);
  14140. const property = event.currentTarget.dataset.property;
  14141. const type = event.currentTarget.dataset.dtype;
  14142. let value = event.currentTarget.value;
  14143. switch (type) {
  14144. case "Number": value = parseInt(value); break;
  14145. case "Boolean": value = value === "true"; break;
  14146. }
  14147. return item.update({[`${property}`]: value});
  14148. }
  14149. /* -------------------------------------------- */
  14150. /** @inheritDoc */
  14151. _onItemCreate(event) {
  14152. event.preventDefault();
  14153. // Handle creating a new crew or passenger row.
  14154. const target = event.currentTarget;
  14155. const type = target.dataset.type;
  14156. if (type === "crew" || type === "passengers") {
  14157. const cargo = foundry.utils.deepClone(this.actor.system.cargo[type]);
  14158. cargo.push(this.constructor.newCargo);
  14159. return this.actor.update({[`system.cargo.${type}`]: cargo});
  14160. }
  14161. return super._onItemCreate(event);
  14162. }
  14163. /* -------------------------------------------- */
  14164. /** @inheritDoc */
  14165. _onItemDelete(event) {
  14166. event.preventDefault();
  14167. // Handle deleting a crew or passenger row.
  14168. const row = event.currentTarget.closest(".item");
  14169. if (row.classList.contains("cargo-row")) {
  14170. const idx = Number(row.dataset.itemIndex);
  14171. const type = row.classList.contains("crew") ? "crew" : "passengers";
  14172. const cargo = foundry.utils.deepClone(this.actor.system.cargo[type]).filter((_, i) => i !== idx);
  14173. return this.actor.update({[`system.cargo.${type}`]: cargo});
  14174. }
  14175. return super._onItemDelete(event);
  14176. }
  14177. /* -------------------------------------------- */
  14178. /** @override */
  14179. async _onDropSingleItem(itemData) {
  14180. const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"];
  14181. const isCargo = cargoTypes.includes(itemData.type) && (this._tabs[0].active === "cargo");
  14182. foundry.utils.setProperty(itemData, "flags.dnd5e.vehicleCargo", isCargo);
  14183. return super._onDropSingleItem(itemData);
  14184. }
  14185. /* -------------------------------------------- */
  14186. /**
  14187. * Special handling for editing HP to clamp it within appropriate range.
  14188. * @param {Event} event Triggering event.
  14189. * @returns {Promise<Item5e>} Item after the update is applied.
  14190. * @private
  14191. */
  14192. _onHPChange(event) {
  14193. event.preventDefault();
  14194. const itemID = event.currentTarget.closest(".item").dataset.itemId;
  14195. const item = this.actor.items.get(itemID);
  14196. let hp = Math.clamped(0, parseInt(event.currentTarget.value), item.system.hp.max);
  14197. if ( Number.isNaN(hp) ) hp = 0;
  14198. return item.update({"system.hp.value": hp});
  14199. }
  14200. /* -------------------------------------------- */
  14201. /**
  14202. * Special handling for editing quantity value of equipment and weapons inside the features tab.
  14203. * @param {Event} event Triggering event.
  14204. * @returns {Promise<Item5e>} Item after the update is applied.
  14205. * @private
  14206. */
  14207. _onQtyChange(event) {
  14208. event.preventDefault();
  14209. const itemID = event.currentTarget.closest(".item").dataset.itemId;
  14210. const item = this.actor.items.get(itemID);
  14211. let qty = parseInt(event.currentTarget.value);
  14212. if ( Number.isNaN(qty) ) qty = 0;
  14213. return item.update({"system.quantity": qty});
  14214. }
  14215. /* -------------------------------------------- */
  14216. /**
  14217. * Handle toggling an item's crewed status.
  14218. * @param {Event} event Triggering event.
  14219. * @returns {Promise<Item5e>} Item after the toggling is applied.
  14220. * @private
  14221. */
  14222. _onToggleItem(event) {
  14223. event.preventDefault();
  14224. const itemID = event.currentTarget.closest(".item").dataset.itemId;
  14225. const item = this.actor.items.get(itemID);
  14226. return item.update({"system.crewed": !item.system.crewed});
  14227. }
  14228. }
  14229. /**
  14230. * A character sheet for group-type Actors.
  14231. * The functionality of this sheet is sufficiently different from other Actor types that we extend the base
  14232. * Foundry VTT ActorSheet instead of the ActorSheet5e abstraction used for character, npc, and vehicle types.
  14233. */
  14234. class GroupActorSheet extends ActorSheet {
  14235. /**
  14236. * IDs for items on the sheet that have been expanded.
  14237. * @type {Set<string>}
  14238. * @protected
  14239. */
  14240. _expanded = new Set();
  14241. /* -------------------------------------------- */
  14242. /** @inheritDoc */
  14243. static get defaultOptions() {
  14244. return foundry.utils.mergeObject(super.defaultOptions, {
  14245. classes: ["dnd5e", "sheet", "actor", "group"],
  14246. template: "systems/dnd5e/templates/actors/group-sheet.hbs",
  14247. tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "members"}],
  14248. scrollY: [".inventory .inventory-list"],
  14249. width: 620,
  14250. height: 620
  14251. });
  14252. }
  14253. /* -------------------------------------------- */
  14254. /**
  14255. * A set of item types that should be prevented from being dropped on this type of actor sheet.
  14256. * @type {Set<string>}
  14257. */
  14258. static unsupportedItemTypes = new Set(["background", "class", "subclass", "feat"]);
  14259. /* -------------------------------------------- */
  14260. /* Context Preparation */
  14261. /* -------------------------------------------- */
  14262. /** @inheritDoc */
  14263. async getData(options={}) {
  14264. const context = super.getData(options);
  14265. context.system = context.data.system;
  14266. context.items = Array.from(this.actor.items);
  14267. // Membership
  14268. const {sections, stats} = this.#prepareMembers();
  14269. Object.assign(context, stats);
  14270. context.sections = sections;
  14271. // Movement
  14272. context.movement = this.#prepareMovementSpeed();
  14273. // Inventory
  14274. context.itemContext = {};
  14275. context.inventory = this.#prepareInventory(context);
  14276. context.expandedData = {};
  14277. for ( const id of this._expanded ) {
  14278. const item = this.actor.items.get(id);
  14279. if ( item ) context.expandedData[id] = await item.getChatData({secrets: this.actor.isOwner});
  14280. }
  14281. context.inventoryFilters = false;
  14282. context.rollableClass = this.isEditable ? "rollable" : "";
  14283. // Biography HTML
  14284. context.descriptionFull = await TextEditor.enrichHTML(this.actor.system.description.full, {
  14285. secrets: this.actor.isOwner,
  14286. rollData: context.rollData,
  14287. async: true,
  14288. relativeTo: this.actor
  14289. });
  14290. // Summary tag
  14291. context.summary = this.#getSummary(stats);
  14292. // Text labels
  14293. context.labels = {
  14294. currencies: Object.entries(CONFIG.DND5E.currencies).reduce((obj, [k, c]) => {
  14295. obj[k] = c.label;
  14296. return obj;
  14297. }, {})
  14298. };
  14299. return context;
  14300. }
  14301. /* -------------------------------------------- */
  14302. /**
  14303. * Prepare a localized summary of group membership.
  14304. * @param {{nMembers: number, nVehicles: number}} stats The number of members in the group
  14305. * @returns {string} The formatted summary string
  14306. */
  14307. #getSummary(stats) {
  14308. const formatter = new Intl.ListFormat(game.i18n.lang, {style: "long", type: "conjunction"});
  14309. const members = [];
  14310. if ( stats.nMembers ) members.push(`${stats.nMembers} ${game.i18n.localize("DND5E.GroupMembers")}`);
  14311. if ( stats.nVehicles ) members.push(`${stats.nVehicles} ${game.i18n.localize("DND5E.GroupVehicles")}`);
  14312. if ( !members.length ) return game.i18n.localize("DND5E.GroupSummaryEmpty");
  14313. return game.i18n.format("DND5E.GroupSummary", {members: formatter.format(members)});
  14314. }
  14315. /* -------------------------------------------- */
  14316. /**
  14317. * Prepare membership data for the sheet.
  14318. * @returns {{sections: object, stats: object}}
  14319. */
  14320. #prepareMembers() {
  14321. const stats = {
  14322. currentHP: 0,
  14323. maxHP: 0,
  14324. nMembers: 0,
  14325. nVehicles: 0
  14326. };
  14327. const sections = {
  14328. character: {label: `${CONFIG.Actor.typeLabels.character}Pl`, members: []},
  14329. npc: {label: `${CONFIG.Actor.typeLabels.npc}Pl`, members: []},
  14330. vehicle: {label: `${CONFIG.Actor.typeLabels.vehicle}Pl`, members: []}
  14331. };
  14332. for ( const member of this.object.system.members ) {
  14333. const m = {
  14334. actor: member,
  14335. id: member.id,
  14336. name: member.name,
  14337. img: member.img,
  14338. hp: {},
  14339. displayHPValues: member.testUserPermission(game.user, "OBSERVER")
  14340. };
  14341. // HP bar
  14342. const hp = member.system.attributes.hp;
  14343. m.hp.current = hp.value + (hp.temp || 0);
  14344. m.hp.max = Math.max(0, hp.max + (hp.tempmax || 0));
  14345. m.hp.pct = Math.clamped((m.hp.current / m.hp.max) * 100, 0, 100).toFixed(2);
  14346. m.hp.color = dnd5e.documents.Actor5e.getHPColor(m.hp.current, m.hp.max).css;
  14347. stats.currentHP += m.hp.current;
  14348. stats.maxHP += m.hp.max;
  14349. if ( member.type === "vehicle" ) stats.nVehicles++;
  14350. else stats.nMembers++;
  14351. sections[member.type].members.push(m);
  14352. }
  14353. for ( const [k, section] of Object.entries(sections) ) {
  14354. if ( !section.members.length ) delete sections[k];
  14355. }
  14356. return {sections, stats};
  14357. }
  14358. /* -------------------------------------------- */
  14359. /**
  14360. * Prepare movement speed data for rendering on the sheet.
  14361. * @returns {{secondary: string, primary: string}}
  14362. */
  14363. #prepareMovementSpeed() {
  14364. const movement = this.object.system.attributes.movement;
  14365. let speeds = [
  14366. [movement.land, `${game.i18n.localize("DND5E.MovementLand")} ${movement.land}`],
  14367. [movement.water, `${game.i18n.localize("DND5E.MovementWater")} ${movement.water}`],
  14368. [movement.air, `${game.i18n.localize("DND5E.MovementAir")} ${movement.air}`]
  14369. ];
  14370. speeds = speeds.filter(s => s[0]).sort((a, b) => b[0] - a[0]);
  14371. const primary = speeds.shift();
  14372. return {
  14373. primary: `${primary ? primary[1] : "0"}`,
  14374. secondary: speeds.map(s => s[1]).join(", ")
  14375. };
  14376. }
  14377. /* -------------------------------------------- */
  14378. /**
  14379. * Prepare inventory items for rendering on the sheet.
  14380. * @param {object} context Prepared rendering context.
  14381. * @returns {Object<string,object>}
  14382. */
  14383. #prepareInventory(context) {
  14384. // Categorize as weapons, equipment, containers, and loot
  14385. const sections = {};
  14386. for ( const type of ["weapon", "equipment", "consumable", "backpack", "loot"] ) {
  14387. sections[type] = {label: `${CONFIG.Item.typeLabels[type]}Pl`, items: [], hasActions: false, dataset: {type}};
  14388. }
  14389. // Classify items
  14390. for ( const item of context.items ) {
  14391. const ctx = context.itemContext[item.id] ??= {};
  14392. const {quantity} = item.system;
  14393. ctx.isStack = Number.isNumeric(quantity) && (quantity > 1);
  14394. ctx.canToggle = false;
  14395. ctx.isExpanded = this._expanded.has(item.id);
  14396. ctx.hasUses = item.hasLimitedUses;
  14397. if ( (item.type in sections) && (item.type !== "loot") ) sections[item.type].items.push(item);
  14398. else sections.loot.items.push(item);
  14399. }
  14400. return sections;
  14401. }
  14402. /* -------------------------------------------- */
  14403. /* Rendering Workflow */
  14404. /* -------------------------------------------- */
  14405. /** @inheritDoc */
  14406. async _render(force, options={}) {
  14407. for ( const member of this.object.system.members) {
  14408. member.apps[this.id] = this;
  14409. }
  14410. return super._render(force, options);
  14411. }
  14412. /* -------------------------------------------- */
  14413. /** @inheritDoc */
  14414. async close(options={}) {
  14415. for ( const member of this.object.system.members ) {
  14416. delete member.apps[this.id];
  14417. }
  14418. return super.close(options);
  14419. }
  14420. /* -------------------------------------------- */
  14421. /* Event Listeners and Handlers */
  14422. /* -------------------------------------------- */
  14423. /** @inheritDoc */
  14424. activateListeners(html) {
  14425. super.activateListeners(html);
  14426. html.find(".group-member .name").click(this._onClickMemberName.bind(this));
  14427. if ( this.isEditable ) {
  14428. // Input focus and update
  14429. const inputs = html.find("input");
  14430. inputs.focus(ev => ev.currentTarget.select());
  14431. inputs.addBack().find('[type="text"][data-dtype="Number"]').change(ActorSheet5e.prototype._onChangeInputDelta.bind(this));
  14432. html.find(".action-button").click(this._onClickActionButton.bind(this));
  14433. html.find(".item-control").click(this._onClickItemControl.bind(this));
  14434. html.find(".item .rollable h4").click(event => this._onClickItemName(event));
  14435. html.find(".item-quantity input, .item-uses input").change(this._onItemPropertyChange.bind(this));
  14436. new ContextMenu(html, ".item-list .item", [], {onOpen: this._onItemContext.bind(this)});
  14437. }
  14438. }
  14439. /* -------------------------------------------- */
  14440. /**
  14441. * Handle clicks to action buttons on the group sheet.
  14442. * @param {PointerEvent} event The initiating click event
  14443. * @protected
  14444. */
  14445. _onClickActionButton(event) {
  14446. event.preventDefault();
  14447. const button = event.currentTarget;
  14448. switch ( button.dataset.action ) {
  14449. case "convertCurrency":
  14450. Dialog.confirm({
  14451. title: `${game.i18n.localize("DND5E.CurrencyConvert")}`,
  14452. content: `<p>${game.i18n.localize("DND5E.CurrencyConvertHint")}</p>`,
  14453. yes: () => this.actor.convertCurrency()
  14454. });
  14455. break;
  14456. case "removeMember":
  14457. const removeMemberId = button.closest("li.group-member").dataset.actorId;
  14458. this.object.system.removeMember(removeMemberId);
  14459. break;
  14460. case "movementConfig":
  14461. const movementConfig = new ActorMovementConfig(this.object);
  14462. movementConfig.render(true);
  14463. break;
  14464. }
  14465. }
  14466. /* -------------------------------------------- */
  14467. /**
  14468. * Handle clicks to item control buttons on the group sheet.
  14469. * @param {PointerEvent} event The initiating click event
  14470. * @protected
  14471. */
  14472. _onClickItemControl(event) {
  14473. event.preventDefault();
  14474. const button = event.currentTarget;
  14475. switch ( button.dataset.action ) {
  14476. case "itemCreate":
  14477. this._createItem(button);
  14478. break;
  14479. case "itemDelete":
  14480. const deleteLi = event.currentTarget.closest(".item");
  14481. const deleteItem = this.actor.items.get(deleteLi.dataset.itemId);
  14482. deleteItem.deleteDialog();
  14483. break;
  14484. case "itemEdit":
  14485. const editLi = event.currentTarget.closest(".item");
  14486. const editItem = this.actor.items.get(editLi.dataset.itemId);
  14487. editItem.sheet.render(true);
  14488. break;
  14489. }
  14490. }
  14491. /* -------------------------------------------- */
  14492. /**
  14493. * Handle workflows to create a new Item directly within the Group Actor sheet.
  14494. * @param {HTMLElement} button The clicked create button
  14495. * @returns {Item5e} The created embedded Item
  14496. * @protected
  14497. */
  14498. _createItem(button) {
  14499. const type = button.dataset.type;
  14500. const system = {...button.dataset};
  14501. delete system.type;
  14502. const name = game.i18n.format("DND5E.ItemNew", {type: game.i18n.localize(CONFIG.Item.typeLabels[type])});
  14503. const itemData = {name, type, system};
  14504. return this.actor.createEmbeddedDocuments("Item", [itemData]);
  14505. }
  14506. /* -------------------------------------------- */
  14507. /**
  14508. * Handle activation of a context menu for an embedded Item document.
  14509. * Dynamically populate the array of context menu options.
  14510. * Reuse the item context options provided by the base ActorSheet5e class.
  14511. * @param {HTMLElement} element The HTML element for which the context menu is activated
  14512. * @protected
  14513. */
  14514. _onItemContext(element) {
  14515. const item = this.actor.items.get(element.dataset.itemId);
  14516. if ( !item ) return;
  14517. ui.context.menuItems = ActorSheet5e.prototype._getItemContextOptions.call(this, item);
  14518. Hooks.call("dnd5e.getItemContextOptions", item, ui.context.menuItems);
  14519. }
  14520. /* -------------------------------------------- */
  14521. /**
  14522. * Handle clicks on member names in the members list.
  14523. * @param {PointerEvent} event The initiating click event
  14524. * @protected
  14525. */
  14526. _onClickMemberName(event) {
  14527. event.preventDefault();
  14528. const member = event.currentTarget.closest("li.group-member");
  14529. const actor = game.actors.get(member.dataset.actorId);
  14530. if ( actor ) actor.sheet.render(true, {focus: true});
  14531. }
  14532. /* -------------------------------------------- */
  14533. /**
  14534. * Handle clicks on an item name to expand its description
  14535. * @param {PointerEvent} event The initiating click event
  14536. * @protected
  14537. */
  14538. _onClickItemName(event) {
  14539. game.system.applications.actor.ActorSheet5e.prototype._onItemSummary.call(this, event);
  14540. }
  14541. /* -------------------------------------------- */
  14542. /**
  14543. * Change the quantity or limited uses of an Owned Item within the actor.
  14544. * @param {Event} event The triggering click event.
  14545. * @returns {Promise<Item5e>} Updated item.
  14546. * @protected
  14547. */
  14548. async _onItemPropertyChange(event) {
  14549. const proto = game.system.applications.actor.ActorSheet5e.prototype;
  14550. const parent = event.currentTarget.parentElement;
  14551. if ( parent.classList.contains("item-quantity") ) return proto._onQuantityChange.call(this, event);
  14552. else if ( parent.classList.contains("item-uses") ) return proto._onUsesChange.call(this, event);
  14553. }
  14554. /* -------------------------------------------- */
  14555. /** @override */
  14556. async _onDropActor(event, data) {
  14557. if ( !this.isEditable ) return;
  14558. const cls = getDocumentClass("Actor");
  14559. const sourceActor = await cls.fromDropData(data);
  14560. if ( !sourceActor ) return;
  14561. return this.object.system.addMember(sourceActor);
  14562. }
  14563. /* -------------------------------------------- */
  14564. /** @override */
  14565. async _onDropItemCreate(itemData) {
  14566. const items = itemData instanceof Array ? itemData : [itemData];
  14567. const toCreate = [];
  14568. for ( const item of items ) {
  14569. const result = await this._onDropSingleItem(item);
  14570. if ( result ) toCreate.push(result);
  14571. }
  14572. // Create the owned items as normal
  14573. return this.actor.createEmbeddedDocuments("Item", toCreate);
  14574. }
  14575. /* -------------------------------------------- */
  14576. /**
  14577. * Handles dropping of a single item onto this group sheet.
  14578. * @param {object} itemData The item data to create.
  14579. * @returns {Promise<object|boolean>} The item data to create after processing, or false if the item should not be
  14580. * created or creation has been otherwise handled.
  14581. * @protected
  14582. */
  14583. async _onDropSingleItem(itemData) {
  14584. // Check to make sure items of this type are allowed on this actor
  14585. if ( this.constructor.unsupportedItemTypes.has(itemData.type) ) {
  14586. ui.notifications.warn(game.i18n.format("DND5E.ActorWarningInvalidItem", {
  14587. itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]),
  14588. actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type])
  14589. }));
  14590. return false;
  14591. }
  14592. // Create a Consumable spell scroll on the Inventory tab
  14593. if ( itemData.type === "spell" ) {
  14594. const scroll = await Item5e.createScrollFromSpell(itemData);
  14595. return scroll.toObject();
  14596. }
  14597. // TODO: Stack identical consumables
  14598. return itemData;
  14599. }
  14600. }
  14601. /**
  14602. * A simple form to set skill configuration for a given skill.
  14603. *
  14604. * @param {Actor} actor The Actor instance being displayed within the sheet.
  14605. * @param {ApplicationOptions} options Additional application configuration options.
  14606. * @param {string} skillId The skill key as defined in CONFIG.DND5E.skills.
  14607. * @deprecated since dnd5e 2.2, targeted for removal in 2.4
  14608. */
  14609. class ActorSkillConfig extends BaseConfigSheet {
  14610. constructor(actor, options, skillId) {
  14611. super(actor, options);
  14612. this._skillId = skillId;
  14613. foundry.utils.logCompatibilityWarning("ActorSkillConfig has been deprecated in favor of the more general "
  14614. + "ProficiencyConfig available at 'dnd5e.applications.actor.ProficiencyConfig'. Support for the old application "
  14615. + "will be removed in a future version.", {since: "DnD5e 2.2", until: "DnD5e 2.4"});
  14616. }
  14617. /* -------------------------------------------- */
  14618. /** @inheritdoc */
  14619. static get defaultOptions() {
  14620. return foundry.utils.mergeObject(super.defaultOptions, {
  14621. classes: ["dnd5e"],
  14622. template: "systems/dnd5e/templates/apps/skill-config.hbs",
  14623. width: 500,
  14624. height: "auto"
  14625. });
  14626. }
  14627. /* -------------------------------------------- */
  14628. /** @inheritdoc */
  14629. get title() {
  14630. const label = CONFIG.DND5E.skills[this._skillId].label;
  14631. return `${game.i18n.format("DND5E.SkillConfigureTitle", {skill: label})}: ${this.document.name}`;
  14632. }
  14633. /* -------------------------------------------- */
  14634. /** @inheritdoc */
  14635. getData(options) {
  14636. const src = this.document.toObject();
  14637. return {
  14638. abilities: CONFIG.DND5E.abilities,
  14639. skill: src.system.skills?.[this._skillId] ?? this.document.system.skills[this._skillId] ?? {},
  14640. skillId: this._skillId,
  14641. proficiencyLevels: CONFIG.DND5E.proficiencyLevels,
  14642. bonusGlobal: src.system.bonuses?.abilities.skill
  14643. };
  14644. }
  14645. /* -------------------------------------------- */
  14646. /** @inheritdoc */
  14647. _updateObject(event, formData) {
  14648. const passive = formData[`system.skills.${this._skillId}.bonuses.passive`];
  14649. const passiveRoll = new Roll(passive);
  14650. if ( !passiveRoll.isDeterministic ) {
  14651. const message = game.i18n.format("DND5E.FormulaCannotContainDiceError", {
  14652. name: game.i18n.localize("DND5E.SkillBonusPassive")
  14653. });
  14654. ui.notifications.error(message);
  14655. throw new Error(message);
  14656. }
  14657. super._updateObject(event, formData);
  14658. }
  14659. }
  14660. var _module$a = /*#__PURE__*/Object.freeze({
  14661. __proto__: null,
  14662. ActorAbilityConfig: ActorAbilityConfig,
  14663. ActorArmorConfig: ActorArmorConfig,
  14664. ActorHitDiceConfig: ActorHitDiceConfig,
  14665. ActorHitPointsConfig: ActorHitPointsConfig,
  14666. ActorInitiativeConfig: ActorInitiativeConfig,
  14667. ActorMovementConfig: ActorMovementConfig,
  14668. ActorSensesConfig: ActorSensesConfig,
  14669. ActorSheet5e: ActorSheet5e,
  14670. ActorSheet5eCharacter: ActorSheet5eCharacter,
  14671. ActorSheet5eNPC: ActorSheet5eNPC,
  14672. ActorSheet5eVehicle: ActorSheet5eVehicle,
  14673. ActorSheetFlags: ActorSheetFlags,
  14674. ActorSkillConfig: ActorSkillConfig,
  14675. ActorTypeConfig: ActorTypeConfig,
  14676. BaseConfigSheet: BaseConfigSheet,
  14677. GroupActorSheet: GroupActorSheet,
  14678. LongRestDialog: LongRestDialog,
  14679. ProficiencyConfig: ProficiencyConfig,
  14680. ShortRestDialog: ShortRestDialog,
  14681. ToolSelector: ToolSelector,
  14682. TraitSelector: TraitSelector$1
  14683. });
  14684. /**
  14685. * Dialog to select which new advancements should be added to an item.
  14686. */
  14687. class AdvancementMigrationDialog extends Dialog {
  14688. /** @inheritdoc */
  14689. static get defaultOptions() {
  14690. return foundry.utils.mergeObject(super.defaultOptions, {
  14691. classes: ["dnd5e", "advancement-migration", "dialog"],
  14692. jQuery: false,
  14693. width: 500
  14694. });
  14695. }
  14696. /* -------------------------------------------- */
  14697. /**
  14698. * A helper constructor function which displays the migration dialog.
  14699. * @param {Item5e} item Item to which the advancements are being added.
  14700. * @param {Advancement[]} advancements New advancements that should be displayed in the prompt.
  14701. * @returns {Promise<Advancement[]|null>} Resolves with the advancements that should be added, if any.
  14702. */
  14703. static createDialog(item, advancements) {
  14704. const advancementContext = advancements.map(a => ({
  14705. id: a.id, icon: a.icon, title: a.title,
  14706. summary: a.levels.length === 1 ? a.summaryForLevel(a.levels[0]) : ""
  14707. }));
  14708. return new Promise(async (resolve, reject) => {
  14709. const dialog = new this({
  14710. title: `${game.i18n.localize("DND5E.AdvancementMigrationTitle")}: ${item.name}`,
  14711. content: await renderTemplate(
  14712. "systems/dnd5e/templates/advancement/advancement-migration-dialog.hbs",
  14713. { item, advancements: advancementContext }
  14714. ),
  14715. buttons: {
  14716. continue: {
  14717. icon: '<i class="fas fa-check"></i>',
  14718. label: game.i18n.localize("DND5E.AdvancementMigrationConfirm"),
  14719. callback: html => resolve(advancements.filter(a => html.querySelector(`[name="${a.id}"]`)?.checked))
  14720. },
  14721. cancel: {
  14722. icon: '<i class="fas fa-times"></i>',
  14723. label: game.i18n.localize("Cancel"),
  14724. callback: html => reject(null)
  14725. }
  14726. },
  14727. default: "continue",
  14728. close: () => reject(null)
  14729. });
  14730. dialog.render(true);
  14731. });
  14732. }
  14733. }
  14734. /**
  14735. * Presents a list of advancement types to create when clicking the new advancement button.
  14736. * Once a type is selected, this hands the process over to the advancement's individual editing interface.
  14737. *
  14738. * @param {Item5e} item Item to which this advancement will be added.
  14739. * @param {object} [dialogData={}] An object of dialog data which configures how the modal window is rendered.
  14740. * @param {object} [options={}] Dialog rendering options.
  14741. */
  14742. class AdvancementSelection extends Dialog {
  14743. constructor(item, dialogData={}, options={}) {
  14744. super(dialogData, options);
  14745. /**
  14746. * Store a reference to the Item to which this Advancement is being added.
  14747. * @type {Item5e}
  14748. */
  14749. this.item = item;
  14750. }
  14751. /* -------------------------------------------- */
  14752. /** @inheritDoc */
  14753. static get defaultOptions() {
  14754. return foundry.utils.mergeObject(super.defaultOptions, {
  14755. classes: ["dnd5e", "sheet", "advancement"],
  14756. template: "systems/dnd5e/templates/advancement/advancement-selection.hbs",
  14757. title: "DND5E.AdvancementSelectionTitle",
  14758. width: 500,
  14759. height: "auto"
  14760. });
  14761. }
  14762. /* -------------------------------------------- */
  14763. /** @inheritDoc */
  14764. get id() {
  14765. return `item-${this.item.id}-advancement-selection`;
  14766. }
  14767. /* -------------------------------------------- */
  14768. /** @inheritDoc */
  14769. getData() {
  14770. const context = { types: {} };
  14771. for ( const [name, advancement] of Object.entries(CONFIG.DND5E.advancementTypes) ) {
  14772. if ( !(advancement.prototype instanceof Advancement)
  14773. || !advancement.metadata.validItemTypes.has(this.item.type) ) continue;
  14774. context.types[name] = {
  14775. label: advancement.metadata.title,
  14776. icon: advancement.metadata.icon,
  14777. hint: advancement.metadata.hint,
  14778. disabled: !advancement.availableForItem(this.item)
  14779. };
  14780. }
  14781. context.types = dnd5e.utils.sortObjectEntries(context.types, "label");
  14782. return context;
  14783. }
  14784. /* -------------------------------------------- */
  14785. /** @inheritDoc */
  14786. activateListeners(html) {
  14787. super.activateListeners(html);
  14788. html.on("change", "input", this._onChangeInput.bind(this));
  14789. }
  14790. /* -------------------------------------------- */
  14791. /** @inheritDoc */
  14792. _onChangeInput(event) {
  14793. const submit = this.element[0].querySelector("button[data-button='submit']");
  14794. submit.disabled = !this.element[0].querySelector("input[name='type']:checked");
  14795. }
  14796. /* -------------------------------------------- */
  14797. /**
  14798. * A helper constructor function which displays the selection dialog and returns a Promise once its workflow has
  14799. * been resolved.
  14800. * @param {Item5e} item Item to which the advancement should be added.
  14801. * @param {object} [config={}]
  14802. * @param {boolean} [config.rejectClose=false] Trigger a rejection if the window was closed without a choice.
  14803. * @param {object} [config.options={}] Additional rendering options passed to the Dialog.
  14804. * @returns {Promise<AdvancementConfig|null>} Result of `Item5e#createAdvancement`.
  14805. */
  14806. static async createDialog(item, { rejectClose=false, options={} }={}) {
  14807. return new Promise((resolve, reject) => {
  14808. const dialog = new this(item, {
  14809. title: `${game.i18n.localize("DND5E.AdvancementSelectionTitle")}: ${item.name}`,
  14810. buttons: {
  14811. submit: {
  14812. callback: html => {
  14813. const formData = new FormDataExtended(html.querySelector("form"));
  14814. const type = formData.get("type");
  14815. resolve(item.createAdvancement(type));
  14816. }
  14817. }
  14818. },
  14819. close: () => {
  14820. if ( rejectClose ) reject("No advancement type was selected");
  14821. else resolve(null);
  14822. }
  14823. }, foundry.utils.mergeObject(options, { jQuery: false }));
  14824. dialog.render(true);
  14825. });
  14826. }
  14827. }
  14828. var _module$9 = /*#__PURE__*/Object.freeze({
  14829. __proto__: null,
  14830. AbilityScoreImprovementConfig: AbilityScoreImprovementConfig,
  14831. AbilityScoreImprovementFlow: AbilityScoreImprovementFlow,
  14832. AdvancementConfig: AdvancementConfig,
  14833. AdvancementConfirmationDialog: AdvancementConfirmationDialog,
  14834. AdvancementFlow: AdvancementFlow,
  14835. AdvancementManager: AdvancementManager,
  14836. AdvancementMigrationDialog: AdvancementMigrationDialog,
  14837. AdvancementSelection: AdvancementSelection,
  14838. HitPointsConfig: HitPointsConfig,
  14839. HitPointsFlow: HitPointsFlow,
  14840. ItemChoiceConfig: ItemChoiceConfig,
  14841. ItemChoiceFlow: ItemChoiceFlow,
  14842. ItemGrantConfig: ItemGrantConfig,
  14843. ItemGrantFlow: ItemGrantFlow,
  14844. ScaleValueConfig: ScaleValueConfig,
  14845. ScaleValueFlow: ScaleValueFlow
  14846. });
  14847. /**
  14848. * An extension of the base CombatTracker class to provide some 5e-specific functionality.
  14849. * @extends {CombatTracker}
  14850. */
  14851. class CombatTracker5e extends CombatTracker {
  14852. /** @inheritdoc */
  14853. async _onCombatantControl(event) {
  14854. const btn = event.currentTarget;
  14855. const combatantId = btn.closest(".combatant").dataset.combatantId;
  14856. const combatant = this.viewed.combatants.get(combatantId);
  14857. if ( (btn.dataset.control === "rollInitiative") && combatant?.actor ) return combatant.actor.rollInitiativeDialog();
  14858. return super._onCombatantControl(event);
  14859. }
  14860. }
  14861. var _module$8 = /*#__PURE__*/Object.freeze({
  14862. __proto__: null,
  14863. CombatTracker5e: CombatTracker5e
  14864. });
  14865. /**
  14866. * A specialized form used to select from a checklist of attributes, traits, or properties.
  14867. * @deprecated since dnd5e 2.1, targeted for removal in 2.3
  14868. */
  14869. class TraitSelector extends DocumentSheet {
  14870. constructor(...args) {
  14871. super(...args);
  14872. if ( !this.options.suppressWarning ) foundry.utils.logCompatibilityWarning(
  14873. `${this.constructor.name} has been deprecated in favor of a more specialized TraitSelector `
  14874. + "available at 'dnd5e.applications.actor.TraitSelector'. Support for the old application will "
  14875. + "be removed in a future version.",
  14876. { since: "DnD5e 2.1", until: "DnD5e 2.3" }
  14877. );
  14878. }
  14879. /* -------------------------------------------- */
  14880. /** @inheritDoc */
  14881. static get defaultOptions() {
  14882. return foundry.utils.mergeObject(super.defaultOptions, {
  14883. id: "trait-selector",
  14884. classes: ["dnd5e", "trait-selector", "subconfig"],
  14885. title: "Actor Trait Selection",
  14886. template: "systems/dnd5e/templates/apps/trait-selector.hbs",
  14887. width: 320,
  14888. height: "auto",
  14889. choices: {},
  14890. allowCustom: true,
  14891. minimum: 0,
  14892. maximum: null,
  14893. labelKey: null,
  14894. valueKey: "value",
  14895. customKey: "custom"
  14896. });
  14897. }
  14898. /* -------------------------------------------- */
  14899. /** @inheritdoc */
  14900. get title() {
  14901. return this.options.title || super.title;
  14902. }
  14903. /* -------------------------------------------- */
  14904. /**
  14905. * Return a reference to the target attribute
  14906. * @type {string}
  14907. */
  14908. get attribute() {
  14909. return this.options.name;
  14910. }
  14911. /* -------------------------------------------- */
  14912. /** @override */
  14913. getData() {
  14914. const attr = foundry.utils.getProperty(this.object, this.attribute);
  14915. const o = this.options;
  14916. const value = (o.valueKey) ? foundry.utils.getProperty(attr, o.valueKey) ?? [] : attr;
  14917. const custom = (o.customKey) ? foundry.utils.getProperty(attr, o.customKey) ?? "" : "";
  14918. // Populate choices
  14919. const choices = Object.entries(o.choices).reduce((obj, e) => {
  14920. let [k, v] = e;
  14921. const label = o.labelKey ? foundry.utils.getProperty(v, o.labelKey) ?? v : v;
  14922. obj[k] = { label, chosen: attr ? value.includes(k) : false };
  14923. return obj;
  14924. }, {});
  14925. // Return data
  14926. return {
  14927. choices: choices,
  14928. custom: custom,
  14929. customPath: o.allowCustom ? "custom" : null
  14930. };
  14931. }
  14932. /* -------------------------------------------- */
  14933. /**
  14934. * Prepare the update data to include choices in the provided object.
  14935. * @param {object} formData Form data to search for choices.
  14936. * @returns {object} Updates to apply to target.
  14937. */
  14938. _prepareUpdateData(formData) {
  14939. const o = this.options;
  14940. formData = foundry.utils.expandObject(formData);
  14941. // Obtain choices
  14942. const chosen = Object.entries(formData.choices).filter(([, v]) => v).map(([k]) => k);
  14943. // Object including custom data
  14944. const updateData = {};
  14945. if ( o.valueKey ) updateData[`${this.attribute}.${o.valueKey}`] = chosen;
  14946. else updateData[this.attribute] = chosen;
  14947. if ( o.allowCustom ) updateData[`${this.attribute}.${o.customKey}`] = formData.custom;
  14948. // Validate the number chosen
  14949. if ( o.minimum && (chosen.length < o.minimum) ) {
  14950. return ui.notifications.error(`You must choose at least ${o.minimum} options`);
  14951. }
  14952. if ( o.maximum && (chosen.length > o.maximum) ) {
  14953. return ui.notifications.error(`You may choose no more than ${o.maximum} options`);
  14954. }
  14955. return updateData;
  14956. }
  14957. /* -------------------------------------------- */
  14958. /** @override */
  14959. async _updateObject(event, formData) {
  14960. const updateData = this._prepareUpdateData(formData);
  14961. if ( updateData ) this.object.update(updateData);
  14962. }
  14963. }
  14964. /**
  14965. * Override and extend the core ItemSheet implementation to handle specific item types.
  14966. */
  14967. class ItemSheet5e extends ItemSheet {
  14968. constructor(...args) {
  14969. super(...args);
  14970. // Expand the default size of the class sheet
  14971. if ( this.object.type === "class" ) {
  14972. this.options.width = this.position.width = 600;
  14973. this.options.height = this.position.height = 680;
  14974. }
  14975. else if ( this.object.type === "subclass" ) {
  14976. this.options.height = this.position.height = 540;
  14977. }
  14978. }
  14979. /* -------------------------------------------- */
  14980. /** @inheritdoc */
  14981. static get defaultOptions() {
  14982. return foundry.utils.mergeObject(super.defaultOptions, {
  14983. width: 560,
  14984. height: 400,
  14985. classes: ["dnd5e", "sheet", "item"],
  14986. resizable: true,
  14987. scrollY: [
  14988. ".tab[data-tab=details]",
  14989. ".tab[data-tab=effects] .items-list",
  14990. ".tab[data-tab=description] .editor-content",
  14991. ".tab[data-tab=advancement] .items-list",
  14992. ],
  14993. tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}],
  14994. dragDrop: [
  14995. {dragSelector: "[data-effect-id]", dropSelector: ".effects-list"},
  14996. {dragSelector: ".advancement-item", dropSelector: ".advancement"}
  14997. ]
  14998. });
  14999. }
  15000. /* -------------------------------------------- */
  15001. /**
  15002. * Whether advancements on embedded items should be configurable.
  15003. * @type {boolean}
  15004. */
  15005. advancementConfigurationMode = false;
  15006. /* -------------------------------------------- */
  15007. /** @inheritdoc */
  15008. get template() {
  15009. return `systems/dnd5e/templates/items/${this.item.type}.hbs`;
  15010. }
  15011. /* -------------------------------------------- */
  15012. /* Context Preparation */
  15013. /* -------------------------------------------- */
  15014. /** @override */
  15015. async getData(options) {
  15016. const context = await super.getData(options);
  15017. const item = context.item;
  15018. const source = item.toObject();
  15019. // Game system configuration
  15020. context.config = CONFIG.DND5E;
  15021. // Item rendering data
  15022. foundry.utils.mergeObject(context, {
  15023. source: source.system,
  15024. system: item.system,
  15025. labels: item.labels,
  15026. isEmbedded: item.isEmbedded,
  15027. advancementEditable: (this.advancementConfigurationMode || !item.isEmbedded) && context.editable,
  15028. rollData: this.item.getRollData(),
  15029. // Item Type, Status, and Details
  15030. itemType: game.i18n.localize(CONFIG.Item.typeLabels[this.item.type]),
  15031. itemStatus: this._getItemStatus(),
  15032. itemProperties: this._getItemProperties(),
  15033. baseItems: await this._getItemBaseTypes(),
  15034. isPhysical: item.system.hasOwnProperty("quantity"),
  15035. // Action Details
  15036. isHealing: item.system.actionType === "heal",
  15037. isFlatDC: item.system.save?.scaling === "flat",
  15038. isLine: ["line", "wall"].includes(item.system.target?.type),
  15039. // Vehicles
  15040. isCrewed: item.system.activation?.type === "crew",
  15041. // Armor Class
  15042. hasDexModifier: item.isArmor && (item.system.armor?.type !== "shield"),
  15043. // Advancement
  15044. advancement: this._getItemAdvancement(item),
  15045. // Prepare Active Effects
  15046. effects: ActiveEffect5e.prepareActiveEffectCategories(item.effects)
  15047. });
  15048. context.abilityConsumptionTargets = this._getItemConsumptionTargets();
  15049. // Special handling for specific item types
  15050. switch ( item.type ) {
  15051. case "feat":
  15052. const featureType = CONFIG.DND5E.featureTypes[item.system.type?.value];
  15053. if ( featureType ) {
  15054. context.itemType = featureType.label;
  15055. context.featureSubtypes = featureType.subtypes;
  15056. }
  15057. break;
  15058. case "spell":
  15059. context.spellComponents = {...CONFIG.DND5E.spellComponents, ...CONFIG.DND5E.spellTags};
  15060. break;
  15061. }
  15062. // Enrich HTML description
  15063. context.descriptionHTML = await TextEditor.enrichHTML(item.system.description.value, {
  15064. secrets: item.isOwner,
  15065. async: true,
  15066. relativeTo: this.item,
  15067. rollData: context.rollData
  15068. });
  15069. return context;
  15070. }
  15071. /* -------------------------------------------- */
  15072. /**
  15073. * Get the display object used to show the advancement tab.
  15074. * @param {Item5e} item The item for which the advancement is being prepared.
  15075. * @returns {object} Object with advancement data grouped by levels.
  15076. */
  15077. _getItemAdvancement(item) {
  15078. if ( !item.system.advancement ) return {};
  15079. const advancement = {};
  15080. const configMode = !item.parent || this.advancementConfigurationMode;
  15081. const maxLevel = !configMode
  15082. ? (item.system.levels ?? item.class?.system.levels ?? item.parent.system.details?.level ?? -1) : -1;
  15083. // Improperly configured advancements
  15084. if ( item.advancement.needingConfiguration.length ) {
  15085. advancement.unconfigured = {
  15086. items: item.advancement.needingConfiguration.map(a => ({
  15087. id: a.id,
  15088. order: a.constructor.order,
  15089. title: a.title,
  15090. icon: a.icon,
  15091. classRestriction: a.classRestriction,
  15092. configured: false
  15093. })),
  15094. configured: "partial"
  15095. };
  15096. }
  15097. // All other advancements by level
  15098. for ( let [level, advancements] of Object.entries(item.advancement.byLevel) ) {
  15099. if ( !configMode ) advancements = advancements.filter(a => a.appliesToClass);
  15100. const items = advancements.map(advancement => ({
  15101. id: advancement.id,
  15102. order: advancement.sortingValueForLevel(level),
  15103. title: advancement.titleForLevel(level, { configMode }),
  15104. icon: advancement.icon,
  15105. classRestriction: advancement.classRestriction,
  15106. summary: advancement.summaryForLevel(level, { configMode }),
  15107. configured: advancement.configuredForLevel(level)
  15108. }));
  15109. if ( !items.length ) continue;
  15110. advancement[level] = {
  15111. items: items.sort((a, b) => a.order.localeCompare(b.order)),
  15112. configured: (level > maxLevel) ? false : items.some(a => !a.configured) ? "partial" : "full"
  15113. };
  15114. }
  15115. return advancement;
  15116. }
  15117. /* -------------------------------------------- */
  15118. /**
  15119. * Get the base weapons and tools based on the selected type.
  15120. * @returns {Promise<object>} Object with base items for this type formatted for selectOptions.
  15121. * @protected
  15122. */
  15123. async _getItemBaseTypes() {
  15124. const type = this.item.type === "equipment" ? "armor" : this.item.type;
  15125. const baseIds = CONFIG.DND5E[`${type}Ids`];
  15126. if ( baseIds === undefined ) return {};
  15127. const typeProperty = type === "armor" ? "armor.type" : `${type}Type`;
  15128. const baseType = foundry.utils.getProperty(this.item.system, typeProperty);
  15129. const items = {};
  15130. for ( const [name, id] of Object.entries(baseIds) ) {
  15131. const baseItem = await getBaseItem(id);
  15132. if ( baseType !== foundry.utils.getProperty(baseItem?.system, typeProperty) ) continue;
  15133. items[name] = baseItem.name;
  15134. }
  15135. return Object.fromEntries(Object.entries(items).sort((lhs, rhs) => lhs[1].localeCompare(rhs[1])));
  15136. }
  15137. /* -------------------------------------------- */
  15138. /**
  15139. * Get the valid item consumption targets which exist on the actor
  15140. * @returns {Object<string>} An object of potential consumption targets
  15141. * @private
  15142. */
  15143. _getItemConsumptionTargets() {
  15144. const consume = this.item.system.consume || {};
  15145. if ( !consume.type ) return [];
  15146. const actor = this.item.actor;
  15147. if ( !actor ) return {};
  15148. // Ammunition
  15149. if ( consume.type === "ammo" ) {
  15150. return actor.itemTypes.consumable.reduce((ammo, i) => {
  15151. if ( i.system.consumableType === "ammo" ) ammo[i.id] = `${i.name} (${i.system.quantity})`;
  15152. return ammo;
  15153. }, {});
  15154. }
  15155. // Attributes
  15156. else if ( consume.type === "attribute" ) {
  15157. const attrData = game.dnd5e.isV10 ? actor.system : actor.type;
  15158. return TokenDocument.implementation.getConsumedAttributes(attrData).reduce((obj, attr) => {
  15159. obj[attr] = attr;
  15160. return obj;
  15161. }, {});
  15162. }
  15163. // Hit Dice
  15164. else if ( consume.type === "hitDice" ) {
  15165. return {
  15166. smallest: game.i18n.localize("DND5E.ConsumeHitDiceSmallest"),
  15167. ...CONFIG.DND5E.hitDieTypes.reduce((obj, hd) => { obj[hd] = hd; return obj; }, {}),
  15168. largest: game.i18n.localize("DND5E.ConsumeHitDiceLargest")
  15169. };
  15170. }
  15171. // Materials
  15172. else if ( consume.type === "material" ) {
  15173. return actor.items.reduce((obj, i) => {
  15174. if ( ["consumable", "loot"].includes(i.type) && !i.system.activation ) {
  15175. obj[i.id] = `${i.name} (${i.system.quantity})`;
  15176. }
  15177. return obj;
  15178. }, {});
  15179. }
  15180. // Charges
  15181. else if ( consume.type === "charges" ) {
  15182. return actor.items.reduce((obj, i) => {
  15183. // Limited-use items
  15184. const uses = i.system.uses || {};
  15185. if ( uses.per && uses.max ) {
  15186. const label = uses.per === "charges"
  15187. ? ` (${game.i18n.format("DND5E.AbilityUseChargesLabel", {value: uses.value})})`
  15188. : ` (${game.i18n.format("DND5E.AbilityUseConsumableLabel", {max: uses.max, per: uses.per})})`;
  15189. obj[i.id] = i.name + label;
  15190. }
  15191. // Recharging items
  15192. const recharge = i.system.recharge || {};
  15193. if ( recharge.value ) obj[i.id] = `${i.name} (${game.i18n.format("DND5E.Recharge")})`;
  15194. return obj;
  15195. }, {});
  15196. }
  15197. else return {};
  15198. }
  15199. /* -------------------------------------------- */
  15200. /**
  15201. * Get the text item status which is shown beneath the Item type in the top-right corner of the sheet.
  15202. * @returns {string|null} Item status string if applicable to item's type.
  15203. * @protected
  15204. */
  15205. _getItemStatus() {
  15206. switch ( this.item.type ) {
  15207. case "class":
  15208. return game.i18n.format("DND5E.LevelCount", {ordinal: this.item.system.levels.ordinalString()});
  15209. case "equipment":
  15210. case "weapon":
  15211. return game.i18n.localize(this.item.system.equipped ? "DND5E.Equipped" : "DND5E.Unequipped");
  15212. case "feat":
  15213. const typeConfig = CONFIG.DND5E.featureTypes[this.item.system.type.value];
  15214. if ( typeConfig?.subtypes ) return typeConfig.subtypes[this.item.system.type.subtype] ?? null;
  15215. break;
  15216. case "spell":
  15217. return CONFIG.DND5E.spellPreparationModes[this.item.system.preparation];
  15218. case "tool":
  15219. return CONFIG.DND5E.proficiencyLevels[this.item.system.prof?.multiplier || 0];
  15220. }
  15221. return null;
  15222. }
  15223. /* -------------------------------------------- */
  15224. /**
  15225. * Get the Array of item properties which are used in the small sidebar of the description tab.
  15226. * @returns {string[]} List of property labels to be shown.
  15227. * @private
  15228. */
  15229. _getItemProperties() {
  15230. const props = [];
  15231. const labels = this.item.labels;
  15232. switch ( this.item.type ) {
  15233. case "consumable":
  15234. for ( const [k, v] of Object.entries(this.item.system.properties ?? {}) ) {
  15235. if ( v === true ) props.push(CONFIG.DND5E.physicalWeaponProperties[k]);
  15236. }
  15237. break;
  15238. case "equipment":
  15239. props.push(CONFIG.DND5E.equipmentTypes[this.item.system.armor.type]);
  15240. if ( this.item.isArmor || this.item.isMountable ) props.push(labels.armor);
  15241. break;
  15242. case "feat":
  15243. props.push(labels.featType);
  15244. break;
  15245. case "spell":
  15246. props.push(labels.components.vsm, labels.materials, ...labels.components.tags);
  15247. break;
  15248. case "weapon":
  15249. for ( const [k, v] of Object.entries(this.item.system.properties) ) {
  15250. if ( v === true ) props.push(CONFIG.DND5E.weaponProperties[k]);
  15251. }
  15252. break;
  15253. }
  15254. // Action type
  15255. if ( this.item.system.actionType ) {
  15256. props.push(CONFIG.DND5E.itemActionTypes[this.item.system.actionType]);
  15257. }
  15258. // Action usage
  15259. if ( (this.item.type !== "weapon") && !foundry.utils.isEmpty(this.item.system.activation) ) {
  15260. props.push(labels.activation, labels.range, labels.target, labels.duration);
  15261. }
  15262. return props.filter(p => !!p);
  15263. }
  15264. /* -------------------------------------------- */
  15265. /** @inheritDoc */
  15266. setPosition(position={}) {
  15267. if ( !(this._minimized || position.height) ) {
  15268. position.height = (this._tabs[0].active === "details") ? "auto" : Math.max(this.height, this.options.height);
  15269. }
  15270. return super.setPosition(position);
  15271. }
  15272. /* -------------------------------------------- */
  15273. /** @inheritdoc */
  15274. async activateEditor(name, options={}, initialContent="") {
  15275. options.relativeLinks = true;
  15276. options.plugins = {
  15277. menu: ProseMirror.ProseMirrorMenu.build(ProseMirror.defaultSchema, {
  15278. compact: true,
  15279. destroyOnSave: true,
  15280. onSave: () => this.saveEditor(name, {remove: true})
  15281. })
  15282. };
  15283. return super.activateEditor(name, options, initialContent);
  15284. }
  15285. /* -------------------------------------------- */
  15286. /* Form Submission */
  15287. /* -------------------------------------------- */
  15288. /** @inheritDoc */
  15289. _getSubmitData(updateData={}) {
  15290. const formData = foundry.utils.expandObject(super._getSubmitData(updateData));
  15291. // Handle Damage array
  15292. const damage = formData.system?.damage;
  15293. if ( damage ) damage.parts = Object.values(damage?.parts || {}).map(d => [d[0] || "", d[1] || ""]);
  15294. // Check max uses formula
  15295. const uses = formData.system?.uses;
  15296. if ( uses?.max ) {
  15297. const maxRoll = new Roll(uses.max);
  15298. if ( !maxRoll.isDeterministic ) {
  15299. uses.max = this.item._source.system.uses.max;
  15300. this.form.querySelector("input[name='system.uses.max']").value = uses.max;
  15301. return ui.notifications.error(game.i18n.format("DND5E.FormulaCannotContainDiceError", {
  15302. name: game.i18n.localize("DND5E.LimitedUses")
  15303. }));
  15304. }
  15305. }
  15306. // Check duration value formula
  15307. const duration = formData.system?.duration;
  15308. if ( duration?.value ) {
  15309. const durationRoll = new Roll(duration.value);
  15310. if ( !durationRoll.isDeterministic ) {
  15311. duration.value = this.item._source.system.duration.value;
  15312. this.form.querySelector("input[name='system.duration.value']").value = duration.value;
  15313. return ui.notifications.error(game.i18n.format("DND5E.FormulaCannotContainDiceError", {
  15314. name: game.i18n.localize("DND5E.Duration")
  15315. }));
  15316. }
  15317. }
  15318. // Check class identifier
  15319. if ( formData.system?.identifier && !dnd5e.utils.validators.isValidIdentifier(formData.system.identifier) ) {
  15320. formData.system.identifier = this.item._source.system.identifier;
  15321. this.form.querySelector("input[name='system.identifier']").value = formData.system.identifier;
  15322. return ui.notifications.error(game.i18n.localize("DND5E.IdentifierError"));
  15323. }
  15324. // Return the flattened submission data
  15325. return foundry.utils.flattenObject(formData);
  15326. }
  15327. /* -------------------------------------------- */
  15328. /** @inheritDoc */
  15329. activateListeners(html) {
  15330. super.activateListeners(html);
  15331. if ( this.isEditable ) {
  15332. html.find(".damage-control").click(this._onDamageControl.bind(this));
  15333. html.find(".trait-selector").click(this._onConfigureTraits.bind(this));
  15334. html.find(".effect-control").click(ev => {
  15335. const unsupported = game.dnd5e.isV10 && this.item.isOwned;
  15336. if ( unsupported ) return ui.notifications.warn("Managing Active Effects within an Owned Item is not currently supported and will be added in a subsequent update.");
  15337. ActiveEffect5e.onManageActiveEffect(ev, this.item);
  15338. });
  15339. html.find(".advancement .item-control").click(event => {
  15340. const t = event.currentTarget;
  15341. if ( t.dataset.action ) this._onAdvancementAction(t, t.dataset.action);
  15342. });
  15343. }
  15344. // Advancement context menu
  15345. const contextOptions = this._getAdvancementContextMenuOptions();
  15346. /**
  15347. * A hook event that fires when the context menu for the advancements list is constructed.
  15348. * @function dnd5e.getItemAdvancementContext
  15349. * @memberof hookEvents
  15350. * @param {jQuery} html The HTML element to which the context options are attached.
  15351. * @param {ContextMenuEntry[]} entryOptions The context menu entries.
  15352. */
  15353. Hooks.call("dnd5e.getItemAdvancementContext", html, contextOptions);
  15354. if ( contextOptions ) new ContextMenu(html, ".advancement-item", contextOptions);
  15355. }
  15356. /* -------------------------------------------- */
  15357. /**
  15358. * Get the set of ContextMenu options which should be applied for advancement entries.
  15359. * @returns {ContextMenuEntry[]} Context menu entries.
  15360. * @protected
  15361. */
  15362. _getAdvancementContextMenuOptions() {
  15363. const condition = li => (this.advancementConfigurationMode || !this.isEmbedded) && this.isEditable;
  15364. return [
  15365. {
  15366. name: "DND5E.AdvancementControlEdit",
  15367. icon: "<i class='fas fa-edit fa-fw'></i>",
  15368. condition,
  15369. callback: li => this._onAdvancementAction(li[0], "edit")
  15370. },
  15371. {
  15372. name: "DND5E.AdvancementControlDuplicate",
  15373. icon: "<i class='fas fa-copy fa-fw'></i>",
  15374. condition: li => {
  15375. const id = li[0].closest(".advancement-item")?.dataset.id;
  15376. const advancement = this.item.advancement.byId[id];
  15377. return condition() && advancement?.constructor.availableForItem(this.item);
  15378. },
  15379. callback: li => this._onAdvancementAction(li[0], "duplicate")
  15380. },
  15381. {
  15382. name: "DND5E.AdvancementControlDelete",
  15383. icon: "<i class='fas fa-trash fa-fw' style='color: rgb(255, 65, 65);'></i>",
  15384. condition,
  15385. callback: li => this._onAdvancementAction(li[0], "delete")
  15386. }
  15387. ];
  15388. }
  15389. /* -------------------------------------------- */
  15390. /**
  15391. * Add or remove a damage part from the damage formula.
  15392. * @param {Event} event The original click event.
  15393. * @returns {Promise<Item5e>|null} Item with updates applied.
  15394. * @private
  15395. */
  15396. async _onDamageControl(event) {
  15397. event.preventDefault();
  15398. const a = event.currentTarget;
  15399. // Add new damage component
  15400. if ( a.classList.contains("add-damage") ) {
  15401. await this._onSubmit(event); // Submit any unsaved changes
  15402. const damage = this.item.system.damage;
  15403. return this.item.update({"system.damage.parts": damage.parts.concat([["", ""]])});
  15404. }
  15405. // Remove a damage component
  15406. if ( a.classList.contains("delete-damage") ) {
  15407. await this._onSubmit(event); // Submit any unsaved changes
  15408. const li = a.closest(".damage-part");
  15409. const damage = foundry.utils.deepClone(this.item.system.damage);
  15410. damage.parts.splice(Number(li.dataset.damagePart), 1);
  15411. return this.item.update({"system.damage.parts": damage.parts});
  15412. }
  15413. }
  15414. /* -------------------------------------------- */
  15415. /** @inheritdoc */
  15416. _onDragStart(event) {
  15417. const li = event.currentTarget;
  15418. if ( event.target.classList.contains("content-link") ) return;
  15419. // Create drag data
  15420. let dragData;
  15421. // Active Effect
  15422. if ( li.dataset.effectId ) {
  15423. const effect = this.item.effects.get(li.dataset.effectId);
  15424. dragData = effect.toDragData();
  15425. } else if ( li.classList.contains("advancement-item") ) {
  15426. dragData = this.item.advancement.byId[li.dataset.id]?.toDragData();
  15427. }
  15428. if ( !dragData ) return;
  15429. // Set data transfer
  15430. event.dataTransfer.setData("text/plain", JSON.stringify(dragData));
  15431. }
  15432. /* -------------------------------------------- */
  15433. /** @inheritdoc */
  15434. _onDrop(event) {
  15435. const data = TextEditor.getDragEventData(event);
  15436. const item = this.item;
  15437. /**
  15438. * A hook event that fires when some useful data is dropped onto an ItemSheet5e.
  15439. * @function dnd5e.dropItemSheetData
  15440. * @memberof hookEvents
  15441. * @param {Item5e} item The Item5e
  15442. * @param {ItemSheet5e} sheet The ItemSheet5e application
  15443. * @param {object} data The data that has been dropped onto the sheet
  15444. * @returns {boolean} Explicitly return `false` to prevent normal drop handling.
  15445. */
  15446. const allowed = Hooks.call("dnd5e.dropItemSheetData", item, this, data);
  15447. if ( allowed === false ) return;
  15448. switch ( data.type ) {
  15449. case "ActiveEffect":
  15450. return this._onDropActiveEffect(event, data);
  15451. case "Advancement":
  15452. case "Item":
  15453. return this._onDropAdvancement(event, data);
  15454. }
  15455. }
  15456. /* -------------------------------------------- */
  15457. /**
  15458. * Handle the dropping of ActiveEffect data onto an Item Sheet
  15459. * @param {DragEvent} event The concluding DragEvent which contains drop data
  15460. * @param {object} data The data transfer extracted from the event
  15461. * @returns {Promise<ActiveEffect|boolean>} The created ActiveEffect object or false if it couldn't be created.
  15462. * @protected
  15463. */
  15464. async _onDropActiveEffect(event, data) {
  15465. const effect = await ActiveEffect.implementation.fromDropData(data);
  15466. if ( !this.item.isOwner || !effect ) return false;
  15467. if ( (this.item.uuid === effect.parent?.uuid) || (this.item.uuid === effect.origin) ) return false;
  15468. return ActiveEffect.create({
  15469. ...effect.toObject(),
  15470. origin: this.item.uuid
  15471. }, {parent: this.item});
  15472. }
  15473. /* -------------------------------------------- */
  15474. /**
  15475. * Handle the dropping of an advancement or item with advancements onto the advancements tab.
  15476. * @param {DragEvent} event The concluding DragEvent which contains drop data.
  15477. * @param {object} data The data transfer extracted from the event.
  15478. */
  15479. async _onDropAdvancement(event, data) {
  15480. let advancements;
  15481. let showDialog = false;
  15482. if ( data.type === "Advancement" ) {
  15483. advancements = [await fromUuid(data.uuid)];
  15484. } else if ( data.type === "Item" ) {
  15485. const item = await Item.implementation.fromDropData(data);
  15486. if ( !item ) return false;
  15487. advancements = Object.values(item.advancement.byId);
  15488. showDialog = true;
  15489. } else {
  15490. return false;
  15491. }
  15492. advancements = advancements.filter(a => {
  15493. return !this.item.advancement.byId[a.id]
  15494. && a.constructor.metadata.validItemTypes.has(this.item.type)
  15495. && a.constructor.availableForItem(this.item);
  15496. });
  15497. // Display dialog prompting for which advancements to add
  15498. if ( showDialog ) {
  15499. try {
  15500. advancements = await AdvancementMigrationDialog.createDialog(this.item, advancements);
  15501. } catch(err) {
  15502. return false;
  15503. }
  15504. }
  15505. if ( !advancements.length ) return false;
  15506. if ( this.item.isEmbedded && !game.settings.get("dnd5e", "disableAdvancements") ) {
  15507. const manager = AdvancementManager.forNewAdvancement(this.item.actor, this.item.id, advancements);
  15508. if ( manager.steps.length ) return manager.render(true);
  15509. }
  15510. // If no advancements need to be applied, just add them to the item
  15511. const advancementArray = this.item.system.toObject().advancement;
  15512. advancementArray.push(...advancements.map(a => a.toObject()));
  15513. this.item.update({"system.advancement": advancementArray});
  15514. }
  15515. /* -------------------------------------------- */
  15516. /**
  15517. * Handle spawning the TraitSelector application for selection various options.
  15518. * @param {Event} event The click event which originated the selection.
  15519. * @private
  15520. */
  15521. _onConfigureTraits(event) {
  15522. event.preventDefault();
  15523. const a = event.currentTarget;
  15524. const options = {
  15525. name: a.dataset.target,
  15526. title: a.parentElement.innerText,
  15527. choices: [],
  15528. allowCustom: false,
  15529. suppressWarning: true
  15530. };
  15531. switch (a.dataset.options) {
  15532. case "saves":
  15533. options.choices = CONFIG.DND5E.abilities;
  15534. options.valueKey = null;
  15535. options.labelKey = "label";
  15536. break;
  15537. case "skills.choices":
  15538. options.choices = CONFIG.DND5E.skills;
  15539. options.valueKey = null;
  15540. options.labelKey = "label";
  15541. break;
  15542. case "skills":
  15543. const skills = this.item.system.skills;
  15544. const choices = skills.choices?.length ? skills.choices : Object.keys(CONFIG.DND5E.skills);
  15545. options.choices = Object.fromEntries(Object.entries(CONFIG.DND5E.skills).filter(([s]) => choices.includes(s)));
  15546. options.maximum = skills.number;
  15547. options.labelKey = "label";
  15548. break;
  15549. }
  15550. new TraitSelector(this.item, options).render(true);
  15551. }
  15552. /* -------------------------------------------- */
  15553. /**
  15554. * Handle one of the advancement actions from the buttons or context menu.
  15555. * @param {Element} target Button or context menu entry that triggered this action.
  15556. * @param {string} action Action being triggered.
  15557. * @returns {Promise|void}
  15558. */
  15559. _onAdvancementAction(target, action) {
  15560. const id = target.closest(".advancement-item")?.dataset.id;
  15561. const advancement = this.item.advancement.byId[id];
  15562. let manager;
  15563. if ( ["edit", "delete", "duplicate"].includes(action) && !advancement ) return;
  15564. switch (action) {
  15565. case "add": return game.dnd5e.applications.advancement.AdvancementSelection.createDialog(this.item);
  15566. case "edit": return new advancement.constructor.metadata.apps.config(advancement).render(true);
  15567. case "delete":
  15568. if ( this.item.isEmbedded && !game.settings.get("dnd5e", "disableAdvancements") ) {
  15569. manager = AdvancementManager.forDeletedAdvancement(this.item.actor, this.item.id, id);
  15570. if ( manager.steps.length ) return manager.render(true);
  15571. }
  15572. return this.item.deleteAdvancement(id);
  15573. case "duplicate": return this.item.duplicateAdvancement(id);
  15574. case "modify-choices":
  15575. const level = target.closest("li")?.dataset.level;
  15576. manager = AdvancementManager.forModifyChoices(this.item.actor, this.item.id, Number(level));
  15577. if ( manager.steps.length ) manager.render(true);
  15578. return;
  15579. case "toggle-configuration":
  15580. this.advancementConfigurationMode = !this.advancementConfigurationMode;
  15581. return this.render();
  15582. }
  15583. }
  15584. /* -------------------------------------------- */
  15585. /** @inheritdoc */
  15586. async _onSubmit(...args) {
  15587. if ( this._tabs[0].active === "details" ) this.position.height = "auto";
  15588. await super._onSubmit(...args);
  15589. }
  15590. }
  15591. var _module$7 = /*#__PURE__*/Object.freeze({
  15592. __proto__: null,
  15593. AbilityUseDialog: AbilityUseDialog,
  15594. ItemSheet5e: ItemSheet5e
  15595. });
  15596. /**
  15597. * Pop out ProseMirror editor window for journal entries with multiple text areas that need editing.
  15598. *
  15599. * @param {JournalEntryPage} document Journal entry page to be edited.
  15600. * @param {object} options
  15601. * @param {string} options.textKeyPath The path to the specific HTML field being edited.
  15602. */
  15603. class JournalEditor extends DocumentSheet {
  15604. /** @inheritdoc */
  15605. static get defaultOptions() {
  15606. return foundry.utils.mergeObject(super.defaultOptions, {
  15607. classes: ["journal-editor"],
  15608. template: "systems/dnd5e/templates/journal/journal-editor.hbs",
  15609. width: 550,
  15610. height: 640,
  15611. textKeyPath: null,
  15612. resizable: true
  15613. });
  15614. }
  15615. /* -------------------------------------------- */
  15616. /** @inheritdoc */
  15617. get title() {
  15618. if ( this.options.title ) return `${this.document.name}: ${this.options.title}`;
  15619. else return this.document.name;
  15620. }
  15621. /* -------------------------------------------- */
  15622. /** @inheritdoc */
  15623. async getData() {
  15624. const data = super.getData();
  15625. const rawText = foundry.utils.getProperty(this.document, this.options.textKeyPath) ?? "";
  15626. return foundry.utils.mergeObject(data, {
  15627. enriched: await TextEditor.enrichHTML(rawText, {
  15628. relativeTo: this.document, secrets: this.document.isOwner, async: true
  15629. })
  15630. });
  15631. }
  15632. /* -------------------------------------------- */
  15633. /** @inheritdoc */
  15634. _updateObject(event, formData) {
  15635. this.document.update(formData);
  15636. }
  15637. }
  15638. /**
  15639. * Journal entry page that displays an automatically generated summary of a class along with additional description.
  15640. */
  15641. class JournalClassPageSheet extends JournalPageSheet {
  15642. /** @inheritdoc */
  15643. static get defaultOptions() {
  15644. const options = foundry.utils.mergeObject(super.defaultOptions, {
  15645. dragDrop: [{dropSelector: ".drop-target"}],
  15646. submitOnChange: true
  15647. });
  15648. options.classes.push("class-journal");
  15649. return options;
  15650. }
  15651. /* -------------------------------------------- */
  15652. /** @inheritdoc */
  15653. get template() {
  15654. return `systems/dnd5e/templates/journal/page-class-${this.isEditable ? "edit" : "view"}.hbs`;
  15655. }
  15656. /* -------------------------------------------- */
  15657. /** @inheritdoc */
  15658. toc = {};
  15659. /* -------------------------------------------- */
  15660. /** @inheritdoc */
  15661. async getData(options) {
  15662. const context = super.getData(options);
  15663. context.system = context.document.system;
  15664. context.title = Object.fromEntries(
  15665. Array.fromRange(4, 1).map(n => [`level${n}`, context.data.title.level + n - 1])
  15666. );
  15667. const linked = await fromUuid(this.document.system.item);
  15668. context.subclasses = await this._getSubclasses(this.document.system.subclassItems);
  15669. if ( !linked ) return context;
  15670. context.linked = {
  15671. document: linked,
  15672. name: linked.name,
  15673. lowercaseName: linked.name.toLowerCase()
  15674. };
  15675. context.advancement = this._getAdvancement(linked);
  15676. context.enriched = await this._getDescriptions(context.document);
  15677. context.table = await this._getTable(linked);
  15678. context.optionalTable = await this._getOptionalTable(linked);
  15679. context.features = await this._getFeatures(linked);
  15680. context.optionalFeatures = await this._getFeatures(linked, true);
  15681. context.subclasses?.sort((lhs, rhs) => lhs.name.localeCompare(rhs.name));
  15682. return context;
  15683. }
  15684. /* -------------------------------------------- */
  15685. /**
  15686. * Prepare features granted by various advancement types.
  15687. * @param {Item5e} item Class item belonging to this journal.
  15688. * @returns {object} Prepared advancement section.
  15689. */
  15690. _getAdvancement(item) {
  15691. const advancement = {};
  15692. const hp = item.advancement.byType.HitPoints?.[0];
  15693. if ( hp ) {
  15694. advancement.hp = {
  15695. hitDice: `1${hp.hitDie}`,
  15696. max: hp.hitDieValue,
  15697. average: Math.floor(hp.hitDieValue / 2) + 1
  15698. };
  15699. }
  15700. return advancement;
  15701. }
  15702. /* -------------------------------------------- */
  15703. /**
  15704. * Enrich all of the entries within the descriptions object on the sheet's system data.
  15705. * @param {JournalEntryPage} page Journal page being enriched.
  15706. * @returns {Promise<object>} Object with enriched descriptions.
  15707. */
  15708. async _getDescriptions(page) {
  15709. const descriptions = await Promise.all(Object.entries(page.system.description ?? {})
  15710. .map(async ([id, text]) => {
  15711. const enriched = await TextEditor.enrichHTML(text, {
  15712. relativeTo: this.object,
  15713. secrets: this.object.isOwner,
  15714. async: true
  15715. });
  15716. return [id, enriched];
  15717. })
  15718. );
  15719. return Object.fromEntries(descriptions);
  15720. }
  15721. /* -------------------------------------------- */
  15722. /**
  15723. * Prepare table based on non-optional GrantItem advancement & ScaleValue advancement.
  15724. * @param {Item5e} item Class item belonging to this journal.
  15725. * @param {number} [initialLevel=1] Level at which the table begins.
  15726. * @returns {object} Prepared table.
  15727. */
  15728. async _getTable(item, initialLevel=1) {
  15729. const hasFeatures = !!item.advancement.byType.ItemGrant;
  15730. const scaleValues = (item.advancement.byType.ScaleValue ?? []);
  15731. const spellProgression = await this._getSpellProgression(item);
  15732. const headers = [[{content: game.i18n.localize("DND5E.Level")}]];
  15733. if ( item.type === "class" ) headers[0].push({content: game.i18n.localize("DND5E.ProficiencyBonus")});
  15734. if ( hasFeatures ) headers[0].push({content: game.i18n.localize("DND5E.Features")});
  15735. headers[0].push(...scaleValues.map(a => ({content: a.title})));
  15736. if ( spellProgression ) {
  15737. if ( spellProgression.headers.length > 1 ) {
  15738. headers[0].forEach(h => h.rowSpan = 2);
  15739. headers[0].push(...spellProgression.headers[0]);
  15740. headers[1] = spellProgression.headers[1];
  15741. } else {
  15742. headers[0].push(...spellProgression.headers[0]);
  15743. }
  15744. }
  15745. const cols = [{ class: "level", span: 1 }];
  15746. if ( item.type === "class" ) cols.push({class: "prof", span: 1});
  15747. if ( hasFeatures ) cols.push({class: "features", span: 1});
  15748. if ( scaleValues.length ) cols.push({class: "scale", span: scaleValues.length});
  15749. if ( spellProgression ) cols.push(...spellProgression.cols);
  15750. const makeLink = async uuid => (await fromUuid(uuid))?.toAnchor({classes: ["content-link"]}).outerHTML;
  15751. const rows = [];
  15752. for ( const level of Array.fromRange((CONFIG.DND5E.maxLevel - (initialLevel - 1)), initialLevel) ) {
  15753. const features = [];
  15754. for ( const advancement of item.advancement.byLevel[level] ) {
  15755. switch ( advancement.constructor.typeName ) {
  15756. case "AbilityScoreImprovement":
  15757. features.push(game.i18n.localize("DND5E.AdvancementAbilityScoreImprovementTitle"));
  15758. continue;
  15759. case "ItemGrant":
  15760. if ( advancement.configuration.optional ) continue;
  15761. features.push(...await Promise.all(advancement.configuration.items.map(makeLink)));
  15762. break;
  15763. }
  15764. }
  15765. // Level & proficiency bonus
  15766. const cells = [{class: "level", content: level.ordinalString()}];
  15767. if ( item.type === "class" ) cells.push({class: "prof", content: `+${Proficiency.calculateMod(level)}`});
  15768. if ( hasFeatures ) cells.push({class: "features", content: features.join(", ")});
  15769. scaleValues.forEach(s => cells.push({class: "scale", content: s.valueForLevel(level)?.display}));
  15770. const spellCells = spellProgression?.rows[rows.length];
  15771. if ( spellCells ) cells.push(...spellCells);
  15772. // Skip empty rows on subclasses
  15773. if ( (item.type === "subclass") && !features.length && !scaleValues.length && !spellCells ) continue;
  15774. rows.push(cells);
  15775. }
  15776. return { headers, cols, rows };
  15777. }
  15778. /* -------------------------------------------- */
  15779. /**
  15780. * Build out the spell progression data.
  15781. * @param {Item5e} item Class item belonging to this journal.
  15782. * @returns {object} Prepared spell progression table.
  15783. */
  15784. async _getSpellProgression(item) {
  15785. const spellcasting = foundry.utils.deepClone(item.spellcasting);
  15786. if ( !spellcasting || (spellcasting.progression === "none") ) return null;
  15787. const table = { rows: [] };
  15788. if ( spellcasting.type === "leveled" ) {
  15789. const spells = {};
  15790. const maxSpellLevel = CONFIG.DND5E.SPELL_SLOT_TABLE[CONFIG.DND5E.SPELL_SLOT_TABLE.length - 1].length;
  15791. Array.fromRange(maxSpellLevel, 1).forEach(l => spells[`spell${l}`] = {});
  15792. let largestSlot;
  15793. for ( const level of Array.fromRange(CONFIG.DND5E.maxLevel, 1).reverse() ) {
  15794. const progression = { slot: 0 };
  15795. spellcasting.levels = level;
  15796. Actor5e.computeClassProgression(progression, item, { spellcasting });
  15797. Actor5e.prepareSpellcastingSlots(spells, "leveled", progression);
  15798. if ( !largestSlot ) largestSlot = Object.entries(spells).reduce((slot, [key, data]) => {
  15799. if ( !data.max ) return slot;
  15800. const level = parseInt(key.slice(5));
  15801. if ( !Number.isNaN(level) && (level > slot) ) return level;
  15802. return slot;
  15803. }, -1);
  15804. table.rows.push(Array.fromRange(largestSlot, 1).map(spellLevel => {
  15805. return {class: "spell-slots", content: spells[`spell${spellLevel}`]?.max || "&mdash;"};
  15806. }));
  15807. }
  15808. // Prepare headers & columns
  15809. table.headers = [
  15810. [{content: game.i18n.localize("JOURNALENTRYPAGE.DND5E.Class.SpellSlotsPerSpellLevel"), colSpan: largestSlot}],
  15811. Array.fromRange(largestSlot, 1).map(spellLevel => ({content: spellLevel.ordinalString()}))
  15812. ];
  15813. table.cols = [{class: "spellcasting", span: largestSlot}];
  15814. table.rows.reverse();
  15815. }
  15816. else if ( spellcasting.type === "pact" ) {
  15817. const spells = { pact: {} };
  15818. table.headers = [[
  15819. { content: game.i18n.localize("JOURNALENTRYPAGE.DND5E.Class.SpellSlots") },
  15820. { content: game.i18n.localize("JOURNALENTRYPAGE.DND5E.Class.SpellSlotLevel") }
  15821. ]];
  15822. table.cols = [{class: "spellcasting", span: 2}];
  15823. // Loop through each level, gathering "Spell Slots" & "Slot Level" for each one
  15824. for ( const level of Array.fromRange(CONFIG.DND5E.maxLevel, 1) ) {
  15825. const progression = { pact: 0 };
  15826. spellcasting.levels = level;
  15827. Actor5e.computeClassProgression(progression, item, { spellcasting });
  15828. Actor5e.prepareSpellcastingSlots(spells, "pact", progression);
  15829. table.rows.push([
  15830. { class: "spell-slots", content: `${spells.pact.max}` },
  15831. { class: "slot-level", content: spells.pact.level.ordinalString() }
  15832. ]);
  15833. }
  15834. }
  15835. else {
  15836. /**
  15837. * A hook event that fires to generate the table for custom spellcasting types.
  15838. * The actual hook names include the spellcasting type (e.g. `dnd5e.buildPsionicSpellcastingTable`).
  15839. * @param {object} table Table definition being built. *Will be mutated.*
  15840. * @param {Item5e} item Class for which the spellcasting table is being built.
  15841. * @param {SpellcastingDescription} spellcasting Spellcasting descriptive object.
  15842. * @function dnd5e.buildSpellcastingTable
  15843. * @memberof hookEvents
  15844. */
  15845. Hooks.callAll(
  15846. `dnd5e.build${spellcasting.type.capitalize()}SpellcastingTable`, table, item, spellcasting
  15847. );
  15848. }
  15849. return table;
  15850. }
  15851. /* -------------------------------------------- */
  15852. /**
  15853. * Prepare options table based on optional GrantItem advancement.
  15854. * @param {Item5e} item Class item belonging to this journal.
  15855. * @returns {object|null} Prepared optional features table.
  15856. */
  15857. async _getOptionalTable(item) {
  15858. const headers = [[
  15859. { content: game.i18n.localize("DND5E.Level") },
  15860. { content: game.i18n.localize("DND5E.Features") }
  15861. ]];
  15862. const cols = [
  15863. { class: "level", span: 1 },
  15864. { class: "features", span: 1 }
  15865. ];
  15866. const makeLink = async uuid => (await fromUuid(uuid))?.toAnchor({classes: ["content-link"]}).outerHTML;
  15867. const rows = [];
  15868. for ( const level of Array.fromRange(CONFIG.DND5E.maxLevel, 1) ) {
  15869. const features = [];
  15870. for ( const advancement of item.advancement.byLevel[level] ) {
  15871. switch ( advancement.constructor.typeName ) {
  15872. case "ItemGrant":
  15873. if ( !advancement.configuration.optional ) continue;
  15874. features.push(...await Promise.all(advancement.configuration.items.map(makeLink)));
  15875. break;
  15876. }
  15877. }
  15878. if ( !features.length ) continue;
  15879. // Level & proficiency bonus
  15880. const cells = [
  15881. { class: "level", content: level.ordinalString() },
  15882. { class: "features", content: features.join(", ") }
  15883. ];
  15884. rows.push(cells);
  15885. }
  15886. if ( !rows.length ) return null;
  15887. return { headers, cols, rows };
  15888. }
  15889. /* -------------------------------------------- */
  15890. /**
  15891. * Fetch data for each class feature listed.
  15892. * @param {Item5e} item Class or subclass item belonging to this journal.
  15893. * @param {boolean} [optional=false] Should optional features be fetched rather than required features?
  15894. * @returns {object[]} Prepared features.
  15895. */
  15896. async _getFeatures(item, optional=false) {
  15897. const prepareFeature = async uuid => {
  15898. const document = await fromUuid(uuid);
  15899. return {
  15900. document,
  15901. name: document.name,
  15902. description: await TextEditor.enrichHTML(document.system.description.value, {
  15903. relativeTo: item, secrets: false, async: true
  15904. })
  15905. };
  15906. };
  15907. let features = [];
  15908. for ( const advancement of item.advancement.byType.ItemGrant ?? [] ) {
  15909. if ( !!advancement.configuration.optional !== optional ) continue;
  15910. features.push(...advancement.configuration.items.map(prepareFeature));
  15911. }
  15912. features = await Promise.all(features);
  15913. return features;
  15914. }
  15915. /* -------------------------------------------- */
  15916. /**
  15917. * Fetch each subclass and their features.
  15918. * @param {string[]} uuids UUIDs for the subclasses to fetch.
  15919. * @returns {object[]|null} Prepared subclasses.
  15920. */
  15921. async _getSubclasses(uuids) {
  15922. const prepareSubclass = async uuid => {
  15923. const document = await fromUuid(uuid);
  15924. return this._getSubclass(document);
  15925. };
  15926. const subclasses = await Promise.all(uuids.map(prepareSubclass));
  15927. return subclasses.length ? subclasses : null;
  15928. }
  15929. /* -------------------------------------------- */
  15930. /**
  15931. * Prepare data for the provided subclass.
  15932. * @param {Item5e} item Subclass item being prepared.
  15933. * @returns {object} Presentation data for this subclass.
  15934. */
  15935. async _getSubclass(item) {
  15936. const initialLevel = Object.entries(item.advancement.byLevel).find(([lvl, d]) => d.length)?.[0] ?? 1;
  15937. return {
  15938. document: item,
  15939. name: item.name,
  15940. description: await TextEditor.enrichHTML(item.system.description.value, {
  15941. relativeTo: item, secrets: false, async: true
  15942. }),
  15943. features: await this._getFeatures(item),
  15944. table: await this._getTable(item, parseInt(initialLevel))
  15945. };
  15946. }
  15947. /* -------------------------------------------- */
  15948. /* Rendering */
  15949. /* -------------------------------------------- */
  15950. /** @inheritdoc */
  15951. async _renderInner(...args) {
  15952. const html = await super._renderInner(...args);
  15953. this.toc = JournalEntryPage.buildTOC(html.get());
  15954. return html;
  15955. }
  15956. /* -------------------------------------------- */
  15957. /* Event Handlers */
  15958. /* -------------------------------------------- */
  15959. /** @inheritdoc */
  15960. activateListeners(html) {
  15961. super.activateListeners(html);
  15962. html[0].querySelectorAll(".item-delete").forEach(e => {
  15963. e.addEventListener("click", this._onDeleteItem.bind(this));
  15964. });
  15965. html[0].querySelectorAll(".launch-text-editor").forEach(e => {
  15966. e.addEventListener("click", this._onLaunchTextEditor.bind(this));
  15967. });
  15968. }
  15969. /* -------------------------------------------- */
  15970. /**
  15971. * Handle deleting a dropped item.
  15972. * @param {Event} event The triggering click event.
  15973. * @returns {JournalClassSummary5ePageSheet}
  15974. */
  15975. async _onDeleteItem(event) {
  15976. event.preventDefault();
  15977. const container = event.currentTarget.closest("[data-item-uuid]");
  15978. const uuidToDelete = container?.dataset.itemUuid;
  15979. if ( !uuidToDelete ) return;
  15980. switch (container.dataset.itemType) {
  15981. case "class":
  15982. await this.document.update({"system.item": ""});
  15983. return this.render();
  15984. case "subclass":
  15985. const itemSet = this.document.system.subclassItems;
  15986. itemSet.delete(uuidToDelete);
  15987. await this.document.update({"system.subclassItems": Array.from(itemSet)});
  15988. return this.render();
  15989. }
  15990. }
  15991. /* -------------------------------------------- */
  15992. /**
  15993. * Handle launching the individual text editing window.
  15994. * @param {Event} event The triggering click event.
  15995. */
  15996. _onLaunchTextEditor(event) {
  15997. event.preventDefault();
  15998. const textKeyPath = event.currentTarget.dataset.target;
  15999. const label = event.target.closest(".form-group").querySelector("label");
  16000. const editor = new JournalEditor(this.document, { textKeyPath, title: label?.innerText });
  16001. editor.render(true);
  16002. }
  16003. /* -------------------------------------------- */
  16004. /** @inheritdoc */
  16005. async _onDrop(event) {
  16006. const data = TextEditor.getDragEventData(event);
  16007. if ( data?.type !== "Item" ) return false;
  16008. const item = await Item.implementation.fromDropData(data);
  16009. switch ( item.type ) {
  16010. case "class":
  16011. await this.document.update({"system.item": item.uuid});
  16012. return this.render();
  16013. case "subclass":
  16014. const itemSet = this.document.system.subclassItems;
  16015. itemSet.add(item.uuid);
  16016. await this.document.update({"system.subclassItems": Array.from(itemSet)});
  16017. return this.render();
  16018. default:
  16019. return false;
  16020. }
  16021. }
  16022. }
  16023. class SRDCompendium extends Compendium {
  16024. /** @inheritdoc */
  16025. static get defaultOptions() {
  16026. return foundry.utils.mergeObject(super.defaultOptions, {
  16027. classes: ["srd-compendium"],
  16028. template: "systems/dnd5e/templates/journal/srd-compendium.hbs",
  16029. width: 800,
  16030. height: 950,
  16031. resizable: true
  16032. });
  16033. }
  16034. /* -------------------------------------------- */
  16035. /**
  16036. * The IDs of some special pages that we use when configuring the display of the compendium.
  16037. * @type {Object<string>}
  16038. * @protected
  16039. */
  16040. static _SPECIAL_PAGES = {
  16041. disclaimer: "xxt7YT2t76JxNTel",
  16042. magicItemList: "sfJtvPjEs50Ruzi4",
  16043. spellList: "plCB5ei1JbVtBseb"
  16044. };
  16045. /* -------------------------------------------- */
  16046. /** @inheritdoc */
  16047. async getData(options) {
  16048. const data = await super.getData(options);
  16049. const documents = await this.collection.getDocuments();
  16050. const getOrder = o => ({chapter: 0, appendix: 100}[o.flags?.dnd5e?.type] ?? 200) + (o.flags?.dnd5e?.position ?? 0);
  16051. data.disclaimer = this.collection.get(this.constructor._SPECIAL_PAGES.disclaimer).pages.contents[0].text.content;
  16052. data.chapters = documents.reduce((arr, entry) => {
  16053. const type = entry.getFlag("dnd5e", "type");
  16054. if ( !type ) return arr;
  16055. const e = entry.toObject();
  16056. e.showPages = (e.pages.length > 1) && (type === "chapter");
  16057. arr.push(e);
  16058. return arr;
  16059. }, []).sort((a, b) => getOrder(a) - getOrder(b));
  16060. // Add spells A-Z to the end of Chapter 10.
  16061. const spellList = this.collection.get(this.constructor._SPECIAL_PAGES.spellList);
  16062. data.chapters[9].pages.push({_id: spellList.id, name: spellList.name, entry: true});
  16063. // Add magic items A-Z to the end of Chapter 11.
  16064. const magicItemList = this.collection.get(this.constructor._SPECIAL_PAGES.magicItemList);
  16065. data.chapters[10].pages.push({_id: magicItemList.id, name: magicItemList.name, entry: true});
  16066. return data;
  16067. }
  16068. /* -------------------------------------------- */
  16069. /** @inheritdoc */
  16070. activateListeners(html) {
  16071. super.activateListeners(html);
  16072. html.find("a").on("click", this._onClickLink.bind(this));
  16073. }
  16074. /* -------------------------------------------- */
  16075. /**
  16076. * Handle clicking a link to a journal entry or page.
  16077. * @param {MouseEvent} event The triggering click event.
  16078. * @protected
  16079. */
  16080. async _onClickLink(event) {
  16081. const target = event.currentTarget;
  16082. const entryId = target.closest("[data-entry-id]")?.dataset.entryId;
  16083. const pageId = target.closest("[data-page-id]")?.dataset.pageId;
  16084. if ( !entryId ) return;
  16085. const options = {};
  16086. if ( pageId ) options.pageId = pageId;
  16087. const entry = await this.collection.getDocument(entryId);
  16088. entry?.sheet.render(true, options);
  16089. }
  16090. }
  16091. var _module$6 = /*#__PURE__*/Object.freeze({
  16092. __proto__: null,
  16093. JournalClassPageSheet: JournalClassPageSheet,
  16094. JournalEditor: JournalEditor,
  16095. SRDCompendium: SRDCompendium
  16096. });
  16097. /**
  16098. * @deprecated since dnd5e 2.1, targeted for removal in 2.3
  16099. */
  16100. class DamageTraitSelector extends TraitSelector {
  16101. /** @inheritdoc */
  16102. static get defaultOptions() {
  16103. return foundry.utils.mergeObject(super.defaultOptions, {
  16104. template: "systems/dnd5e/templates/apps/damage-trait-selector.hbs"
  16105. });
  16106. }
  16107. /* -------------------------------------------- */
  16108. /** @override */
  16109. getData() {
  16110. const data = super.getData();
  16111. const attr = foundry.utils.getProperty(this.object, this.attribute);
  16112. data.bypasses = Object.entries(this.options.bypasses).reduce((obj, [k, v]) => {
  16113. obj[k] = { label: v, chosen: attr ? attr.bypasses.includes(k) : false };
  16114. return obj;
  16115. }, {});
  16116. return data;
  16117. }
  16118. /* -------------------------------------------- */
  16119. /** @override */
  16120. async _updateObject(event, formData) {
  16121. const data = foundry.utils.expandObject(formData);
  16122. const updateData = this._prepareUpdateData(data.choices);
  16123. if ( !updateData ) return;
  16124. updateData[`${this.attribute}.bypasses`] = Object.entries(data.bypasses).filter(([, v]) => v).map(([k]) => k);
  16125. this.object.update(updateData);
  16126. }
  16127. }
  16128. /**
  16129. * An application for selecting proficiencies with categories that can contain children.
  16130. * @deprecated since dnd5e 2.1, targeted for removal in 2.3
  16131. */
  16132. class ProficiencySelector extends TraitSelector {
  16133. /** @inheritdoc */
  16134. static get defaultOptions() {
  16135. return foundry.utils.mergeObject(super.defaultOptions, {
  16136. title: "Actor Proficiency Selection",
  16137. type: ""
  16138. });
  16139. }
  16140. /* -------------------------------------------- */
  16141. /** @inheritdoc */
  16142. async getData() {
  16143. const attr = foundry.utils.getProperty(this.object, this.attribute);
  16144. const chosen = (this.options.valueKey) ? foundry.utils.getProperty(attr, this.options.valueKey) ?? [] : attr;
  16145. const data = super.getData();
  16146. data.choices = await choices(this.options.type, chosen);
  16147. return data;
  16148. }
  16149. /* -------------------------------------------- */
  16150. /**
  16151. * A static helper method to get a list of choices for a proficiency type.
  16152. *
  16153. * @param {string} type Proficiency type to select, either `armor`, `tool`, or `weapon`.
  16154. * @param {string[]} [chosen] Optional list of items to be marked as chosen.
  16155. * @returns {Object<string, SelectChoices>} Object mapping proficiency ids to choice objects.
  16156. * @deprecated since dnd5e 2.1, targeted for removal in 2.3
  16157. */
  16158. static async getChoices(type, chosen=[]) {
  16159. foundry.utils.logCompatibilityWarning(
  16160. "ProficiencySelector#getChoices has been deprecated in favor of Trait#choices.",
  16161. { since: "DnD5e 2.1", until: "DnD5e 2.3" }
  16162. );
  16163. return choices(type, chosen);
  16164. }
  16165. /* -------------------------------------------- */
  16166. /**
  16167. * Fetch an item for the provided ID. If the provided ID contains a compendium pack name
  16168. * it will be fetched from that pack, otherwise it will be fetched from the compendium defined
  16169. * in `DND5E.sourcePacks.ITEMS`.
  16170. *
  16171. * @param {string} identifier Simple ID or compendium name and ID separated by a dot.
  16172. * @param {object} [options]
  16173. * @param {boolean} [options.indexOnly] If set to true, only the index data will be fetched (will never return
  16174. * Promise).
  16175. * @param {boolean} [options.fullItem] If set to true, the full item will be returned as long as `indexOnly` is
  16176. * false.
  16177. * @returns {Promise<Item5e>|object} Promise for a `Document` if `indexOnly` is false & `fullItem` is true,
  16178. * otherwise else a simple object containing the minimal index data.
  16179. * @deprecated since dnd5e 2.1, targeted for removal in 2.3
  16180. */
  16181. static getBaseItem(identifier, options) {
  16182. foundry.utils.logCompatibilityWarning(
  16183. "ProficiencySelector#getBaseItem has been deprecated in favor of Trait#getBaseItem.",
  16184. { since: "DnD5e 2.1", until: "DnD5e 2.3" }
  16185. );
  16186. return getBaseItem(identifier, options);
  16187. }
  16188. /* -------------------------------------------- */
  16189. /** @inheritdoc */
  16190. activateListeners(html) {
  16191. super.activateListeners(html);
  16192. for ( const checkbox of html[0].querySelectorAll("input[type='checkbox']") ) {
  16193. if ( checkbox.checked ) this._onToggleCategory(checkbox);
  16194. }
  16195. }
  16196. /* -------------------------------------------- */
  16197. /** @inheritdoc */
  16198. async _onChangeInput(event) {
  16199. super._onChangeInput(event);
  16200. if ( event.target.tagName === "INPUT" ) this._onToggleCategory(event.target);
  16201. }
  16202. /* -------------------------------------------- */
  16203. /**
  16204. * Enable/disable all children when a category is checked.
  16205. *
  16206. * @param {HTMLElement} checkbox Checkbox that was changed.
  16207. * @private
  16208. */
  16209. _onToggleCategory(checkbox) {
  16210. const children = checkbox.closest("li")?.querySelector("ol");
  16211. if ( !children ) return;
  16212. for ( const child of children.querySelectorAll("input[type='checkbox']") ) {
  16213. child.checked = child.disabled = checkbox.checked;
  16214. }
  16215. }
  16216. }
  16217. var applications = /*#__PURE__*/Object.freeze({
  16218. __proto__: null,
  16219. DamageTraitSelector: DamageTraitSelector,
  16220. ProficiencySelector: ProficiencySelector,
  16221. PropertyAttribution: PropertyAttribution,
  16222. TraitSelector: TraitSelector,
  16223. actor: _module$a,
  16224. advancement: _module$9,
  16225. combat: _module$8,
  16226. item: _module$7,
  16227. journal: _module$6
  16228. });
  16229. /**
  16230. * A helper class for building MeasuredTemplates for 5e spells and abilities
  16231. */
  16232. class AbilityTemplate extends MeasuredTemplate {
  16233. /**
  16234. * Track the timestamp when the last mouse move event was captured.
  16235. * @type {number}
  16236. */
  16237. #moveTime = 0;
  16238. /* -------------------------------------------- */
  16239. /**
  16240. * The initially active CanvasLayer to re-activate after the workflow is complete.
  16241. * @type {CanvasLayer}
  16242. */
  16243. #initialLayer;
  16244. /* -------------------------------------------- */
  16245. /**
  16246. * Track the bound event handlers so they can be properly canceled later.
  16247. * @type {object}
  16248. */
  16249. #events;
  16250. /* -------------------------------------------- */
  16251. /**
  16252. * A factory method to create an AbilityTemplate instance using provided data from an Item5e instance
  16253. * @param {Item5e} item The Item object for which to construct the template
  16254. * @returns {AbilityTemplate|null} The template object, or null if the item does not produce a template
  16255. */
  16256. static fromItem(item) {
  16257. const target = item.system.target ?? {};
  16258. const templateShape = dnd5e.config.areaTargetTypes[target.type]?.template;
  16259. if ( !templateShape ) return null;
  16260. // Prepare template data
  16261. const templateData = {
  16262. t: templateShape,
  16263. user: game.user.id,
  16264. distance: target.value,
  16265. direction: 0,
  16266. x: 0,
  16267. y: 0,
  16268. fillColor: game.user.color,
  16269. flags: { dnd5e: { origin: item.uuid } }
  16270. };
  16271. // Additional type-specific data
  16272. switch ( templateShape ) {
  16273. case "cone":
  16274. templateData.angle = CONFIG.MeasuredTemplate.defaults.angle;
  16275. break;
  16276. case "rect": // 5e rectangular AoEs are always cubes
  16277. templateData.distance = Math.hypot(target.value, target.value);
  16278. templateData.width = target.value;
  16279. templateData.direction = 45;
  16280. break;
  16281. case "ray": // 5e rays are most commonly 1 square (5 ft) in width
  16282. templateData.width = target.width ?? canvas.dimensions.distance;
  16283. break;
  16284. }
  16285. // Return the template constructed from the item data
  16286. const cls = CONFIG.MeasuredTemplate.documentClass;
  16287. const template = new cls(templateData, {parent: canvas.scene});
  16288. const object = new this(template);
  16289. object.item = item;
  16290. object.actorSheet = item.actor?.sheet || null;
  16291. return object;
  16292. }
  16293. /* -------------------------------------------- */
  16294. /**
  16295. * Creates a preview of the spell template.
  16296. * @returns {Promise} A promise that resolves with the final measured template if created.
  16297. */
  16298. drawPreview() {
  16299. const initialLayer = canvas.activeLayer;
  16300. // Draw the template and switch to the template layer
  16301. this.draw();
  16302. this.layer.activate();
  16303. this.layer.preview.addChild(this);
  16304. // Hide the sheet that originated the preview
  16305. this.actorSheet?.minimize();
  16306. // Activate interactivity
  16307. return this.activatePreviewListeners(initialLayer);
  16308. }
  16309. /* -------------------------------------------- */
  16310. /**
  16311. * Activate listeners for the template preview
  16312. * @param {CanvasLayer} initialLayer The initially active CanvasLayer to re-activate after the workflow is complete
  16313. * @returns {Promise} A promise that resolves with the final measured template if created.
  16314. */
  16315. activatePreviewListeners(initialLayer) {
  16316. return new Promise((resolve, reject) => {
  16317. this.#initialLayer = initialLayer;
  16318. this.#events = {
  16319. cancel: this._onCancelPlacement.bind(this),
  16320. confirm: this._onConfirmPlacement.bind(this),
  16321. move: this._onMovePlacement.bind(this),
  16322. resolve,
  16323. reject,
  16324. rotate: this._onRotatePlacement.bind(this)
  16325. };
  16326. // Activate listeners
  16327. canvas.stage.on("mousemove", this.#events.move);
  16328. canvas.stage.on("mousedown", this.#events.confirm);
  16329. canvas.app.view.oncontextmenu = this.#events.cancel;
  16330. canvas.app.view.onwheel = this.#events.rotate;
  16331. });
  16332. }
  16333. /* -------------------------------------------- */
  16334. /**
  16335. * Shared code for when template placement ends by being confirmed or canceled.
  16336. * @param {Event} event Triggering event that ended the placement.
  16337. */
  16338. async _finishPlacement(event) {
  16339. this.layer._onDragLeftCancel(event);
  16340. canvas.stage.off("mousemove", this.#events.move);
  16341. canvas.stage.off("mousedown", this.#events.confirm);
  16342. canvas.app.view.oncontextmenu = null;
  16343. canvas.app.view.onwheel = null;
  16344. this.#initialLayer.activate();
  16345. await this.actorSheet?.maximize();
  16346. }
  16347. /* -------------------------------------------- */
  16348. /**
  16349. * Move the template preview when the mouse moves.
  16350. * @param {Event} event Triggering mouse event.
  16351. */
  16352. _onMovePlacement(event) {
  16353. event.stopPropagation();
  16354. const now = Date.now(); // Apply a 20ms throttle
  16355. if ( now - this.#moveTime <= 20 ) return;
  16356. const center = event.data.getLocalPosition(this.layer);
  16357. const interval = canvas.grid.type === CONST.GRID_TYPES.GRIDLESS ? 0 : 2;
  16358. const snapped = canvas.grid.getSnappedPosition(center.x, center.y, interval);
  16359. this.document.updateSource({x: snapped.x, y: snapped.y});
  16360. this.refresh();
  16361. this.#moveTime = now;
  16362. }
  16363. /* -------------------------------------------- */
  16364. /**
  16365. * Rotate the template preview by 3˚ increments when the mouse wheel is rotated.
  16366. * @param {Event} event Triggering mouse event.
  16367. */
  16368. _onRotatePlacement(event) {
  16369. if ( event.ctrlKey ) event.preventDefault(); // Avoid zooming the browser window
  16370. event.stopPropagation();
  16371. const delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15;
  16372. const snap = event.shiftKey ? delta : 5;
  16373. const update = {direction: this.document.direction + (snap * Math.sign(event.deltaY))};
  16374. this.document.updateSource(update);
  16375. this.refresh();
  16376. }
  16377. /* -------------------------------------------- */
  16378. /**
  16379. * Confirm placement when the left mouse button is clicked.
  16380. * @param {Event} event Triggering mouse event.
  16381. */
  16382. async _onConfirmPlacement(event) {
  16383. await this._finishPlacement(event);
  16384. const interval = canvas.grid.type === CONST.GRID_TYPES.GRIDLESS ? 0 : 2;
  16385. const destination = canvas.grid.getSnappedPosition(this.document.x, this.document.y, interval);
  16386. this.document.updateSource(destination);
  16387. this.#events.resolve(canvas.scene.createEmbeddedDocuments("MeasuredTemplate", [this.document.toObject()]));
  16388. }
  16389. /* -------------------------------------------- */
  16390. /**
  16391. * Cancel placement when the right mouse button is clicked.
  16392. * @param {Event} event Triggering mouse event.
  16393. */
  16394. async _onCancelPlacement(event) {
  16395. await this._finishPlacement(event);
  16396. this.#events.reject();
  16397. }
  16398. }
  16399. /**
  16400. * The detection mode for Blindsight.
  16401. */
  16402. class DetectionModeBlindsight extends DetectionMode {
  16403. constructor() {
  16404. super({
  16405. id: "blindsight",
  16406. label: "DND5E.SenseBlindsight",
  16407. type: DetectionMode.DETECTION_TYPES.OTHER,
  16408. walls: true,
  16409. angle: false
  16410. });
  16411. }
  16412. /** @override */
  16413. static getDetectionFilter() {
  16414. return this._detectionFilter ??= OutlineOverlayFilter.create({
  16415. outlineColor: [1, 1, 1, 1],
  16416. knockout: true,
  16417. wave: true
  16418. });
  16419. }
  16420. /** @override */
  16421. _canDetect(visionSource, target) {
  16422. // Blindsight can detect anything.
  16423. return true;
  16424. }
  16425. /** @override */
  16426. _testLOS(visionSource, mode, target, test) {
  16427. const polygonBackend = foundry.utils.isNewerVersion(game.version, 11)
  16428. ? CONFIG.Canvas.polygonBackends.sight
  16429. : CONFIG.Canvas.losBackend;
  16430. return !polygonBackend.testCollision(
  16431. { x: visionSource.x, y: visionSource.y },
  16432. test.point,
  16433. {
  16434. type: "sight",
  16435. mode: "any",
  16436. source: visionSource,
  16437. // Blindsight is restricted by total cover and therefore cannot see
  16438. // through windows. So we do not want blindsight to see through
  16439. // a window as we get close to it. That's why we ignore thresholds.
  16440. // We make the assumption that all windows are configured as threshold
  16441. // walls. A move-based visibility check would also be an option to check
  16442. // for total cover, but this would have the undesirable side effect that
  16443. // blindsight wouldn't work through fences, portcullises, etc.
  16444. useThreshold: false
  16445. }
  16446. );
  16447. }
  16448. }
  16449. CONFIG.Canvas.detectionModes.blindsight = new DetectionModeBlindsight();
  16450. var _module$5 = /*#__PURE__*/Object.freeze({
  16451. __proto__: null,
  16452. DetectionModeBlindsight: DetectionModeBlindsight
  16453. });
  16454. /**
  16455. * Extend the base Token class to implement additional system-specific logic.
  16456. */
  16457. class Token5e extends Token {
  16458. /** @inheritdoc */
  16459. _drawBar(number, bar, data) {
  16460. if ( data.attribute === "attributes.hp" ) return this._drawHPBar(number, bar, data);
  16461. return super._drawBar(number, bar, data);
  16462. }
  16463. /* -------------------------------------------- */
  16464. /**
  16465. * Specialized drawing function for HP bars.
  16466. * @param {number} number The Bar number
  16467. * @param {PIXI.Graphics} bar The Bar container
  16468. * @param {object} data Resource data for this bar
  16469. * @private
  16470. */
  16471. _drawHPBar(number, bar, data) {
  16472. // Extract health data
  16473. let {value, max, temp, tempmax} = this.document.actor.system.attributes.hp;
  16474. temp = Number(temp || 0);
  16475. tempmax = Number(tempmax || 0);
  16476. // Differentiate between effective maximum and displayed maximum
  16477. const effectiveMax = Math.max(0, max + tempmax);
  16478. let displayMax = max + (tempmax > 0 ? tempmax : 0);
  16479. // Allocate percentages of the total
  16480. const tempPct = Math.clamped(temp, 0, displayMax) / displayMax;
  16481. const colorPct = Math.clamped(value, 0, effectiveMax) / displayMax;
  16482. const hpColor = dnd5e.documents.Actor5e.getHPColor(value, effectiveMax);
  16483. // Determine colors to use
  16484. const blk = 0x000000;
  16485. const c = CONFIG.DND5E.tokenHPColors;
  16486. // Determine the container size (logic borrowed from core)
  16487. const w = this.w;
  16488. let h = Math.max((canvas.dimensions.size / 12), 8);
  16489. if ( this.document.height >= 2 ) h *= 1.6;
  16490. const bs = Math.clamped(h / 8, 1, 2);
  16491. const bs1 = bs+1;
  16492. // Overall bar container
  16493. bar.clear();
  16494. bar.beginFill(blk, 0.5).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, w, h, 3);
  16495. // Temporary maximum HP
  16496. if (tempmax > 0) {
  16497. const pct = max / effectiveMax;
  16498. bar.beginFill(c.tempmax, 1.0).lineStyle(1, blk, 1.0).drawRoundedRect(pct*w, 0, (1-pct)*w, h, 2);
  16499. }
  16500. // Maximum HP penalty
  16501. else if (tempmax < 0) {
  16502. const pct = (max + tempmax) / max;
  16503. bar.beginFill(c.negmax, 1.0).lineStyle(1, blk, 1.0).drawRoundedRect(pct*w, 0, (1-pct)*w, h, 2);
  16504. }
  16505. // Health bar
  16506. bar.beginFill(hpColor, 1.0).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, colorPct*w, h, 2);
  16507. // Temporary hit points
  16508. if ( temp > 0 ) {
  16509. bar.beginFill(c.temp, 1.0).lineStyle(0).drawRoundedRect(bs1, bs1, (tempPct*w)-(2*bs1), h-(2*bs1), 1);
  16510. }
  16511. // Set position
  16512. let posY = (number === 0) ? (this.h - h) : 0;
  16513. bar.position.set(0, posY);
  16514. }
  16515. }
  16516. /** @inheritDoc */
  16517. function measureDistances(segments, options={}) {
  16518. if ( !options.gridSpaces ) return BaseGrid.prototype.measureDistances.call(this, segments, options);
  16519. // Track the total number of diagonals
  16520. let nDiagonal = 0;
  16521. const rule = this.parent.diagonalRule;
  16522. const d = canvas.dimensions;
  16523. // Iterate over measured segments
  16524. return segments.map(s => {
  16525. let r = s.ray;
  16526. // Determine the total distance traveled
  16527. let nx = Math.ceil(Math.abs(r.dx / d.size));
  16528. let ny = Math.ceil(Math.abs(r.dy / d.size));
  16529. // Determine the number of straight and diagonal moves
  16530. let nd = Math.min(nx, ny);
  16531. let ns = Math.abs(ny - nx);
  16532. nDiagonal += nd;
  16533. // Alternative DMG Movement
  16534. if (rule === "5105") {
  16535. let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2);
  16536. let spaces = (nd10 * 2) + (nd - nd10) + ns;
  16537. return spaces * canvas.dimensions.distance;
  16538. }
  16539. // Euclidean Measurement
  16540. else if (rule === "EUCL") {
  16541. return Math.hypot(nx, ny) * canvas.scene.grid.distance;
  16542. }
  16543. // Standard PHB Movement
  16544. else return (ns + nd) * canvas.scene.grid.distance;
  16545. });
  16546. }
  16547. var canvas$1 = /*#__PURE__*/Object.freeze({
  16548. __proto__: null,
  16549. AbilityTemplate: AbilityTemplate,
  16550. Token5e: Token5e,
  16551. detectionModes: _module$5,
  16552. measureDistances: measureDistances
  16553. });
  16554. /**
  16555. * Shared contents of the attributes schema between various actor types.
  16556. */
  16557. class AttributesFields {
  16558. /**
  16559. * Fields shared between characters, NPCs, and vehicles.
  16560. *
  16561. * @type {object}
  16562. * @property {object} init
  16563. * @property {number} init.value Calculated initiative modifier.
  16564. * @property {number} init.bonus Fixed bonus provided to initiative rolls.
  16565. * @property {object} movement
  16566. * @property {number} movement.burrow Actor burrowing speed.
  16567. * @property {number} movement.climb Actor climbing speed.
  16568. * @property {number} movement.fly Actor flying speed.
  16569. * @property {number} movement.swim Actor swimming speed.
  16570. * @property {number} movement.walk Actor walking speed.
  16571. * @property {string} movement.units Movement used to measure the various speeds.
  16572. * @property {boolean} movement.hover Is this flying creature able to hover in place.
  16573. */
  16574. static get common() {
  16575. return {
  16576. init: new foundry.data.fields.SchemaField({
  16577. ability: new foundry.data.fields.StringField({label: "DND5E.AbilityModifier"}),
  16578. bonus: new FormulaField({label: "DND5E.InitiativeBonus"})
  16579. }, { label: "DND5E.Initiative" }),
  16580. movement: new foundry.data.fields.SchemaField({
  16581. burrow: new foundry.data.fields.NumberField({
  16582. nullable: false, min: 0, step: 0.1, initial: 0, label: "DND5E.MovementBurrow"
  16583. }),
  16584. climb: new foundry.data.fields.NumberField({
  16585. nullable: false, min: 0, step: 0.1, initial: 0, label: "DND5E.MovementClimb"
  16586. }),
  16587. fly: new foundry.data.fields.NumberField({
  16588. nullable: false, min: 0, step: 0.1, initial: 0, label: "DND5E.MovementFly"
  16589. }),
  16590. swim: new foundry.data.fields.NumberField({
  16591. nullable: false, min: 0, step: 0.1, initial: 0, label: "DND5E.MovementSwim"
  16592. }),
  16593. walk: new foundry.data.fields.NumberField({
  16594. nullable: false, min: 0, step: 0.1, initial: 30, label: "DND5E.MovementWalk"
  16595. }),
  16596. units: new foundry.data.fields.StringField({initial: "ft", label: "DND5E.MovementUnits"}),
  16597. hover: new foundry.data.fields.BooleanField({label: "DND5E.MovementHover"})
  16598. }, {label: "DND5E.Movement"})
  16599. };
  16600. }
  16601. /* -------------------------------------------- */
  16602. /**
  16603. * Fields shared between characters and NPCs.
  16604. *
  16605. * @type {object}
  16606. * @property {object} attunement
  16607. * @property {number} attunement.max Maximum number of attuned items.
  16608. * @property {object} senses
  16609. * @property {number} senses.darkvision Creature's darkvision range.
  16610. * @property {number} senses.blindsight Creature's blindsight range.
  16611. * @property {number} senses.tremorsense Creature's tremorsense range.
  16612. * @property {number} senses.truesight Creature's truesight range.
  16613. * @property {string} senses.units Distance units used to measure senses.
  16614. * @property {string} senses.special Description of any special senses or restrictions.
  16615. * @property {string} spellcasting Primary spellcasting ability.
  16616. */
  16617. static get creature() {
  16618. return {
  16619. attunement: new foundry.data.fields.SchemaField({
  16620. max: new foundry.data.fields.NumberField({
  16621. required: true, nullable: false, integer: true, min: 0, initial: 3, label: "DND5E.AttunementMax"
  16622. })
  16623. }, {label: "DND5E.Attunement"}),
  16624. senses: new foundry.data.fields.SchemaField({
  16625. darkvision: new foundry.data.fields.NumberField({
  16626. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.SenseDarkvision"
  16627. }),
  16628. blindsight: new foundry.data.fields.NumberField({
  16629. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.SenseBlindsight"
  16630. }),
  16631. tremorsense: new foundry.data.fields.NumberField({
  16632. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.SenseTremorsense"
  16633. }),
  16634. truesight: new foundry.data.fields.NumberField({
  16635. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.SenseTruesight"
  16636. }),
  16637. units: new foundry.data.fields.StringField({required: true, initial: "ft", label: "DND5E.SenseUnits"}),
  16638. special: new foundry.data.fields.StringField({required: true, label: "DND5E.SenseSpecial"})
  16639. }, {label: "DND5E.Senses"}),
  16640. spellcasting: new foundry.data.fields.StringField({
  16641. required: true, blank: true, initial: "int", label: "DND5E.SpellAbility"
  16642. })
  16643. };
  16644. }
  16645. /* -------------------------------------------- */
  16646. /**
  16647. * Migrate the old init.value and incorporate it into init.bonus.
  16648. * @param {object} source The source attributes object.
  16649. * @internal
  16650. */
  16651. static _migrateInitiative(source) {
  16652. const init = source?.init;
  16653. if ( !init?.value || (typeof init?.bonus === "string") ) return;
  16654. if ( init.bonus ) init.bonus += init.value < 0 ? ` - ${init.value * -1}` : ` + ${init.value}`;
  16655. else init.bonus = `${init.value}`;
  16656. }
  16657. }
  16658. /**
  16659. * A template for currently held currencies.
  16660. *
  16661. * @property {object} currency Object containing currencies as numbers.
  16662. * @mixin
  16663. */
  16664. class CurrencyTemplate extends foundry.abstract.DataModel {
  16665. /** @inheritdoc */
  16666. static defineSchema() {
  16667. return {
  16668. currency: new MappingField(new foundry.data.fields.NumberField({
  16669. required: true, nullable: false, integer: true, min: 0, initial: 0
  16670. }), {initialKeys: CONFIG.DND5E.currencies, initialKeysOnly: true, label: "DND5E.Currency"})
  16671. };
  16672. }
  16673. }
  16674. /**
  16675. * @typedef {object} AbilityData
  16676. * @property {number} value Ability score.
  16677. * @property {number} proficient Proficiency value for saves.
  16678. * @property {number} max Maximum possible score for the ability.
  16679. * @property {object} bonuses Bonuses that modify ability checks and saves.
  16680. * @property {string} bonuses.check Numeric or dice bonus to ability checks.
  16681. * @property {string} bonuses.save Numeric or dice bonus to ability saving throws.
  16682. */
  16683. /**
  16684. * A template for all actors that share the common template.
  16685. *
  16686. * @property {Object<string, AbilityData>} abilities Actor's abilities.
  16687. * @mixin
  16688. */
  16689. class CommonTemplate extends SystemDataModel.mixin(CurrencyTemplate) {
  16690. /** @inheritdoc */
  16691. static defineSchema() {
  16692. return this.mergeSchema(super.defineSchema(), {
  16693. abilities: new MappingField(new foundry.data.fields.SchemaField({
  16694. value: new foundry.data.fields.NumberField({
  16695. required: true, nullable: false, integer: true, min: 0, initial: 10, label: "DND5E.AbilityScore"
  16696. }),
  16697. proficient: new foundry.data.fields.NumberField({
  16698. required: true, integer: true, min: 0, max: 1, initial: 0, label: "DND5E.ProficiencyLevel"
  16699. }),
  16700. max: new foundry.data.fields.NumberField({
  16701. required: true, integer: true, nullable: true, min: 0, initial: null, label: "DND5E.AbilityScoreMax"
  16702. }),
  16703. bonuses: new foundry.data.fields.SchemaField({
  16704. check: new FormulaField({required: true, label: "DND5E.AbilityCheckBonus"}),
  16705. save: new FormulaField({required: true, label: "DND5E.SaveBonus"})
  16706. }, {label: "DND5E.AbilityBonuses"})
  16707. }), {
  16708. initialKeys: CONFIG.DND5E.abilities, initialValue: this._initialAbilityValue.bind(this),
  16709. initialKeysOnly: true, label: "DND5E.Abilities"
  16710. })
  16711. });
  16712. }
  16713. /* -------------------------------------------- */
  16714. /**
  16715. * Populate the proper initial value for abilities.
  16716. * @param {string} key Key for which the initial data will be created.
  16717. * @param {object} initial The initial skill object created by SkillData.
  16718. * @param {object} existing Any existing mapping data.
  16719. * @returns {object} Initial ability object.
  16720. * @private
  16721. */
  16722. static _initialAbilityValue(key, initial, existing) {
  16723. const config = CONFIG.DND5E.abilities[key];
  16724. if ( config ) {
  16725. let defaultValue = config.defaults?.[this._systemType] ?? initial.value;
  16726. if ( typeof defaultValue === "string" ) defaultValue = existing?.[defaultValue]?.value ?? initial.value;
  16727. initial.value = defaultValue;
  16728. }
  16729. return initial;
  16730. }
  16731. /* -------------------------------------------- */
  16732. /* Migrations */
  16733. /* -------------------------------------------- */
  16734. /** @inheritdoc */
  16735. static migrateData(source) {
  16736. super.migrateData(source);
  16737. CommonTemplate.#migrateACData(source);
  16738. CommonTemplate.#migrateMovementData(source);
  16739. }
  16740. /* -------------------------------------------- */
  16741. /**
  16742. * Migrate the actor ac.value to new ac.flat override field.
  16743. * @param {object} source The candidate source data from which the model will be constructed.
  16744. */
  16745. static #migrateACData(source) {
  16746. if ( !source.attributes?.ac ) return;
  16747. const ac = source.attributes.ac;
  16748. // If the actor has a numeric ac.value, then their AC has not been migrated to the auto-calculation schema yet.
  16749. if ( Number.isNumeric(ac.value) ) {
  16750. ac.flat = parseInt(ac.value);
  16751. ac.calc = this._systemType === "npc" ? "natural" : "flat";
  16752. return;
  16753. }
  16754. // Migrate ac.base in custom formulas to ac.armor
  16755. if ( (typeof ac.formula === "string") && ac.formula.includes("@attributes.ac.base") ) {
  16756. ac.formula = ac.formula.replaceAll("@attributes.ac.base", "@attributes.ac.armor");
  16757. }
  16758. }
  16759. /* -------------------------------------------- */
  16760. /**
  16761. * Migrate the actor speed string to movement object.
  16762. * @param {object} source The candidate source data from which the model will be constructed.
  16763. */
  16764. static #migrateMovementData(source) {
  16765. const original = source.attributes?.speed?.value ?? source.attributes?.speed;
  16766. if ( (typeof original !== "string") || (source.attributes.movement?.walk !== undefined) ) return;
  16767. source.attributes.movement ??= {};
  16768. const s = original.split(" ");
  16769. if ( s.length > 0 ) source.attributes.movement.walk = Number.isNumeric(s[0]) ? parseInt(s[0]) : 0;
  16770. }
  16771. }
  16772. /**
  16773. * @typedef {object} SkillData
  16774. * @property {number} value Proficiency level creature has in this skill.
  16775. * @property {string} ability Default ability used for this skill.
  16776. * @property {object} bonuses Bonuses for this skill.
  16777. * @property {string} bonuses.check Numeric or dice bonus to skill's check.
  16778. * @property {string} bonuses.passive Numeric bonus to skill's passive check.
  16779. */
  16780. /**
  16781. * A template for all actors that are creatures
  16782. *
  16783. * @property {object} bonuses
  16784. * @property {AttackBonusesData} bonuses.mwak Bonuses to melee weapon attacks.
  16785. * @property {AttackBonusesData} bonuses.rwak Bonuses to ranged weapon attacks.
  16786. * @property {AttackBonusesData} bonuses.msak Bonuses to melee spell attacks.
  16787. * @property {AttackBonusesData} bonuses.rsak Bonuses to ranged spell attacks.
  16788. * @property {object} bonuses.abilities Bonuses to ability scores.
  16789. * @property {string} bonuses.abilities.check Numeric or dice bonus to ability checks.
  16790. * @property {string} bonuses.abilities.save Numeric or dice bonus to ability saves.
  16791. * @property {string} bonuses.abilities.skill Numeric or dice bonus to skill checks.
  16792. * @property {object} bonuses.spell Bonuses to spells.
  16793. * @property {string} bonuses.spell.dc Numeric bonus to spellcasting DC.
  16794. * @property {Object<string, SkillData>} skills Actor's skills.
  16795. * @property {Object<string, SpellSlotData>} spells Actor's spell slots.
  16796. */
  16797. class CreatureTemplate extends CommonTemplate {
  16798. static defineSchema() {
  16799. return this.mergeSchema(super.defineSchema(), {
  16800. bonuses: new foundry.data.fields.SchemaField({
  16801. mwak: makeAttackBonuses({label: "DND5E.BonusMWAttack"}),
  16802. rwak: makeAttackBonuses({label: "DND5E.BonusRWAttack"}),
  16803. msak: makeAttackBonuses({label: "DND5E.BonusMSAttack"}),
  16804. rsak: makeAttackBonuses({label: "DND5E.BonusRSAttack"}),
  16805. abilities: new foundry.data.fields.SchemaField({
  16806. check: new FormulaField({required: true, label: "DND5E.BonusAbilityCheck"}),
  16807. save: new FormulaField({required: true, label: "DND5E.BonusAbilitySave"}),
  16808. skill: new FormulaField({required: true, label: "DND5E.BonusAbilitySkill"})
  16809. }, {label: "DND5E.BonusAbility"}),
  16810. spell: new foundry.data.fields.SchemaField({
  16811. dc: new FormulaField({required: true, deterministic: true, label: "DND5E.BonusSpellDC"})
  16812. }, {label: "DND5E.BonusSpell"})
  16813. }, {label: "DND5E.Bonuses"}),
  16814. skills: new MappingField(new foundry.data.fields.SchemaField({
  16815. value: new foundry.data.fields.NumberField({
  16816. required: true, nullable: false, min: 0, max: 2, step: 0.5, initial: 0, label: "DND5E.ProficiencyLevel"
  16817. }),
  16818. ability: new foundry.data.fields.StringField({required: true, initial: "dex", label: "DND5E.Ability"}),
  16819. bonuses: new foundry.data.fields.SchemaField({
  16820. check: new FormulaField({required: true, label: "DND5E.SkillBonusCheck"}),
  16821. passive: new FormulaField({required: true, label: "DND5E.SkillBonusPassive"})
  16822. }, {label: "DND5E.SkillBonuses"})
  16823. }), {
  16824. initialKeys: CONFIG.DND5E.skills, initialValue: this._initialSkillValue,
  16825. initialKeysOnly: true, label: "DND5E.Skills"
  16826. }),
  16827. tools: new MappingField(new foundry.data.fields.SchemaField({
  16828. value: new foundry.data.fields.NumberField({
  16829. required: true, nullable: false, min: 0, max: 2, step: 0.5, initial: 1, label: "DND5E.ProficiencyLevel"
  16830. }),
  16831. ability: new foundry.data.fields.StringField({required: true, initial: "int", label: "DND5E.Ability"}),
  16832. bonuses: new foundry.data.fields.SchemaField({
  16833. check: new FormulaField({required: true, label: "DND5E.CheckBonus"})
  16834. }, {label: "DND5E.ToolBonuses"})
  16835. })),
  16836. spells: new MappingField(new foundry.data.fields.SchemaField({
  16837. value: new foundry.data.fields.NumberField({
  16838. nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.SpellProgAvailable"
  16839. }),
  16840. override: new foundry.data.fields.NumberField({
  16841. integer: true, min: 0, label: "DND5E.SpellProgOverride"
  16842. })
  16843. }), {initialKeys: this._spellLevels, label: "DND5E.SpellLevels"})
  16844. });
  16845. }
  16846. /* -------------------------------------------- */
  16847. /**
  16848. * Populate the proper initial abilities for the skills.
  16849. * @param {string} key Key for which the initial data will be created.
  16850. * @param {object} initial The initial skill object created by SkillData.
  16851. * @returns {object} Initial skills object with the ability defined.
  16852. * @private
  16853. */
  16854. static _initialSkillValue(key, initial) {
  16855. if ( CONFIG.DND5E.skills[key]?.ability ) initial.ability = CONFIG.DND5E.skills[key].ability;
  16856. return initial;
  16857. }
  16858. /* -------------------------------------------- */
  16859. /**
  16860. * Helper for building the default list of spell levels.
  16861. * @type {string[]}
  16862. * @private
  16863. */
  16864. static get _spellLevels() {
  16865. const levels = Object.keys(CONFIG.DND5E.spellLevels).filter(a => a !== "0").map(l => `spell${l}`);
  16866. return [...levels, "pact"];
  16867. }
  16868. /* -------------------------------------------- */
  16869. /* Migrations */
  16870. /* -------------------------------------------- */
  16871. /** @inheritdoc */
  16872. static migrateData(source) {
  16873. super.migrateData(source);
  16874. CreatureTemplate.#migrateSensesData(source);
  16875. CreatureTemplate.#migrateToolData(source);
  16876. }
  16877. /* -------------------------------------------- */
  16878. /**
  16879. * Migrate the actor traits.senses string to attributes.senses object.
  16880. * @param {object} source The candidate source data from which the model will be constructed.
  16881. */
  16882. static #migrateSensesData(source) {
  16883. const original = source.traits?.senses;
  16884. if ( (original === undefined) || (typeof original !== "string") ) return;
  16885. source.attributes ??= {};
  16886. source.attributes.senses ??= {};
  16887. // Try to match old senses with the format like "Darkvision 60 ft, Blindsight 30 ft"
  16888. const pattern = /([A-z]+)\s?([0-9]+)\s?([A-z]+)?/;
  16889. let wasMatched = false;
  16890. // Match each comma-separated term
  16891. for ( let s of original.split(",") ) {
  16892. s = s.trim();
  16893. const match = s.match(pattern);
  16894. if ( !match ) continue;
  16895. const type = match[1].toLowerCase();
  16896. if ( (type in CONFIG.DND5E.senses) && !(type in source.attributes.senses) ) {
  16897. source.attributes.senses[type] = Number(match[2]).toNearest(0.5);
  16898. wasMatched = true;
  16899. }
  16900. }
  16901. // If nothing was matched, but there was an old string - put the whole thing in "special"
  16902. if ( !wasMatched && original ) source.attributes.senses.special = original;
  16903. }
  16904. /* -------------------------------------------- */
  16905. /**
  16906. * Migrate traits.toolProf to the tools field.
  16907. * @param {object} source The candidate source data from which the model will be constructed.
  16908. */
  16909. static #migrateToolData(source) {
  16910. const original = source.traits?.toolProf;
  16911. if ( !original || foundry.utils.isEmpty(original.value) ) return;
  16912. source.tools ??= {};
  16913. for ( const prof of original.value ) {
  16914. const validProf = (prof in CONFIG.DND5E.toolProficiencies) || (prof in CONFIG.DND5E.toolIds);
  16915. if ( !validProf || (prof in source.tools) ) continue;
  16916. source.tools[prof] = {
  16917. value: 1,
  16918. ability: "int",
  16919. bonuses: {check: ""}
  16920. };
  16921. }
  16922. }
  16923. }
  16924. /* -------------------------------------------- */
  16925. /**
  16926. * Data on configuration of a specific spell slot.
  16927. *
  16928. * @typedef {object} SpellSlotData
  16929. * @property {number} value Currently available spell slots.
  16930. * @property {number} override Number to replace auto-calculated max slots.
  16931. */
  16932. /* -------------------------------------------- */
  16933. /**
  16934. * Data structure for actor's attack bonuses.
  16935. *
  16936. * @typedef {object} AttackBonusesData
  16937. * @property {string} attack Numeric or dice bonus to attack rolls.
  16938. * @property {string} damage Numeric or dice bonus to damage rolls.
  16939. */
  16940. /**
  16941. * Produce the schema field for a simple trait.
  16942. * @param {object} schemaOptions Options passed to the outer schema.
  16943. * @returns {AttackBonusesData}
  16944. */
  16945. function makeAttackBonuses(schemaOptions={}) {
  16946. return new foundry.data.fields.SchemaField({
  16947. attack: new FormulaField({required: true, label: "DND5E.BonusAttack"}),
  16948. damage: new FormulaField({required: true, label: "DND5E.BonusDamage"})
  16949. }, schemaOptions);
  16950. }
  16951. /**
  16952. * Shared contents of the details schema between various actor types.
  16953. */
  16954. class DetailsField {
  16955. /**
  16956. * Fields shared between characters, NPCs, and vehicles.
  16957. *
  16958. * @type {object}
  16959. * @property {object} biography Actor's biography data.
  16960. * @property {string} biography.value Full HTML biography information.
  16961. * @property {string} biography.public Biography that will be displayed to players with observer privileges.
  16962. */
  16963. static get common() {
  16964. return {
  16965. biography: new foundry.data.fields.SchemaField({
  16966. value: new foundry.data.fields.HTMLField({label: "DND5E.Biography"}),
  16967. public: new foundry.data.fields.HTMLField({label: "DND5E.BiographyPublic"})
  16968. }, {label: "DND5E.Biography"})
  16969. };
  16970. }
  16971. /* -------------------------------------------- */
  16972. /**
  16973. * Fields shared between characters and NPCs.
  16974. *
  16975. * @type {object}
  16976. * @property {string} alignment Creature's alignment.
  16977. * @property {string} race Creature's race.
  16978. */
  16979. static get creature() {
  16980. return {
  16981. alignment: new foundry.data.fields.StringField({required: true, label: "DND5E.Alignment"}),
  16982. race: new foundry.data.fields.StringField({required: true, label: "DND5E.Race"})
  16983. };
  16984. }
  16985. }
  16986. /**
  16987. * Shared contents of the traits schema between various actor types.
  16988. */
  16989. class TraitsField {
  16990. /**
  16991. * Data structure for a standard actor trait.
  16992. *
  16993. * @typedef {object} SimpleTraitData
  16994. * @property {Set<string>} value Keys for currently selected traits.
  16995. * @property {string} custom Semicolon-separated list of custom traits.
  16996. */
  16997. /**
  16998. * Data structure for a damage actor trait.
  16999. *
  17000. * @typedef {object} DamageTraitData
  17001. * @property {Set<string>} value Keys for currently selected traits.
  17002. * @property {Set<string>} bypasses Keys for physical weapon properties that cause resistances to be bypassed.
  17003. * @property {string} custom Semicolon-separated list of custom traits.
  17004. */
  17005. /* -------------------------------------------- */
  17006. /**
  17007. * Fields shared between characters, NPCs, and vehicles.
  17008. *
  17009. * @type {object}
  17010. * @property {string} size Actor's size.
  17011. * @property {DamageTraitData} di Damage immunities.
  17012. * @property {DamageTraitData} dr Damage resistances.
  17013. * @property {DamageTraitData} dv Damage vulnerabilities.
  17014. * @property {SimpleTraitData} ci Condition immunities.
  17015. */
  17016. static get common() {
  17017. return {
  17018. size: new foundry.data.fields.StringField({required: true, initial: "med", label: "DND5E.Size"}),
  17019. di: this.makeDamageTrait({label: "DND5E.DamImm"}),
  17020. dr: this.makeDamageTrait({label: "DND5E.DamRes"}),
  17021. dv: this.makeDamageTrait({label: "DND5E.DamVuln"}),
  17022. ci: this.makeSimpleTrait({label: "DND5E.ConImm"})
  17023. };
  17024. }
  17025. /* -------------------------------------------- */
  17026. /**
  17027. * Fields shared between characters and NPCs.
  17028. *
  17029. * @type {object}
  17030. * @property {SimpleTraitData} languages Languages known by this creature.
  17031. */
  17032. static get creature() {
  17033. return {
  17034. languages: this.makeSimpleTrait({label: "DND5E.Languages"})
  17035. };
  17036. }
  17037. /* -------------------------------------------- */
  17038. /**
  17039. * Produce the schema field for a simple trait.
  17040. * @param {object} [schemaOptions={}] Options passed to the outer schema.
  17041. * @param {object} [options={}]
  17042. * @param {string[]} [options.initial={}] The initial value for the value set.
  17043. * @param {object} [options.extraFields={}] Additional fields added to schema.
  17044. * @returns {SchemaField}
  17045. */
  17046. static makeSimpleTrait(schemaOptions={}, {initial=[], extraFields={}}={}) {
  17047. return new foundry.data.fields.SchemaField({
  17048. ...extraFields,
  17049. value: new foundry.data.fields.SetField(
  17050. new foundry.data.fields.StringField(), {label: "DND5E.TraitsChosen", initial}
  17051. ),
  17052. custom: new foundry.data.fields.StringField({required: true, label: "DND5E.Special"})
  17053. }, schemaOptions);
  17054. }
  17055. /* -------------------------------------------- */
  17056. /**
  17057. * Produce the schema field for a damage trait.
  17058. * @param {object} [schemaOptions={}] Options passed to the outer schema.
  17059. * @param {object} [options={}]
  17060. * @param {string[]} [options.initial={}] The initial value for the value set.
  17061. * @param {object} [options.extraFields={}] Additional fields added to schema.
  17062. * @returns {SchemaField}
  17063. */
  17064. static makeDamageTrait(schemaOptions={}, {initial=[], initialBypasses=[], extraFields={}}={}) {
  17065. return this.makeSimpleTrait(schemaOptions, {initial, extraFields: {
  17066. ...extraFields,
  17067. bypasses: new foundry.data.fields.SetField(new foundry.data.fields.StringField(), {
  17068. label: "DND5E.DamagePhysicalBypass", hint: "DND5E.DamagePhysicalBypassHint", initial: initialBypasses
  17069. })
  17070. }});
  17071. }
  17072. }
  17073. /**
  17074. * System data definition for Characters.
  17075. *
  17076. * @property {object} attributes
  17077. * @property {object} attributes.ac
  17078. * @property {number} attributes.ac.flat Flat value used for flat or natural armor calculation.
  17079. * @property {string} attributes.ac.calc Name of one of the built-in formulas to use.
  17080. * @property {string} attributes.ac.formula Custom formula to use.
  17081. * @property {object} attributes.hp
  17082. * @property {number} attributes.hp.value Current hit points.
  17083. * @property {number} attributes.hp.max Override for maximum HP.
  17084. * @property {number} attributes.hp.temp Temporary HP applied on top of value.
  17085. * @property {number} attributes.hp.tempmax Temporary change to the maximum HP.
  17086. * @property {object} attributes.hp.bonuses
  17087. * @property {string} attributes.hp.bonuses.level Bonus formula applied for each class level.
  17088. * @property {string} attributes.hp.bonuses.overall Bonus formula applied to total HP.
  17089. * @property {object} attributes.death
  17090. * @property {number} attributes.death.success Number of successful death saves.
  17091. * @property {number} attributes.death.failure Number of failed death saves.
  17092. * @property {number} attributes.exhaustion Number of levels of exhaustion.
  17093. * @property {number} attributes.inspiration Does this character have inspiration?
  17094. * @property {object} details
  17095. * @property {string} details.background Name of character's background.
  17096. * @property {string} details.originalClass ID of first class taken by character.
  17097. * @property {XPData} details.xp Experience points gained.
  17098. * @property {number} details.xp.value Total experience points earned.
  17099. * @property {string} details.appearance Description of character's appearance.
  17100. * @property {string} details.trait Character's personality traits.
  17101. * @property {string} details.ideal Character's ideals.
  17102. * @property {string} details.bond Character's bonds.
  17103. * @property {string} details.flaw Character's flaws.
  17104. * @property {object} traits
  17105. * @property {SimpleTraitData} traits.weaponProf Character's weapon proficiencies.
  17106. * @property {SimpleTraitData} traits.armorProf Character's armor proficiencies.
  17107. * @property {object} resources
  17108. * @property {CharacterResourceData} resources.primary Resource number one.
  17109. * @property {CharacterResourceData} resources.secondary Resource number two.
  17110. * @property {CharacterResourceData} resources.tertiary Resource number three.
  17111. */
  17112. class CharacterData extends CreatureTemplate {
  17113. /** @inheritdoc */
  17114. static _systemType = "character";
  17115. /* -------------------------------------------- */
  17116. /** @inheritdoc */
  17117. static defineSchema() {
  17118. return this.mergeSchema(super.defineSchema(), {
  17119. attributes: new foundry.data.fields.SchemaField({
  17120. ...AttributesFields.common,
  17121. ...AttributesFields.creature,
  17122. ac: new foundry.data.fields.SchemaField({
  17123. flat: new foundry.data.fields.NumberField({integer: true, min: 0, label: "DND5E.ArmorClassFlat"}),
  17124. calc: new foundry.data.fields.StringField({initial: "default", label: "DND5E.ArmorClassCalculation"}),
  17125. formula: new FormulaField({deterministic: true, label: "DND5E.ArmorClassFormula"})
  17126. }, {label: "DND5E.ArmorClass"}),
  17127. hp: new foundry.data.fields.SchemaField({
  17128. value: new foundry.data.fields.NumberField({
  17129. nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.HitPointsCurrent"
  17130. }),
  17131. max: new foundry.data.fields.NumberField({
  17132. nullable: true, integer: true, min: 0, initial: null, label: "DND5E.HitPointsOverride"
  17133. }),
  17134. temp: new foundry.data.fields.NumberField({integer: true, initial: 0, min: 0, label: "DND5E.HitPointsTemp"}),
  17135. tempmax: new foundry.data.fields.NumberField({integer: true, initial: 0, label: "DND5E.HitPointsTempMax"}),
  17136. bonuses: new foundry.data.fields.SchemaField({
  17137. level: new FormulaField({deterministic: true, label: "DND5E.HitPointsBonusLevel"}),
  17138. overall: new FormulaField({deterministic: true, label: "DND5E.HitPointsBonusOverall"})
  17139. })
  17140. }, {label: "DND5E.HitPoints"}),
  17141. death: new foundry.data.fields.SchemaField({
  17142. success: new foundry.data.fields.NumberField({
  17143. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.DeathSaveSuccesses"
  17144. }),
  17145. failure: new foundry.data.fields.NumberField({
  17146. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.DeathSaveFailures"
  17147. })
  17148. }, {label: "DND5E.DeathSave"}),
  17149. exhaustion: new foundry.data.fields.NumberField({
  17150. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.Exhaustion"
  17151. }),
  17152. inspiration: new foundry.data.fields.BooleanField({required: true, label: "DND5E.Inspiration"})
  17153. }, {label: "DND5E.Attributes"}),
  17154. details: new foundry.data.fields.SchemaField({
  17155. ...DetailsField.common,
  17156. ...DetailsField.creature,
  17157. background: new foundry.data.fields.StringField({required: true, label: "DND5E.Background"}),
  17158. originalClass: new foundry.data.fields.StringField({required: true, label: "DND5E.ClassOriginal"}),
  17159. xp: new foundry.data.fields.SchemaField({
  17160. value: new foundry.data.fields.NumberField({
  17161. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.ExperiencePointsCurrent"
  17162. })
  17163. }, {label: "DND5E.ExperiencePoints"}),
  17164. appearance: new foundry.data.fields.StringField({required: true, label: "DND5E.Appearance"}),
  17165. trait: new foundry.data.fields.StringField({required: true, label: "DND5E.PersonalityTraits"}),
  17166. ideal: new foundry.data.fields.StringField({required: true, label: "DND5E.Ideals"}),
  17167. bond: new foundry.data.fields.StringField({required: true, label: "DND5E.Bonds"}),
  17168. flaw: new foundry.data.fields.StringField({required: true, label: "DND5E.Flaws"})
  17169. }, {label: "DND5E.Details"}),
  17170. traits: new foundry.data.fields.SchemaField({
  17171. ...TraitsField.common,
  17172. ...TraitsField.creature,
  17173. weaponProf: TraitsField.makeSimpleTrait({label: "DND5E.TraitWeaponProf"}),
  17174. armorProf: TraitsField.makeSimpleTrait({label: "DND5E.TraitArmorProf"})
  17175. }, {label: "DND5E.Traits"}),
  17176. resources: new foundry.data.fields.SchemaField({
  17177. primary: makeResourceField({label: "DND5E.ResourcePrimary"}),
  17178. secondary: makeResourceField({label: "DND5E.ResourceSecondary"}),
  17179. tertiary: makeResourceField({label: "DND5E.ResourceTertiary"})
  17180. }, {label: "DND5E.Resources"})
  17181. });
  17182. }
  17183. /* -------------------------------------------- */
  17184. /** @inheritdoc */
  17185. static migrateData(source) {
  17186. super.migrateData(source);
  17187. AttributesFields._migrateInitiative(source.attributes);
  17188. }
  17189. }
  17190. /* -------------------------------------------- */
  17191. /**
  17192. * Data structure for character's resources.
  17193. *
  17194. * @typedef {object} ResourceData
  17195. * @property {number} value Available uses of this resource.
  17196. * @property {number} max Maximum allowed uses of this resource.
  17197. * @property {boolean} sr Does this resource recover on a short rest?
  17198. * @property {boolean} lr Does this resource recover on a long rest?
  17199. * @property {string} label Displayed name.
  17200. */
  17201. /**
  17202. * Produce the schema field for a simple trait.
  17203. * @param {object} schemaOptions Options passed to the outer schema.
  17204. * @returns {ResourceData}
  17205. */
  17206. function makeResourceField(schemaOptions={}) {
  17207. return new foundry.data.fields.SchemaField({
  17208. value: new foundry.data.fields.NumberField({
  17209. required: true, integer: true, initial: 0, labels: "DND5E.ResourceValue"
  17210. }),
  17211. max: new foundry.data.fields.NumberField({
  17212. required: true, integer: true, initial: 0, labels: "DND5E.ResourceMax"
  17213. }),
  17214. sr: new foundry.data.fields.BooleanField({required: true, labels: "DND5E.ShortRestRecovery"}),
  17215. lr: new foundry.data.fields.BooleanField({required: true, labels: "DND5E.LongRestRecovery"}),
  17216. label: new foundry.data.fields.StringField({required: true, labels: "DND5E.ResourceLabel"})
  17217. }, schemaOptions);
  17218. }
  17219. /**
  17220. * A data model and API layer which handles the schema and functionality of "group" type Actors in the dnd5e system.
  17221. * @mixes CurrencyTemplate
  17222. *
  17223. * @property {object} description
  17224. * @property {string} description.full Description of this group.
  17225. * @property {string} description.summary Summary description (currently unused).
  17226. * @property {Set<string>} members IDs of actors belonging to this group in the world collection.
  17227. * @property {object} attributes
  17228. * @property {object} attributes.movement
  17229. * @property {number} attributes.movement.land Base movement speed over land.
  17230. * @property {number} attributes.movement.water Base movement speed over water.
  17231. * @property {number} attributes.movement.air Base movement speed through the air.
  17232. *
  17233. * @example Create a new Group
  17234. * const g = new dnd5e.documents.Actor5e({
  17235. * type: "group",
  17236. * name: "Test Group",
  17237. * system: {
  17238. * members: ["3f3hoYFWUgDqBP4U"]
  17239. * }
  17240. * });
  17241. */
  17242. class GroupActor extends SystemDataModel.mixin(CurrencyTemplate) {
  17243. /** @inheritdoc */
  17244. static defineSchema() {
  17245. return this.mergeSchema(super.defineSchema(), {
  17246. description: new foundry.data.fields.SchemaField({
  17247. full: new foundry.data.fields.HTMLField({label: "DND5E.Description"}),
  17248. summary: new foundry.data.fields.HTMLField({label: "DND5E.DescriptionSummary"})
  17249. }),
  17250. members: new foundry.data.fields.SetField(
  17251. new foundry.data.fields.ForeignDocumentField(foundry.documents.BaseActor, {idOnly: true}),
  17252. {label: "DND5E.GroupMembers"}
  17253. ),
  17254. attributes: new foundry.data.fields.SchemaField({
  17255. movement: new foundry.data.fields.SchemaField({
  17256. land: new foundry.data.fields.NumberField({
  17257. nullable: false, min: 0, step: 0.1, initial: 0, label: "DND5E.MovementLand"
  17258. }),
  17259. water: new foundry.data.fields.NumberField({
  17260. nullable: false, min: 0, step: 0.1, initial: 0, label: "DND5E.MovementWater"
  17261. }),
  17262. air: new foundry.data.fields.NumberField({
  17263. nullable: false, min: 0, step: 0.1, initial: 0, label: "DND5E.MovementAir"
  17264. })
  17265. })
  17266. }, {label: "DND5E.Attributes"})
  17267. });
  17268. }
  17269. /* -------------------------------------------- */
  17270. /* Data Preparation */
  17271. /* -------------------------------------------- */
  17272. /**
  17273. * Prepare base data for group actors.
  17274. * @internal
  17275. */
  17276. _prepareBaseData() {
  17277. this.members.clear();
  17278. for ( const id of this._source.members ) {
  17279. const a = game.actors.get(id);
  17280. if ( a ) {
  17281. if ( a.type === "group" ) {
  17282. console.warn(`Group "${this._id}" may not contain another Group "${a.id}" as a member.`);
  17283. }
  17284. else this.members.add(a);
  17285. }
  17286. else console.warn(`Actor "${id}" in group "${this._id}" does not exist within the World.`);
  17287. }
  17288. }
  17289. /**
  17290. * Prepare derived data for group actors.
  17291. * @internal
  17292. */
  17293. _prepareDerivedData() {
  17294. // No preparation needed at this time
  17295. }
  17296. /* -------------------------------------------- */
  17297. /* Methods */
  17298. /* -------------------------------------------- */
  17299. /**
  17300. * Add a new member to the group.
  17301. * @param {Actor5e} actor A non-group Actor to add to the group
  17302. * @returns {Promise<Actor5e>} The updated group Actor
  17303. */
  17304. async addMember(actor) {
  17305. if ( actor.type === "group" ) throw new Error("You may not add a group within a group.");
  17306. if ( actor.pack ) throw new Error("You may only add Actors to the group which exist within the World.");
  17307. const memberIds = this._source.members;
  17308. if ( memberIds.includes(actor.id) ) return;
  17309. return this.parent.update({
  17310. system: {
  17311. members: memberIds.concat([actor.id])
  17312. }
  17313. });
  17314. }
  17315. /* -------------------------------------------- */
  17316. /**
  17317. * Remove a member from the group.
  17318. * @param {Actor5e|string} actor An Actor or ID to remove from this group
  17319. * @returns {Promise<Actor5e>} The updated group Actor
  17320. */
  17321. async removeMember(actor) {
  17322. const memberIds = foundry.utils.deepClone(this._source.members);
  17323. // Handle user input
  17324. let actorId;
  17325. if ( typeof actor === "string" ) actorId = actor;
  17326. else if ( actor instanceof Actor ) actorId = actor.id;
  17327. else throw new Error("You must provide an Actor document or an actor ID to remove a group member");
  17328. if ( !memberIds.includes(actorId) ) throw new Error(`Actor id "${actorId}" is not a group member`);
  17329. // Remove the actor and update the parent document
  17330. memberIds.findSplice(id => id === actorId);
  17331. return this.parent.update({
  17332. system: {
  17333. members: memberIds
  17334. }
  17335. });
  17336. }
  17337. }
  17338. /**
  17339. * System data definition for NPCs.
  17340. *
  17341. * @property {object} attributes
  17342. * @property {object} attributes.ac
  17343. * @property {number} attributes.ac.flat Flat value used for flat or natural armor calculation.
  17344. * @property {string} attributes.ac.calc Name of one of the built-in formulas to use.
  17345. * @property {string} attributes.ac.formula Custom formula to use.
  17346. * @property {object} attributes.hp
  17347. * @property {number} attributes.hp.value Current hit points.
  17348. * @property {number} attributes.hp.max Maximum allowed HP value.
  17349. * @property {number} attributes.hp.temp Temporary HP applied on top of value.
  17350. * @property {number} attributes.hp.tempmax Temporary change to the maximum HP.
  17351. * @property {string} attributes.hp.formula Formula used to determine hit points.
  17352. * @property {object} details
  17353. * @property {TypeData} details.type Creature type of this NPC.
  17354. * @property {string} details.type.value NPC's type as defined in the system configuration.
  17355. * @property {string} details.type.subtype NPC's subtype usually displayed in parenthesis after main type.
  17356. * @property {string} details.type.swarm Size of the individual creatures in a swarm, if a swarm.
  17357. * @property {string} details.type.custom Custom type beyond what is available in the configuration.
  17358. * @property {string} details.environment Common environments in which this NPC is found.
  17359. * @property {number} details.cr NPC's challenge rating.
  17360. * @property {number} details.spellLevel Spellcasting level of this NPC.
  17361. * @property {string} details.source What book or adventure is this NPC from?
  17362. * @property {object} resources
  17363. * @property {object} resources.legact NPC's legendary actions.
  17364. * @property {number} resources.legact.value Currently available legendary actions.
  17365. * @property {number} resources.legact.max Maximum number of legendary actions.
  17366. * @property {object} resources.legres NPC's legendary resistances.
  17367. * @property {number} resources.legres.value Currently available legendary resistances.
  17368. * @property {number} resources.legres.max Maximum number of legendary resistances.
  17369. * @property {object} resources.lair NPC's lair actions.
  17370. * @property {boolean} resources.lair.value Does this NPC use lair actions.
  17371. * @property {number} resources.lair.initiative Initiative count when lair actions are triggered.
  17372. */
  17373. class NPCData extends CreatureTemplate {
  17374. /** @inheritdoc */
  17375. static _systemType = "npc";
  17376. /* -------------------------------------------- */
  17377. /** @inheritdoc */
  17378. static defineSchema() {
  17379. return this.mergeSchema(super.defineSchema(), {
  17380. attributes: new foundry.data.fields.SchemaField({
  17381. ...AttributesFields.common,
  17382. ...AttributesFields.creature,
  17383. ac: new foundry.data.fields.SchemaField({
  17384. flat: new foundry.data.fields.NumberField({integer: true, min: 0, label: "DND5E.ArmorClassFlat"}),
  17385. calc: new foundry.data.fields.StringField({initial: "default", label: "DND5E.ArmorClassCalculation"}),
  17386. formula: new FormulaField({deterministic: true, label: "DND5E.ArmorClassFormula"})
  17387. }, {label: "DND5E.ArmorClass"}),
  17388. hp: new foundry.data.fields.SchemaField({
  17389. value: new foundry.data.fields.NumberField({
  17390. nullable: false, integer: true, min: 0, initial: 10, label: "DND5E.HitPointsCurrent"
  17391. }),
  17392. max: new foundry.data.fields.NumberField({
  17393. nullable: false, integer: true, min: 0, initial: 10, label: "DND5E.HitPointsMax"
  17394. }),
  17395. temp: new foundry.data.fields.NumberField({integer: true, initial: 0, min: 0, label: "DND5E.HitPointsTemp"}),
  17396. tempmax: new foundry.data.fields.NumberField({integer: true, initial: 0, label: "DND5E.HitPointsTempMax"}),
  17397. formula: new FormulaField({required: true, label: "DND5E.HPFormula"})
  17398. }, {label: "DND5E.HitPoints"})
  17399. }, {label: "DND5E.Attributes"}),
  17400. details: new foundry.data.fields.SchemaField({
  17401. ...DetailsField.common,
  17402. ...DetailsField.creature,
  17403. type: new foundry.data.fields.SchemaField({
  17404. value: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.CreatureType"}),
  17405. subtype: new foundry.data.fields.StringField({required: true, label: "DND5E.CreatureTypeSelectorSubtype"}),
  17406. swarm: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.CreatureSwarmSize"}),
  17407. custom: new foundry.data.fields.StringField({required: true, label: "DND5E.CreatureTypeSelectorCustom"})
  17408. }, {label: "DND5E.CreatureType"}),
  17409. environment: new foundry.data.fields.StringField({required: true, label: "DND5E.Environment"}),
  17410. cr: new foundry.data.fields.NumberField({
  17411. required: true, nullable: false, min: 0, initial: 1, label: "DND5E.ChallengeRating"
  17412. }),
  17413. spellLevel: new foundry.data.fields.NumberField({
  17414. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.SpellcasterLevel"
  17415. }),
  17416. source: new foundry.data.fields.StringField({required: true, label: "DND5E.Source"})
  17417. }, {label: "DND5E.Details"}),
  17418. resources: new foundry.data.fields.SchemaField({
  17419. legact: new foundry.data.fields.SchemaField({
  17420. value: new foundry.data.fields.NumberField({
  17421. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.LegActRemaining"
  17422. }),
  17423. max: new foundry.data.fields.NumberField({
  17424. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.LegActMax"
  17425. })
  17426. }, {label: "DND5E.LegAct"}),
  17427. legres: new foundry.data.fields.SchemaField({
  17428. value: new foundry.data.fields.NumberField({
  17429. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.LegResRemaining"
  17430. }),
  17431. max: new foundry.data.fields.NumberField({
  17432. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.LegResMax"
  17433. })
  17434. }, {label: "DND5E.LegRes"}),
  17435. lair: new foundry.data.fields.SchemaField({
  17436. value: new foundry.data.fields.BooleanField({required: true, label: "DND5E.LairAct"}),
  17437. initiative: new foundry.data.fields.NumberField({
  17438. required: true, integer: true, label: "DND5E.LairActionInitiative"
  17439. })
  17440. }, {label: "DND5E.LairActionLabel"})
  17441. }, {label: "DND5E.Resources"}),
  17442. traits: new foundry.data.fields.SchemaField({
  17443. ...TraitsField.common,
  17444. ...TraitsField.creature
  17445. }, {label: "DND5E.Traits"})
  17446. });
  17447. }
  17448. /* -------------------------------------------- */
  17449. /** @inheritdoc */
  17450. static migrateData(source) {
  17451. super.migrateData(source);
  17452. NPCData.#migrateTypeData(source);
  17453. AttributesFields._migrateInitiative(source.attributes);
  17454. }
  17455. /* -------------------------------------------- */
  17456. /**
  17457. * Migrate the actor type string to type object.
  17458. * @param {object} source The candidate source data from which the model will be constructed.
  17459. */
  17460. static #migrateTypeData(source) {
  17461. const original = source.type;
  17462. if ( typeof original !== "string" ) return;
  17463. source.type = {
  17464. value: "",
  17465. subtype: "",
  17466. swarm: "",
  17467. custom: ""
  17468. };
  17469. // Match the existing string
  17470. const pattern = /^(?:swarm of (?<size>[\w-]+) )?(?<type>[^(]+?)(?:\((?<subtype>[^)]+)\))?$/i;
  17471. const match = original.trim().match(pattern);
  17472. if ( match ) {
  17473. // Match a known creature type
  17474. const typeLc = match.groups.type.trim().toLowerCase();
  17475. const typeMatch = Object.entries(CONFIG.DND5E.creatureTypes).find(([k, v]) => {
  17476. return (typeLc === k)
  17477. || (typeLc === game.i18n.localize(v).toLowerCase())
  17478. || (typeLc === game.i18n.localize(`${v}Pl`).toLowerCase());
  17479. });
  17480. if ( typeMatch ) source.type.value = typeMatch[0];
  17481. else {
  17482. source.type.value = "custom";
  17483. source.type.custom = match.groups.type.trim().titleCase();
  17484. }
  17485. source.type.subtype = match.groups.subtype?.trim().titleCase() ?? "";
  17486. // Match a swarm
  17487. if ( match.groups.size ) {
  17488. const sizeLc = match.groups.size ? match.groups.size.trim().toLowerCase() : "tiny";
  17489. const sizeMatch = Object.entries(CONFIG.DND5E.actorSizes).find(([k, v]) => {
  17490. return (sizeLc === k) || (sizeLc === game.i18n.localize(v).toLowerCase());
  17491. });
  17492. source.type.swarm = sizeMatch ? sizeMatch[0] : "tiny";
  17493. }
  17494. else source.type.swarm = "";
  17495. }
  17496. // No match found
  17497. else {
  17498. source.type.value = "custom";
  17499. source.type.custom = original;
  17500. }
  17501. }
  17502. }
  17503. /**
  17504. * System data definition for Vehicles.
  17505. *
  17506. * @property {string} vehicleType Type of vehicle as defined in `DND5E.vehicleTypes`.
  17507. * @property {object} attributes
  17508. * @property {object} attributes.ac
  17509. * @property {number} attributes.ac.flat Flat value used for flat or natural armor calculation.
  17510. * @property {string} attributes.ac.calc Name of one of the built-in formulas to use.
  17511. * @property {string} attributes.ac.formula Custom formula to use.
  17512. * @property {string} attributes.ac.motionless Changes to vehicle AC when not moving.
  17513. * @property {object} attributes.hp
  17514. * @property {number} attributes.hp.value Current hit points.
  17515. * @property {number} attributes.hp.max Maximum allowed HP value.
  17516. * @property {number} attributes.hp.temp Temporary HP applied on top of value.
  17517. * @property {number} attributes.hp.tempmax Temporary change to the maximum HP.
  17518. * @property {number} attributes.hp.dt Damage threshold.
  17519. * @property {number} attributes.hp.mt Mishap threshold.
  17520. * @property {object} attributes.actions Information on how the vehicle performs actions.
  17521. * @property {boolean} attributes.actions.stations Does this vehicle rely on action stations that required
  17522. * individual crewing rather than general crew thresholds?
  17523. * @property {number} attributes.actions.value Maximum number of actions available with full crewing.
  17524. * @property {object} attributes.actions.thresholds Crew thresholds needed to perform various actions.
  17525. * @property {number} attributes.actions.thresholds.2 Minimum crew needed to take full action complement.
  17526. * @property {number} attributes.actions.thresholds.1 Minimum crew needed to take reduced action complement.
  17527. * @property {number} attributes.actions.thresholds.0 Minimum crew needed to perform any actions.
  17528. * @property {object} attributes.capacity Information on the vehicle's carrying capacity.
  17529. * @property {string} attributes.capacity.creature Description of the number of creatures the vehicle can carry.
  17530. * @property {number} attributes.capacity.cargo Cargo carrying capacity measured in tons.
  17531. * @property {object} traits
  17532. * @property {string} traits.dimensions Width and length of the vehicle.
  17533. * @property {object} cargo Details on this vehicle's crew and cargo capacities.
  17534. * @property {PassengerData[]} cargo.crew Creatures responsible for operating the vehicle.
  17535. * @property {PassengerData[]} cargo.passengers Creatures just takin' a ride.
  17536. */
  17537. class VehicleData extends CommonTemplate {
  17538. /** @inheritdoc */
  17539. static _systemType = "vehicle";
  17540. /* -------------------------------------------- */
  17541. /** @inheritdoc */
  17542. static defineSchema() {
  17543. return this.mergeSchema(super.defineSchema(), {
  17544. vehicleType: new foundry.data.fields.StringField({required: true, initial: "water", label: "DND5E.VehicleType"}),
  17545. attributes: new foundry.data.fields.SchemaField({
  17546. ...AttributesFields.common,
  17547. ac: new foundry.data.fields.SchemaField({
  17548. flat: new foundry.data.fields.NumberField({integer: true, min: 0, label: "DND5E.ArmorClassFlat"}),
  17549. calc: new foundry.data.fields.StringField({initial: "default", label: "DND5E.ArmorClassCalculation"}),
  17550. formula: new FormulaField({deterministic: true, label: "DND5E.ArmorClassFormula"}),
  17551. motionless: new foundry.data.fields.StringField({required: true, label: "DND5E.ArmorClassMotionless"})
  17552. }, {label: "DND5E.ArmorClass"}),
  17553. hp: new foundry.data.fields.SchemaField({
  17554. value: new foundry.data.fields.NumberField({
  17555. nullable: true, integer: true, min: 0, initial: null, label: "DND5E.HitPointsCurrent"
  17556. }),
  17557. max: new foundry.data.fields.NumberField({
  17558. nullable: true, integer: true, min: 0, initial: null, label: "DND5E.HitPointsMax"
  17559. }),
  17560. temp: new foundry.data.fields.NumberField({integer: true, initial: 0, min: 0, label: "DND5E.HitPointsTemp"}),
  17561. tempmax: new foundry.data.fields.NumberField({integer: true, initial: 0, label: "DND5E.HitPointsTempMax"}),
  17562. dt: new foundry.data.fields.NumberField({
  17563. required: true, integer: true, min: 0, label: "DND5E.DamageThreshold"
  17564. }),
  17565. mt: new foundry.data.fields.NumberField({
  17566. required: true, integer: true, min: 0, label: "DND5E.VehicleMishapThreshold"
  17567. })
  17568. }, {label: "DND5E.HitPoints"}),
  17569. actions: new foundry.data.fields.SchemaField({
  17570. stations: new foundry.data.fields.BooleanField({required: true, label: "DND5E.VehicleActionStations"}),
  17571. value: new foundry.data.fields.NumberField({
  17572. required: true, nullable: false, integer: true, initial: 0, min: 0, label: "DND5E.VehicleActionMax"
  17573. }),
  17574. thresholds: new foundry.data.fields.SchemaField({
  17575. 2: new foundry.data.fields.NumberField({
  17576. required: true, integer: true, min: 0, label: "DND5E.VehicleActionThresholdsFull"
  17577. }),
  17578. 1: new foundry.data.fields.NumberField({
  17579. required: true, integer: true, min: 0, label: "DND5E.VehicleActionThresholdsMid"
  17580. }),
  17581. 0: new foundry.data.fields.NumberField({
  17582. required: true, integer: true, min: 0, label: "DND5E.VehicleActionThresholdsMin"
  17583. })
  17584. }, {label: "DND5E.VehicleActionThresholds"})
  17585. }, {label: "DND5E.VehicleActions"}),
  17586. capacity: new foundry.data.fields.SchemaField({
  17587. creature: new foundry.data.fields.StringField({required: true, label: "DND5E.VehicleCreatureCapacity"}),
  17588. cargo: new foundry.data.fields.NumberField({
  17589. required: true, nullable: false, integer: true, initial: 0, min: 0, label: "DND5E.VehicleCargoCapacity"
  17590. })
  17591. }, {label: "DND5E.VehicleCargoCrew"})
  17592. }, {label: "DND5E.Attributes"}),
  17593. details: new foundry.data.fields.SchemaField({
  17594. ...DetailsField.common,
  17595. source: new foundry.data.fields.StringField({required: true, label: "DND5E.Source"})
  17596. }, {label: "DND5E.Details"}),
  17597. traits: new foundry.data.fields.SchemaField({
  17598. ...TraitsField.common,
  17599. size: new foundry.data.fields.StringField({required: true, initial: "lg", label: "DND5E.Size"}),
  17600. di: TraitsField.makeDamageTrait({label: "DND5E.DamImm"}, {initial: ["poison", "psychic"]}),
  17601. ci: TraitsField.makeSimpleTrait({label: "DND5E.ConImm"}, {initial: [
  17602. "blinded", "charmed", "deafened", "frightened", "paralyzed",
  17603. "petrified", "poisoned", "stunned", "unconscious"
  17604. ]}),
  17605. dimensions: new foundry.data.fields.StringField({required: true, label: "DND5E.Dimensions"})
  17606. }, {label: "DND5E.Traits"}),
  17607. cargo: new foundry.data.fields.SchemaField({
  17608. crew: new foundry.data.fields.ArrayField(makePassengerData(), {label: "DND5E.VehicleCrew"}),
  17609. passengers: new foundry.data.fields.ArrayField(makePassengerData(), {label: "DND5E.VehiclePassengers"})
  17610. }, {label: "DND5E.VehicleCrewPassengers"})
  17611. });
  17612. }
  17613. /* -------------------------------------------- */
  17614. /** @inheritdoc */
  17615. static migrateData(source) {
  17616. super.migrateData(source);
  17617. AttributesFields._migrateInitiative(source.attributes);
  17618. }
  17619. }
  17620. /* -------------------------------------------- */
  17621. /**
  17622. * Data structure for an entry in a vehicle's crew or passenger lists.
  17623. *
  17624. * @typedef {object} PassengerData
  17625. * @property {string} name Name of individual or type of creature.
  17626. * @property {number} quantity How many of this creature are onboard?
  17627. */
  17628. /**
  17629. * Produce the schema field for a simple trait.
  17630. * @param {object} schemaOptions Options passed to the outer schema.
  17631. * @returns {PassengerData}
  17632. */
  17633. function makePassengerData(schemaOptions={}) {
  17634. return new foundry.data.fields.SchemaField({
  17635. name: new foundry.data.fields.StringField({required: true, label: "DND5E.VehiclePassengerName"}),
  17636. quantity: new foundry.data.fields.NumberField({
  17637. required: true, nullable: false, integer: true, initial: 0, min: 0, label: "DND5E.VehiclePassengerQuantity"
  17638. })
  17639. }, schemaOptions);
  17640. }
  17641. const config$2 = {
  17642. character: CharacterData,
  17643. group: GroupActor,
  17644. npc: NPCData,
  17645. vehicle: VehicleData
  17646. };
  17647. var _module$4 = /*#__PURE__*/Object.freeze({
  17648. __proto__: null,
  17649. AttributesFields: AttributesFields,
  17650. CharacterData: CharacterData,
  17651. CommonTemplate: CommonTemplate,
  17652. CreatureTemplate: CreatureTemplate,
  17653. DetailsFields: DetailsField,
  17654. GroupData: GroupActor,
  17655. NPCData: NPCData,
  17656. TraitsFields: TraitsField,
  17657. VehicleData: VehicleData,
  17658. config: config$2
  17659. });
  17660. var _module$3 = /*#__PURE__*/Object.freeze({
  17661. __proto__: null,
  17662. AbilityScoreImprovementConfigurationData: AbilityScoreImprovementConfigurationData,
  17663. AbilityScoreImprovementValueData: AbilityScoreImprovementValueData,
  17664. BaseAdvancement: BaseAdvancement,
  17665. ItemChoiceConfigurationData: ItemChoiceConfigurationData,
  17666. ItemGrantConfigurationData: ItemGrantConfigurationData,
  17667. SpellConfigurationData: SpellConfigurationData,
  17668. scaleValue: scaleValue
  17669. });
  17670. /**
  17671. * Data model template with item description & source.
  17672. *
  17673. * @property {object} description Various item descriptions.
  17674. * @property {string} description.value Full item description.
  17675. * @property {string} description.chat Description displayed in chat card.
  17676. * @property {string} description.unidentified Description displayed if item is unidentified.
  17677. * @property {string} source Adventure or sourcebook where this item originated.
  17678. * @mixin
  17679. */
  17680. class ItemDescriptionTemplate extends foundry.abstract.DataModel {
  17681. /** @inheritdoc */
  17682. static defineSchema() {
  17683. return {
  17684. description: new foundry.data.fields.SchemaField({
  17685. value: new foundry.data.fields.HTMLField({required: true, nullable: true, label: "DND5E.Description"}),
  17686. chat: new foundry.data.fields.HTMLField({required: true, nullable: true, label: "DND5E.DescriptionChat"}),
  17687. unidentified: new foundry.data.fields.HTMLField({
  17688. required: true, nullable: true, label: "DND5E.DescriptionUnidentified"
  17689. })
  17690. }),
  17691. source: new foundry.data.fields.StringField({required: true, label: "DND5E.Source"})
  17692. };
  17693. }
  17694. /* -------------------------------------------- */
  17695. /* Migrations */
  17696. /* -------------------------------------------- */
  17697. /** @inheritdoc */
  17698. static migrateData(source) {
  17699. ItemDescriptionTemplate.#migrateSource(source);
  17700. }
  17701. /* -------------------------------------------- */
  17702. /**
  17703. * Convert null source to the blank string.
  17704. * @param {object} source The candidate source data from which the model will be constructed.
  17705. */
  17706. static #migrateSource(source) {
  17707. if ( source.source === null ) source.source = "";
  17708. }
  17709. }
  17710. /**
  17711. * Data definition for Background items.
  17712. * @mixes ItemDescriptionTemplate
  17713. *
  17714. * @property {object[]} advancement Advancement objects for this background.
  17715. */
  17716. class BackgroundData extends SystemDataModel.mixin(ItemDescriptionTemplate) {
  17717. /** @inheritdoc */
  17718. static defineSchema() {
  17719. return this.mergeSchema(super.defineSchema(), {
  17720. advancement: new foundry.data.fields.ArrayField(new AdvancementField(), {label: "DND5E.AdvancementTitle"})
  17721. });
  17722. }
  17723. }
  17724. /**
  17725. * Data definition for Class items.
  17726. * @mixes ItemDescriptionTemplate
  17727. *
  17728. * @property {string} identifier Identifier slug for this class.
  17729. * @property {number} levels Current number of levels in this class.
  17730. * @property {string} hitDice Denomination of hit dice available as defined in `DND5E.hitDieTypes`.
  17731. * @property {number} hitDiceUsed Number of hit dice consumed.
  17732. * @property {object[]} advancement Advancement objects for this class.
  17733. * @property {string[]} saves Savings throws in which this class grants proficiency.
  17734. * @property {object} skills Available class skills and selected skills.
  17735. * @property {number} skills.number Number of skills selectable by the player.
  17736. * @property {string[]} skills.choices List of skill keys that are valid to be chosen.
  17737. * @property {string[]} skills.value List of skill keys the player has chosen.
  17738. * @property {object} spellcasting Details on class's spellcasting ability.
  17739. * @property {string} spellcasting.progression Spell progression granted by class as from `DND5E.spellProgression`.
  17740. * @property {string} spellcasting.ability Ability score to use for spellcasting.
  17741. */
  17742. class ClassData extends SystemDataModel.mixin(ItemDescriptionTemplate) {
  17743. /** @inheritdoc */
  17744. static defineSchema() {
  17745. return this.mergeSchema(super.defineSchema(), {
  17746. identifier: new IdentifierField({required: true, label: "DND5E.Identifier"}),
  17747. levels: new foundry.data.fields.NumberField({
  17748. required: true, nullable: false, integer: true, min: 0, initial: 1, label: "DND5E.ClassLevels"
  17749. }),
  17750. hitDice: new foundry.data.fields.StringField({
  17751. required: true, initial: "d6", blank: false, label: "DND5E.HitDice",
  17752. validate: v => /d\d+/.test(v), validationError: "must be a dice value in the format d#"
  17753. }),
  17754. hitDiceUsed: new foundry.data.fields.NumberField({
  17755. required: true, nullable: false, integer: true, initial: 0, min: 0, label: "DND5E.HitDiceUsed"
  17756. }),
  17757. advancement: new foundry.data.fields.ArrayField(new AdvancementField(), {label: "DND5E.AdvancementTitle"}),
  17758. saves: new foundry.data.fields.ArrayField(new foundry.data.fields.StringField(), {label: "DND5E.ClassSaves"}),
  17759. skills: new foundry.data.fields.SchemaField({
  17760. number: new foundry.data.fields.NumberField({
  17761. required: true, nullable: false, integer: true, min: 0, initial: 2, label: "DND5E.ClassSkillsNumber"
  17762. }),
  17763. choices: new foundry.data.fields.ArrayField(
  17764. new foundry.data.fields.StringField(), {label: "DND5E.ClassSkillsEligible"}
  17765. ),
  17766. value: new foundry.data.fields.ArrayField(
  17767. new foundry.data.fields.StringField(), {label: "DND5E.ClassSkillsChosen"}
  17768. )
  17769. }),
  17770. spellcasting: new foundry.data.fields.SchemaField({
  17771. progression: new foundry.data.fields.StringField({
  17772. required: true, initial: "none", blank: false, label: "DND5E.SpellProgression"
  17773. }),
  17774. ability: new foundry.data.fields.StringField({required: true, label: "DND5E.SpellAbility"})
  17775. }, {label: "DND5E.Spellcasting"})
  17776. });
  17777. }
  17778. /* -------------------------------------------- */
  17779. /* Migrations */
  17780. /* -------------------------------------------- */
  17781. /** @inheritdoc */
  17782. static migrateData(source) {
  17783. super.migrateData(source);
  17784. ClassData.#migrateLevels(source);
  17785. ClassData.#migrateSpellcastingData(source);
  17786. }
  17787. /* -------------------------------------------- */
  17788. /**
  17789. * Migrate the class levels.
  17790. * @param {object} source The candidate source data from which the model will be constructed.
  17791. */
  17792. static #migrateLevels(source) {
  17793. if ( typeof source.levels !== "string" ) return;
  17794. if ( source.levels === "" ) source.levels = 1;
  17795. else if ( Number.isNumeric(source.levels) ) source.levels = Number(source.levels);
  17796. }
  17797. /* -------------------------------------------- */
  17798. /**
  17799. * Migrate the class's spellcasting string to object.
  17800. * @param {object} source The candidate source data from which the model will be constructed.
  17801. */
  17802. static #migrateSpellcastingData(source) {
  17803. if ( source.spellcasting?.progression === "" ) source.spellcasting.progression = "none";
  17804. if ( typeof source.spellcasting !== "string" ) return;
  17805. source.spellcasting = {
  17806. progression: source.spellcasting,
  17807. ability: ""
  17808. };
  17809. }
  17810. }
  17811. /**
  17812. * Data model template for item actions.
  17813. *
  17814. * @property {string} ability Ability score to use when determining modifier.
  17815. * @property {string} actionType Action type as defined in `DND5E.itemActionTypes`.
  17816. * @property {string} attackBonus Numeric or dice bonus to attack rolls.
  17817. * @property {string} chatFlavor Extra text displayed in chat.
  17818. * @property {object} critical Information on how critical hits are handled.
  17819. * @property {number} critical.threshold Minimum number on the dice to roll a critical hit.
  17820. * @property {string} critical.damage Extra damage on critical hit.
  17821. * @property {object} damage Item damage formulas.
  17822. * @property {string[][]} damage.parts Array of damage formula and types.
  17823. * @property {string} damage.versatile Special versatile damage formula.
  17824. * @property {string} formula Other roll formula.
  17825. * @property {object} save Item saving throw data.
  17826. * @property {string} save.ability Ability required for the save.
  17827. * @property {number} save.dc Custom saving throw value.
  17828. * @property {string} save.scaling Method for automatically determining saving throw DC.
  17829. * @mixin
  17830. */
  17831. class ActionTemplate extends foundry.abstract.DataModel {
  17832. /** @inheritdoc */
  17833. static defineSchema() {
  17834. return {
  17835. ability: new foundry.data.fields.StringField({
  17836. required: true, nullable: true, initial: null, label: "DND5E.AbilityModifier"
  17837. }),
  17838. actionType: new foundry.data.fields.StringField({
  17839. required: true, nullable: true, initial: null, label: "DND5E.ItemActionType"
  17840. }),
  17841. attackBonus: new FormulaField({required: true, label: "DND5E.ItemAttackBonus"}),
  17842. chatFlavor: new foundry.data.fields.StringField({required: true, label: "DND5E.ChatFlavor"}),
  17843. critical: new foundry.data.fields.SchemaField({
  17844. threshold: new foundry.data.fields.NumberField({
  17845. required: true, integer: true, initial: null, positive: true, label: "DND5E.ItemCritThreshold"
  17846. }),
  17847. damage: new FormulaField({required: true, label: "DND5E.ItemCritExtraDamage"})
  17848. }),
  17849. damage: new foundry.data.fields.SchemaField({
  17850. parts: new foundry.data.fields.ArrayField(new foundry.data.fields.ArrayField(
  17851. new foundry.data.fields.StringField({nullable: true})
  17852. ), {required: true}),
  17853. versatile: new FormulaField({required: true, label: "DND5E.VersatileDamage"})
  17854. }, {label: "DND5E.Damage"}),
  17855. formula: new FormulaField({required: true, label: "DND5E.OtherFormula"}),
  17856. save: new foundry.data.fields.SchemaField({
  17857. ability: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.Ability"}),
  17858. dc: new foundry.data.fields.NumberField({
  17859. required: true, min: 0, integer: true, label: "DND5E.AbbreviationDC"
  17860. }),
  17861. scaling: new foundry.data.fields.StringField({
  17862. required: true, blank: false, initial: "spell", label: "DND5E.ScalingFormula"
  17863. })
  17864. }, {label: "DND5E.SavingThrow"})
  17865. };
  17866. }
  17867. /* -------------------------------------------- */
  17868. /* Migrations */
  17869. /* -------------------------------------------- */
  17870. /** @inheritdoc */
  17871. static migrateData(source) {
  17872. ActionTemplate.#migrateAbility(source);
  17873. ActionTemplate.#migrateAttackBonus(source);
  17874. ActionTemplate.#migrateCritical(source);
  17875. ActionTemplate.#migrateSave(source);
  17876. ActionTemplate.#migrateDamage(source);
  17877. }
  17878. /* -------------------------------------------- */
  17879. /**
  17880. * Migrate the ability field.
  17881. * @param {object} source The candidate source data from which the model will be constructed.
  17882. */
  17883. static #migrateAbility(source) {
  17884. if ( Array.isArray(source.ability) ) source.ability = source.ability[0];
  17885. }
  17886. /* -------------------------------------------- */
  17887. /**
  17888. * Ensure a 0 or null in attack bonus is converted to an empty string rather than "0".
  17889. * @param {object} source The candidate source data from which the model will be constructed.
  17890. */
  17891. static #migrateAttackBonus(source) {
  17892. if ( [0, "0", null].includes(source.attackBonus) ) source.attackBonus = "";
  17893. else if ( typeof source.attackBonus === "number" ) source.attackBonus = source.attackBonus.toString();
  17894. }
  17895. /* -------------------------------------------- */
  17896. /**
  17897. * Ensure the critical field is an object.
  17898. * @param {object} source The candidate source data from which the model will be constructed.
  17899. */
  17900. static #migrateCritical(source) {
  17901. if ( !("critical" in source) ) return;
  17902. if ( (typeof source.critical !== "object") || (source.critical === null) ) source.critical = {
  17903. threshold: null,
  17904. damage: ""
  17905. };
  17906. if ( source.critical.damage === null ) source.critical.damage = "";
  17907. }
  17908. /* -------------------------------------------- */
  17909. /**
  17910. * Migrate the save field.
  17911. * @param {object} source The candidate source data from which the model will be constructed.
  17912. */
  17913. static #migrateSave(source) {
  17914. if ( !("save" in source) ) return;
  17915. source.save ??= {};
  17916. if ( source.save.scaling === "" ) source.save.scaling = "spell";
  17917. if ( source.save.ability === null ) source.save.ability = "";
  17918. if ( typeof source.save.dc === "string" ) {
  17919. if ( source.save.dc === "" ) source.save.dc = null;
  17920. else if ( Number.isNumeric(source.save.dc) ) source.save.dc = Number(source.save.dc);
  17921. }
  17922. }
  17923. /* -------------------------------------------- */
  17924. /**
  17925. * Migrate damage parts.
  17926. * @param {object} source The candidate source data from which the model will be constructed.
  17927. */
  17928. static #migrateDamage(source) {
  17929. if ( !("damage" in source) ) return;
  17930. source.damage ??= {};
  17931. source.damage.parts ??= [];
  17932. }
  17933. /* -------------------------------------------- */
  17934. /* Getters */
  17935. /* -------------------------------------------- */
  17936. /**
  17937. * Which ability score modifier is used by this item?
  17938. * @type {string|null}
  17939. */
  17940. get abilityMod() {
  17941. if ( this.ability === "none" ) return null;
  17942. return this.ability || this._typeAbilityMod || {
  17943. mwak: "str",
  17944. rwak: "dex",
  17945. msak: this.parent?.actor?.system.attributes.spellcasting || "int",
  17946. rsak: this.parent?.actor?.system.attributes.spellcasting || "int"
  17947. }[this.actionType] || null;
  17948. }
  17949. /* -------------------------------------------- */
  17950. /**
  17951. * Default ability key defined for this type.
  17952. * @type {string|null}
  17953. * @internal
  17954. */
  17955. get _typeAbilityMod() {
  17956. return null;
  17957. }
  17958. /* -------------------------------------------- */
  17959. /**
  17960. * What is the critical hit threshold for this item? Uses the smallest value from among the following sources:
  17961. * - `critical.threshold` defined on the item
  17962. * - `critical.threshold` defined on ammunition, if consumption mode is set to ammo
  17963. * - Type-specific critical threshold
  17964. * @type {number|null}
  17965. */
  17966. get criticalThreshold() {
  17967. if ( !this.hasAttack ) return null;
  17968. let ammoThreshold = Infinity;
  17969. if ( this.consume?.type === "ammo" ) {
  17970. ammoThreshold = this.parent?.actor?.items.get(this.consume.target).system.critical.threshold ?? Infinity;
  17971. }
  17972. const threshold = Math.min(this.critical.threshold ?? Infinity, this._typeCriticalThreshold, ammoThreshold);
  17973. return threshold < Infinity ? threshold : 20;
  17974. }
  17975. /* -------------------------------------------- */
  17976. /**
  17977. * Default critical threshold for this type.
  17978. * @type {number}
  17979. * @internal
  17980. */
  17981. get _typeCriticalThreshold() {
  17982. return Infinity;
  17983. }
  17984. /* -------------------------------------------- */
  17985. /**
  17986. * Does the Item implement an ability check as part of its usage?
  17987. * @type {boolean}
  17988. */
  17989. get hasAbilityCheck() {
  17990. return (this.actionType === "abil") && !!this.ability;
  17991. }
  17992. /* -------------------------------------------- */
  17993. /**
  17994. * Does the Item implement an attack roll as part of its usage?
  17995. * @type {boolean}
  17996. */
  17997. get hasAttack() {
  17998. return ["mwak", "rwak", "msak", "rsak"].includes(this.actionType);
  17999. }
  18000. /* -------------------------------------------- */
  18001. /**
  18002. * Does the Item implement a damage roll as part of its usage?
  18003. * @type {boolean}
  18004. */
  18005. get hasDamage() {
  18006. return this.actionType && (this.damage.parts.length > 0);
  18007. }
  18008. /* -------------------------------------------- */
  18009. /**
  18010. * Does the Item implement a saving throw as part of its usage?
  18011. * @type {boolean}
  18012. */
  18013. get hasSave() {
  18014. return this.actionType && !!(this.save.ability && this.save.scaling);
  18015. }
  18016. /* -------------------------------------------- */
  18017. /**
  18018. * Does the Item provide an amount of healing instead of conventional damage?
  18019. * @type {boolean}
  18020. */
  18021. get isHealing() {
  18022. return (this.actionType === "heal") && this.hasDamage;
  18023. }
  18024. /* -------------------------------------------- */
  18025. /**
  18026. * Does the Item implement a versatile damage roll as part of its usage?
  18027. * @type {boolean}
  18028. */
  18029. get isVersatile() {
  18030. return this.actionType && !!(this.hasDamage && this.damage.versatile);
  18031. }
  18032. }
  18033. /**
  18034. * Data model template for items that can be used as some sort of action.
  18035. *
  18036. * @property {object} activation Effect's activation conditions.
  18037. * @property {string} activation.type Activation type as defined in `DND5E.abilityActivationTypes`.
  18038. * @property {number} activation.cost How much of the activation type is needed to use this item's effect.
  18039. * @property {string} activation.condition Special conditions required to activate the item.
  18040. * @property {object} duration Effect's duration.
  18041. * @property {number} duration.value How long the effect lasts.
  18042. * @property {string} duration.units Time duration period as defined in `DND5E.timePeriods`.
  18043. * @property {number} cover Amount of cover does this item affords to its crew on a vehicle.
  18044. * @property {object} target Effect's valid targets.
  18045. * @property {number} target.value Length or radius of target depending on targeting mode selected.
  18046. * @property {number} target.width Width of line when line type is selected.
  18047. * @property {string} target.units Units used for value and width as defined in `DND5E.distanceUnits`.
  18048. * @property {string} target.type Targeting mode as defined in `DND5E.targetTypes`.
  18049. * @property {object} range Effect's range.
  18050. * @property {number} range.value Regular targeting distance for item's effect.
  18051. * @property {number} range.long Maximum targeting distance for features that have a separate long range.
  18052. * @property {string} range.units Units used for value and long as defined in `DND5E.distanceUnits`.
  18053. * @property {object} uses Effect's limited uses.
  18054. * @property {number} uses.value Current available uses.
  18055. * @property {string} uses.max Maximum possible uses or a formula to derive that number.
  18056. * @property {string} uses.per Recharge time for limited uses as defined in `DND5E.limitedUsePeriods`.
  18057. * @property {object} consume Effect's resource consumption.
  18058. * @property {string} consume.type Type of resource to consume as defined in `DND5E.abilityConsumptionTypes`.
  18059. * @property {string} consume.target Item ID or resource key path of resource to consume.
  18060. * @property {number} consume.amount Quantity of the resource to consume per use.
  18061. * @mixin
  18062. */
  18063. class ActivatedEffectTemplate extends foundry.abstract.DataModel {
  18064. /** @inheritdoc */
  18065. static defineSchema() {
  18066. return {
  18067. activation: new foundry.data.fields.SchemaField({
  18068. type: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.ItemActivationType"}),
  18069. cost: new foundry.data.fields.NumberField({required: true, label: "DND5E.ItemActivationCost"}),
  18070. condition: new foundry.data.fields.StringField({required: true, label: "DND5E.ItemActivationCondition"})
  18071. }, {label: "DND5E.ItemActivation"}),
  18072. duration: new foundry.data.fields.SchemaField({
  18073. value: new FormulaField({required: true, deterministic: true, label: "DND5E.Duration"}),
  18074. units: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.DurationType"})
  18075. }, {label: "DND5E.Duration"}),
  18076. cover: new foundry.data.fields.NumberField({
  18077. required: true, nullable: true, min: 0, max: 1, label: "DND5E.Cover"
  18078. }),
  18079. crewed: new foundry.data.fields.BooleanField({label: "DND5E.Crewed"}),
  18080. target: new foundry.data.fields.SchemaField({
  18081. value: new foundry.data.fields.NumberField({required: true, min: 0, label: "DND5E.TargetValue"}),
  18082. width: new foundry.data.fields.NumberField({required: true, min: 0, label: "DND5E.TargetWidth"}),
  18083. units: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.TargetUnits"}),
  18084. type: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.TargetType"})
  18085. }, {label: "DND5E.Target"}),
  18086. range: new foundry.data.fields.SchemaField({
  18087. value: new foundry.data.fields.NumberField({required: true, min: 0, label: "DND5E.RangeNormal"}),
  18088. long: new foundry.data.fields.NumberField({required: true, min: 0, label: "DND5E.RangeLong"}),
  18089. units: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.RangeUnits"})
  18090. }, {label: "DND5E.Range"}),
  18091. uses: new this.ItemUsesField({}, {label: "DND5E.LimitedUses"}),
  18092. consume: new foundry.data.fields.SchemaField({
  18093. type: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.ConsumeType"}),
  18094. target: new foundry.data.fields.StringField({
  18095. required: true, nullable: true, initial: null, label: "DND5E.ConsumeTarget"
  18096. }),
  18097. amount: new foundry.data.fields.NumberField({required: true, integer: true, label: "DND5E.ConsumeAmount"})
  18098. }, {label: "DND5E.ConsumeTitle"})
  18099. };
  18100. }
  18101. /* -------------------------------------------- */
  18102. /**
  18103. * Extension of SchemaField used to track item uses.
  18104. * @internal
  18105. */
  18106. static ItemUsesField = class ItemUsesField extends foundry.data.fields.SchemaField {
  18107. constructor(extraSchema, options) {
  18108. super(SystemDataModel.mergeSchema({
  18109. value: new foundry.data.fields.NumberField({
  18110. required: true, min: 0, integer: true, label: "DND5E.LimitedUsesAvailable"
  18111. }),
  18112. max: new FormulaField({required: true, deterministic: true, label: "DND5E.LimitedUsesMax"}),
  18113. per: new foundry.data.fields.StringField({
  18114. required: true, nullable: true, blank: false, initial: null, label: "DND5E.LimitedUsesPer"
  18115. }),
  18116. recovery: new FormulaField({required: true, label: "DND5E.RecoveryFormula"})
  18117. }, extraSchema), options);
  18118. }
  18119. };
  18120. /* -------------------------------------------- */
  18121. /* Migrations */
  18122. /* -------------------------------------------- */
  18123. /** @inheritdoc */
  18124. static migrateData(source) {
  18125. ActivatedEffectTemplate.#migrateFormulaFields(source);
  18126. ActivatedEffectTemplate.#migrateRanges(source);
  18127. ActivatedEffectTemplate.#migrateTargets(source);
  18128. ActivatedEffectTemplate.#migrateUses(source);
  18129. ActivatedEffectTemplate.#migrateConsume(source);
  18130. }
  18131. /* -------------------------------------------- */
  18132. /**
  18133. * Ensure a 0 or null in max uses & durations are converted to an empty string rather than "0". Convert numbers into
  18134. * strings.
  18135. * @param {object} source The candidate source data from which the model will be constructed.
  18136. */
  18137. static #migrateFormulaFields(source) {
  18138. if ( [0, "0", null].includes(source.uses?.max) ) source.uses.max = "";
  18139. else if ( typeof source.uses?.max === "number" ) source.uses.max = source.uses.max.toString();
  18140. if ( [0, "0", null].includes(source.duration?.value) ) source.duration.value = "";
  18141. else if ( typeof source.duration?.value === "number" ) source.duration.value = source.duration.value.toString();
  18142. }
  18143. /* -------------------------------------------- */
  18144. /**
  18145. * Fix issue with some imported range data that uses the format "100/400" in the range field,
  18146. * rather than splitting it between "range.value" & "range.long".
  18147. * @param {object} source The candidate source data from which the model will be constructed.
  18148. */
  18149. static #migrateRanges(source) {
  18150. if ( !("range" in source) ) return;
  18151. source.range ??= {};
  18152. if ( source.range.units === null ) source.range.units = "";
  18153. if ( typeof source.range.long === "string" ) {
  18154. if ( source.range.long === "" ) source.range.long = null;
  18155. else if ( Number.isNumeric(source.range.long) ) source.range.long = Number(source.range.long);
  18156. }
  18157. if ( typeof source.range.value !== "string" ) return;
  18158. if ( source.range.value === "" ) {
  18159. source.range.value = null;
  18160. return;
  18161. }
  18162. const [value, long] = source.range.value.split("/");
  18163. if ( Number.isNumeric(value) ) source.range.value = Number(value);
  18164. if ( Number.isNumeric(long) ) source.range.long = Number(long);
  18165. }
  18166. /* -------------------------------------------- */
  18167. /**
  18168. * Ensure blank strings in targets are converted to null.
  18169. * @param {object} source The candidate source data from which the model will be constructed.
  18170. */
  18171. static #migrateTargets(source) {
  18172. if ( !("target" in source) ) return;
  18173. source.target ??= {};
  18174. if ( source.target.value === "" ) source.target.value = null;
  18175. if ( source.target.units === null ) source.target.units = "";
  18176. if ( source.target.type === null ) source.target.type = "";
  18177. }
  18178. /* -------------------------------------------- */
  18179. /**
  18180. * Ensure a blank string in uses.value is converted to null.
  18181. * @param {object} source The candidate source data from which the model will be constructed.
  18182. */
  18183. static #migrateUses(source) {
  18184. if ( !("uses" in source) ) return;
  18185. source.uses ??= {};
  18186. const value = source.uses.value;
  18187. if ( typeof value === "string" ) {
  18188. if ( value === "" ) source.uses.value = null;
  18189. else if ( Number.isNumeric(value) ) source.uses.value = Number(source.uses.value);
  18190. }
  18191. }
  18192. /* -------------------------------------------- */
  18193. /**
  18194. * Migrate the consume field.
  18195. * @param {object} source The candidate source data from which the model will be constructed.
  18196. */
  18197. static #migrateConsume(source) {
  18198. if ( !("consume" in source) ) return;
  18199. source.consume ??= {};
  18200. if ( source.consume.type === null ) source.consume.type = "";
  18201. const amount = source.consume.amount;
  18202. if ( typeof amount === "string" ) {
  18203. if ( amount === "" ) source.consume.amount = null;
  18204. else if ( Number.isNumeric(amount) ) source.consume.amount = Number(amount);
  18205. }
  18206. }
  18207. /* -------------------------------------------- */
  18208. /* Getters */
  18209. /* -------------------------------------------- */
  18210. /**
  18211. * Chat properties for activated effects.
  18212. * @type {string[]}
  18213. */
  18214. get activatedEffectChatProperties() {
  18215. return [
  18216. this.parent.labels.activation + (this.activation.condition ? ` (${this.activation.condition})` : ""),
  18217. this.parent.labels.target,
  18218. this.parent.labels.range,
  18219. this.parent.labels.duration
  18220. ];
  18221. }
  18222. /* -------------------------------------------- */
  18223. /**
  18224. * Does the Item have an area of effect target?
  18225. * @type {boolean}
  18226. */
  18227. get hasAreaTarget() {
  18228. return this.target.type in CONFIG.DND5E.areaTargetTypes;
  18229. }
  18230. /* -------------------------------------------- */
  18231. /**
  18232. * Does the Item target one or more distinct targets?
  18233. * @type {boolean}
  18234. */
  18235. get hasIndividualTarget() {
  18236. return this.target.type in CONFIG.DND5E.individualTargetTypes;
  18237. }
  18238. /* -------------------------------------------- */
  18239. /**
  18240. * Is this Item limited in its ability to be used by charges or by recharge?
  18241. * @type {boolean}
  18242. */
  18243. get hasLimitedUses() {
  18244. return !!this.uses.per && (this.uses.max > 0);
  18245. }
  18246. /* -------------------------------------------- */
  18247. /**
  18248. * Does the Item duration accept an associated numeric value or formula?
  18249. * @type {boolean}
  18250. */
  18251. get hasScalarDuration() {
  18252. return this.duration.units in CONFIG.DND5E.scalarTimePeriods;
  18253. }
  18254. /* -------------------------------------------- */
  18255. /**
  18256. * Does the Item range accept an associated numeric value?
  18257. * @type {boolean}
  18258. */
  18259. get hasScalarRange() {
  18260. return this.range.units in CONFIG.DND5E.movementUnits;
  18261. }
  18262. /* -------------------------------------------- */
  18263. /**
  18264. * Does the Item target accept an associated numeric value?
  18265. * @type {boolean}
  18266. */
  18267. get hasScalarTarget() {
  18268. return ![null, "", "self"].includes(this.target.type);
  18269. }
  18270. /* -------------------------------------------- */
  18271. /**
  18272. * Does the Item have a target?
  18273. * @type {boolean}
  18274. */
  18275. get hasTarget() {
  18276. return !["", null].includes(this.target.type);
  18277. }
  18278. }
  18279. /**
  18280. * Data model template with information on items that can be attuned and equipped.
  18281. *
  18282. * @property {number} attunement Attunement information as defined in `DND5E.attunementTypes`.
  18283. * @property {boolean} equipped Is this item equipped on its owning actor.
  18284. * @mixin
  18285. */
  18286. class EquippableItemTemplate extends foundry.abstract.DataModel {
  18287. /** @inheritdoc */
  18288. static defineSchema() {
  18289. return {
  18290. attunement: new foundry.data.fields.NumberField({
  18291. required: true, integer: true, initial: CONFIG.DND5E.attunementTypes.NONE, label: "DND5E.Attunement"
  18292. }),
  18293. equipped: new foundry.data.fields.BooleanField({required: true, label: "DND5E.Equipped"})
  18294. };
  18295. }
  18296. /* -------------------------------------------- */
  18297. /* Migrations */
  18298. /* -------------------------------------------- */
  18299. /** @inheritdoc */
  18300. static migrateData(source) {
  18301. EquippableItemTemplate.#migrateAttunement(source);
  18302. EquippableItemTemplate.#migrateEquipped(source);
  18303. }
  18304. /* -------------------------------------------- */
  18305. /**
  18306. * Migrate the item's attuned boolean to attunement string.
  18307. * @param {object} source The candidate source data from which the model will be constructed.
  18308. */
  18309. static #migrateAttunement(source) {
  18310. if ( (source.attuned === undefined) || (source.attunement !== undefined) ) return;
  18311. source.attunement = source.attuned ? CONFIG.DND5E.attunementTypes.ATTUNED : CONFIG.DND5E.attunementTypes.NONE;
  18312. }
  18313. /* -------------------------------------------- */
  18314. /**
  18315. * Migrate the equipped field.
  18316. * @param {object} source The candidate source data from which the model will be constructed.
  18317. */
  18318. static #migrateEquipped(source) {
  18319. if ( !("equipped" in source) ) return;
  18320. if ( (source.equipped === null) || (source.equipped === undefined) ) source.equipped = false;
  18321. }
  18322. /* -------------------------------------------- */
  18323. /* Getters */
  18324. /* -------------------------------------------- */
  18325. /**
  18326. * Chat properties for equippable items.
  18327. * @type {string[]}
  18328. */
  18329. get equippableItemChatProperties() {
  18330. const req = CONFIG.DND5E.attunementTypes.REQUIRED;
  18331. return [
  18332. this.attunement === req ? CONFIG.DND5E.attunements[req] : null,
  18333. game.i18n.localize(this.equipped ? "DND5E.Equipped" : "DND5E.Unequipped"),
  18334. ("proficient" in this) ? CONFIG.DND5E.proficiencyLevels[this.prof?.multiplier || 0] : null
  18335. ];
  18336. }
  18337. }
  18338. /**
  18339. * Data model template with information on physical items.
  18340. *
  18341. * @property {number} quantity Number of items in a stack.
  18342. * @property {number} weight Item's weight in pounds or kilograms (depending on system setting).
  18343. * @property {object} price
  18344. * @property {number} price.value Item's cost in the specified denomination.
  18345. * @property {string} price.denomination Currency denomination used to determine price.
  18346. * @property {string} rarity Item rarity as defined in `DND5E.itemRarity`.
  18347. * @property {boolean} identified Has this item been identified?
  18348. * @mixin
  18349. */
  18350. class PhysicalItemTemplate extends foundry.abstract.DataModel {
  18351. /** @inheritdoc */
  18352. static defineSchema() {
  18353. return {
  18354. quantity: new foundry.data.fields.NumberField({
  18355. required: true, nullable: false, integer: true, initial: 1, min: 0, label: "DND5E.Quantity"
  18356. }),
  18357. weight: new foundry.data.fields.NumberField({
  18358. required: true, nullable: false, initial: 0, min: 0, label: "DND5E.Weight"
  18359. }),
  18360. price: new foundry.data.fields.SchemaField({
  18361. value: new foundry.data.fields.NumberField({
  18362. required: true, nullable: false, initial: 0, min: 0, label: "DND5E.Price"
  18363. }),
  18364. denomination: new foundry.data.fields.StringField({
  18365. required: true, blank: false, initial: "gp", label: "DND5E.Currency"
  18366. })
  18367. }, {label: "DND5E.Price"}),
  18368. rarity: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.Rarity"}),
  18369. identified: new foundry.data.fields.BooleanField({required: true, initial: true, label: "DND5E.Identified"})
  18370. };
  18371. }
  18372. /* -------------------------------------------- */
  18373. /* Migrations */
  18374. /* -------------------------------------------- */
  18375. /** @inheritdoc */
  18376. static migrateData(source) {
  18377. PhysicalItemTemplate.#migratePrice(source);
  18378. PhysicalItemTemplate.#migrateRarity(source);
  18379. PhysicalItemTemplate.#migrateWeight(source);
  18380. }
  18381. /* -------------------------------------------- */
  18382. /**
  18383. * Migrate the item's price from a single field to an object with currency.
  18384. * @param {object} source The candidate source data from which the model will be constructed.
  18385. */
  18386. static #migratePrice(source) {
  18387. if ( !("price" in source) || foundry.utils.getType(source.price) === "Object" ) return;
  18388. source.price = {
  18389. value: Number.isNumeric(source.price) ? Number(source.price) : 0,
  18390. denomination: "gp"
  18391. };
  18392. }
  18393. /* -------------------------------------------- */
  18394. /**
  18395. * Migrate the item's rarity from freeform string to enum value.
  18396. * @param {object} source The candidate source data from which the model will be constructed.
  18397. */
  18398. static #migrateRarity(source) {
  18399. if ( !("rarity" in source) || CONFIG.DND5E.itemRarity[source.rarity] ) return;
  18400. source.rarity = Object.keys(CONFIG.DND5E.itemRarity).find(key =>
  18401. CONFIG.DND5E.itemRarity[key].toLowerCase() === source.rarity.toLowerCase()
  18402. ) ?? "";
  18403. }
  18404. /* -------------------------------------------- */
  18405. /**
  18406. * Convert null weights to 0.
  18407. * @param {object} source The candidate source data from which the model will be constructed.
  18408. */
  18409. static #migrateWeight(source) {
  18410. if ( !("weight" in source) ) return;
  18411. if ( (source.weight === null) || (source.weight === undefined) ) source.weight = 0;
  18412. }
  18413. }
  18414. /**
  18415. * Data definition for Consumable items.
  18416. * @mixes ItemDescriptionTemplate
  18417. * @mixes PhysicalItemTemplate
  18418. * @mixes EquippableItemTemplate
  18419. * @mixes ActivatedEffectTemplate
  18420. * @mixes ActionTemplate
  18421. *
  18422. * @property {string} consumableType Type of consumable as defined in `DND5E.consumableTypes`.
  18423. * @property {object} uses
  18424. * @property {boolean} uses.autoDestroy Should this item be destroyed when it runs out of uses.
  18425. */
  18426. class ConsumableData extends SystemDataModel.mixin(
  18427. ItemDescriptionTemplate, PhysicalItemTemplate, EquippableItemTemplate, ActivatedEffectTemplate, ActionTemplate
  18428. ) {
  18429. /** @inheritdoc */
  18430. static defineSchema() {
  18431. return this.mergeSchema(super.defineSchema(), {
  18432. consumableType: new foundry.data.fields.StringField({
  18433. required: true, initial: "potion", label: "DND5E.ItemConsumableType"
  18434. }),
  18435. properties: new MappingField(new foundry.data.fields.BooleanField(), {
  18436. required: false, label: "DND5E.ItemAmmoProperties"
  18437. }),
  18438. uses: new ActivatedEffectTemplate.ItemUsesField({
  18439. autoDestroy: new foundry.data.fields.BooleanField({required: true, label: "DND5E.ItemDestroyEmpty"})
  18440. }, {label: "DND5E.LimitedUses"})
  18441. });
  18442. }
  18443. /* -------------------------------------------- */
  18444. /* Getters */
  18445. /* -------------------------------------------- */
  18446. /**
  18447. * Properties displayed in chat.
  18448. * @type {string[]}
  18449. */
  18450. get chatProperties() {
  18451. return [
  18452. CONFIG.DND5E.consumableTypes[this.consumableType],
  18453. this.hasLimitedUses ? `${this.uses.value}/${this.uses.max} ${game.i18n.localize("DND5E.Charges")}` : null
  18454. ];
  18455. }
  18456. /* -------------------------------------------- */
  18457. /** @inheritdoc */
  18458. get _typeAbilityMod() {
  18459. if ( this.consumableType !== "scroll" ) return null;
  18460. return this.parent?.actor?.system.attributes.spellcasting || "int";
  18461. }
  18462. /* -------------------------------------------- */
  18463. /**
  18464. * The proficiency multiplier for this item.
  18465. * @returns {number}
  18466. */
  18467. get proficiencyMultiplier() {
  18468. const isProficient = this.parent?.actor?.getFlag("dnd5e", "tavernBrawlerFeat");
  18469. return isProficient ? 1 : 0;
  18470. }
  18471. }
  18472. /**
  18473. * Data definition for Backpack items.
  18474. * @mixes ItemDescriptionTemplate
  18475. * @mixes PhysicalItemTemplate
  18476. * @mixes EquippableItemTemplate
  18477. * @mixes CurrencyTemplate
  18478. *
  18479. * @property {object} capacity Information on container's carrying capacity.
  18480. * @property {string} capacity.type Method for tracking max capacity as defined in `DND5E.itemCapacityTypes`.
  18481. * @property {number} capacity.value Total amount of the type this container can carry.
  18482. * @property {boolean} capacity.weightless Does the weight of the items in the container carry over to the actor?
  18483. */
  18484. class ContainerData extends SystemDataModel.mixin(
  18485. ItemDescriptionTemplate, PhysicalItemTemplate, EquippableItemTemplate, CurrencyTemplate
  18486. ) {
  18487. /** @inheritdoc */
  18488. static defineSchema() {
  18489. return this.mergeSchema(super.defineSchema(), {
  18490. capacity: new foundry.data.fields.SchemaField({
  18491. type: new foundry.data.fields.StringField({
  18492. required: true, initial: "weight", blank: false, label: "DND5E.ItemContainerCapacityType"
  18493. }),
  18494. value: new foundry.data.fields.NumberField({
  18495. required: true, min: 0, label: "DND5E.ItemContainerCapacityMax"
  18496. }),
  18497. weightless: new foundry.data.fields.BooleanField({required: true, label: "DND5E.ItemContainerWeightless"})
  18498. }, {label: "DND5E.ItemContainerCapacity"})
  18499. });
  18500. }
  18501. }
  18502. /**
  18503. * Data model template for equipment that can be mounted on a vehicle.
  18504. *
  18505. * @property {object} armor Equipment's armor class.
  18506. * @property {number} armor.value Armor class value for equipment.
  18507. * @property {object} hp Equipment's hit points.
  18508. * @property {number} hp.value Current hit point value.
  18509. * @property {number} hp.max Max hit points.
  18510. * @property {number} hp.dt Damage threshold.
  18511. * @property {string} hp.conditions Conditions that are triggered when this equipment takes damage.
  18512. * @mixin
  18513. */
  18514. class MountableTemplate extends foundry.abstract.DataModel {
  18515. /** @inheritdoc */
  18516. static defineSchema() {
  18517. return {
  18518. armor: new foundry.data.fields.SchemaField({
  18519. value: new foundry.data.fields.NumberField({
  18520. required: true, integer: true, min: 0, label: "DND5E.ArmorClass"
  18521. })
  18522. }, {label: "DND5E.ArmorClass"}),
  18523. hp: new foundry.data.fields.SchemaField({
  18524. value: new foundry.data.fields.NumberField({
  18525. required: true, integer: true, min: 0, label: "DND5E.HitPointsCurrent"
  18526. }),
  18527. max: new foundry.data.fields.NumberField({
  18528. required: true, integer: true, min: 0, label: "DND5E.HitPointsMax"
  18529. }),
  18530. dt: new foundry.data.fields.NumberField({
  18531. required: true, integer: true, min: 0, label: "DND5E.DamageThreshold"
  18532. }),
  18533. conditions: new foundry.data.fields.StringField({required: true, label: "DND5E.HealthConditions"})
  18534. }, {label: "DND5E.HitPoints"})
  18535. };
  18536. }
  18537. }
  18538. /**
  18539. * Data definition for Equipment items.
  18540. * @mixes ItemDescriptionTemplate
  18541. * @mixes PhysicalItemTemplate
  18542. * @mixes EquippableItemTemplate
  18543. * @mixes ActivatedEffectTemplate
  18544. * @mixes ActionTemplate
  18545. * @mixes MountableTemplate
  18546. *
  18547. * @property {object} armor Armor details and equipment type information.
  18548. * @property {string} armor.type Equipment type as defined in `DND5E.equipmentTypes`.
  18549. * @property {number} armor.value Base armor class or shield bonus.
  18550. * @property {number} armor.dex Maximum dex bonus added to armor class.
  18551. * @property {string} baseItem Base armor as defined in `DND5E.armorIds` for determining proficiency.
  18552. * @property {object} speed Speed granted by a piece of vehicle equipment.
  18553. * @property {number} speed.value Speed granted by this piece of equipment measured in feet or meters
  18554. * depending on system setting.
  18555. * @property {string} speed.conditions Conditions that may affect item's speed.
  18556. * @property {number} strength Minimum strength required to use a piece of armor.
  18557. * @property {boolean} stealth Does this equipment grant disadvantage on stealth checks when used?
  18558. * @property {number} proficient Does the owner have proficiency in this piece of equipment?
  18559. */
  18560. class EquipmentData extends SystemDataModel.mixin(
  18561. ItemDescriptionTemplate, PhysicalItemTemplate, EquippableItemTemplate,
  18562. ActivatedEffectTemplate, ActionTemplate, MountableTemplate
  18563. ) {
  18564. /** @inheritdoc */
  18565. static defineSchema() {
  18566. return this.mergeSchema(super.defineSchema(), {
  18567. armor: new foundry.data.fields.SchemaField({
  18568. type: new foundry.data.fields.StringField({
  18569. required: true, initial: "light", label: "DND5E.ItemEquipmentType"
  18570. }),
  18571. value: new foundry.data.fields.NumberField({required: true, integer: true, min: 0, label: "DND5E.ArmorClass"}),
  18572. dex: new foundry.data.fields.NumberField({required: true, integer: true, label: "DND5E.ItemEquipmentDexMod"})
  18573. }, {label: ""}),
  18574. baseItem: new foundry.data.fields.StringField({required: true, label: "DND5E.ItemEquipmentBase"}),
  18575. speed: new foundry.data.fields.SchemaField({
  18576. value: new foundry.data.fields.NumberField({required: true, min: 0, label: "DND5E.Speed"}),
  18577. conditions: new foundry.data.fields.StringField({required: true, label: "DND5E.SpeedConditions"})
  18578. }, {label: "DND5E.Speed"}),
  18579. strength: new foundry.data.fields.NumberField({
  18580. required: true, integer: true, min: 0, label: "DND5E.ItemRequiredStr"
  18581. }),
  18582. stealth: new foundry.data.fields.BooleanField({required: true, label: "DND5E.ItemEquipmentStealthDisav"}),
  18583. proficient: new foundry.data.fields.NumberField({
  18584. required: true, min: 0, max: 1, integer: true, initial: null, label: "DND5E.ProficiencyLevel"
  18585. })
  18586. });
  18587. }
  18588. /* -------------------------------------------- */
  18589. /* Migrations */
  18590. /* -------------------------------------------- */
  18591. /** @inheritdoc */
  18592. static migrateData(source) {
  18593. super.migrateData(source);
  18594. EquipmentData.#migrateArmor(source);
  18595. EquipmentData.#migrateStrength(source);
  18596. EquipmentData.#migrateProficient(source);
  18597. }
  18598. /* -------------------------------------------- */
  18599. /**
  18600. * Apply migrations to the armor field.
  18601. * @param {object} source The candidate source data from which the model will be constructed.
  18602. */
  18603. static #migrateArmor(source) {
  18604. if ( !("armor" in source) ) return;
  18605. source.armor ??= {};
  18606. if ( source.armor.type === "bonus" ) source.armor.type = "trinket";
  18607. if ( (typeof source.armor.dex === "string") ) {
  18608. const dex = source.armor.dex;
  18609. if ( dex === "" ) source.armor.dex = null;
  18610. else if ( Number.isNumeric(dex) ) source.armor.dex = Number(dex);
  18611. }
  18612. }
  18613. /* -------------------------------------------- */
  18614. /**
  18615. * Ensure blank strength values are migrated to null, and string values are converted to numbers.
  18616. * @param {object} source The candidate source data from which the model will be constructed.
  18617. */
  18618. static #migrateStrength(source) {
  18619. if ( typeof source.strength !== "string" ) return;
  18620. if ( source.strength === "" ) source.strength = null;
  18621. if ( Number.isNumeric(source.strength) ) source.strength = Number(source.strength);
  18622. }
  18623. /* -------------------------------------------- */
  18624. /**
  18625. * Migrate the proficient field to convert boolean values.
  18626. * @param {object} source The candidate source data from which the model will be constructed.
  18627. */
  18628. static #migrateProficient(source) {
  18629. if ( typeof source.proficient === "boolean" ) source.proficient = Number(source.proficient);
  18630. }
  18631. /* -------------------------------------------- */
  18632. /* Getters */
  18633. /* -------------------------------------------- */
  18634. /**
  18635. * Properties displayed in chat.
  18636. * @type {string[]}
  18637. */
  18638. get chatProperties() {
  18639. return [
  18640. CONFIG.DND5E.equipmentTypes[this.armor.type],
  18641. this.parent.labels?.armor ?? null,
  18642. this.stealth ? game.i18n.localize("DND5E.StealthDisadvantage") : null
  18643. ];
  18644. }
  18645. /* -------------------------------------------- */
  18646. /**
  18647. * Is this Item any of the armor subtypes?
  18648. * @type {boolean}
  18649. */
  18650. get isArmor() {
  18651. return this.armor.type in CONFIG.DND5E.armorTypes;
  18652. }
  18653. /* -------------------------------------------- */
  18654. /**
  18655. * Is this item a separate large object like a siege engine or vehicle component that is
  18656. * usually mounted on fixtures rather than equipped, and has its own AC and HP?
  18657. * @type {boolean}
  18658. */
  18659. get isMountable() {
  18660. return this.armor.type === "vehicle";
  18661. }
  18662. /* -------------------------------------------- */
  18663. /**
  18664. * The proficiency multiplier for this item.
  18665. * @returns {number}
  18666. */
  18667. get proficiencyMultiplier() {
  18668. if ( Number.isFinite(this.proficient) ) return this.proficient;
  18669. const actor = this.parent.actor;
  18670. if ( !actor ) return 0;
  18671. if ( actor.type === "npc" ) return 1; // NPCs are always considered proficient with any armor in their stat block.
  18672. const config = CONFIG.DND5E.armorProficienciesMap;
  18673. const itemProf = config[this.armor?.type];
  18674. const actorProfs = actor.system.traits?.armorProf?.value ?? new Set();
  18675. const isProficient = (itemProf === true) || actorProfs.has(itemProf) || actorProfs.has(this.baseItem);
  18676. return Number(isProficient);
  18677. }
  18678. }
  18679. /**
  18680. * Data definition for Feature items.
  18681. * @mixes ItemDescriptionTemplate
  18682. * @mixes ActivatedEffectTemplate
  18683. * @mixes ActionTemplate
  18684. *
  18685. * @property {object} type
  18686. * @property {string} type.value Category to which this feature belongs.
  18687. * @property {string} type.subtype Feature subtype according to its category.
  18688. * @property {string} requirements Actor details required to use this feature.
  18689. * @property {object} recharge Details on how a feature can roll for recharges.
  18690. * @property {number} recharge.value Minimum number needed to roll on a d6 to recharge this feature.
  18691. * @property {boolean} recharge.charged Does this feature have a charge remaining?
  18692. */
  18693. class FeatData extends SystemDataModel.mixin(
  18694. ItemDescriptionTemplate, ActivatedEffectTemplate, ActionTemplate
  18695. ) {
  18696. /** @inheritdoc */
  18697. static defineSchema() {
  18698. return this.mergeSchema(super.defineSchema(), {
  18699. type: new foundry.data.fields.SchemaField({
  18700. value: new foundry.data.fields.StringField({required: true, label: "DND5E.Type"}),
  18701. subtype: new foundry.data.fields.StringField({required: true, label: "DND5E.Subtype"})
  18702. }, {label: "DND5E.ItemFeatureType"}),
  18703. requirements: new foundry.data.fields.StringField({required: true, nullable: true, label: "DND5E.Requirements"}),
  18704. recharge: new foundry.data.fields.SchemaField({
  18705. value: new foundry.data.fields.NumberField({
  18706. required: true, integer: true, min: 1, label: "DND5E.FeatureRechargeOn"
  18707. }),
  18708. charged: new foundry.data.fields.BooleanField({required: true, label: "DND5E.Charged"})
  18709. }, {label: "DND5E.FeatureActionRecharge"})
  18710. });
  18711. }
  18712. /* -------------------------------------------- */
  18713. /* Migrations */
  18714. /* -------------------------------------------- */
  18715. /** @inheritdoc */
  18716. static migrateData(source) {
  18717. super.migrateData(source);
  18718. FeatData.#migrateType(source);
  18719. FeatData.#migrateRecharge(source);
  18720. }
  18721. /* -------------------------------------------- */
  18722. /**
  18723. * Ensure feats have a type object.
  18724. * @param {object} source The candidate source data from which the model will be constructed.
  18725. */
  18726. static #migrateType(source) {
  18727. if ( !("type" in source) ) return;
  18728. if ( !source.type ) source.type = {value: "", subtype: ""};
  18729. }
  18730. /* -------------------------------------------- */
  18731. /**
  18732. * Migrate 0 values to null.
  18733. * @param {object} source The candidate source data from which the model will be constructed.
  18734. */
  18735. static #migrateRecharge(source) {
  18736. if ( !("recharge" in source) ) return;
  18737. const value = source.recharge.value;
  18738. if ( (value === 0) || (value === "") ) source.recharge.value = null;
  18739. else if ( (typeof value === "string") && Number.isNumeric(value) ) source.recharge.value = Number(value);
  18740. if ( source.recharge.charged === null ) source.recharge.charged = false;
  18741. }
  18742. /* -------------------------------------------- */
  18743. /* Getters */
  18744. /* -------------------------------------------- */
  18745. /**
  18746. * Properties displayed in chat.
  18747. * @type {string[]}
  18748. */
  18749. get chatProperties() {
  18750. return [this.requirements];
  18751. }
  18752. /* -------------------------------------------- */
  18753. /** @inheritdoc */
  18754. get hasLimitedUses() {
  18755. return !!this.recharge.value || super.hasLimitedUses;
  18756. }
  18757. /* -------------------------------------------- */
  18758. /**
  18759. * The proficiency multiplier for this item.
  18760. * @returns {number}
  18761. */
  18762. get proficiencyMultiplier() {
  18763. return 1;
  18764. }
  18765. }
  18766. /**
  18767. * Data definition for Loot items.
  18768. * @mixes ItemDescriptionTemplate
  18769. * @mixes PhysicalItemTemplate
  18770. */
  18771. class LootData extends SystemDataModel.mixin(ItemDescriptionTemplate, PhysicalItemTemplate) {
  18772. /* -------------------------------------------- */
  18773. /* Getters */
  18774. /* -------------------------------------------- */
  18775. /**
  18776. * Properties displayed in chat.
  18777. * @type {string[]}
  18778. */
  18779. get chatProperties() {
  18780. return [
  18781. game.i18n.localize(CONFIG.Item.typeLabels.loot),
  18782. this.weight ? `${this.weight} ${game.i18n.localize("DND5E.AbbreviationLbs")}` : null
  18783. ];
  18784. }
  18785. }
  18786. /**
  18787. * Data definition for Spell items.
  18788. * @mixes ItemDescriptionTemplate
  18789. * @mixes ActivatedEffectTemplate
  18790. * @mixes ActionTemplate
  18791. *
  18792. * @property {number} level Base level of the spell.
  18793. * @property {string} school Magical school to which this spell belongs.
  18794. * @property {object} components General components and tags for this spell.
  18795. * @property {boolean} components.vocal Does this spell require vocal components?
  18796. * @property {boolean} components.somatic Does this spell require somatic components?
  18797. * @property {boolean} components.material Does this spell require material components?
  18798. * @property {boolean} components.ritual Can this spell be cast as a ritual?
  18799. * @property {boolean} components.concentration Does this spell require concentration?
  18800. * @property {object} materials Details on material components required for this spell.
  18801. * @property {string} materials.value Description of the material components required for casting.
  18802. * @property {boolean} materials.consumed Are these material components consumed during casting?
  18803. * @property {number} materials.cost GP cost for the required components.
  18804. * @property {number} materials.supply Quantity of this component available.
  18805. * @property {object} preparation Details on how this spell is prepared.
  18806. * @property {string} preparation.mode Spell preparation mode as defined in `DND5E.spellPreparationModes`.
  18807. * @property {boolean} preparation.prepared Is the spell currently prepared?
  18808. * @property {object} scaling Details on how casting at higher levels affects this spell.
  18809. * @property {string} scaling.mode Spell scaling mode as defined in `DND5E.spellScalingModes`.
  18810. * @property {string} scaling.formula Dice formula used for scaling.
  18811. */
  18812. class SpellData extends SystemDataModel.mixin(
  18813. ItemDescriptionTemplate, ActivatedEffectTemplate, ActionTemplate
  18814. ) {
  18815. /** @inheritdoc */
  18816. static defineSchema() {
  18817. return this.mergeSchema(super.defineSchema(), {
  18818. level: new foundry.data.fields.NumberField({
  18819. required: true, integer: true, initial: 1, min: 0, label: "DND5E.SpellLevel"
  18820. }),
  18821. school: new foundry.data.fields.StringField({required: true, label: "DND5E.SpellSchool"}),
  18822. components: new MappingField(new foundry.data.fields.BooleanField(), {
  18823. required: true, label: "DND5E.SpellComponents",
  18824. initialKeys: [...Object.keys(CONFIG.DND5E.spellComponents), ...Object.keys(CONFIG.DND5E.spellTags)]
  18825. }),
  18826. materials: new foundry.data.fields.SchemaField({
  18827. value: new foundry.data.fields.StringField({required: true, label: "DND5E.SpellMaterialsDescription"}),
  18828. consumed: new foundry.data.fields.BooleanField({required: true, label: "DND5E.SpellMaterialsConsumed"}),
  18829. cost: new foundry.data.fields.NumberField({
  18830. required: true, initial: 0, min: 0, label: "DND5E.SpellMaterialsCost"
  18831. }),
  18832. supply: new foundry.data.fields.NumberField({
  18833. required: true, initial: 0, min: 0, label: "DND5E.SpellMaterialsSupply"
  18834. })
  18835. }, {label: "DND5E.SpellMaterials"}),
  18836. preparation: new foundry.data.fields.SchemaField({
  18837. mode: new foundry.data.fields.StringField({
  18838. required: true, initial: "prepared", label: "DND5E.SpellPreparationMode"
  18839. }),
  18840. prepared: new foundry.data.fields.BooleanField({required: true, label: "DND5E.SpellPrepared"})
  18841. }, {label: "DND5E.SpellPreparation"}),
  18842. scaling: new foundry.data.fields.SchemaField({
  18843. mode: new foundry.data.fields.StringField({required: true, initial: "none", label: "DND5E.ScalingMode"}),
  18844. formula: new FormulaField({required: true, nullable: true, initial: null, label: "DND5E.ScalingFormula"})
  18845. }, {label: "DND5E.LevelScaling"})
  18846. });
  18847. }
  18848. /* -------------------------------------------- */
  18849. /* Migrations */
  18850. /* -------------------------------------------- */
  18851. /** @inheritdoc */
  18852. static migrateData(source) {
  18853. super.migrateData(source);
  18854. SpellData.#migrateComponentData(source);
  18855. SpellData.#migrateScaling(source);
  18856. }
  18857. /* -------------------------------------------- */
  18858. /**
  18859. * Migrate the spell's component object to remove any old, non-boolean values.
  18860. * @param {object} source The candidate source data from which the model will be constructed.
  18861. */
  18862. static #migrateComponentData(source) {
  18863. if ( !source.components ) return;
  18864. for ( const [key, value] of Object.entries(source.components) ) {
  18865. if ( typeof value !== "boolean" ) delete source.components[key];
  18866. }
  18867. }
  18868. /* -------------------------------------------- */
  18869. /**
  18870. * Migrate spell scaling.
  18871. * @param {object} source The candidate source data from which the model will be constructed.
  18872. */
  18873. static #migrateScaling(source) {
  18874. if ( !("scaling" in source) ) return;
  18875. if ( (source.scaling.mode === "") || (source.scaling.mode === null) ) source.scaling.mode = "none";
  18876. }
  18877. /* -------------------------------------------- */
  18878. /* Getters */
  18879. /* -------------------------------------------- */
  18880. /**
  18881. * Properties displayed in chat.
  18882. * @type {string[]}
  18883. */
  18884. get chatProperties() {
  18885. return [
  18886. this.parent.labels.level,
  18887. this.parent.labels.components.vsm + (this.parent.labels.materials ? ` (${this.parent.labels.materials})` : ""),
  18888. ...this.parent.labels.components.tags
  18889. ];
  18890. }
  18891. /* -------------------------------------------- */
  18892. /** @inheritdoc */
  18893. get _typeAbilityMod() {
  18894. return this.parent?.actor?.system.attributes.spellcasting || "int";
  18895. }
  18896. /* -------------------------------------------- */
  18897. /** @inheritdoc */
  18898. get _typeCriticalThreshold() {
  18899. return this.parent?.actor?.flags.dnd5e?.spellCriticalThreshold ?? Infinity;
  18900. }
  18901. /* -------------------------------------------- */
  18902. /**
  18903. * The proficiency multiplier for this item.
  18904. * @returns {number}
  18905. */
  18906. get proficiencyMultiplier() {
  18907. return 1;
  18908. }
  18909. }
  18910. /**
  18911. * Data definition for Subclass items.
  18912. * @mixes ItemDescriptionTemplate
  18913. *
  18914. * @property {string} identifier Identifier slug for this subclass.
  18915. * @property {string} classIdentifier Identifier slug for the class with which this subclass should be associated.
  18916. * @property {object[]} advancement Advancement objects for this subclass.
  18917. * @property {object} spellcasting Details on subclass's spellcasting ability.
  18918. * @property {string} spellcasting.progression Spell progression granted by class as from `DND5E.spellProgression`.
  18919. * @property {string} spellcasting.ability Ability score to use for spellcasting.
  18920. */
  18921. class SubclassData extends SystemDataModel.mixin(ItemDescriptionTemplate) {
  18922. /** @inheritdoc */
  18923. static defineSchema() {
  18924. return this.mergeSchema(super.defineSchema(), {
  18925. identifier: new IdentifierField({required: true, label: "DND5E.Identifier"}),
  18926. classIdentifier: new IdentifierField({
  18927. required: true, label: "DND5E.ClassIdentifier", hint: "DND5E.ClassIdentifierHint"
  18928. }),
  18929. advancement: new foundry.data.fields.ArrayField(new AdvancementField(), {label: "DND5E.AdvancementTitle"}),
  18930. spellcasting: new foundry.data.fields.SchemaField({
  18931. progression: new foundry.data.fields.StringField({
  18932. required: true, initial: "none", blank: false, label: "DND5E.SpellProgression"
  18933. }),
  18934. ability: new foundry.data.fields.StringField({required: true, label: "DND5E.SpellAbility"})
  18935. }, {label: "DND5E.Spellcasting"})
  18936. });
  18937. }
  18938. }
  18939. /**
  18940. * Data definition for Tool items.
  18941. * @mixes ItemDescriptionTemplate
  18942. * @mixes PhysicalItemTemplate
  18943. * @mixes EquippableItemTemplate
  18944. *
  18945. * @property {string} toolType Tool category as defined in `DND5E.toolTypes`.
  18946. * @property {string} baseItem Base tool as defined in `DND5E.toolIds` for determining proficiency.
  18947. * @property {string} ability Default ability when this tool is being used.
  18948. * @property {string} chatFlavor Additional text added to chat when this tool is used.
  18949. * @property {number} proficient Level of proficiency in this tool as defined in `DND5E.proficiencyLevels`.
  18950. * @property {string} bonus Bonus formula added to tool rolls.
  18951. */
  18952. class ToolData extends SystemDataModel.mixin(
  18953. ItemDescriptionTemplate, PhysicalItemTemplate, EquippableItemTemplate
  18954. ) {
  18955. /** @inheritdoc */
  18956. static defineSchema() {
  18957. return this.mergeSchema(super.defineSchema(), {
  18958. toolType: new foundry.data.fields.StringField({required: true, label: "DND5E.ItemToolType"}),
  18959. baseItem: new foundry.data.fields.StringField({required: true, label: "DND5E.ItemToolBase"}),
  18960. ability: new foundry.data.fields.StringField({
  18961. required: true, blank: true, label: "DND5E.DefaultAbilityCheck"
  18962. }),
  18963. chatFlavor: new foundry.data.fields.StringField({required: true, label: "DND5E.ChatFlavor"}),
  18964. proficient: new foundry.data.fields.NumberField({
  18965. required: true, initial: null, min: 0, max: 2, step: 0.5, label: "DND5E.ItemToolProficiency"
  18966. }),
  18967. bonus: new FormulaField({required: true, label: "DND5E.ItemToolBonus"})
  18968. });
  18969. }
  18970. /* -------------------------------------------- */
  18971. /* Migrations */
  18972. /* -------------------------------------------- */
  18973. /** @inheritdoc */
  18974. static migrateData(source) {
  18975. super.migrateData(source);
  18976. ToolData.#migrateAbility(source);
  18977. }
  18978. /* -------------------------------------------- */
  18979. /**
  18980. * Migrate the ability field.
  18981. * @param {object} source The candidate source data from which the model will be constructed.
  18982. */
  18983. static #migrateAbility(source) {
  18984. if ( Array.isArray(source.ability) ) source.ability = source.ability[0];
  18985. }
  18986. /* -------------------------------------------- */
  18987. /* Getters */
  18988. /* -------------------------------------------- */
  18989. /**
  18990. * Properties displayed in chat.
  18991. * @type {string[]}
  18992. */
  18993. get chatProperties() {
  18994. return [CONFIG.DND5E.abilities[this.ability]?.label];
  18995. }
  18996. /* -------------------------------------------- */
  18997. /**
  18998. * Which ability score modifier is used by this item?
  18999. * @type {string|null}
  19000. */
  19001. get abilityMod() {
  19002. return this.ability || "int";
  19003. }
  19004. /* -------------------------------------------- */
  19005. /**
  19006. * The proficiency multiplier for this item.
  19007. * @returns {number}
  19008. */
  19009. get proficiencyMultiplier() {
  19010. if ( Number.isFinite(this.proficient) ) return this.proficient;
  19011. const actor = this.parent.actor;
  19012. if ( !actor ) return 0;
  19013. if ( actor.type === "npc" ) return 1;
  19014. const baseItemProf = actor.system.tools?.[this.baseItem];
  19015. const categoryProf = actor.system.tools?.[this.toolType];
  19016. return Math.max(baseItemProf?.value ?? 0, categoryProf?.value ?? 0);
  19017. }
  19018. }
  19019. /**
  19020. * Data definition for Weapon items.
  19021. * @mixes ItemDescriptionTemplate
  19022. * @mixes PhysicalItemTemplate
  19023. * @mixes EquippableItemTemplate
  19024. * @mixes ActivatedEffectTemplate
  19025. * @mixes ActionTemplate
  19026. * @mixes MountableTemplate
  19027. *
  19028. * @property {string} weaponType Weapon category as defined in `DND5E.weaponTypes`.
  19029. * @property {string} baseItem Base weapon as defined in `DND5E.weaponIds` for determining proficiency.
  19030. * @property {object} properties Mapping of various weapon property booleans.
  19031. * @property {number} proficient Does the weapon's owner have proficiency?
  19032. */
  19033. class WeaponData extends SystemDataModel.mixin(
  19034. ItemDescriptionTemplate, PhysicalItemTemplate, EquippableItemTemplate,
  19035. ActivatedEffectTemplate, ActionTemplate, MountableTemplate
  19036. ) {
  19037. /** @inheritdoc */
  19038. static defineSchema() {
  19039. return this.mergeSchema(super.defineSchema(), {
  19040. weaponType: new foundry.data.fields.StringField({
  19041. required: true, initial: "simpleM", label: "DND5E.ItemWeaponType"
  19042. }),
  19043. baseItem: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.ItemWeaponBase"}),
  19044. properties: new MappingField(new foundry.data.fields.BooleanField(), {
  19045. required: true, initialKeys: CONFIG.DND5E.weaponProperties, label: "DND5E.ItemWeaponProperties"
  19046. }),
  19047. proficient: new foundry.data.fields.NumberField({
  19048. required: true, min: 0, max: 1, integer: true, initial: null, label: "DND5E.ProficiencyLevel"
  19049. })
  19050. });
  19051. }
  19052. /* -------------------------------------------- */
  19053. /* Migrations */
  19054. /* -------------------------------------------- */
  19055. /** @inheritdoc */
  19056. static migrateData(source) {
  19057. super.migrateData(source);
  19058. WeaponData.#migratePropertiesData(source);
  19059. WeaponData.#migrateProficient(source);
  19060. WeaponData.#migrateWeaponType(source);
  19061. }
  19062. /* -------------------------------------------- */
  19063. /**
  19064. * Migrate the weapons's properties object to remove any old, non-boolean values.
  19065. * @param {object} source The candidate source data from which the model will be constructed.
  19066. */
  19067. static #migratePropertiesData(source) {
  19068. if ( !source.properties ) return;
  19069. for ( const [key, value] of Object.entries(source.properties) ) {
  19070. if ( typeof value !== "boolean" ) delete source.properties[key];
  19071. }
  19072. }
  19073. /* -------------------------------------------- */
  19074. /**
  19075. * Migrate the proficient field to convert boolean values.
  19076. * @param {object} source The candidate source data from which the model will be constructed.
  19077. */
  19078. static #migrateProficient(source) {
  19079. if ( typeof source.proficient === "boolean" ) source.proficient = Number(source.proficient);
  19080. }
  19081. /* -------------------------------------------- */
  19082. /**
  19083. * Migrate the weapon type.
  19084. * @param {object} source The candidate source data from which the model will be constructed.
  19085. */
  19086. static #migrateWeaponType(source) {
  19087. if ( source.weaponType === null ) source.weaponType = "simpleM";
  19088. }
  19089. /* -------------------------------------------- */
  19090. /* Getters */
  19091. /* -------------------------------------------- */
  19092. /**
  19093. * Properties displayed in chat.
  19094. * @type {string[]}
  19095. */
  19096. get chatProperties() {
  19097. return [CONFIG.DND5E.weaponTypes[this.weaponType]];
  19098. }
  19099. /* -------------------------------------------- */
  19100. /** @inheritdoc */
  19101. get _typeAbilityMod() {
  19102. if ( ["simpleR", "martialR"].includes(this.weaponType) ) return "dex";
  19103. const abilities = this.parent?.actor?.system.abilities;
  19104. if ( this.properties.fin && abilities ) {
  19105. return (abilities.dex?.mod ?? 0) >= (abilities.str?.mod ?? 0) ? "dex" : "str";
  19106. }
  19107. return null;
  19108. }
  19109. /* -------------------------------------------- */
  19110. /** @inheritdoc */
  19111. get _typeCriticalThreshold() {
  19112. return this.parent?.actor?.flags.dnd5e?.weaponCriticalThreshold ?? Infinity;
  19113. }
  19114. /* -------------------------------------------- */
  19115. /**
  19116. * Is this item a separate large object like a siege engine or vehicle component that is
  19117. * usually mounted on fixtures rather than equipped, and has its own AC and HP?
  19118. * @type {boolean}
  19119. */
  19120. get isMountable() {
  19121. return this.weaponType === "siege";
  19122. }
  19123. /* -------------------------------------------- */
  19124. /**
  19125. * The proficiency multiplier for this item.
  19126. * @returns {number}
  19127. */
  19128. get proficiencyMultiplier() {
  19129. if ( Number.isFinite(this.proficient) ) return this.proficient;
  19130. const actor = this.parent.actor;
  19131. if ( !actor ) return 0;
  19132. if ( actor.type === "npc" ) return 1; // NPCs are always considered proficient with any weapon in their stat block.
  19133. const config = CONFIG.DND5E.weaponProficienciesMap;
  19134. const itemProf = config[this.weaponType];
  19135. const actorProfs = actor.system.traits?.weaponProf?.value ?? new Set();
  19136. const natural = this.weaponType === "natural";
  19137. const improvised = (this.weaponType === "improv") && !!actor.getFlag("dnd5e", "tavernBrawlerFeat");
  19138. const isProficient = natural || improvised || actorProfs.has(itemProf) || actorProfs.has(this.baseItem);
  19139. return Number(isProficient);
  19140. }
  19141. }
  19142. const config$1 = {
  19143. background: BackgroundData,
  19144. backpack: ContainerData,
  19145. class: ClassData,
  19146. consumable: ConsumableData,
  19147. equipment: EquipmentData,
  19148. feat: FeatData,
  19149. loot: LootData,
  19150. spell: SpellData,
  19151. subclass: SubclassData,
  19152. tool: ToolData,
  19153. weapon: WeaponData
  19154. };
  19155. var _module$2 = /*#__PURE__*/Object.freeze({
  19156. __proto__: null,
  19157. ActionTemplate: ActionTemplate,
  19158. ActivatedEffectTemplate: ActivatedEffectTemplate,
  19159. BackgroundData: BackgroundData,
  19160. ClassData: ClassData,
  19161. ConsumableData: ConsumableData,
  19162. ContainerData: ContainerData,
  19163. EquipmentData: EquipmentData,
  19164. EquippableItemTemplate: EquippableItemTemplate,
  19165. FeatData: FeatData,
  19166. ItemDescriptionTemplate: ItemDescriptionTemplate,
  19167. LootData: LootData,
  19168. MountableTemplate: MountableTemplate,
  19169. PhysicalItemTemplate: PhysicalItemTemplate,
  19170. SpellData: SpellData,
  19171. SubclassData: SubclassData,
  19172. ToolData: ToolData,
  19173. WeaponData: WeaponData,
  19174. config: config$1
  19175. });
  19176. /**
  19177. * Data definition for Class Summary journal entry pages.
  19178. *
  19179. * @property {string} item UUID of the class item included.
  19180. * @property {object} description
  19181. * @property {string} description.value Introductory description for the class.
  19182. * @property {string} description.additionalHitPoints Additional text displayed beneath the hit points section.
  19183. * @property {string} description.additionalTraits Additional text displayed beneath the traits section.
  19184. * @property {string} description.additionalEquipment Additional text displayed beneath the equipment section.
  19185. * @property {string} description.subclass Introduction to the subclass section.
  19186. * @property {string} subclassHeader Subclass header to replace the default.
  19187. * @property {Set<string>} subclassItems UUIDs of all subclasses to display.
  19188. */
  19189. class ClassJournalPageData extends foundry.abstract.DataModel {
  19190. static defineSchema() {
  19191. return {
  19192. item: new foundry.data.fields.StringField({required: true, label: "JOURNALENTRYPAGE.DND5E.Class.Item"}),
  19193. description: new foundry.data.fields.SchemaField({
  19194. value: new foundry.data.fields.HTMLField({
  19195. label: "JOURNALENTRYPAGE.DND5E.Class.Description",
  19196. hint: "JOURNALENTRYPAGE.DND5E.Class.DescriptionHint"
  19197. }),
  19198. additionalHitPoints: new foundry.data.fields.HTMLField({
  19199. label: "JOURNALENTRYPAGE.DND5E.Class.AdditionalHitPoints",
  19200. hint: "JOURNALENTRYPAGE.DND5E.Class.AdditionalHitPointsHint"
  19201. }),
  19202. additionalTraits: new foundry.data.fields.HTMLField({
  19203. label: "JOURNALENTRYPAGE.DND5E.Class.AdditionalTraits",
  19204. hint: "JOURNALENTRYPAGE.DND5E.Class.AdditionalTraitsHint"
  19205. }),
  19206. additionalEquipment: new foundry.data.fields.HTMLField({
  19207. label: "JOURNALENTRYPAGE.DND5E.Class.AdditionalEquipment",
  19208. hint: "JOURNALENTRYPAGE.DND5E.Class.AdditionalEquipmentHint"
  19209. }),
  19210. subclass: new foundry.data.fields.HTMLField({
  19211. label: "JOURNALENTRYPAGE.DND5E.Class.SubclassDescription",
  19212. hint: "JOURNALENTRYPAGE.DND5E.Class.SubclassDescriptionHint"
  19213. })
  19214. }),
  19215. subclassHeader: new foundry.data.fields.StringField({
  19216. label: "JOURNALENTRYPAGE.DND5E.Class.SubclassHeader"
  19217. }),
  19218. subclassItems: new foundry.data.fields.SetField(new foundry.data.fields.StringField(), {
  19219. label: "JOURNALENTRYPAGE.DND5E.Class.SubclassItems"
  19220. })
  19221. };
  19222. }
  19223. }
  19224. const config = {
  19225. class: ClassJournalPageData
  19226. };
  19227. var _module$1 = /*#__PURE__*/Object.freeze({
  19228. __proto__: null,
  19229. ClassJournalPageData: ClassJournalPageData,
  19230. config: config
  19231. });
  19232. var _module = /*#__PURE__*/Object.freeze({
  19233. __proto__: null,
  19234. CurrencyTemplate: CurrencyTemplate
  19235. });
  19236. var dataModels = /*#__PURE__*/Object.freeze({
  19237. __proto__: null,
  19238. SparseDataModel: SparseDataModel,
  19239. SystemDataModel: SystemDataModel,
  19240. actor: _module$4,
  19241. advancement: _module$3,
  19242. fields: fields,
  19243. item: _module$2,
  19244. journal: _module$1,
  19245. shared: _module
  19246. });
  19247. /**
  19248. * A type of Roll specific to a d20-based check, save, or attack roll in the 5e system.
  19249. * @param {string} formula The string formula to parse
  19250. * @param {object} data The data object against which to parse attributes within the formula
  19251. * @param {object} [options={}] Extra optional arguments which describe or modify the D20Roll
  19252. * @param {number} [options.advantageMode] What advantage modifier to apply to the roll (none, advantage,
  19253. * disadvantage)
  19254. * @param {number} [options.critical] The value of d20 result which represents a critical success
  19255. * @param {number} [options.fumble] The value of d20 result which represents a critical failure
  19256. * @param {(number)} [options.targetValue] Assign a target value against which the result of this roll should be
  19257. * compared
  19258. * @param {boolean} [options.elvenAccuracy=false] Allow Elven Accuracy to modify this roll?
  19259. * @param {boolean} [options.halflingLucky=false] Allow Halfling Luck to modify this roll?
  19260. * @param {boolean} [options.reliableTalent=false] Allow Reliable Talent to modify this roll?
  19261. */
  19262. class D20Roll extends Roll {
  19263. constructor(formula, data, options) {
  19264. super(formula, data, options);
  19265. if ( !this.options.configured ) this.configureModifiers();
  19266. }
  19267. /* -------------------------------------------- */
  19268. /**
  19269. * Create a D20Roll from a standard Roll instance.
  19270. * @param {Roll} roll
  19271. * @returns {D20Roll}
  19272. */
  19273. static fromRoll(roll) {
  19274. const newRoll = new this(roll.formula, roll.data, roll.options);
  19275. Object.assign(newRoll, roll);
  19276. return newRoll;
  19277. }
  19278. /* -------------------------------------------- */
  19279. /**
  19280. * Determine whether a d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied.
  19281. * @param {object} [options]
  19282. * @param {Event} [options.event] The Event that triggered the roll.
  19283. * @param {boolean} [options.advantage] Is something granting this roll advantage?
  19284. * @param {boolean} [options.disadvantage] Is something granting this roll disadvantage?
  19285. * @param {boolean} [options.fastForward] Should the roll dialog be skipped?
  19286. * @returns {{advantageMode: D20Roll.ADV_MODE, isFF: boolean}} Whether the roll is fast-forwarded, and its advantage
  19287. * mode.
  19288. */
  19289. static determineAdvantageMode({event, advantage=false, disadvantage=false, fastForward}={}) {
  19290. const isFF = fastForward ?? (event?.shiftKey || event?.altKey || event?.ctrlKey || event?.metaKey);
  19291. let advantageMode = this.ADV_MODE.NORMAL;
  19292. if ( advantage || event?.altKey ) advantageMode = this.ADV_MODE.ADVANTAGE;
  19293. else if ( disadvantage || event?.ctrlKey || event?.metaKey ) advantageMode = this.ADV_MODE.DISADVANTAGE;
  19294. return {isFF: !!isFF, advantageMode};
  19295. }
  19296. /* -------------------------------------------- */
  19297. /**
  19298. * Advantage mode of a 5e d20 roll
  19299. * @enum {number}
  19300. */
  19301. static ADV_MODE = {
  19302. NORMAL: 0,
  19303. ADVANTAGE: 1,
  19304. DISADVANTAGE: -1
  19305. }
  19306. /* -------------------------------------------- */
  19307. /**
  19308. * The HTML template path used to configure evaluation of this Roll
  19309. * @type {string}
  19310. */
  19311. static EVALUATION_TEMPLATE = "systems/dnd5e/templates/chat/roll-dialog.hbs";
  19312. /* -------------------------------------------- */
  19313. /**
  19314. * Does this roll start with a d20?
  19315. * @type {boolean}
  19316. */
  19317. get validD20Roll() {
  19318. return (this.terms[0] instanceof Die) && (this.terms[0].faces === 20);
  19319. }
  19320. /* -------------------------------------------- */
  19321. /**
  19322. * A convenience reference for whether this D20Roll has advantage
  19323. * @type {boolean}
  19324. */
  19325. get hasAdvantage() {
  19326. return this.options.advantageMode === D20Roll.ADV_MODE.ADVANTAGE;
  19327. }
  19328. /* -------------------------------------------- */
  19329. /**
  19330. * A convenience reference for whether this D20Roll has disadvantage
  19331. * @type {boolean}
  19332. */
  19333. get hasDisadvantage() {
  19334. return this.options.advantageMode === D20Roll.ADV_MODE.DISADVANTAGE;
  19335. }
  19336. /* -------------------------------------------- */
  19337. /**
  19338. * Is this roll a critical success? Returns undefined if roll isn't evaluated.
  19339. * @type {boolean|void}
  19340. */
  19341. get isCritical() {
  19342. if ( !this.validD20Roll || !this._evaluated ) return undefined;
  19343. if ( !Number.isNumeric(this.options.critical) ) return false;
  19344. return this.dice[0].total >= this.options.critical;
  19345. }
  19346. /* -------------------------------------------- */
  19347. /**
  19348. * Is this roll a critical failure? Returns undefined if roll isn't evaluated.
  19349. * @type {boolean|void}
  19350. */
  19351. get isFumble() {
  19352. if ( !this.validD20Roll || !this._evaluated ) return undefined;
  19353. if ( !Number.isNumeric(this.options.fumble) ) return false;
  19354. return this.dice[0].total <= this.options.fumble;
  19355. }
  19356. /* -------------------------------------------- */
  19357. /* D20 Roll Methods */
  19358. /* -------------------------------------------- */
  19359. /**
  19360. * Apply optional modifiers which customize the behavior of the d20term
  19361. * @private
  19362. */
  19363. configureModifiers() {
  19364. if ( !this.validD20Roll ) return;
  19365. const d20 = this.terms[0];
  19366. d20.modifiers = [];
  19367. // Halfling Lucky
  19368. if ( this.options.halflingLucky ) d20.modifiers.push("r1=1");
  19369. // Reliable Talent
  19370. if ( this.options.reliableTalent ) d20.modifiers.push("min10");
  19371. // Handle Advantage or Disadvantage
  19372. if ( this.hasAdvantage ) {
  19373. d20.number = this.options.elvenAccuracy ? 3 : 2;
  19374. d20.modifiers.push("kh");
  19375. d20.options.advantage = true;
  19376. }
  19377. else if ( this.hasDisadvantage ) {
  19378. d20.number = 2;
  19379. d20.modifiers.push("kl");
  19380. d20.options.disadvantage = true;
  19381. }
  19382. else d20.number = 1;
  19383. // Assign critical and fumble thresholds
  19384. if ( this.options.critical ) d20.options.critical = this.options.critical;
  19385. if ( this.options.fumble ) d20.options.fumble = this.options.fumble;
  19386. if ( this.options.targetValue ) d20.options.target = this.options.targetValue;
  19387. // Re-compile the underlying formula
  19388. this._formula = this.constructor.getFormula(this.terms);
  19389. // Mark configuration as complete
  19390. this.options.configured = true;
  19391. }
  19392. /* -------------------------------------------- */
  19393. /** @inheritdoc */
  19394. async toMessage(messageData={}, options={}) {
  19395. // Evaluate the roll now so we have the results available to determine whether reliable talent came into play
  19396. if ( !this._evaluated ) await this.evaluate({async: true});
  19397. // Add appropriate advantage mode message flavor and dnd5e roll flags
  19398. messageData.flavor = messageData.flavor || this.options.flavor;
  19399. if ( this.hasAdvantage ) messageData.flavor += ` (${game.i18n.localize("DND5E.Advantage")})`;
  19400. else if ( this.hasDisadvantage ) messageData.flavor += ` (${game.i18n.localize("DND5E.Disadvantage")})`;
  19401. // Add reliable talent to the d20-term flavor text if it applied
  19402. if ( this.validD20Roll && this.options.reliableTalent ) {
  19403. const d20 = this.dice[0];
  19404. const isRT = d20.results.every(r => !r.active || (r.result < 10));
  19405. const label = `(${game.i18n.localize("DND5E.FlagsReliableTalent")})`;
  19406. if ( isRT ) d20.options.flavor = d20.options.flavor ? `${d20.options.flavor} (${label})` : label;
  19407. }
  19408. // Record the preferred rollMode
  19409. options.rollMode = options.rollMode ?? this.options.rollMode;
  19410. return super.toMessage(messageData, options);
  19411. }
  19412. /* -------------------------------------------- */
  19413. /* Configuration Dialog */
  19414. /* -------------------------------------------- */
  19415. /**
  19416. * Create a Dialog prompt used to configure evaluation of an existing D20Roll instance.
  19417. * @param {object} data Dialog configuration data
  19418. * @param {string} [data.title] The title of the shown dialog window
  19419. * @param {number} [data.defaultRollMode] The roll mode that the roll mode select element should default to
  19420. * @param {number} [data.defaultAction] The button marked as default
  19421. * @param {boolean} [data.chooseModifier] Choose which ability modifier should be applied to the roll?
  19422. * @param {string} [data.defaultAbility] For tool rolls, the default ability modifier applied to the roll
  19423. * @param {string} [data.template] A custom path to an HTML template to use instead of the default
  19424. * @param {object} options Additional Dialog customization options
  19425. * @returns {Promise<D20Roll|null>} A resulting D20Roll object constructed with the dialog, or null if the
  19426. * dialog was closed
  19427. */
  19428. async configureDialog({title, defaultRollMode, defaultAction=D20Roll.ADV_MODE.NORMAL, chooseModifier=false,
  19429. defaultAbility, template}={}, options={}) {
  19430. // Render the Dialog inner HTML
  19431. const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
  19432. formula: `${this.formula} + @bonus`,
  19433. defaultRollMode,
  19434. rollModes: CONFIG.Dice.rollModes,
  19435. chooseModifier,
  19436. defaultAbility,
  19437. abilities: CONFIG.DND5E.abilities
  19438. });
  19439. let defaultButton = "normal";
  19440. switch ( defaultAction ) {
  19441. case D20Roll.ADV_MODE.ADVANTAGE: defaultButton = "advantage"; break;
  19442. case D20Roll.ADV_MODE.DISADVANTAGE: defaultButton = "disadvantage"; break;
  19443. }
  19444. // Create the Dialog window and await submission of the form
  19445. return new Promise(resolve => {
  19446. new Dialog({
  19447. title,
  19448. content,
  19449. buttons: {
  19450. advantage: {
  19451. label: game.i18n.localize("DND5E.Advantage"),
  19452. callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.ADVANTAGE))
  19453. },
  19454. normal: {
  19455. label: game.i18n.localize("DND5E.Normal"),
  19456. callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.NORMAL))
  19457. },
  19458. disadvantage: {
  19459. label: game.i18n.localize("DND5E.Disadvantage"),
  19460. callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.DISADVANTAGE))
  19461. }
  19462. },
  19463. default: defaultButton,
  19464. close: () => resolve(null)
  19465. }, options).render(true);
  19466. });
  19467. }
  19468. /* -------------------------------------------- */
  19469. /**
  19470. * Handle submission of the Roll evaluation configuration Dialog
  19471. * @param {jQuery} html The submitted dialog content
  19472. * @param {number} advantageMode The chosen advantage mode
  19473. * @returns {D20Roll} This damage roll.
  19474. * @private
  19475. */
  19476. _onDialogSubmit(html, advantageMode) {
  19477. const form = html[0].querySelector("form");
  19478. // Append a situational bonus term
  19479. if ( form.bonus.value ) {
  19480. const bonus = new Roll(form.bonus.value, this.data);
  19481. if ( !(bonus.terms[0] instanceof OperatorTerm) ) this.terms.push(new OperatorTerm({operator: "+"}));
  19482. this.terms = this.terms.concat(bonus.terms);
  19483. }
  19484. // Customize the modifier
  19485. if ( form.ability?.value ) {
  19486. const abl = this.data.abilities[form.ability.value];
  19487. this.terms = this.terms.flatMap(t => {
  19488. if ( t.term === "@mod" ) return new NumericTerm({number: abl.mod});
  19489. if ( t.term === "@abilityCheckBonus" ) {
  19490. const bonus = abl.bonuses?.check;
  19491. if ( bonus ) return new Roll(bonus, this.data).terms;
  19492. return new NumericTerm({number: 0});
  19493. }
  19494. return t;
  19495. });
  19496. this.options.flavor += ` (${CONFIG.DND5E.abilities[form.ability.value]?.label ?? ""})`;
  19497. }
  19498. // Apply advantage or disadvantage
  19499. this.options.advantageMode = advantageMode;
  19500. this.options.rollMode = form.rollMode.value;
  19501. this.configureModifiers();
  19502. return this;
  19503. }
  19504. }
  19505. /**
  19506. * A type of Roll specific to a damage (or healing) roll in the 5e system.
  19507. * @param {string} formula The string formula to parse
  19508. * @param {object} data The data object against which to parse attributes within the formula
  19509. * @param {object} [options={}] Extra optional arguments which describe or modify the DamageRoll
  19510. * @param {number} [options.criticalBonusDice=0] A number of bonus damage dice that are added for critical hits
  19511. * @param {number} [options.criticalMultiplier=2] A critical hit multiplier which is applied to critical hits
  19512. * @param {boolean} [options.multiplyNumeric=false] Multiply numeric terms by the critical multiplier
  19513. * @param {boolean} [options.powerfulCritical=false] Apply the "powerful criticals" house rule to critical hits
  19514. * @param {string} [options.criticalBonusDamage] An extra damage term that is applied only on a critical hit
  19515. */
  19516. class DamageRoll extends Roll {
  19517. constructor(formula, data, options) {
  19518. super(formula, data, options);
  19519. if ( !this.options.preprocessed ) this.preprocessFormula();
  19520. // For backwards compatibility, skip rolls which do not have the "critical" option defined
  19521. if ( (this.options.critical !== undefined) && !this.options.configured ) this.configureDamage();
  19522. }
  19523. /* -------------------------------------------- */
  19524. /**
  19525. * Create a DamageRoll from a standard Roll instance.
  19526. * @param {Roll} roll
  19527. * @returns {DamageRoll}
  19528. */
  19529. static fromRoll(roll) {
  19530. const newRoll = new this(roll.formula, roll.data, roll.options);
  19531. Object.assign(newRoll, roll);
  19532. return newRoll;
  19533. }
  19534. /* -------------------------------------------- */
  19535. /**
  19536. * The HTML template path used to configure evaluation of this Roll
  19537. * @type {string}
  19538. */
  19539. static EVALUATION_TEMPLATE = "systems/dnd5e/templates/chat/roll-dialog.hbs";
  19540. /* -------------------------------------------- */
  19541. /**
  19542. * A convenience reference for whether this DamageRoll is a critical hit
  19543. * @type {boolean}
  19544. */
  19545. get isCritical() {
  19546. return this.options.critical;
  19547. }
  19548. /* -------------------------------------------- */
  19549. /* Damage Roll Methods */
  19550. /* -------------------------------------------- */
  19551. /**
  19552. * Perform any term-merging required to ensure that criticals can be calculated successfully.
  19553. * @protected
  19554. */
  19555. preprocessFormula() {
  19556. for ( let [i, term] of this.terms.entries() ) {
  19557. const nextTerm = this.terms[i + 1];
  19558. const prevTerm = this.terms[i - 1];
  19559. // Convert shorthand dX terms to 1dX preemptively to allow them to be appropriately doubled for criticals
  19560. if ( (term instanceof StringTerm) && /^d\d+/.test(term.term) && !(prevTerm instanceof ParentheticalTerm) ) {
  19561. const formula = `1${term.term}`;
  19562. const newTerm = new Roll(formula).terms[0];
  19563. this.terms.splice(i, 1, newTerm);
  19564. term = newTerm;
  19565. }
  19566. // Merge parenthetical terms that follow string terms to build a dice term (to allow criticals)
  19567. else if ( (term instanceof ParentheticalTerm) && (prevTerm instanceof StringTerm)
  19568. && prevTerm.term.match(/^[0-9]*d$/)) {
  19569. if ( term.isDeterministic ) {
  19570. let newFormula = `${prevTerm.term}${term.evaluate().total}`;
  19571. let deleteCount = 2;
  19572. // Merge in any roll modifiers
  19573. if ( nextTerm instanceof StringTerm ) {
  19574. newFormula += nextTerm.term;
  19575. deleteCount += 1;
  19576. }
  19577. const newTerm = (new Roll(newFormula)).terms[0];
  19578. this.terms.splice(i - 1, deleteCount, newTerm);
  19579. term = newTerm;
  19580. }
  19581. }
  19582. // Merge any parenthetical terms followed by string terms
  19583. else if ( (term instanceof ParentheticalTerm || term instanceof MathTerm) && (nextTerm instanceof StringTerm)
  19584. && nextTerm.term.match(/^d[0-9]*$/)) {
  19585. if ( term.isDeterministic ) {
  19586. const newFormula = `${term.evaluate().total}${nextTerm.term}`;
  19587. const newTerm = (new Roll(newFormula)).terms[0];
  19588. this.terms.splice(i, 2, newTerm);
  19589. term = newTerm;
  19590. }
  19591. }
  19592. }
  19593. // Re-compile the underlying formula
  19594. this._formula = this.constructor.getFormula(this.terms);
  19595. // Mark configuration as complete
  19596. this.options.preprocessed = true;
  19597. }
  19598. /* -------------------------------------------- */
  19599. /**
  19600. * Apply optional modifiers which customize the behavior of the d20term.
  19601. * @protected
  19602. */
  19603. configureDamage() {
  19604. let flatBonus = 0;
  19605. for ( let [i, term] of this.terms.entries() ) {
  19606. // Multiply dice terms
  19607. if ( term instanceof DiceTerm ) {
  19608. term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
  19609. term.number = term.options.baseNumber;
  19610. if ( this.isCritical ) {
  19611. let cm = this.options.criticalMultiplier ?? 2;
  19612. // Powerful critical - maximize damage and reduce the multiplier by 1
  19613. if ( this.options.powerfulCritical ) {
  19614. flatBonus += (term.number * term.faces);
  19615. cm = Math.max(1, cm-1);
  19616. }
  19617. // Alter the damage term
  19618. let cb = (this.options.criticalBonusDice && (i === 0)) ? this.options.criticalBonusDice : 0;
  19619. term.alter(cm, cb);
  19620. term.options.critical = true;
  19621. }
  19622. }
  19623. // Multiply numeric terms
  19624. else if ( this.options.multiplyNumeric && (term instanceof NumericTerm) ) {
  19625. term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
  19626. term.number = term.options.baseNumber;
  19627. if ( this.isCritical ) {
  19628. term.number *= (this.options.criticalMultiplier ?? 2);
  19629. term.options.critical = true;
  19630. }
  19631. }
  19632. }
  19633. // Add powerful critical bonus
  19634. if ( this.options.powerfulCritical && (flatBonus > 0) ) {
  19635. this.terms.push(new OperatorTerm({operator: "+"}));
  19636. this.terms.push(new NumericTerm({number: flatBonus}, {flavor: game.i18n.localize("DND5E.PowerfulCritical")}));
  19637. }
  19638. // Add extra critical damage term
  19639. if ( this.isCritical && this.options.criticalBonusDamage ) {
  19640. const extra = new Roll(this.options.criticalBonusDamage, this.data);
  19641. if ( !(extra.terms[0] instanceof OperatorTerm) ) this.terms.push(new OperatorTerm({operator: "+"}));
  19642. this.terms.push(...extra.terms);
  19643. }
  19644. // Re-compile the underlying formula
  19645. this._formula = this.constructor.getFormula(this.terms);
  19646. // Mark configuration as complete
  19647. this.options.configured = true;
  19648. }
  19649. /* -------------------------------------------- */
  19650. /** @inheritdoc */
  19651. toMessage(messageData={}, options={}) {
  19652. messageData.flavor = messageData.flavor || this.options.flavor;
  19653. if ( this.isCritical ) {
  19654. const label = game.i18n.localize("DND5E.CriticalHit");
  19655. messageData.flavor = messageData.flavor ? `${messageData.flavor} (${label})` : label;
  19656. }
  19657. options.rollMode = options.rollMode ?? this.options.rollMode;
  19658. return super.toMessage(messageData, options);
  19659. }
  19660. /* -------------------------------------------- */
  19661. /* Configuration Dialog */
  19662. /* -------------------------------------------- */
  19663. /**
  19664. * Create a Dialog prompt used to configure evaluation of an existing D20Roll instance.
  19665. * @param {object} data Dialog configuration data
  19666. * @param {string} [data.title] The title of the shown dialog window
  19667. * @param {number} [data.defaultRollMode] The roll mode that the roll mode select element should default to
  19668. * @param {string} [data.defaultCritical] Should critical be selected as default
  19669. * @param {string} [data.template] A custom path to an HTML template to use instead of the default
  19670. * @param {boolean} [data.allowCritical=true] Allow critical hit to be chosen as a possible damage mode
  19671. * @param {object} options Additional Dialog customization options
  19672. * @returns {Promise<D20Roll|null>} A resulting D20Roll object constructed with the dialog, or null if the
  19673. * dialog was closed
  19674. */
  19675. async configureDialog({title, defaultRollMode, defaultCritical=false, template, allowCritical=true}={}, options={}) {
  19676. // Render the Dialog inner HTML
  19677. const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
  19678. formula: `${this.formula} + @bonus`,
  19679. defaultRollMode,
  19680. rollModes: CONFIG.Dice.rollModes
  19681. });
  19682. // Create the Dialog window and await submission of the form
  19683. return new Promise(resolve => {
  19684. new Dialog({
  19685. title,
  19686. content,
  19687. buttons: {
  19688. critical: {
  19689. condition: allowCritical,
  19690. label: game.i18n.localize("DND5E.CriticalHit"),
  19691. callback: html => resolve(this._onDialogSubmit(html, true))
  19692. },
  19693. normal: {
  19694. label: game.i18n.localize(allowCritical ? "DND5E.Normal" : "DND5E.Roll"),
  19695. callback: html => resolve(this._onDialogSubmit(html, false))
  19696. }
  19697. },
  19698. default: defaultCritical ? "critical" : "normal",
  19699. close: () => resolve(null)
  19700. }, options).render(true);
  19701. });
  19702. }
  19703. /* -------------------------------------------- */
  19704. /**
  19705. * Handle submission of the Roll evaluation configuration Dialog
  19706. * @param {jQuery} html The submitted dialog content
  19707. * @param {boolean} isCritical Is the damage a critical hit?
  19708. * @returns {DamageRoll} This damage roll.
  19709. * @private
  19710. */
  19711. _onDialogSubmit(html, isCritical) {
  19712. const form = html[0].querySelector("form");
  19713. // Append a situational bonus term
  19714. if ( form.bonus.value ) {
  19715. const bonus = new DamageRoll(form.bonus.value, this.data);
  19716. if ( !(bonus.terms[0] instanceof OperatorTerm) ) this.terms.push(new OperatorTerm({operator: "+"}));
  19717. this.terms = this.terms.concat(bonus.terms);
  19718. }
  19719. // Apply advantage or disadvantage
  19720. this.options.critical = isCritical;
  19721. this.options.rollMode = form.rollMode.value;
  19722. this.configureDamage();
  19723. return this;
  19724. }
  19725. /* -------------------------------------------- */
  19726. /** @inheritdoc */
  19727. static fromData(data) {
  19728. const roll = super.fromData(data);
  19729. roll._formula = this.getFormula(roll.terms);
  19730. return roll;
  19731. }
  19732. }
  19733. var dice = /*#__PURE__*/Object.freeze({
  19734. __proto__: null,
  19735. D20Roll: D20Roll,
  19736. DamageRoll: DamageRoll,
  19737. d20Roll: d20Roll,
  19738. damageRoll: damageRoll,
  19739. simplifyRollFormula: simplifyRollFormula
  19740. });
  19741. /**
  19742. * Extend the base TokenDocument class to implement system-specific HP bar logic.
  19743. */
  19744. class TokenDocument5e extends TokenDocument {
  19745. /** @inheritdoc */
  19746. getBarAttribute(...args) {
  19747. const data = super.getBarAttribute(...args);
  19748. if ( data && (data.attribute === "attributes.hp") ) {
  19749. const hp = this.actor.system.attributes.hp || {};
  19750. data.value += (hp.temp || 0);
  19751. data.max = Math.max(0, data.max + (hp.tempmax || 0));
  19752. }
  19753. return data;
  19754. }
  19755. /* -------------------------------------------- */
  19756. /** @inheritdoc */
  19757. static getTrackedAttributes(data, _path=[]) {
  19758. if ( !game.dnd5e.isV10 ) return super.getTrackedAttributes(data, _path);
  19759. if ( data instanceof foundry.abstract.DataModel ) return this._getTrackedAttributesFromSchema(data.schema, _path);
  19760. const attributes = super.getTrackedAttributes(data, _path);
  19761. if ( _path.length ) return attributes;
  19762. const allowed = CONFIG.DND5E.trackableAttributes;
  19763. attributes.value = attributes.value.filter(attrs => this._isAllowedAttribute(allowed, attrs));
  19764. return attributes;
  19765. }
  19766. /* -------------------------------------------- */
  19767. /** @inheritdoc */
  19768. static _getTrackedAttributesFromSchema(schema, _path=[]) {
  19769. const isSchema = field => field instanceof foundry.data.fields.SchemaField;
  19770. const isModel = field => field instanceof foundry.data.fields.EmbeddedDataField;
  19771. const attributes = {bar: [], value: []};
  19772. for ( const [name, field] of Object.entries(schema.fields) ) {
  19773. const p = _path.concat([name]);
  19774. if ( field instanceof foundry.data.fields.NumberField ) attributes.value.push(p);
  19775. if ( isSchema(field) || isModel(field) ) {
  19776. const schema = isModel(field) ? field.model.schema : field;
  19777. const isBar = schema.has("value") && schema.has("max");
  19778. if ( isBar ) attributes.bar.push(p);
  19779. else {
  19780. const inner = this._getTrackedAttributesFromSchema(schema, p);
  19781. attributes.bar.push(...inner.bar);
  19782. attributes.value.push(...inner.value);
  19783. }
  19784. }
  19785. if ( !(field instanceof MappingField) ) continue;
  19786. if ( !field.initialKeys || foundry.utils.isEmpty(field.initialKeys) ) continue;
  19787. if ( !isSchema(field.model) && !isModel(field.model) ) continue;
  19788. const keys = Array.isArray(field.initialKeys) ? field.initialKeys : Object.keys(field.initialKeys);
  19789. for ( const key of keys ) {
  19790. const inner = this._getTrackedAttributesFromSchema(field.model, p.concat([key]));
  19791. attributes.bar.push(...inner.bar);
  19792. attributes.value.push(...inner.value);
  19793. }
  19794. }
  19795. return attributes;
  19796. }
  19797. /* -------------------------------------------- */
  19798. /**
  19799. * Get an Array of attribute choices which are suitable for being consumed by an item usage.
  19800. * @param {object} data The actor data.
  19801. * @returns {string[]}
  19802. */
  19803. static getConsumedAttributes(data) {
  19804. return CONFIG.DND5E.consumableResources;
  19805. }
  19806. /* -------------------------------------------- */
  19807. /**
  19808. * Traverse the configured allowed attributes to see if the provided one matches.
  19809. * @param {object} allowed The allowed attributes structure.
  19810. * @param {string[]} attrs The attributes list to test.
  19811. * @returns {boolean} Whether the given attribute is allowed.
  19812. * @private
  19813. */
  19814. static _isAllowedAttribute(allowed, attrs) {
  19815. let allow = allowed;
  19816. for ( const attr of attrs ) {
  19817. if ( allow === undefined ) return false;
  19818. if ( allow === true ) return true;
  19819. if ( allow["*"] !== undefined ) allow = allow["*"];
  19820. else allow = allow[attr];
  19821. }
  19822. return allow !== undefined;
  19823. }
  19824. }
  19825. /**
  19826. * Highlight critical success or failure on d20 rolls.
  19827. * @param {ChatMessage} message Message being prepared.
  19828. * @param {HTMLElement} html Rendered contents of the message.
  19829. * @param {object} data Configuration data passed to the message.
  19830. */
  19831. function highlightCriticalSuccessFailure(message, html, data) {
  19832. if ( !message.isRoll || !message.isContentVisible || !message.rolls.length ) return;
  19833. // Highlight rolls where the first part is a d20 roll
  19834. let d20Roll = message.rolls.find(r => {
  19835. const d0 = r.dice[0];
  19836. return (d0?.faces === 20) && (d0?.values.length === 1);
  19837. });
  19838. if ( !d20Roll ) return;
  19839. d20Roll = dnd5e.dice.D20Roll.fromRoll(d20Roll);
  19840. const d = d20Roll.dice[0];
  19841. const isModifiedRoll = ("success" in d.results[0]) || d.options.marginSuccess || d.options.marginFailure;
  19842. if ( isModifiedRoll ) return;
  19843. // Highlight successes and failures
  19844. if ( d20Roll.isCritical ) html.find(".dice-total").addClass("critical");
  19845. else if ( d20Roll.isFumble ) html.find(".dice-total").addClass("fumble");
  19846. else if ( d.options.target ) {
  19847. if ( d20Roll.total >= d.options.target ) html.find(".dice-total").addClass("success");
  19848. else html.find(".dice-total").addClass("failure");
  19849. }
  19850. }
  19851. /* -------------------------------------------- */
  19852. /**
  19853. * Optionally hide the display of chat card action buttons which cannot be performed by the user
  19854. * @param {ChatMessage} message Message being prepared.
  19855. * @param {HTMLElement} html Rendered contents of the message.
  19856. * @param {object} data Configuration data passed to the message.
  19857. */
  19858. function displayChatActionButtons(message, html, data) {
  19859. const chatCard = html.find(".dnd5e.chat-card");
  19860. if ( chatCard.length > 0 ) {
  19861. const flavor = html.find(".flavor-text");
  19862. if ( flavor.text() === html.find(".item-name").text() ) flavor.remove();
  19863. // If the user is the message author or the actor owner, proceed
  19864. let actor = game.actors.get(data.message.speaker.actor);
  19865. if ( actor && actor.isOwner ) return;
  19866. else if ( game.user.isGM || (data.author.id === game.user.id)) return;
  19867. // Otherwise conceal action buttons except for saving throw
  19868. const buttons = chatCard.find("button[data-action]");
  19869. buttons.each((i, btn) => {
  19870. if ( btn.dataset.action === "save" ) return;
  19871. btn.style.display = "none";
  19872. });
  19873. }
  19874. }
  19875. /* -------------------------------------------- */
  19876. /**
  19877. * This function is used to hook into the Chat Log context menu to add additional options to each message
  19878. * These options make it easy to conveniently apply damage to controlled tokens based on the value of a Roll
  19879. *
  19880. * @param {HTMLElement} html The Chat Message being rendered
  19881. * @param {object[]} options The Array of Context Menu options
  19882. *
  19883. * @returns {object[]} The extended options Array including new context choices
  19884. */
  19885. function addChatMessageContextOptions(html, options) {
  19886. let canApply = li => {
  19887. const message = game.messages.get(li.data("messageId"));
  19888. return message?.isRoll && message?.isContentVisible && canvas.tokens?.controlled.length;
  19889. };
  19890. options.push(
  19891. {
  19892. name: game.i18n.localize("DND5E.ChatContextDamage"),
  19893. icon: '<i class="fas fa-user-minus"></i>',
  19894. condition: canApply,
  19895. callback: li => applyChatCardDamage(li, 1)
  19896. },
  19897. {
  19898. name: game.i18n.localize("DND5E.ChatContextHealing"),
  19899. icon: '<i class="fas fa-user-plus"></i>',
  19900. condition: canApply,
  19901. callback: li => applyChatCardDamage(li, -1)
  19902. },
  19903. {
  19904. name: game.i18n.localize("DND5E.ChatContextTempHP"),
  19905. icon: '<i class="fas fa-user-clock"></i>',
  19906. condition: canApply,
  19907. callback: li => applyChatCardTemp(li)
  19908. },
  19909. {
  19910. name: game.i18n.localize("DND5E.ChatContextDoubleDamage"),
  19911. icon: '<i class="fas fa-user-injured"></i>',
  19912. condition: canApply,
  19913. callback: li => applyChatCardDamage(li, 2)
  19914. },
  19915. {
  19916. name: game.i18n.localize("DND5E.ChatContextHalfDamage"),
  19917. icon: '<i class="fas fa-user-shield"></i>',
  19918. condition: canApply,
  19919. callback: li => applyChatCardDamage(li, 0.5)
  19920. }
  19921. );
  19922. return options;
  19923. }
  19924. /* -------------------------------------------- */
  19925. /**
  19926. * Apply rolled dice damage to the token or tokens which are currently controlled.
  19927. * This allows for damage to be scaled by a multiplier to account for healing, critical hits, or resistance
  19928. *
  19929. * @param {HTMLElement} li The chat entry which contains the roll data
  19930. * @param {number} multiplier A damage multiplier to apply to the rolled damage.
  19931. * @returns {Promise}
  19932. */
  19933. function applyChatCardDamage(li, multiplier) {
  19934. const message = game.messages.get(li.data("messageId"));
  19935. const roll = message.rolls[0];
  19936. return Promise.all(canvas.tokens.controlled.map(t => {
  19937. const a = t.actor;
  19938. return a.applyDamage(roll.total, multiplier);
  19939. }));
  19940. }
  19941. /* -------------------------------------------- */
  19942. /**
  19943. * Apply rolled dice as temporary hit points to the controlled token(s).
  19944. * @param {HTMLElement} li The chat entry which contains the roll data
  19945. * @returns {Promise}
  19946. */
  19947. function applyChatCardTemp(li) {
  19948. const message = game.messages.get(li.data("messageId"));
  19949. const roll = message.rolls[0];
  19950. return Promise.all(canvas.tokens.controlled.map(t => {
  19951. const a = t.actor;
  19952. return a.applyTempHP(roll.total);
  19953. }));
  19954. }
  19955. /* -------------------------------------------- */
  19956. /**
  19957. * Handle rendering of a chat message to the log
  19958. * @param {ChatLog} app The ChatLog instance
  19959. * @param {jQuery} html Rendered chat message HTML
  19960. * @param {object} data Data passed to the render context
  19961. */
  19962. function onRenderChatMessage(app, html, data) {
  19963. displayChatActionButtons(app, html, data);
  19964. highlightCriticalSuccessFailure(app, html);
  19965. if (game.settings.get("dnd5e", "autoCollapseItemCards")) html.find(".card-content").hide();
  19966. }
  19967. var chatMessage = /*#__PURE__*/Object.freeze({
  19968. __proto__: null,
  19969. addChatMessageContextOptions: addChatMessageContextOptions,
  19970. displayChatActionButtons: displayChatActionButtons,
  19971. highlightCriticalSuccessFailure: highlightCriticalSuccessFailure,
  19972. onRenderChatMessage: onRenderChatMessage
  19973. });
  19974. /**
  19975. * Override the core method for obtaining a Roll instance used for the Combatant.
  19976. * @see {Actor5e#getInitiativeRoll}
  19977. * @param {string} [formula] A formula to use if no Actor is defined
  19978. * @returns {D20Roll} The D20Roll instance which is used to determine initiative for the Combatant
  19979. */
  19980. function getInitiativeRoll(formula="1d20") {
  19981. if ( !this.actor ) return new CONFIG.Dice.D20Roll(formula ?? "1d20", {});
  19982. return this.actor.getInitiativeRoll();
  19983. }
  19984. var combat = /*#__PURE__*/Object.freeze({
  19985. __proto__: null,
  19986. getInitiativeRoll: getInitiativeRoll
  19987. });
  19988. /**
  19989. * Attempt to create a macro from the dropped data. Will use an existing macro if one exists.
  19990. * @param {object} dropData The dropped data
  19991. * @param {number} slot The hotbar slot to use
  19992. */
  19993. async function create5eMacro(dropData, slot) {
  19994. const macroData = { type: "script", scope: "actor" };
  19995. switch ( dropData.type ) {
  19996. case "Item":
  19997. const itemData = await Item.implementation.fromDropData(dropData);
  19998. if ( !itemData ) return ui.notifications.warn(game.i18n.localize("MACRO.5eUnownedWarn"));
  19999. foundry.utils.mergeObject(macroData, {
  20000. name: itemData.name,
  20001. img: itemData.img,
  20002. command: `dnd5e.documents.macro.rollItem("${itemData.name}")`,
  20003. flags: {"dnd5e.itemMacro": true}
  20004. });
  20005. break;
  20006. case "ActiveEffect":
  20007. const effectData = await ActiveEffect.implementation.fromDropData(dropData);
  20008. if ( !effectData ) return ui.notifications.warn(game.i18n.localize("MACRO.5eUnownedWarn"));
  20009. foundry.utils.mergeObject(macroData, {
  20010. name: effectData.label,
  20011. img: effectData.icon,
  20012. command: `dnd5e.documents.macro.toggleEffect("${effectData.label}")`,
  20013. flags: {"dnd5e.effectMacro": true}
  20014. });
  20015. break;
  20016. default:
  20017. return;
  20018. }
  20019. // Assign the macro to the hotbar
  20020. const macro = game.macros.find(m => {
  20021. return (m.name === macroData.name) && (m.command === macroData.command) && m.isAuthor;
  20022. }) || await Macro.create(macroData);
  20023. game.user.assignHotbarMacro(macro, slot);
  20024. }
  20025. /* -------------------------------------------- */
  20026. /**
  20027. * Find a document of the specified name and type on an assigned or selected actor.
  20028. * @param {string} name Document name to locate.
  20029. * @param {string} documentType Type of embedded document (e.g. "Item" or "ActiveEffect").
  20030. * @returns {Document} Document if found, otherwise nothing.
  20031. */
  20032. function getMacroTarget(name, documentType) {
  20033. let actor;
  20034. const speaker = ChatMessage.getSpeaker();
  20035. if ( speaker.token ) actor = game.actors.tokens[speaker.token];
  20036. actor ??= game.actors.get(speaker.actor);
  20037. if ( !actor ) return ui.notifications.warn(game.i18n.localize("MACRO.5eNoActorSelected"));
  20038. const collection = (documentType === "Item") ? actor.items : actor.effects;
  20039. const nameKeyPath = (documentType === "Item") ? "name" : "label";
  20040. // Find item in collection
  20041. const documents = collection.filter(i => foundry.utils.getProperty(i, nameKeyPath) === name);
  20042. const type = game.i18n.localize(`DOCUMENT.${documentType}`);
  20043. if ( documents.length === 0 ) {
  20044. return ui.notifications.warn(game.i18n.format("MACRO.5eMissingTargetWarn", { actor: actor.name, type, name }));
  20045. }
  20046. if ( documents.length > 1 ) {
  20047. ui.notifications.warn(game.i18n.format("MACRO.5eMultipleTargetsWarn", { actor: actor.name, type, name }));
  20048. }
  20049. return documents[0];
  20050. }
  20051. /* -------------------------------------------- */
  20052. /**
  20053. * Trigger an item to roll when a macro is clicked.
  20054. * @param {string} itemName Name of the item on the selected actor to trigger.
  20055. * @returns {Promise<ChatMessage|object>} Roll result.
  20056. */
  20057. function rollItem(itemName) {
  20058. return getMacroTarget(itemName, "Item")?.use();
  20059. }
  20060. /* -------------------------------------------- */
  20061. /**
  20062. * Toggle an effect on and off when a macro is clicked.
  20063. * @param {string} effectName Name of the effect to be toggled.
  20064. * @returns {Promise<ActiveEffect>} The effect after it has been toggled.
  20065. */
  20066. function toggleEffect(effectName) {
  20067. const effect = getMacroTarget(effectName, "ActiveEffect");
  20068. return effect?.update({disabled: !effect.disabled});
  20069. }
  20070. var macro = /*#__PURE__*/Object.freeze({
  20071. __proto__: null,
  20072. create5eMacro: create5eMacro,
  20073. rollItem: rollItem,
  20074. toggleEffect: toggleEffect
  20075. });
  20076. // Document Classes
  20077. var documents = /*#__PURE__*/Object.freeze({
  20078. __proto__: null,
  20079. ActiveEffect5e: ActiveEffect5e,
  20080. Actor5e: Actor5e,
  20081. Item5e: Item5e,
  20082. Proficiency: Proficiency,
  20083. TokenDocument5e: TokenDocument5e,
  20084. Trait: trait,
  20085. advancement: _module$b,
  20086. chat: chatMessage,
  20087. combat: combat,
  20088. macro: macro
  20089. });
  20090. /**
  20091. * Perform a system migration for the entire World, applying migrations for Actors, Items, and Compendium packs
  20092. * @returns {Promise} A Promise which resolves once the migration is completed
  20093. */
  20094. const migrateWorld = async function() {
  20095. const version = game.system.version;
  20096. ui.notifications.info(game.i18n.format("MIGRATION.5eBegin", {version}), {permanent: true});
  20097. const migrationData = await getMigrationData();
  20098. // Migrate World Actors
  20099. const actors = game.actors.map(a => [a, true])
  20100. .concat(Array.from(game.actors.invalidDocumentIds).map(id => [game.actors.getInvalid(id), false]));
  20101. for ( const [actor, valid] of actors ) {
  20102. try {
  20103. const source = valid ? actor.toObject() : game.data.actors.find(a => a._id === actor.id);
  20104. const updateData = migrateActorData(source, migrationData);
  20105. if ( !foundry.utils.isEmpty(updateData) ) {
  20106. console.log(`Migrating Actor document ${actor.name}`);
  20107. await actor.update(updateData, {enforceTypes: false, diff: valid});
  20108. }
  20109. } catch(err) {
  20110. err.message = `Failed dnd5e system migration for Actor ${actor.name}: ${err.message}`;
  20111. console.error(err);
  20112. }
  20113. }
  20114. // Migrate World Items
  20115. const items = game.items.map(i => [i, true])
  20116. .concat(Array.from(game.items.invalidDocumentIds).map(id => [game.items.getInvalid(id), false]));
  20117. for ( const [item, valid] of items ) {
  20118. try {
  20119. const source = valid ? item.toObject() : game.data.items.find(i => i._id === item.id);
  20120. const updateData = migrateItemData(source, migrationData);
  20121. if ( !foundry.utils.isEmpty(updateData) ) {
  20122. console.log(`Migrating Item document ${item.name}`);
  20123. await item.update(updateData, {enforceTypes: false, diff: valid});
  20124. }
  20125. } catch(err) {
  20126. err.message = `Failed dnd5e system migration for Item ${item.name}: ${err.message}`;
  20127. console.error(err);
  20128. }
  20129. }
  20130. // Migrate World Macros
  20131. for ( const m of game.macros ) {
  20132. try {
  20133. const updateData = migrateMacroData(m.toObject(), migrationData);
  20134. if ( !foundry.utils.isEmpty(updateData) ) {
  20135. console.log(`Migrating Macro document ${m.name}`);
  20136. await m.update(updateData, {enforceTypes: false});
  20137. }
  20138. } catch(err) {
  20139. err.message = `Failed dnd5e system migration for Macro ${m.name}: ${err.message}`;
  20140. console.error(err);
  20141. }
  20142. }
  20143. // Migrate World Roll Tables
  20144. for ( const table of game.tables ) {
  20145. try {
  20146. const updateData = migrateRollTableData(table.toObject(), migrationData);
  20147. if ( !foundry.utils.isEmpty(updateData) ) {
  20148. console.log(`Migrating RollTable document ${table.name}`);
  20149. await table.update(updateData, { enforceTypes: false });
  20150. }
  20151. } catch ( err ) {
  20152. err.message = `Failed dnd5e system migration for RollTable ${table.name}: ${err.message}`;
  20153. console.error(err);
  20154. }
  20155. }
  20156. // Migrate Actor Override Tokens
  20157. for ( let s of game.scenes ) {
  20158. try {
  20159. const updateData = migrateSceneData(s, migrationData);
  20160. if ( !foundry.utils.isEmpty(updateData) ) {
  20161. console.log(`Migrating Scene document ${s.name}`);
  20162. await s.update(updateData, {enforceTypes: false});
  20163. // If we do not do this, then synthetic token actors remain in cache
  20164. // with the un-updated actorData.
  20165. s.tokens.forEach(t => t._actor = null);
  20166. }
  20167. } catch(err) {
  20168. err.message = `Failed dnd5e system migration for Scene ${s.name}: ${err.message}`;
  20169. console.error(err);
  20170. }
  20171. }
  20172. // Migrate World Compendium Packs
  20173. for ( let p of game.packs ) {
  20174. if ( p.metadata.packageType !== "world" ) continue;
  20175. if ( !["Actor", "Item", "Scene"].includes(p.documentName) ) continue;
  20176. await migrateCompendium(p);
  20177. }
  20178. // Set the migration as complete
  20179. game.settings.set("dnd5e", "systemMigrationVersion", game.system.version);
  20180. ui.notifications.info(game.i18n.format("MIGRATION.5eComplete", {version}), {permanent: true});
  20181. };
  20182. /* -------------------------------------------- */
  20183. /**
  20184. * Apply migration rules to all Documents within a single Compendium pack
  20185. * @param {CompendiumCollection} pack Pack to be migrated.
  20186. * @returns {Promise}
  20187. */
  20188. const migrateCompendium = async function(pack) {
  20189. const documentName = pack.documentName;
  20190. if ( !["Actor", "Item", "Scene"].includes(documentName) ) return;
  20191. const migrationData = await getMigrationData();
  20192. // Unlock the pack for editing
  20193. const wasLocked = pack.locked;
  20194. await pack.configure({locked: false});
  20195. // Begin by requesting server-side data model migration and get the migrated content
  20196. await pack.migrate();
  20197. const documents = await pack.getDocuments();
  20198. // Iterate over compendium entries - applying fine-tuned migration functions
  20199. for ( let doc of documents ) {
  20200. let updateData = {};
  20201. try {
  20202. switch (documentName) {
  20203. case "Actor":
  20204. updateData = migrateActorData(doc.toObject(), migrationData);
  20205. break;
  20206. case "Item":
  20207. updateData = migrateItemData(doc.toObject(), migrationData);
  20208. break;
  20209. case "Scene":
  20210. updateData = migrateSceneData(doc.toObject(), migrationData);
  20211. break;
  20212. }
  20213. // Save the entry, if data was changed
  20214. if ( foundry.utils.isEmpty(updateData) ) continue;
  20215. await doc.update(updateData);
  20216. console.log(`Migrated ${documentName} document ${doc.name} in Compendium ${pack.collection}`);
  20217. }
  20218. // Handle migration failures
  20219. catch(err) {
  20220. err.message = `Failed dnd5e system migration for document ${doc.name} in pack ${pack.collection}: ${err.message}`;
  20221. console.error(err);
  20222. }
  20223. }
  20224. // Apply the original locked status for the pack
  20225. await pack.configure({locked: wasLocked});
  20226. console.log(`Migrated all ${documentName} documents from Compendium ${pack.collection}`);
  20227. };
  20228. /* -------------------------------------------- */
  20229. /**
  20230. * Update all compendium packs using the new system data model.
  20231. */
  20232. async function refreshAllCompendiums() {
  20233. for ( const pack of game.packs ) {
  20234. await refreshCompendium(pack);
  20235. }
  20236. }
  20237. /* -------------------------------------------- */
  20238. /**
  20239. * Update all Documents in a compendium using the new system data model.
  20240. * @param {CompendiumCollection} pack Pack to refresh.
  20241. */
  20242. async function refreshCompendium(pack) {
  20243. if ( !pack?.documentName ) return;
  20244. dnd5e.moduleArt.suppressArt = true;
  20245. const DocumentClass = CONFIG[pack.documentName].documentClass;
  20246. const wasLocked = pack.locked;
  20247. await pack.configure({locked: false});
  20248. await pack.migrate();
  20249. ui.notifications.info(`Beginning to refresh Compendium ${pack.collection}`);
  20250. const documents = await pack.getDocuments();
  20251. for ( const doc of documents ) {
  20252. const data = doc.toObject();
  20253. await doc.delete();
  20254. await DocumentClass.create(data, {keepId: true, keepEmbeddedIds: true, pack: pack.collection});
  20255. }
  20256. await pack.configure({locked: wasLocked});
  20257. dnd5e.moduleArt.suppressArt = false;
  20258. ui.notifications.info(`Refreshed all documents from Compendium ${pack.collection}`);
  20259. }
  20260. /* -------------------------------------------- */
  20261. /**
  20262. * Apply 'smart' AC migration to a given Actor compendium. This will perform the normal AC migration but additionally
  20263. * check to see if the actor has armor already equipped, and opt to use that instead.
  20264. * @param {CompendiumCollection|string} pack Pack or name of pack to migrate.
  20265. * @returns {Promise}
  20266. */
  20267. const migrateArmorClass = async function(pack) {
  20268. if ( typeof pack === "string" ) pack = game.packs.get(pack);
  20269. if ( pack.documentName !== "Actor" ) return;
  20270. const wasLocked = pack.locked;
  20271. await pack.configure({locked: false});
  20272. const actors = await pack.getDocuments();
  20273. const updates = [];
  20274. const armor = new Set(Object.keys(CONFIG.DND5E.armorTypes));
  20275. for ( const actor of actors ) {
  20276. try {
  20277. console.log(`Migrating ${actor.name}...`);
  20278. const src = actor.toObject();
  20279. const update = {_id: actor.id};
  20280. // Perform the normal migration.
  20281. _migrateActorAC(src, update);
  20282. // TODO: See if AC migration within DataModel is enough to handle this
  20283. updates.push(update);
  20284. // CASE 1: Armor is equipped
  20285. const hasArmorEquipped = actor.itemTypes.equipment.some(e => {
  20286. return armor.has(e.system.armor?.type) && e.system.equipped;
  20287. });
  20288. if ( hasArmorEquipped ) update["system.attributes.ac.calc"] = "default";
  20289. // CASE 2: NPC Natural Armor
  20290. else if ( src.type === "npc" ) update["system.attributes.ac.calc"] = "natural";
  20291. } catch(e) {
  20292. console.warn(`Failed to migrate armor class for Actor ${actor.name}`, e);
  20293. }
  20294. }
  20295. await Actor.implementation.updateDocuments(updates, {pack: pack.collection});
  20296. await pack.getDocuments(); // Force a re-prepare of all actors.
  20297. await pack.configure({locked: wasLocked});
  20298. console.log(`Migrated the AC of all Actors from Compendium ${pack.collection}`);
  20299. };
  20300. /* -------------------------------------------- */
  20301. /* Document Type Migration Helpers */
  20302. /* -------------------------------------------- */
  20303. /**
  20304. * Migrate a single Actor document to incorporate latest data model changes
  20305. * Return an Object of updateData to be applied
  20306. * @param {object} actor The actor data object to update
  20307. * @param {object} [migrationData] Additional data to perform the migration
  20308. * @returns {object} The updateData to apply
  20309. */
  20310. const migrateActorData = function(actor, migrationData) {
  20311. const updateData = {};
  20312. _migrateTokenImage(actor, updateData);
  20313. _migrateActorAC(actor, updateData);
  20314. // Migrate embedded effects
  20315. if ( actor.effects ) {
  20316. const effects = migrateEffects(actor, migrationData);
  20317. if ( effects.length > 0 ) updateData.effects = effects;
  20318. }
  20319. // Migrate Owned Items
  20320. if ( !actor.items ) return updateData;
  20321. const items = actor.items.reduce((arr, i) => {
  20322. // Migrate the Owned Item
  20323. const itemData = i instanceof CONFIG.Item.documentClass ? i.toObject() : i;
  20324. let itemUpdate = migrateItemData(itemData, migrationData);
  20325. // Prepared, Equipped, and Proficient for NPC actors
  20326. if ( actor.type === "npc" ) {
  20327. if (foundry.utils.getProperty(itemData.system, "preparation.prepared") === false) itemUpdate["system.preparation.prepared"] = true;
  20328. if (foundry.utils.getProperty(itemData.system, "equipped") === false) itemUpdate["system.equipped"] = true;
  20329. }
  20330. // Update the Owned Item
  20331. if ( !foundry.utils.isEmpty(itemUpdate) ) {
  20332. itemUpdate._id = itemData._id;
  20333. arr.push(foundry.utils.expandObject(itemUpdate));
  20334. }
  20335. // Update tool expertise.
  20336. if ( actor.system.tools ) {
  20337. const hasToolProf = itemData.system.baseItem in actor.system.tools;
  20338. if ( (itemData.type === "tool") && (itemData.system.proficient > 1) && hasToolProf ) {
  20339. updateData[`system.tools.${itemData.system.baseItem}.value`] = itemData.system.proficient;
  20340. }
  20341. }
  20342. return arr;
  20343. }, []);
  20344. if ( items.length > 0 ) updateData.items = items;
  20345. return updateData;
  20346. };
  20347. /* -------------------------------------------- */
  20348. /**
  20349. * Migrate a single Item document to incorporate latest data model changes
  20350. *
  20351. * @param {object} item Item data to migrate
  20352. * @param {object} [migrationData] Additional data to perform the migration
  20353. * @returns {object} The updateData to apply
  20354. */
  20355. function migrateItemData(item, migrationData) {
  20356. const updateData = {};
  20357. _migrateDocumentIcon(item, updateData, migrationData);
  20358. // Migrate embedded effects
  20359. if ( item.effects ) {
  20360. const effects = migrateEffects(item, migrationData);
  20361. if ( effects.length > 0 ) updateData.effects = effects;
  20362. }
  20363. return updateData;
  20364. }
  20365. /* -------------------------------------------- */
  20366. /**
  20367. * Migrate any active effects attached to the provided parent.
  20368. * @param {object} parent Data of the parent being migrated.
  20369. * @param {object} [migrationData] Additional data to perform the migration.
  20370. * @returns {object[]} Updates to apply on the embedded effects.
  20371. */
  20372. const migrateEffects = function(parent, migrationData) {
  20373. if ( !parent.effects ) return {};
  20374. return parent.effects.reduce((arr, e) => {
  20375. const effectData = e instanceof CONFIG.ActiveEffect.documentClass ? e.toObject() : e;
  20376. let effectUpdate = migrateEffectData(effectData, migrationData);
  20377. if ( !foundry.utils.isEmpty(effectUpdate) ) {
  20378. effectUpdate._id = effectData._id;
  20379. arr.push(foundry.utils.expandObject(effectUpdate));
  20380. }
  20381. return arr;
  20382. }, []);
  20383. };
  20384. /* -------------------------------------------- */
  20385. /**
  20386. * Migrate the provided active effect data.
  20387. * @param {object} effect Effect data to migrate.
  20388. * @param {object} [migrationData] Additional data to perform the migration.
  20389. * @returns {object} The updateData to apply.
  20390. */
  20391. const migrateEffectData = function(effect, migrationData) {
  20392. const updateData = {};
  20393. _migrateDocumentIcon(effect, updateData, {...migrationData, field: "icon"});
  20394. _migrateEffectArmorClass(effect, updateData);
  20395. return updateData;
  20396. };
  20397. /* -------------------------------------------- */
  20398. /**
  20399. * Migrate a single Macro document to incorporate latest data model changes.
  20400. * @param {object} macro Macro data to migrate
  20401. * @param {object} [migrationData] Additional data to perform the migration
  20402. * @returns {object} The updateData to apply
  20403. */
  20404. const migrateMacroData = function(macro, migrationData) {
  20405. const updateData = {};
  20406. _migrateDocumentIcon(macro, updateData, migrationData);
  20407. _migrateMacroCommands(macro, updateData);
  20408. return updateData;
  20409. };
  20410. /* -------------------------------------------- */
  20411. /**
  20412. * Migrate a single RollTable document to incorporate the latest data model changes.
  20413. * @param {object} table Roll table data to migrate.
  20414. * @param {object} [migrationData] Additional data to perform the migration.
  20415. * @returns {object} The update delta to apply.
  20416. */
  20417. function migrateRollTableData(table, migrationData) {
  20418. const updateData = {};
  20419. _migrateDocumentIcon(table, updateData, migrationData);
  20420. if ( !table.results?.length ) return updateData;
  20421. const results = table.results.reduce((arr, result) => {
  20422. const resultUpdate = {};
  20423. _migrateDocumentIcon(result, resultUpdate, migrationData);
  20424. if ( !foundry.utils.isEmpty(resultUpdate) ) {
  20425. resultUpdate._id = result._id;
  20426. arr.push(foundry.utils.expandObject(resultUpdate));
  20427. }
  20428. return arr;
  20429. }, []);
  20430. if ( results.length ) updateData.results = results;
  20431. return updateData;
  20432. }
  20433. /* -------------------------------------------- */
  20434. /**
  20435. * Migrate a single Scene document to incorporate changes to the data model of it's actor data overrides
  20436. * Return an Object of updateData to be applied
  20437. * @param {object} scene The Scene data to Update
  20438. * @param {object} [migrationData] Additional data to perform the migration
  20439. * @returns {object} The updateData to apply
  20440. */
  20441. const migrateSceneData = function(scene, migrationData) {
  20442. const tokens = scene.tokens.map(token => {
  20443. const t = token instanceof foundry.abstract.DataModel ? token.toObject() : token;
  20444. const update = {};
  20445. _migrateTokenImage(t, update);
  20446. if ( Object.keys(update).length ) foundry.utils.mergeObject(t, update);
  20447. if ( !game.actors.has(t.actorId) ) t.actorId = null;
  20448. if ( !t.actorId || t.actorLink ) t.actorData = {};
  20449. else if ( !t.actorLink ) {
  20450. const actorData = token.delta?.toObject() ?? foundry.utils.deepClone(t.actorData);
  20451. actorData.type = token.actor?.type;
  20452. const update = migrateActorData(actorData, migrationData);
  20453. if ( game.dnd5e.isV10 ) {
  20454. ["items", "effects"].forEach(embeddedName => {
  20455. if ( !update[embeddedName]?.length ) return;
  20456. const updates = new Map(update[embeddedName].map(u => [u._id, u]));
  20457. t.actorData[embeddedName].forEach(original => {
  20458. const update = updates.get(original._id);
  20459. if ( update ) foundry.utils.mergeObject(original, update);
  20460. });
  20461. delete update[embeddedName];
  20462. });
  20463. foundry.utils.mergeObject(t.actorData, update);
  20464. }
  20465. else t.delta = update;
  20466. }
  20467. return t;
  20468. });
  20469. return {tokens};
  20470. };
  20471. /* -------------------------------------------- */
  20472. /**
  20473. * Fetch bundled data for large-scale migrations.
  20474. * @returns {Promise<object>} Object mapping original system icons to their core replacements.
  20475. */
  20476. const getMigrationData = async function() {
  20477. const data = {};
  20478. try {
  20479. const icons = await fetch("systems/dnd5e/json/icon-migration.json");
  20480. const spellIcons = await fetch("systems/dnd5e/json/spell-icon-migration.json");
  20481. data.iconMap = {...await icons.json(), ...await spellIcons.json()};
  20482. } catch(err) {
  20483. console.warn(`Failed to retrieve icon migration data: ${err.message}`);
  20484. }
  20485. return data;
  20486. };
  20487. /* -------------------------------------------- */
  20488. /* Low level migration utilities
  20489. /* -------------------------------------------- */
  20490. /**
  20491. * Migrate the actor attributes.ac.value to the new ac.flat override field.
  20492. * @param {object} actorData Actor data being migrated.
  20493. * @param {object} updateData Existing updates being applied to actor. *Will be mutated.*
  20494. * @returns {object} Modified version of update data.
  20495. * @private
  20496. */
  20497. function _migrateActorAC(actorData, updateData) {
  20498. const ac = actorData.system?.attributes?.ac;
  20499. // If the actor has a numeric ac.value, then their AC has not been migrated to the auto-calculation schema yet.
  20500. if ( Number.isNumeric(ac?.value) ) {
  20501. updateData["system.attributes.ac.flat"] = parseInt(ac.value);
  20502. updateData["system.attributes.ac.calc"] = actorData.type === "npc" ? "natural" : "flat";
  20503. updateData["system.attributes.ac.-=value"] = null;
  20504. return updateData;
  20505. }
  20506. // Migrate ac.base in custom formulas to ac.armor
  20507. if ( (typeof ac?.formula === "string") && ac?.formula.includes("@attributes.ac.base") ) {
  20508. updateData["system.attributes.ac.formula"] = ac.formula.replaceAll("@attributes.ac.base", "@attributes.ac.armor");
  20509. }
  20510. // Protect against string values created by character sheets or importers that don't enforce data types
  20511. if ( (typeof ac?.flat === "string") && Number.isNumeric(ac.flat) ) {
  20512. updateData["system.attributes.ac.flat"] = parseInt(ac.flat);
  20513. }
  20514. // Remove invalid AC formula strings.
  20515. if ( ac?.formula ) {
  20516. try {
  20517. const roll = new Roll(ac.formula);
  20518. Roll.safeEval(roll.formula);
  20519. } catch( e ) {
  20520. updateData["system.attributes.ac.formula"] = "";
  20521. }
  20522. }
  20523. return updateData;
  20524. }
  20525. /* -------------------------------------------- */
  20526. /**
  20527. * Migrate any system token images from PNG to WEBP.
  20528. * @param {object} actorData Actor or token data to migrate.
  20529. * @param {object} updateData Existing update to expand upon.
  20530. * @returns {object} The updateData to apply
  20531. * @private
  20532. */
  20533. function _migrateTokenImage(actorData, updateData) {
  20534. const oldSystemPNG = /^systems\/dnd5e\/tokens\/([a-z]+)\/([A-z]+).png$/;
  20535. for ( const path of ["texture.src", "prototypeToken.texture.src"] ) {
  20536. const v = foundry.utils.getProperty(actorData, path);
  20537. if ( oldSystemPNG.test(v) ) {
  20538. const [type, fileName] = v.match(oldSystemPNG).slice(1);
  20539. updateData[path] = `systems/dnd5e/tokens/${type}/${fileName}.webp`;
  20540. }
  20541. }
  20542. return updateData;
  20543. }
  20544. /* -------------------------------------------- */
  20545. /**
  20546. * Convert system icons to use bundled core webp icons.
  20547. * @param {object} document Document data to migrate
  20548. * @param {object} updateData Existing update to expand upon
  20549. * @param {object} [migrationData={}] Additional data to perform the migration
  20550. * @param {Object<string, string>} [migrationData.iconMap] A mapping of system icons to core foundry icons
  20551. * @param {string} [migrationData.field] The document field to migrate
  20552. * @returns {object} The updateData to apply
  20553. * @private
  20554. */
  20555. function _migrateDocumentIcon(document, updateData, {iconMap, field="img"}={}) {
  20556. let path = document?.[field];
  20557. if ( path && iconMap ) {
  20558. if ( path.startsWith("/") || path.startsWith("\\") ) path = path.substring(1);
  20559. const rename = iconMap[path];
  20560. if ( rename ) updateData[field] = rename;
  20561. }
  20562. return updateData;
  20563. }
  20564. /* -------------------------------------------- */
  20565. /**
  20566. * Change active effects that target AC.
  20567. * @param {object} effect Effect data to migrate.
  20568. * @param {object} updateData Existing update to expand upon.
  20569. * @returns {object} The updateData to apply.
  20570. */
  20571. function _migrateEffectArmorClass(effect, updateData) {
  20572. let containsUpdates = false;
  20573. const changes = (effect.changes || []).map(c => {
  20574. if ( c.key !== "system.attributes.ac.base" ) return c;
  20575. c.key = "system.attributes.ac.armor";
  20576. containsUpdates = true;
  20577. return c;
  20578. });
  20579. if ( containsUpdates ) updateData.changes = changes;
  20580. return updateData;
  20581. }
  20582. /* -------------------------------------------- */
  20583. /**
  20584. * Migrate macros from the old 'dnd5e.rollItemMacro' and 'dnd5e.macros' commands to the new location.
  20585. * @param {object} macro Macro data to migrate.
  20586. * @param {object} updateData Existing update to expand upon.
  20587. * @returns {object} The updateData to apply.
  20588. */
  20589. function _migrateMacroCommands(macro, updateData) {
  20590. if ( macro.command.includes("game.dnd5e.rollItemMacro") ) {
  20591. updateData.command = macro.command.replaceAll("game.dnd5e.rollItemMacro", "dnd5e.documents.macro.rollItem");
  20592. } else if ( macro.command.includes("game.dnd5e.macros.") ) {
  20593. updateData.command = macro.command.replaceAll("game.dnd5e.macros.", "dnd5e.documents.macro.");
  20594. }
  20595. return updateData;
  20596. }
  20597. /* -------------------------------------------- */
  20598. /**
  20599. * A general tool to purge flags from all documents in a Compendium pack.
  20600. * @param {CompendiumCollection} pack The compendium pack to clean.
  20601. * @private
  20602. */
  20603. async function purgeFlags(pack) {
  20604. const cleanFlags = flags => {
  20605. const flags5e = flags.dnd5e || null;
  20606. return flags5e ? {dnd5e: flags5e} : {};
  20607. };
  20608. await pack.configure({locked: false});
  20609. const content = await pack.getDocuments();
  20610. for ( let doc of content ) {
  20611. const update = {flags: cleanFlags(doc.flags)};
  20612. if ( pack.documentName === "Actor" ) {
  20613. update.items = doc.items.map(i => {
  20614. i.flags = cleanFlags(i.flags);
  20615. return i;
  20616. });
  20617. }
  20618. await doc.update(update, {recursive: false});
  20619. console.log(`Purged flags from ${doc.name}`);
  20620. }
  20621. await pack.configure({locked: true});
  20622. }
  20623. var migrations = /*#__PURE__*/Object.freeze({
  20624. __proto__: null,
  20625. getMigrationData: getMigrationData,
  20626. migrateActorData: migrateActorData,
  20627. migrateArmorClass: migrateArmorClass,
  20628. migrateCompendium: migrateCompendium,
  20629. migrateEffectData: migrateEffectData,
  20630. migrateEffects: migrateEffects,
  20631. migrateItemData: migrateItemData,
  20632. migrateMacroData: migrateMacroData,
  20633. migrateRollTableData: migrateRollTableData,
  20634. migrateSceneData: migrateSceneData,
  20635. migrateWorld: migrateWorld,
  20636. purgeFlags: purgeFlags,
  20637. refreshAllCompendiums: refreshAllCompendiums,
  20638. refreshCompendium: refreshCompendium
  20639. });
  20640. /**
  20641. * The DnD5e game system for Foundry Virtual Tabletop
  20642. * A system for playing the fifth edition of the world's most popular role-playing game.
  20643. * Author: Atropos
  20644. * Software License: MIT
  20645. * Content License: https://www.dndbeyond.com/attachments/39j2li89/SRD5.1-CCBY4.0License.pdf
  20646. * Repository: https://github.com/foundryvtt/dnd5e
  20647. * Issue Tracker: https://github.com/foundryvtt/dnd5e/issues
  20648. */
  20649. /* -------------------------------------------- */
  20650. /* Define Module Structure */
  20651. /* -------------------------------------------- */
  20652. globalThis.dnd5e = {
  20653. applications,
  20654. canvas: canvas$1,
  20655. config: DND5E,
  20656. dataModels,
  20657. dice,
  20658. documents,
  20659. migrations,
  20660. utils
  20661. };
  20662. /* -------------------------------------------- */
  20663. /* Foundry VTT Initialization */
  20664. /* -------------------------------------------- */
  20665. Hooks.once("init", function() {
  20666. globalThis.dnd5e = game.dnd5e = Object.assign(game.system, globalThis.dnd5e);
  20667. console.log(`DnD5e | Initializing the DnD5e Game System - Version ${dnd5e.version}\n${DND5E.ASCII}`);
  20668. // Record Configuration Values
  20669. CONFIG.DND5E = DND5E;
  20670. CONFIG.ActiveEffect.documentClass = ActiveEffect5e;
  20671. CONFIG.Actor.documentClass = Actor5e;
  20672. CONFIG.Item.documentClass = Item5e;
  20673. CONFIG.Token.documentClass = TokenDocument5e;
  20674. CONFIG.Token.objectClass = Token5e;
  20675. CONFIG.time.roundTime = 6;
  20676. CONFIG.Dice.DamageRoll = DamageRoll;
  20677. CONFIG.Dice.D20Roll = D20Roll;
  20678. CONFIG.MeasuredTemplate.defaults.angle = 53.13; // 5e cone RAW should be 53.13 degrees
  20679. CONFIG.ui.combat = CombatTracker5e;
  20680. CONFIG.compatibility.excludePatterns.push(/\bActiveEffect5e#label\b/); // backwards compatibility with v10
  20681. game.dnd5e.isV10 = game.release.generation < 11;
  20682. // Register System Settings
  20683. registerSystemSettings();
  20684. // Validation strictness.
  20685. if ( game.dnd5e.isV10 ) _determineValidationStrictness();
  20686. // Configure module art.
  20687. game.dnd5e.moduleArt = new ModuleArt();
  20688. // Remove honor & sanity from configuration if they aren't enabled
  20689. if ( !game.settings.get("dnd5e", "honorScore") ) delete DND5E.abilities.hon;
  20690. if ( !game.settings.get("dnd5e", "sanityScore") ) delete DND5E.abilities.san;
  20691. // Configure trackable & consumable attributes.
  20692. _configureTrackableAttributes();
  20693. _configureConsumableAttributes();
  20694. // Patch Core Functions
  20695. Combatant.prototype.getInitiativeRoll = getInitiativeRoll;
  20696. // Register Roll Extensions
  20697. CONFIG.Dice.rolls.push(D20Roll);
  20698. CONFIG.Dice.rolls.push(DamageRoll);
  20699. // Hook up system data types
  20700. const modelType = game.dnd5e.isV10 ? "systemDataModels" : "dataModels";
  20701. CONFIG.Actor[modelType] = config$2;
  20702. CONFIG.Item[modelType] = config$1;
  20703. CONFIG.JournalEntryPage[modelType] = config;
  20704. // Register sheet application classes
  20705. Actors.unregisterSheet("core", ActorSheet);
  20706. Actors.registerSheet("dnd5e", ActorSheet5eCharacter, {
  20707. types: ["character"],
  20708. makeDefault: true,
  20709. label: "DND5E.SheetClassCharacter"
  20710. });
  20711. Actors.registerSheet("dnd5e", ActorSheet5eNPC, {
  20712. types: ["npc"],
  20713. makeDefault: true,
  20714. label: "DND5E.SheetClassNPC"
  20715. });
  20716. Actors.registerSheet("dnd5e", ActorSheet5eVehicle, {
  20717. types: ["vehicle"],
  20718. makeDefault: true,
  20719. label: "DND5E.SheetClassVehicle"
  20720. });
  20721. Actors.registerSheet("dnd5e", GroupActorSheet, {
  20722. types: ["group"],
  20723. makeDefault: true,
  20724. label: "DND5E.SheetClassGroup"
  20725. });
  20726. Items.unregisterSheet("core", ItemSheet);
  20727. Items.registerSheet("dnd5e", ItemSheet5e, {
  20728. makeDefault: true,
  20729. label: "DND5E.SheetClassItem"
  20730. });
  20731. DocumentSheetConfig.registerSheet(JournalEntryPage, "dnd5e", JournalClassPageSheet, {
  20732. label: "DND5E.SheetClassClassSummary",
  20733. types: ["class"]
  20734. });
  20735. // Preload Handlebars helpers & partials
  20736. registerHandlebarsHelpers();
  20737. preloadHandlebarsTemplates();
  20738. });
  20739. /**
  20740. * Determine if this is a 'legacy' world with permissive validation, or one where strict validation is enabled.
  20741. * @internal
  20742. */
  20743. function _determineValidationStrictness() {
  20744. SystemDataModel._enableV10Validation = game.settings.get("dnd5e", "strictValidation");
  20745. }
  20746. /**
  20747. * Update the world's validation strictness setting based on whether validation errors were encountered.
  20748. * @internal
  20749. */
  20750. async function _configureValidationStrictness() {
  20751. if ( !game.user.isGM ) return;
  20752. const invalidDocuments = game.actors.invalidDocumentIds.size + game.items.invalidDocumentIds.size
  20753. + game.scenes.invalidDocumentIds.size;
  20754. const strictValidation = game.settings.get("dnd5e", "strictValidation");
  20755. if ( invalidDocuments && strictValidation ) {
  20756. await game.settings.set("dnd5e", "strictValidation", false);
  20757. game.socket.emit("reload");
  20758. foundry.utils.debouncedReload();
  20759. }
  20760. }
  20761. /**
  20762. * Configure explicit lists of attributes that are trackable on the token HUD and in the combat tracker.
  20763. * @internal
  20764. */
  20765. function _configureTrackableAttributes() {
  20766. const common = {
  20767. bar: [],
  20768. value: [
  20769. ...Object.keys(DND5E.abilities).map(ability => `abilities.${ability}.value`),
  20770. ...Object.keys(DND5E.movementTypes).map(movement => `attributes.movement.${movement}`),
  20771. "attributes.ac.value", "attributes.init.total"
  20772. ]
  20773. };
  20774. const creature = {
  20775. bar: [...common.bar, "attributes.hp", "spells.pact"],
  20776. value: [
  20777. ...common.value,
  20778. ...Object.keys(DND5E.skills).map(skill => `skills.${skill}.passive`),
  20779. ...Object.keys(DND5E.senses).map(sense => `attributes.senses.${sense}`),
  20780. "attributes.spelldc"
  20781. ]
  20782. };
  20783. CONFIG.Actor.trackableAttributes = {
  20784. character: {
  20785. bar: [...creature.bar, "resources.primary", "resources.secondary", "resources.tertiary", "details.xp"],
  20786. value: [...creature.value]
  20787. },
  20788. npc: {
  20789. bar: [...creature.bar, "resources.legact", "resources.legres"],
  20790. value: [...creature.value, "details.cr", "details.spellLevel", "details.xp.value"]
  20791. },
  20792. vehicle: {
  20793. bar: [...common.bar, "attributes.hp"],
  20794. value: [...common.value]
  20795. },
  20796. group: {
  20797. bar: [],
  20798. value: []
  20799. }
  20800. };
  20801. }
  20802. /**
  20803. * Configure which attributes are available for item consumption.
  20804. * @internal
  20805. */
  20806. function _configureConsumableAttributes() {
  20807. CONFIG.DND5E.consumableResources = [
  20808. ...Object.keys(DND5E.abilities).map(ability => `abilities.${ability}.value`),
  20809. "attributes.ac.flat",
  20810. "attributes.hp.value",
  20811. ...Object.keys(DND5E.senses).map(sense => `attributes.senses.${sense}`),
  20812. ...Object.keys(DND5E.movementTypes).map(type => `attributes.movement.${type}`),
  20813. ...Object.keys(DND5E.currencies).map(denom => `currency.${denom}`),
  20814. "details.xp.value",
  20815. "resources.primary.value", "resources.secondary.value", "resources.tertiary.value",
  20816. "resources.legact.value", "resources.legres.value",
  20817. "spells.pact.value",
  20818. ...Array.fromRange(Object.keys(DND5E.spellLevels).length - 1, 1).map(level => `spells.spell${level}.value`)
  20819. ];
  20820. }
  20821. /* -------------------------------------------- */
  20822. /* Foundry VTT Setup */
  20823. /* -------------------------------------------- */
  20824. /**
  20825. * Prepare attribute lists.
  20826. */
  20827. Hooks.once("setup", function() {
  20828. CONFIG.DND5E.trackableAttributes = expandAttributeList(CONFIG.DND5E.trackableAttributes);
  20829. game.dnd5e.moduleArt.registerModuleArt();
  20830. // Apply custom compendium styles to the SRD rules compendium.
  20831. if ( !game.dnd5e.isV10 ) {
  20832. const rules = game.packs.get("dnd5e.rules");
  20833. rules.applicationClass = SRDCompendium;
  20834. }
  20835. });
  20836. /* --------------------------------------------- */
  20837. /**
  20838. * Expand a list of attribute paths into an object that can be traversed.
  20839. * @param {string[]} attributes The initial attributes configuration.
  20840. * @returns {object} The expanded object structure.
  20841. */
  20842. function expandAttributeList(attributes) {
  20843. return attributes.reduce((obj, attr) => {
  20844. foundry.utils.setProperty(obj, attr, true);
  20845. return obj;
  20846. }, {});
  20847. }
  20848. /* --------------------------------------------- */
  20849. /**
  20850. * Perform one-time pre-localization and sorting of some configuration objects
  20851. */
  20852. Hooks.once("i18nInit", () => performPreLocalization(CONFIG.DND5E));
  20853. /* -------------------------------------------- */
  20854. /* Foundry VTT Ready */
  20855. /* -------------------------------------------- */
  20856. /**
  20857. * Once the entire VTT framework is initialized, check to see if we should perform a data migration
  20858. */
  20859. Hooks.once("ready", function() {
  20860. if ( game.dnd5e.isV10 ) {
  20861. // Configure validation strictness.
  20862. _configureValidationStrictness();
  20863. // Apply custom compendium styles to the SRD rules compendium.
  20864. const rules = game.packs.get("dnd5e.rules");
  20865. rules.apps = [new SRDCompendium(rules)];
  20866. }
  20867. // Wait to register hotbar drop hook on ready so that modules could register earlier if they want to
  20868. Hooks.on("hotbarDrop", (bar, data, slot) => {
  20869. if ( ["Item", "ActiveEffect"].includes(data.type) ) {
  20870. create5eMacro(data, slot);
  20871. return false;
  20872. }
  20873. });
  20874. // Determine whether a system migration is required and feasible
  20875. if ( !game.user.isGM ) return;
  20876. const cv = game.settings.get("dnd5e", "systemMigrationVersion") || game.world.flags.dnd5e?.version;
  20877. const totalDocuments = game.actors.size + game.scenes.size + game.items.size;
  20878. if ( !cv && totalDocuments === 0 ) return game.settings.set("dnd5e", "systemMigrationVersion", game.system.version);
  20879. if ( cv && !isNewerVersion(game.system.flags.needsMigrationVersion, cv) ) return;
  20880. // Perform the migration
  20881. if ( cv && isNewerVersion(game.system.flags.compatibleMigrationVersion, cv) ) {
  20882. ui.notifications.error(game.i18n.localize("MIGRATION.5eVersionTooOldWarning"), {permanent: true});
  20883. }
  20884. migrateWorld();
  20885. });
  20886. /* -------------------------------------------- */
  20887. /* Canvas Initialization */
  20888. /* -------------------------------------------- */
  20889. Hooks.on("canvasInit", gameCanvas => {
  20890. gameCanvas.grid.diagonalRule = game.settings.get("dnd5e", "diagonalMovement");
  20891. SquareGrid.prototype.measureDistances = measureDistances;
  20892. });
  20893. /* -------------------------------------------- */
  20894. /* Other Hooks */
  20895. /* -------------------------------------------- */
  20896. Hooks.on("renderChatMessage", onRenderChatMessage);
  20897. Hooks.on("getChatLogEntryContext", addChatMessageContextOptions);
  20898. Hooks.on("renderChatLog", (app, html, data) => Item5e.chatListeners(html));
  20899. Hooks.on("renderChatPopout", (app, html, data) => Item5e.chatListeners(html));
  20900. Hooks.on("getActorDirectoryEntryContext", Actor5e.addDirectoryContextOptions);
  20901. export { DND5E, applications, canvas$1 as canvas, dataModels, dice, documents, migrations, utils };
  20902. //# sourceMappingURL=dnd5e-compiled.mjs.map