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.

23006 lines
815 KiB

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. /** @inheritdoc */
  250. getData() {
  251. return {
  252. appId: this.id,
  253. advancement: this.advancement,
  254. type: this.advancement.constructor.typeName,
  255. title: this.title,
  256. summary: this.advancement.summaryForLevel(this.level),
  257. level: this.level
  258. };
  259. }
  260. /* -------------------------------------------- */
  261. /** @inheritdoc */
  262. async _updateObject(event, formData) {
  263. await this.advancement.apply(this.level, formData);
  264. }
  265. }
  266. /**
  267. * Data field that selects the appropriate advancement data model if available, otherwise defaults to generic
  268. * `ObjectField` to prevent issues with custom advancement types that aren't currently loaded.
  269. */
  270. class AdvancementField extends foundry.data.fields.ObjectField {
  271. /**
  272. * Get the BaseAdvancement definition for the specified advancement type.
  273. * @param {string} type The Advancement type.
  274. * @returns {typeof BaseAdvancement|null} The BaseAdvancement class, or null.
  275. */
  276. getModelForType(type) {
  277. return CONFIG.DND5E.advancementTypes[type] ?? null;
  278. }
  279. /* -------------------------------------------- */
  280. /** @inheritdoc */
  281. _cleanType(value, options) {
  282. if ( !(typeof value === "object") ) value = {};
  283. const cls = this.getModelForType(value.type);
  284. if ( cls ) return cls.cleanData(value, options);
  285. return value;
  286. }
  287. /* -------------------------------------------- */
  288. /** @inheritdoc */
  289. initialize(value, model, options={}) {
  290. const cls = this.getModelForType(value.type);
  291. if ( cls ) return new cls(value, {parent: model, ...options});
  292. return foundry.utils.deepClone(value);
  293. }
  294. }
  295. /* -------------------------------------------- */
  296. /**
  297. * Data field that automatically selects the Advancement-specific configuration or value data models.
  298. *
  299. * @param {Advancement} advancementType Advancement class to which this field belongs.
  300. */
  301. class AdvancementDataField extends foundry.data.fields.ObjectField {
  302. constructor(advancementType, options={}) {
  303. super(options);
  304. this.advancementType = advancementType;
  305. }
  306. /* -------------------------------------------- */
  307. /** @inheritdoc */
  308. static get _defaults() {
  309. return foundry.utils.mergeObject(super._defaults, {required: true});
  310. }
  311. /**
  312. * Get the DataModel definition for the specified field as defined in metadata.
  313. * @returns {typeof DataModel|null} The DataModel class, or null.
  314. */
  315. getModel() {
  316. return this.advancementType.metadata?.dataModels?.[this.name];
  317. }
  318. /* -------------------------------------------- */
  319. /**
  320. * Get the defaults object for the specified field as defined in metadata.
  321. * @returns {object}
  322. */
  323. getDefaults() {
  324. return this.advancementType.metadata?.defaults?.[this.name] ?? {};
  325. }
  326. /* -------------------------------------------- */
  327. /** @inheritdoc */
  328. _cleanType(value, options) {
  329. if ( !(typeof value === "object") ) value = {};
  330. // Use a defined DataModel
  331. const cls = this.getModel();
  332. if ( cls ) return cls.cleanData(value, options);
  333. if ( options.partial ) return value;
  334. // Use the defined defaults
  335. const defaults = this.getDefaults();
  336. return foundry.utils.mergeObject(defaults, value, {inplace: false});
  337. }
  338. /* -------------------------------------------- */
  339. /** @inheritdoc */
  340. initialize(value, model, options={}) {
  341. const cls = this.getModel();
  342. if ( cls ) return new cls(value, {parent: model, ...options});
  343. return foundry.utils.deepClone(value);
  344. }
  345. }
  346. /* -------------------------------------------- */
  347. /**
  348. * @typedef {StringFieldOptions} FormulaFieldOptions
  349. * @property {boolean} [deterministic=false] Is this formula not allowed to have dice values?
  350. */
  351. /**
  352. * Special case StringField which represents a formula.
  353. *
  354. * @param {FormulaFieldOptions} [options={}] Options which configure the behavior of the field.
  355. * @property {boolean} deterministic=false Is this formula not allowed to have dice values?
  356. */
  357. class FormulaField extends foundry.data.fields.StringField {
  358. /** @inheritdoc */
  359. static get _defaults() {
  360. return foundry.utils.mergeObject(super._defaults, {
  361. deterministic: false
  362. });
  363. }
  364. /* -------------------------------------------- */
  365. /** @inheritdoc */
  366. _validateType(value) {
  367. if ( this.options.deterministic ) {
  368. const roll = new Roll(value);
  369. if ( !roll.isDeterministic ) throw new Error("must not contain dice terms");
  370. Roll.safeEval(roll.formula);
  371. }
  372. else Roll.validate(value);
  373. super._validateType(value);
  374. }
  375. }
  376. /* -------------------------------------------- */
  377. /**
  378. * Special case StringField that includes automatic validation for identifiers.
  379. */
  380. class IdentifierField extends foundry.data.fields.StringField {
  381. /** @override */
  382. _validateType(value) {
  383. if ( !dnd5e.utils.validators.isValidIdentifier(value) ) {
  384. throw new Error(game.i18n.localize("DND5E.IdentifierError"));
  385. }
  386. }
  387. }
  388. /* -------------------------------------------- */
  389. /**
  390. * @callback MappingFieldInitialValueBuilder
  391. * @param {string} key The key within the object where this new value is being generated.
  392. * @param {*} initial The generic initial data provided by the contained model.
  393. * @param {object} existing Any existing mapping data.
  394. * @returns {object} Value to use as default for this key.
  395. */
  396. /**
  397. * @typedef {DataFieldOptions} MappingFieldOptions
  398. * @property {string[]} [initialKeys] Keys that will be created if no data is provided.
  399. * @property {MappingFieldInitialValueBuilder} [initialValue] Function to calculate the initial value for a key.
  400. * @property {boolean} [initialKeysOnly=false] Should the keys in the initialized data be limited to the keys provided
  401. * by `options.initialKeys`?
  402. */
  403. /**
  404. * A subclass of ObjectField that represents a mapping of keys to the provided DataField type.
  405. *
  406. * @param {DataField} model The class of DataField which should be embedded in this field.
  407. * @param {MappingFieldOptions} [options={}] Options which configure the behavior of the field.
  408. * @property {string[]} [initialKeys] Keys that will be created if no data is provided.
  409. * @property {MappingFieldInitialValueBuilder} [initialValue] Function to calculate the initial value for a key.
  410. * @property {boolean} [initialKeysOnly=false] Should the keys in the initialized data be limited to the keys provided
  411. * by `options.initialKeys`?
  412. */
  413. class MappingField extends foundry.data.fields.ObjectField {
  414. constructor(model, options) {
  415. if ( !(model instanceof foundry.data.fields.DataField) ) {
  416. throw new Error("MappingField must have a DataField as its contained element");
  417. }
  418. super(options);
  419. /**
  420. * The embedded DataField definition which is contained in this field.
  421. * @type {DataField}
  422. */
  423. this.model = model;
  424. }
  425. /* -------------------------------------------- */
  426. /** @inheritdoc */
  427. static get _defaults() {
  428. return foundry.utils.mergeObject(super._defaults, {
  429. initialKeys: null,
  430. initialValue: null,
  431. initialKeysOnly: false
  432. });
  433. }
  434. /* -------------------------------------------- */
  435. /** @inheritdoc */
  436. _cleanType(value, options) {
  437. Object.entries(value).forEach(([k, v]) => value[k] = this.model.clean(v, options));
  438. return value;
  439. }
  440. /* -------------------------------------------- */
  441. /** @inheritdoc */
  442. getInitialValue(data) {
  443. let keys = this.initialKeys;
  444. const initial = super.getInitialValue(data);
  445. if ( !keys || !foundry.utils.isEmpty(initial) ) return initial;
  446. if ( !(keys instanceof Array) ) keys = Object.keys(keys);
  447. for ( const key of keys ) initial[key] = this._getInitialValueForKey(key);
  448. return initial;
  449. }
  450. /* -------------------------------------------- */
  451. /**
  452. * Get the initial value for the provided key.
  453. * @param {string} key Key within the object being built.
  454. * @param {object} [object] Any existing mapping data.
  455. * @returns {*} Initial value based on provided field type.
  456. */
  457. _getInitialValueForKey(key, object) {
  458. const initial = this.model.getInitialValue();
  459. return this.initialValue?.(key, initial, object) ?? initial;
  460. }
  461. /* -------------------------------------------- */
  462. /** @override */
  463. _validateType(value, options={}) {
  464. if ( foundry.utils.getType(value) !== "Object" ) throw new Error("must be an Object");
  465. const errors = this._validateValues(value, options);
  466. if ( !foundry.utils.isEmpty(errors) ) throw new foundry.data.fields.ModelValidationError(errors);
  467. }
  468. /* -------------------------------------------- */
  469. /**
  470. * Validate each value of the object.
  471. * @param {object} value The object to validate.
  472. * @param {object} options Validation options.
  473. * @returns {Object<Error>} An object of value-specific errors by key.
  474. */
  475. _validateValues(value, options) {
  476. const errors = {};
  477. for ( const [k, v] of Object.entries(value) ) {
  478. const error = this.model.validate(v, options);
  479. if ( error ) errors[k] = error;
  480. }
  481. return errors;
  482. }
  483. /* -------------------------------------------- */
  484. /** @override */
  485. initialize(value, model, options={}) {
  486. if ( !value ) return value;
  487. const obj = {};
  488. const initialKeys = (this.initialKeys instanceof Array) ? this.initialKeys : Object.keys(this.initialKeys ?? {});
  489. const keys = this.initialKeysOnly ? initialKeys : Object.keys(value);
  490. for ( const key of keys ) {
  491. const data = value[key] ?? this._getInitialValueForKey(key, value);
  492. obj[key] = this.model.initialize(data, model, options);
  493. }
  494. return obj;
  495. }
  496. /* -------------------------------------------- */
  497. /** @inheritdoc */
  498. _getField(path) {
  499. if ( path.length === 0 ) return this;
  500. else if ( path.length === 1 ) return this.model;
  501. path.shift();
  502. return this.model._getField(path);
  503. }
  504. }
  505. var fields = /*#__PURE__*/Object.freeze({
  506. __proto__: null,
  507. AdvancementDataField: AdvancementDataField,
  508. AdvancementField: AdvancementField,
  509. FormulaField: FormulaField,
  510. IdentifierField: IdentifierField,
  511. MappingField: MappingField
  512. });
  513. class BaseAdvancement extends foundry.abstract.DataModel {
  514. /**
  515. * Name of this advancement type that will be stored in config and used for lookups.
  516. * @type {string}
  517. * @protected
  518. */
  519. static get typeName() {
  520. return this.name.replace(/Advancement$/, "");
  521. }
  522. /* -------------------------------------------- */
  523. static defineSchema() {
  524. return {
  525. _id: new foundry.data.fields.DocumentIdField({initial: () => foundry.utils.randomID()}),
  526. type: new foundry.data.fields.StringField({
  527. required: true, initial: this.typeName, validate: v => v === this.typeName,
  528. validationError: `must be the same as the Advancement type name ${this.typeName}`
  529. }),
  530. configuration: new AdvancementDataField(this, {required: true}),
  531. value: new AdvancementDataField(this, {required: true}),
  532. level: new foundry.data.fields.NumberField({
  533. integer: true, initial: this.metadata?.multiLevel ? undefined : 1, min: 0, label: "DND5E.Level"
  534. }),
  535. title: new foundry.data.fields.StringField({initial: undefined, label: "DND5E.AdvancementCustomTitle"}),
  536. icon: new foundry.data.fields.FilePathField({
  537. initial: undefined, categories: ["IMAGE"], label: "DND5E.AdvancementCustomIcon"
  538. }),
  539. classRestriction: new foundry.data.fields.StringField({
  540. initial: undefined, choices: ["primary", "secondary"], label: "DND5E.AdvancementClassRestriction"
  541. })
  542. };
  543. }
  544. /* -------------------------------------------- */
  545. /** @inheritdoc */
  546. toObject(source=true) {
  547. if ( !source ) return super.toObject(source);
  548. const clone = foundry.utils.deepClone(this._source);
  549. // Remove any undefined keys from the source data
  550. Object.keys(clone).filter(k => clone[k] === undefined).forEach(k => delete clone[k]);
  551. return clone;
  552. }
  553. }
  554. /**
  555. * Error that can be thrown during the advancement update preparation process.
  556. */
  557. class AdvancementError extends Error {
  558. constructor(...args) {
  559. super(...args);
  560. this.name = "AdvancementError";
  561. }
  562. }
  563. /**
  564. * Abstract base class which various advancement types can subclass.
  565. * @param {Item5e} item Item to which this advancement belongs.
  566. * @param {object} [data={}] Raw data stored in the advancement object.
  567. * @param {object} [options={}] Options which affect DataModel construction.
  568. * @abstract
  569. */
  570. class Advancement extends BaseAdvancement {
  571. constructor(data, {parent=null, ...options}={}) {
  572. if ( parent instanceof Item ) parent = parent.system;
  573. super(data, {parent, ...options});
  574. /**
  575. * A collection of Application instances which should be re-rendered whenever this document is updated.
  576. * The keys of this object are the application ids and the values are Application instances. Each
  577. * Application in this object will have its render method called by {@link Document#render}.
  578. * @type {Object<Application>}
  579. */
  580. Object.defineProperty(this, "apps", {
  581. value: {},
  582. writable: false,
  583. enumerable: false
  584. });
  585. }
  586. /* -------------------------------------------- */
  587. /** @inheritdoc */
  588. _initialize(options) {
  589. super._initialize(options);
  590. return this.prepareData();
  591. }
  592. static ERROR = AdvancementError;
  593. /* -------------------------------------------- */
  594. /**
  595. * Information on how an advancement type is configured.
  596. *
  597. * @typedef {object} AdvancementMetadata
  598. * @property {object} dataModels
  599. * @property {DataModel} configuration Data model used for validating configuration data.
  600. * @property {DataModel} value Data model used for validating value data.
  601. * @property {number} order Number used to determine default sorting order of advancement items.
  602. * @property {string} icon Icon used for this advancement type if no user icon is specified.
  603. * @property {string} title Title to be displayed if no user title is specified.
  604. * @property {string} hint Description of this type shown in the advancement selection dialog.
  605. * @property {boolean} multiLevel Can this advancement affect more than one level? If this is set to true,
  606. * the level selection control in the configuration window is hidden and the
  607. * advancement should provide its own implementation of `Advancement#levels`
  608. * and potentially its own level configuration interface.
  609. * @property {Set<string>} validItemTypes Set of types to which this advancement can be added.
  610. * @property {object} apps
  611. * @property {*} apps.config Subclass of AdvancementConfig that allows for editing of this advancement type.
  612. * @property {*} apps.flow Subclass of AdvancementFlow that is displayed while fulfilling this advancement.
  613. */
  614. /**
  615. * Configuration information for this advancement type.
  616. * @type {AdvancementMetadata}
  617. */
  618. static get metadata() {
  619. return {
  620. order: 100,
  621. icon: "icons/svg/upgrade.svg",
  622. title: game.i18n.localize("DND5E.AdvancementTitle"),
  623. hint: "",
  624. multiLevel: false,
  625. validItemTypes: new Set(["background", "class", "subclass"]),
  626. apps: {
  627. config: AdvancementConfig,
  628. flow: AdvancementFlow
  629. }
  630. };
  631. }
  632. /* -------------------------------------------- */
  633. /* Instance Properties */
  634. /* -------------------------------------------- */
  635. /**
  636. * Unique identifier for this advancement within its item.
  637. * @type {string}
  638. */
  639. get id() {
  640. return this._id;
  641. }
  642. /* -------------------------------------------- */
  643. /**
  644. * Globally unique identifier for this advancement.
  645. * @type {string}
  646. */
  647. get uuid() {
  648. return `${this.item.uuid}.Advancement.${this.id}`;
  649. }
  650. /* -------------------------------------------- */
  651. /**
  652. * Item to which this advancement belongs.
  653. * @type {Item5e}
  654. */
  655. get item() {
  656. return this.parent.parent;
  657. }
  658. /* -------------------------------------------- */
  659. /**
  660. * Actor to which this advancement's item belongs, if the item is embedded.
  661. * @type {Actor5e|null}
  662. */
  663. get actor() {
  664. return this.item.parent ?? null;
  665. }
  666. /* -------------------------------------------- */
  667. /**
  668. * List of levels in which this advancement object should be displayed. Will be a list of class levels if this
  669. * advancement is being applied to classes or subclasses, otherwise a list of character levels.
  670. * @returns {number[]}
  671. */
  672. get levels() {
  673. return this.level !== undefined ? [this.level] : [];
  674. }
  675. /* -------------------------------------------- */
  676. /**
  677. * Should this advancement be applied to a class based on its class restriction setting? This will always return
  678. * true for advancements that are not within an embedded class item.
  679. * @type {boolean}
  680. * @protected
  681. */
  682. get appliesToClass() {
  683. const originalClass = this.item.isOriginalClass;
  684. return (originalClass === null) || !this.classRestriction
  685. || (this.classRestriction === "primary" && originalClass)
  686. || (this.classRestriction === "secondary" && !originalClass);
  687. }
  688. /* -------------------------------------------- */
  689. /* Preparation Methods */
  690. /* -------------------------------------------- */
  691. /**
  692. * Prepare data for the Advancement.
  693. */
  694. prepareData() {
  695. this.title = this.title || this.constructor.metadata.title;
  696. this.icon = this.icon || this.constructor.metadata.icon;
  697. }
  698. /* -------------------------------------------- */
  699. /* Display Methods */
  700. /* -------------------------------------------- */
  701. /**
  702. * Has the player made choices for this advancement at the specified level?
  703. * @param {number} level Level for which to check configuration.
  704. * @returns {boolean} Have any available choices been made?
  705. */
  706. configuredForLevel(level) {
  707. return true;
  708. }
  709. /* -------------------------------------------- */
  710. /**
  711. * Value used for sorting this advancement at a certain level.
  712. * @param {number} level Level for which this entry is being sorted.
  713. * @returns {string} String that can be used for sorting.
  714. */
  715. sortingValueForLevel(level) {
  716. return `${this.constructor.metadata.order.paddedString(4)} ${this.titleForLevel(level)}`;
  717. }
  718. /* -------------------------------------------- */
  719. /**
  720. * Title displayed in advancement list for a specific level.
  721. * @param {number} level Level for which to generate a title.
  722. * @param {object} [options={}]
  723. * @param {object} [options.configMode=false] Is the advancement's item sheet in configuration mode? When in
  724. * config mode, the choices already made on this actor should not
  725. * be displayed.
  726. * @returns {string} HTML title with any level-specific information.
  727. */
  728. titleForLevel(level, { configMode=false }={}) {
  729. return this.title;
  730. }
  731. /* -------------------------------------------- */
  732. /**
  733. * Summary content displayed beneath the title in the advancement list.
  734. * @param {number} level Level for which to generate the summary.
  735. * @param {object} [options={}]
  736. * @param {object} [options.configMode=false] Is the advancement's item sheet in configuration mode? When in
  737. * config mode, the choices already made on this actor should not
  738. * be displayed.
  739. * @returns {string} HTML content of the summary.
  740. */
  741. summaryForLevel(level, { configMode=false }={}) {
  742. return "";
  743. }
  744. /* -------------------------------------------- */
  745. /**
  746. * Render all of the Application instances which are connected to this advancement.
  747. * @param {boolean} [force=false] Force rendering
  748. * @param {object} [context={}] Optional context
  749. */
  750. render(force=false, context={}) {
  751. for ( const app of Object.values(this.apps) ) app.render(force, context);
  752. }
  753. /* -------------------------------------------- */
  754. /* Editing Methods */
  755. /* -------------------------------------------- */
  756. /**
  757. * Update this advancement.
  758. * @param {object} updates Updates to apply to this advancement.
  759. * @returns {Promise<Advancement>} This advancement after updates have been applied.
  760. */
  761. async update(updates) {
  762. await this.item.updateAdvancement(this.id, updates);
  763. return this;
  764. }
  765. /* -------------------------------------------- */
  766. /**
  767. * Update this advancement's data on the item without performing a database commit.
  768. * @param {object} updates Updates to apply to this advancement.
  769. * @returns {Advancement} This advancement after updates have been applied.
  770. */
  771. updateSource(updates) {
  772. super.updateSource(updates);
  773. return this;
  774. }
  775. /* -------------------------------------------- */
  776. /**
  777. * Can an advancement of this type be added to the provided item?
  778. * @param {Item5e} item Item to check against.
  779. * @returns {boolean} Should this be enabled as an option on the `AdvancementSelection` dialog?
  780. */
  781. static availableForItem(item) {
  782. return true;
  783. }
  784. /* -------------------------------------------- */
  785. /**
  786. * Serialize salient information for this Advancement when dragging it.
  787. * @returns {object} An object of drag data.
  788. */
  789. toDragData() {
  790. const dragData = { type: "Advancement" };
  791. if ( this.id ) dragData.uuid = this.uuid;
  792. else dragData.data = this.toObject();
  793. return dragData;
  794. }
  795. /* -------------------------------------------- */
  796. /* Application Methods */
  797. /* -------------------------------------------- */
  798. /**
  799. * Locally apply this advancement to the actor.
  800. * @param {number} level Level being advanced.
  801. * @param {object} data Data from the advancement form.
  802. * @abstract
  803. */
  804. async apply(level, data) { }
  805. /* -------------------------------------------- */
  806. /**
  807. * Locally apply this advancement from stored data, if possible. If stored data can not be restored for any reason,
  808. * throw an AdvancementError to display the advancement flow UI.
  809. * @param {number} level Level being advanced.
  810. * @param {object} data Data from `Advancement#reverse` needed to restore this advancement.
  811. * @abstract
  812. */
  813. async restore(level, data) { }
  814. /* -------------------------------------------- */
  815. /**
  816. * Locally remove this advancement's changes from the actor.
  817. * @param {number} level Level being removed.
  818. * @returns {object} Data that can be passed to the `Advancement#restore` method to restore this reversal.
  819. * @abstract
  820. */
  821. async reverse(level) { }
  822. }
  823. /**
  824. * Configuration application for hit points.
  825. */
  826. class HitPointsConfig extends AdvancementConfig {
  827. /** @inheritdoc */
  828. static get defaultOptions() {
  829. return foundry.utils.mergeObject(super.defaultOptions, {
  830. template: "systems/dnd5e/templates/advancement/hit-points-config.hbs"
  831. });
  832. }
  833. /* -------------------------------------------- */
  834. /** @inheritdoc */
  835. getData() {
  836. return foundry.utils.mergeObject(super.getData(), {
  837. hitDie: this.advancement.hitDie
  838. });
  839. }
  840. }
  841. /**
  842. * Inline application that presents hit points selection upon level up.
  843. */
  844. class HitPointsFlow extends AdvancementFlow {
  845. /** @inheritdoc */
  846. static get defaultOptions() {
  847. return foundry.utils.mergeObject(super.defaultOptions, {
  848. template: "systems/dnd5e/templates/advancement/hit-points-flow.hbs"
  849. });
  850. }
  851. /* -------------------------------------------- */
  852. /** @inheritdoc */
  853. getData() {
  854. const source = this.retainedData ?? this.advancement.value;
  855. const value = source[this.level];
  856. // If value is empty, `useAverage` should default to the value selected at the previous level
  857. let useAverage = value === "avg";
  858. if ( !value ) {
  859. const lastValue = source[this.level - 1];
  860. if ( lastValue === "avg" ) useAverage = true;
  861. }
  862. return foundry.utils.mergeObject(super.getData(), {
  863. isFirstClassLevel: (this.level === 1) && this.advancement.item.isOriginalClass,
  864. hitDie: this.advancement.hitDie,
  865. dieValue: this.advancement.hitDieValue,
  866. data: {
  867. value: Number.isInteger(value) ? value : "",
  868. useAverage
  869. }
  870. });
  871. }
  872. /* -------------------------------------------- */
  873. /** @inheritdoc */
  874. activateListeners(html) {
  875. this.form.querySelector(".averageCheckbox")?.addEventListener("change", event => {
  876. this.form.querySelector(".rollResult").disabled = event.target.checked;
  877. this.form.querySelector(".rollButton").disabled = event.target.checked;
  878. this._updateRollResult();
  879. });
  880. this.form.querySelector(".rollButton")?.addEventListener("click", async () => {
  881. const roll = await this.advancement.actor.rollClassHitPoints(this.advancement.item);
  882. this.form.querySelector(".rollResult").value = roll.total;
  883. });
  884. this._updateRollResult();
  885. }
  886. /* -------------------------------------------- */
  887. /**
  888. * Update the roll result display when the average result is taken.
  889. * @protected
  890. */
  891. _updateRollResult() {
  892. if ( !this.form.elements.useAverage?.checked ) return;
  893. this.form.elements.value.value = (this.advancement.hitDieValue / 2) + 1;
  894. }
  895. /* -------------------------------------------- */
  896. /** @inheritdoc */
  897. _updateObject(event, formData) {
  898. let value;
  899. if ( formData.useMax ) value = "max";
  900. else if ( formData.useAverage ) value = "avg";
  901. else if ( Number.isInteger(formData.value) ) value = parseInt(formData.value);
  902. if ( value !== undefined ) return this.advancement.apply(this.level, { [this.level]: value });
  903. this.form.querySelector(".rollResult")?.classList.add("error");
  904. const errorType = formData.value ? "Invalid" : "Empty";
  905. throw new Advancement.ERROR(game.i18n.localize(`DND5E.AdvancementHitPoints${errorType}Error`));
  906. }
  907. }
  908. /* -------------------------------------------- */
  909. /* Formulas */
  910. /* -------------------------------------------- */
  911. /**
  912. * Convert a bonus value to a simple integer for displaying on the sheet.
  913. * @param {number|string|null} bonus Bonus formula.
  914. * @param {object} [data={}] Data to use for replacing @ strings.
  915. * @returns {number} Simplified bonus as an integer.
  916. * @protected
  917. */
  918. function simplifyBonus(bonus, data={}) {
  919. if ( !bonus ) return 0;
  920. if ( Number.isNumeric(bonus) ) return Number(bonus);
  921. try {
  922. const roll = new Roll(bonus, data);
  923. return roll.isDeterministic ? Roll.safeEval(roll.formula) : 0;
  924. } catch(error) {
  925. console.error(error);
  926. return 0;
  927. }
  928. }
  929. /* -------------------------------------------- */
  930. /* Object Helpers */
  931. /* -------------------------------------------- */
  932. /**
  933. * Sort the provided object by its values or by an inner sortKey.
  934. * @param {object} obj The object to sort.
  935. * @param {string} [sortKey] An inner key upon which to sort.
  936. * @returns {object} A copy of the original object that has been sorted.
  937. */
  938. function sortObjectEntries(obj, sortKey) {
  939. let sorted = Object.entries(obj);
  940. if ( sortKey ) sorted = sorted.sort((a, b) => a[1][sortKey].localeCompare(b[1][sortKey]));
  941. else sorted = sorted.sort((a, b) => a[1].localeCompare(b[1]));
  942. return Object.fromEntries(sorted);
  943. }
  944. /* -------------------------------------------- */
  945. /**
  946. * Retrieve the indexed data for a Document using its UUID. Will never return a result for embedded documents.
  947. * @param {string} uuid The UUID of the Document index to retrieve.
  948. * @returns {object} Document's index if one could be found.
  949. */
  950. function indexFromUuid(uuid) {
  951. const parts = uuid.split(".");
  952. let index;
  953. // Compendium Documents
  954. if ( parts[0] === "Compendium" ) {
  955. const [, scope, packName, id] = parts;
  956. const pack = game.packs.get(`${scope}.${packName}`);
  957. index = pack?.index.get(id);
  958. }
  959. // World Documents
  960. else if ( parts.length < 3 ) {
  961. const [docName, id] = parts;
  962. const collection = CONFIG[docName].collection.instance;
  963. index = collection.get(id);
  964. }
  965. return index || null;
  966. }
  967. /* -------------------------------------------- */
  968. /**
  969. * Creates an HTML document link for the provided UUID.
  970. * @param {string} uuid UUID for which to produce the link.
  971. * @returns {string} Link to the item or empty string if item wasn't found.
  972. */
  973. function linkForUuid(uuid) {
  974. return TextEditor._createContentLink(["", "UUID", uuid]).outerHTML;
  975. }
  976. /* -------------------------------------------- */
  977. /* Validators */
  978. /* -------------------------------------------- */
  979. /**
  980. * Ensure the provided string contains only the characters allowed in identifiers.
  981. * @param {string} identifier
  982. * @returns {boolean}
  983. */
  984. function isValidIdentifier(identifier) {
  985. return /^([a-z0-9_-]+)$/i.test(identifier);
  986. }
  987. const validators = {
  988. isValidIdentifier: isValidIdentifier
  989. };
  990. /* -------------------------------------------- */
  991. /* Handlebars Template Helpers */
  992. /* -------------------------------------------- */
  993. /**
  994. * Define a set of template paths to pre-load. Pre-loaded templates are compiled and cached for fast access when
  995. * rendering. These paths will also be available as Handlebars partials by using the file name
  996. * (e.g. "dnd5e.actor-traits").
  997. * @returns {Promise}
  998. */
  999. async function preloadHandlebarsTemplates() {
  1000. const partials = [
  1001. // Shared Partials
  1002. "systems/dnd5e/templates/actors/parts/active-effects.hbs",
  1003. "systems/dnd5e/templates/apps/parts/trait-list.hbs",
  1004. // Actor Sheet Partials
  1005. "systems/dnd5e/templates/actors/parts/actor-traits.hbs",
  1006. "systems/dnd5e/templates/actors/parts/actor-inventory.hbs",
  1007. "systems/dnd5e/templates/actors/parts/actor-features.hbs",
  1008. "systems/dnd5e/templates/actors/parts/actor-spellbook.hbs",
  1009. "systems/dnd5e/templates/actors/parts/actor-warnings.hbs",
  1010. // Item Sheet Partials
  1011. "systems/dnd5e/templates/items/parts/item-action.hbs",
  1012. "systems/dnd5e/templates/items/parts/item-activation.hbs",
  1013. "systems/dnd5e/templates/items/parts/item-advancement.hbs",
  1014. "systems/dnd5e/templates/items/parts/item-description.hbs",
  1015. "systems/dnd5e/templates/items/parts/item-mountable.hbs",
  1016. "systems/dnd5e/templates/items/parts/item-spellcasting.hbs",
  1017. "systems/dnd5e/templates/items/parts/item-summary.hbs",
  1018. // Journal Partials
  1019. "systems/dnd5e/templates/journal/parts/journal-table.hbs",
  1020. // Advancement Partials
  1021. "systems/dnd5e/templates/advancement/parts/advancement-controls.hbs",
  1022. "systems/dnd5e/templates/advancement/parts/advancement-spell-config.hbs"
  1023. ];
  1024. const paths = {};
  1025. for ( const path of partials ) {
  1026. paths[path.replace(".hbs", ".html")] = path;
  1027. paths[`dnd5e.${path.split("/").pop().replace(".hbs", "")}`] = path;
  1028. }
  1029. return loadTemplates(paths);
  1030. }
  1031. /* -------------------------------------------- */
  1032. /**
  1033. * A helper that fetch the appropriate item context from root and adds it to the first block parameter.
  1034. * @param {object} context Current evaluation context.
  1035. * @param {object} options Handlebars options.
  1036. * @returns {string}
  1037. */
  1038. function itemContext(context, options) {
  1039. if ( arguments.length !== 2 ) throw new Error("#dnd5e-itemContext requires exactly one argument");
  1040. if ( foundry.utils.getType(context) === "function" ) context = context.call(this);
  1041. const ctx = options.data.root.itemContext?.[context.id];
  1042. if ( !ctx ) {
  1043. const inverse = options.inverse(this);
  1044. if ( inverse ) return options.inverse(this);
  1045. }
  1046. return options.fn(context, { data: options.data, blockParams: [ctx] });
  1047. }
  1048. /* -------------------------------------------- */
  1049. /**
  1050. * Register custom Handlebars helpers used by 5e.
  1051. */
  1052. function registerHandlebarsHelpers() {
  1053. Handlebars.registerHelper({
  1054. getProperty: foundry.utils.getProperty,
  1055. "dnd5e-linkForUuid": linkForUuid,
  1056. "dnd5e-itemContext": itemContext
  1057. });
  1058. }
  1059. /* -------------------------------------------- */
  1060. /* Config Pre-Localization */
  1061. /* -------------------------------------------- */
  1062. /**
  1063. * Storage for pre-localization configuration.
  1064. * @type {object}
  1065. * @private
  1066. */
  1067. const _preLocalizationRegistrations = {};
  1068. /**
  1069. * Mark the provided config key to be pre-localized during the init stage.
  1070. * @param {string} configKeyPath Key path within `CONFIG.DND5E` to localize.
  1071. * @param {object} [options={}]
  1072. * @param {string} [options.key] If each entry in the config enum is an object,
  1073. * localize and sort using this property.
  1074. * @param {string[]} [options.keys=[]] Array of localization keys. First key listed will be used for sorting
  1075. * if multiple are provided.
  1076. * @param {boolean} [options.sort=false] Sort this config enum, using the key if set.
  1077. */
  1078. function preLocalize(configKeyPath, { key, keys=[], sort=false }={}) {
  1079. if ( key ) keys.unshift(key);
  1080. _preLocalizationRegistrations[configKeyPath] = { keys, sort };
  1081. }
  1082. /* -------------------------------------------- */
  1083. /**
  1084. * Execute previously defined pre-localization tasks on the provided config object.
  1085. * @param {object} config The `CONFIG.DND5E` object to localize and sort. *Will be mutated.*
  1086. */
  1087. function performPreLocalization(config) {
  1088. for ( const [keyPath, settings] of Object.entries(_preLocalizationRegistrations) ) {
  1089. const target = foundry.utils.getProperty(config, keyPath);
  1090. _localizeObject(target, settings.keys);
  1091. if ( settings.sort ) foundry.utils.setProperty(config, keyPath, sortObjectEntries(target, settings.keys[0]));
  1092. }
  1093. }
  1094. /* -------------------------------------------- */
  1095. /**
  1096. * Localize the values of a configuration object by translating them in-place.
  1097. * @param {object} obj The configuration object to localize.
  1098. * @param {string[]} [keys] List of inner keys that should be localized if this is an object.
  1099. * @private
  1100. */
  1101. function _localizeObject(obj, keys) {
  1102. for ( const [k, v] of Object.entries(obj) ) {
  1103. const type = typeof v;
  1104. if ( type === "string" ) {
  1105. obj[k] = game.i18n.localize(v);
  1106. continue;
  1107. }
  1108. if ( type !== "object" ) {
  1109. console.error(new Error(
  1110. `Pre-localized configuration values must be a string or object, ${type} found for "${k}" instead.`
  1111. ));
  1112. continue;
  1113. }
  1114. if ( !keys?.length ) {
  1115. console.error(new Error(
  1116. "Localization keys must be provided for pre-localizing when target is an object."
  1117. ));
  1118. continue;
  1119. }
  1120. for ( const key of keys ) {
  1121. if ( !v[key] ) continue;
  1122. v[key] = game.i18n.localize(v[key]);
  1123. }
  1124. }
  1125. }
  1126. /* -------------------------------------------- */
  1127. /* Migration */
  1128. /* -------------------------------------------- */
  1129. /**
  1130. * Synchronize the spells for all Actors in some collection with source data from an Item compendium pack.
  1131. * @param {CompendiumCollection} actorPack An Actor compendium pack which will be updated
  1132. * @param {CompendiumCollection} spellsPack An Item compendium pack which provides source data for spells
  1133. * @returns {Promise<void>}
  1134. */
  1135. async function synchronizeActorSpells(actorPack, spellsPack) {
  1136. // Load all actors and spells
  1137. const actors = await actorPack.getDocuments();
  1138. const spells = await spellsPack.getDocuments();
  1139. const spellsMap = spells.reduce((obj, item) => {
  1140. obj[item.name] = item;
  1141. return obj;
  1142. }, {});
  1143. // Unlock the pack
  1144. await actorPack.configure({locked: false});
  1145. // Iterate over actors
  1146. SceneNavigation.displayProgressBar({label: "Synchronizing Spell Data", pct: 0});
  1147. for ( const [i, actor] of actors.entries() ) {
  1148. const {toDelete, toCreate} = _synchronizeActorSpells(actor, spellsMap);
  1149. if ( toDelete.length ) await actor.deleteEmbeddedDocuments("Item", toDelete);
  1150. if ( toCreate.length ) await actor.createEmbeddedDocuments("Item", toCreate, {keepId: true});
  1151. console.debug(`${actor.name} | Synchronized ${toCreate.length} spells`);
  1152. SceneNavigation.displayProgressBar({label: actor.name, pct: ((i / actors.length) * 100).toFixed(0)});
  1153. }
  1154. // Re-lock the pack
  1155. await actorPack.configure({locked: true});
  1156. SceneNavigation.displayProgressBar({label: "Synchronizing Spell Data", pct: 100});
  1157. }
  1158. /* -------------------------------------------- */
  1159. /**
  1160. * A helper function to synchronize spell data for a specific Actor.
  1161. * @param {Actor5e} actor
  1162. * @param {Object<string,Item5e>} spellsMap
  1163. * @returns {{toDelete: string[], toCreate: object[]}}
  1164. * @private
  1165. */
  1166. function _synchronizeActorSpells(actor, spellsMap) {
  1167. const spells = actor.itemTypes.spell;
  1168. const toDelete = [];
  1169. const toCreate = [];
  1170. if ( !spells.length ) return {toDelete, toCreate};
  1171. for ( const spell of spells ) {
  1172. const source = spellsMap[spell.name];
  1173. if ( !source ) {
  1174. console.warn(`${actor.name} | ${spell.name} | Does not exist in spells compendium pack`);
  1175. continue;
  1176. }
  1177. // Combine source data with the preparation and uses data from the actor
  1178. const spellData = source.toObject();
  1179. const {preparation, uses, save} = spell.toObject().system;
  1180. Object.assign(spellData.system, {preparation, uses});
  1181. spellData.system.save.dc = save.dc;
  1182. foundry.utils.setProperty(spellData, "flags.core.sourceId", source.uuid);
  1183. // Record spells to be deleted and created
  1184. toDelete.push(spell.id);
  1185. toCreate.push(spellData);
  1186. }
  1187. return {toDelete, toCreate};
  1188. }
  1189. var utils = /*#__PURE__*/Object.freeze({
  1190. __proto__: null,
  1191. indexFromUuid: indexFromUuid,
  1192. linkForUuid: linkForUuid,
  1193. performPreLocalization: performPreLocalization,
  1194. preLocalize: preLocalize,
  1195. preloadHandlebarsTemplates: preloadHandlebarsTemplates,
  1196. registerHandlebarsHelpers: registerHandlebarsHelpers,
  1197. simplifyBonus: simplifyBonus,
  1198. sortObjectEntries: sortObjectEntries,
  1199. synchronizeActorSpells: synchronizeActorSpells,
  1200. validators: validators
  1201. });
  1202. /**
  1203. * Advancement that presents the player with the option to roll hit points at each level or select the average value.
  1204. * Keeps track of player hit point rolls or selection for each class level. **Can only be added to classes and each
  1205. * class can only have one.**
  1206. */
  1207. class HitPointsAdvancement extends Advancement {
  1208. /** @inheritdoc */
  1209. static get metadata() {
  1210. return foundry.utils.mergeObject(super.metadata, {
  1211. order: 10,
  1212. icon: "systems/dnd5e/icons/svg/hit-points.svg",
  1213. title: game.i18n.localize("DND5E.AdvancementHitPointsTitle"),
  1214. hint: game.i18n.localize("DND5E.AdvancementHitPointsHint"),
  1215. multiLevel: true,
  1216. validItemTypes: new Set(["class"]),
  1217. apps: {
  1218. config: HitPointsConfig,
  1219. flow: HitPointsFlow
  1220. }
  1221. });
  1222. }
  1223. /* -------------------------------------------- */
  1224. /* Instance Properties */
  1225. /* -------------------------------------------- */
  1226. /** @inheritdoc */
  1227. get levels() {
  1228. return Array.fromRange(CONFIG.DND5E.maxLevel + 1).slice(1);
  1229. }
  1230. /* -------------------------------------------- */
  1231. /**
  1232. * Shortcut to the hit die used by the class.
  1233. * @returns {string}
  1234. */
  1235. get hitDie() {
  1236. return this.item.system.hitDice;
  1237. }
  1238. /* -------------------------------------------- */
  1239. /**
  1240. * The face value of the hit die used.
  1241. * @returns {number}
  1242. */
  1243. get hitDieValue() {
  1244. return Number(this.hitDie.substring(1));
  1245. }
  1246. /* -------------------------------------------- */
  1247. /* Display Methods */
  1248. /* -------------------------------------------- */
  1249. /** @inheritdoc */
  1250. configuredForLevel(level) {
  1251. return this.valueForLevel(level) !== null;
  1252. }
  1253. /* -------------------------------------------- */
  1254. /** @inheritdoc */
  1255. titleForLevel(level, { configMode=false }={}) {
  1256. const hp = this.valueForLevel(level);
  1257. if ( !hp || configMode ) return this.title;
  1258. return `${this.title}: <strong>${hp}</strong>`;
  1259. }
  1260. /* -------------------------------------------- */
  1261. /**
  1262. * Hit points given at the provided level.
  1263. * @param {number} level Level for which to get hit points.
  1264. * @returns {number|null} Hit points for level or null if none have been taken.
  1265. */
  1266. valueForLevel(level) {
  1267. return this.constructor.valueForLevel(this.value, this.hitDieValue, level);
  1268. }
  1269. /* -------------------------------------------- */
  1270. /**
  1271. * Hit points given at the provided level.
  1272. * @param {object} data Contents of `value` used to determine this value.
  1273. * @param {number} hitDieValue Face value of the hit die used by this advancement.
  1274. * @param {number} level Level for which to get hit points.
  1275. * @returns {number|null} Hit points for level or null if none have been taken.
  1276. */
  1277. static valueForLevel(data, hitDieValue, level) {
  1278. const value = data[level];
  1279. if ( !value ) return null;
  1280. if ( value === "max" ) return hitDieValue;
  1281. if ( value === "avg" ) return (hitDieValue / 2) + 1;
  1282. return value;
  1283. }
  1284. /* -------------------------------------------- */
  1285. /**
  1286. * Total hit points provided by this advancement.
  1287. * @returns {number} Hit points currently selected.
  1288. */
  1289. total() {
  1290. return Object.keys(this.value).reduce((total, level) => total + this.valueForLevel(parseInt(level)), 0);
  1291. }
  1292. /* -------------------------------------------- */
  1293. /**
  1294. * Total hit points taking the provided ability modifier into account, with a minimum of 1 per level.
  1295. * @param {number} mod Modifier to add per level.
  1296. * @returns {number} Total hit points plus modifier.
  1297. */
  1298. getAdjustedTotal(mod) {
  1299. return Object.keys(this.value).reduce((total, level) => {
  1300. return total + Math.max(this.valueForLevel(parseInt(level)) + mod, 1);
  1301. }, 0);
  1302. }
  1303. /* -------------------------------------------- */
  1304. /* Editing Methods */
  1305. /* -------------------------------------------- */
  1306. /** @inheritdoc */
  1307. static availableForItem(item) {
  1308. return !item.advancement.byType.HitPoints?.length;
  1309. }
  1310. /* -------------------------------------------- */
  1311. /* Application Methods */
  1312. /* -------------------------------------------- */
  1313. /**
  1314. * Add the ability modifier and any bonuses to the provided hit points value to get the number to apply.
  1315. * @param {number} value Hit points taken at a given level.
  1316. * @returns {number} Hit points adjusted with ability modifier and per-level bonuses.
  1317. */
  1318. #getApplicableValue(value) {
  1319. const abilityId = CONFIG.DND5E.hitPointsAbility || "con";
  1320. value = Math.max(value + (this.actor.system.abilities[abilityId]?.mod ?? 0), 1);
  1321. value += simplifyBonus(this.actor.system.attributes.hp.bonuses.level, this.actor.getRollData());
  1322. return value;
  1323. }
  1324. /* -------------------------------------------- */
  1325. /** @inheritdoc */
  1326. apply(level, data) {
  1327. let value = this.constructor.valueForLevel(data, this.hitDieValue, level);
  1328. if ( value === undefined ) return;
  1329. this.actor.updateSource({
  1330. "system.attributes.hp.value": this.actor.system.attributes.hp.value + this.#getApplicableValue(value)
  1331. });
  1332. this.updateSource({ value: data });
  1333. }
  1334. /* -------------------------------------------- */
  1335. /** @inheritdoc */
  1336. restore(level, data) {
  1337. this.apply(level, data);
  1338. }
  1339. /* -------------------------------------------- */
  1340. /** @inheritdoc */
  1341. reverse(level) {
  1342. let value = this.valueForLevel(level);
  1343. if ( value === undefined ) return;
  1344. this.actor.updateSource({
  1345. "system.attributes.hp.value": this.actor.system.attributes.hp.value - this.#getApplicableValue(value)
  1346. });
  1347. const source = { [level]: this.value[level] };
  1348. this.updateSource({ [`value.-=${level}`]: null });
  1349. return source;
  1350. }
  1351. }
  1352. /**
  1353. * Configuration application for item grants.
  1354. */
  1355. class ItemGrantConfig extends AdvancementConfig {
  1356. /** @inheritdoc */
  1357. static get defaultOptions() {
  1358. return foundry.utils.mergeObject(super.defaultOptions, {
  1359. classes: ["dnd5e", "advancement", "item-grant"],
  1360. dragDrop: [{ dropSelector: ".drop-target" }],
  1361. dropKeyPath: "items",
  1362. template: "systems/dnd5e/templates/advancement/item-grant-config.hbs"
  1363. });
  1364. }
  1365. /* -------------------------------------------- */
  1366. /** @inheritdoc */
  1367. getData(options={}) {
  1368. const context = super.getData(options);
  1369. context.showSpellConfig = context.configuration.items.map(uuid => fromUuidSync(uuid)).some(i => i?.type === "spell");
  1370. return context;
  1371. }
  1372. /* -------------------------------------------- */
  1373. /** @inheritdoc */
  1374. _validateDroppedItem(event, item) {
  1375. this.advancement._validateItemType(item);
  1376. }
  1377. }
  1378. /**
  1379. * Inline application that presents the player with a list of items to be added.
  1380. */
  1381. class ItemGrantFlow extends AdvancementFlow {
  1382. /** @inheritdoc */
  1383. static get defaultOptions() {
  1384. return foundry.utils.mergeObject(super.defaultOptions, {
  1385. template: "systems/dnd5e/templates/advancement/item-grant-flow.hbs"
  1386. });
  1387. }
  1388. /* -------------------------------------------- */
  1389. /**
  1390. * Produce the rendering context for this flow.
  1391. * @returns {object}
  1392. */
  1393. async getContext() {
  1394. const config = this.advancement.configuration.items;
  1395. const added = this.retainedData?.items.map(i => foundry.utils.getProperty(i, "flags.dnd5e.sourceId"))
  1396. ?? this.advancement.value.added;
  1397. const checked = new Set(Object.values(added ?? {}));
  1398. return {
  1399. optional: this.advancement.configuration.optional,
  1400. items: (await Promise.all(config.map(uuid => fromUuid(uuid)))).reduce((arr, item) => {
  1401. if ( !item ) return arr;
  1402. item.checked = added ? checked.has(item.uuid) : true;
  1403. arr.push(item);
  1404. return arr;
  1405. }, [])
  1406. };
  1407. }
  1408. /* -------------------------------------------- */
  1409. /** @inheritdoc */
  1410. async getData(options={}) {
  1411. return foundry.utils.mergeObject(super.getData(options), await this.getContext());
  1412. }
  1413. /* -------------------------------------------- */
  1414. /** @inheritdoc */
  1415. activateListeners(html) {
  1416. super.activateListeners(html);
  1417. html.find("a[data-uuid]").click(this._onClickFeature.bind(this));
  1418. }
  1419. /* -------------------------------------------- */
  1420. /**
  1421. * Handle clicking on a feature during item grant to preview the feature.
  1422. * @param {MouseEvent} event The triggering event.
  1423. * @protected
  1424. */
  1425. async _onClickFeature(event) {
  1426. event.preventDefault();
  1427. const uuid = event.currentTarget.dataset.uuid;
  1428. const item = await fromUuid(uuid);
  1429. item?.sheet.render(true);
  1430. }
  1431. /* -------------------------------------------- */
  1432. /** @inheritdoc */
  1433. async _updateObject(event, formData) {
  1434. const retainedData = this.retainedData?.items.reduce((obj, i) => {
  1435. obj[foundry.utils.getProperty(i, "flags.dnd5e.sourceId")] = i;
  1436. return obj;
  1437. }, {});
  1438. await this.advancement.apply(this.level, formData, retainedData);
  1439. }
  1440. }
  1441. class SpellConfigurationData extends foundry.abstract.DataModel {
  1442. static defineSchema() {
  1443. return {
  1444. ability: new foundry.data.fields.StringField({label: "DND5E.AbilityModifier"}),
  1445. preparation: new foundry.data.fields.StringField({label: "DND5E.SpellPreparationMode"}),
  1446. uses: new foundry.data.fields.SchemaField({
  1447. max: new FormulaField({deterministic: true, label: "DND5E.UsesMax"}),
  1448. per: new foundry.data.fields.StringField({label: "DND5E.UsesPeriod"})
  1449. }, {label: "DND5E.LimitedUses"})
  1450. };
  1451. }
  1452. /* -------------------------------------------- */
  1453. /**
  1454. * Changes that this spell configuration indicates should be performed on spells.
  1455. * @type {object}
  1456. */
  1457. get spellChanges() {
  1458. const updates = {};
  1459. if ( this.ability ) updates["system.ability"] = this.ability;
  1460. if ( this.preparation ) updates["system.preparation.mode"] = this.preparation;
  1461. if ( this.uses.max && this.uses.per ) {
  1462. updates["system.uses.max"] = this.uses.max;
  1463. updates["system.uses.per"] = this.uses.per;
  1464. if ( Number.isNumeric(this.uses.max) ) updates["system.uses.value"] = parseInt(this.uses.max);
  1465. else {
  1466. try {
  1467. const rollData = this.parent.parent.actor.getRollData({ deterministic: true });
  1468. const formula = Roll.replaceFormulaData(this.uses.max, rollData, {missing: 0});
  1469. updates["system.uses.value"] = Roll.safeEval(formula);
  1470. } catch(e) { }
  1471. }
  1472. }
  1473. return updates;
  1474. }
  1475. }
  1476. class ItemGrantConfigurationData extends foundry.abstract.DataModel {
  1477. /** @inheritdoc */
  1478. static defineSchema() {
  1479. return {
  1480. items: new foundry.data.fields.ArrayField(new foundry.data.fields.StringField(), {
  1481. required: true, label: "DOCUMENT.Items"
  1482. }),
  1483. optional: new foundry.data.fields.BooleanField({
  1484. required: true, label: "DND5E.AdvancementItemGrantOptional", hint: "DND5E.AdvancementItemGrantOptionalHint"
  1485. }),
  1486. spell: new foundry.data.fields.EmbeddedDataField(SpellConfigurationData, {
  1487. required: true, nullable: true, initial: null
  1488. })
  1489. };
  1490. }
  1491. }
  1492. /**
  1493. * Advancement that automatically grants one or more items to the player. Presents the player with the option of
  1494. * skipping any or all of the items.
  1495. */
  1496. class ItemGrantAdvancement extends Advancement {
  1497. /** @inheritdoc */
  1498. static get metadata() {
  1499. return foundry.utils.mergeObject(super.metadata, {
  1500. dataModels: {
  1501. configuration: ItemGrantConfigurationData
  1502. },
  1503. order: 40,
  1504. icon: "systems/dnd5e/icons/svg/item-grant.svg",
  1505. title: game.i18n.localize("DND5E.AdvancementItemGrantTitle"),
  1506. hint: game.i18n.localize("DND5E.AdvancementItemGrantHint"),
  1507. apps: {
  1508. config: ItemGrantConfig,
  1509. flow: ItemGrantFlow
  1510. }
  1511. });
  1512. }
  1513. /* -------------------------------------------- */
  1514. /**
  1515. * The item types that are supported in Item Grant.
  1516. * @type {Set<string>}
  1517. */
  1518. static VALID_TYPES = new Set(["feat", "spell", "consumable", "backpack", "equipment", "loot", "tool", "weapon"]);
  1519. /* -------------------------------------------- */
  1520. /* Display Methods */
  1521. /* -------------------------------------------- */
  1522. /** @inheritdoc */
  1523. configuredForLevel(level) {
  1524. return !foundry.utils.isEmpty(this.value);
  1525. }
  1526. /* -------------------------------------------- */
  1527. /** @inheritdoc */
  1528. summaryForLevel(level, { configMode=false }={}) {
  1529. // Link to compendium items
  1530. if ( !this.value.added || configMode ) {
  1531. return this.configuration.items.reduce((html, uuid) => html + dnd5e.utils.linkForUuid(uuid), "");
  1532. }
  1533. // Link to items on the actor
  1534. else {
  1535. return Object.keys(this.value.added).map(id => {
  1536. const item = this.actor.items.get(id);
  1537. return item?.toAnchor({classes: ["content-link"]}).outerHTML ?? "";
  1538. }).join("");
  1539. }
  1540. }
  1541. /* -------------------------------------------- */
  1542. /* Application Methods */
  1543. /* -------------------------------------------- */
  1544. /**
  1545. * Location where the added items are stored for the specified level.
  1546. * @param {number} level Level being advanced.
  1547. * @returns {string}
  1548. */
  1549. storagePath(level) {
  1550. return "value.added";
  1551. }
  1552. /* -------------------------------------------- */
  1553. /**
  1554. * Locally apply this advancement to the actor.
  1555. * @param {number} level Level being advanced.
  1556. * @param {object} data Data from the advancement form.
  1557. * @param {object} [retainedData={}] Item data grouped by UUID. If present, this data will be used rather than
  1558. * fetching new data from the source.
  1559. */
  1560. async apply(level, data, retainedData={}) {
  1561. const items = [];
  1562. const updates = {};
  1563. const spellChanges = this.configuration.spell?.spellChanges ?? {};
  1564. for ( const [uuid, selected] of Object.entries(data) ) {
  1565. if ( !selected ) continue;
  1566. let itemData = retainedData[uuid];
  1567. if ( !itemData ) {
  1568. const source = await fromUuid(uuid);
  1569. if ( !source ) continue;
  1570. itemData = source.clone({
  1571. _id: foundry.utils.randomID(),
  1572. "flags.dnd5e.sourceId": uuid,
  1573. "flags.dnd5e.advancementOrigin": `${this.item.id}.${this.id}`
  1574. }, {keepId: true}).toObject();
  1575. }
  1576. if ( itemData.type === "spell" ) foundry.utils.mergeObject(itemData, spellChanges);
  1577. items.push(itemData);
  1578. updates[itemData._id] = uuid;
  1579. }
  1580. this.actor.updateSource({items});
  1581. this.updateSource({[this.storagePath(level)]: updates});
  1582. }
  1583. /* -------------------------------------------- */
  1584. /** @inheritdoc */
  1585. restore(level, data) {
  1586. const updates = {};
  1587. for ( const item of data.items ) {
  1588. this.actor.updateSource({items: [item]});
  1589. updates[item._id] = item.flags.dnd5e.sourceId;
  1590. }
  1591. this.updateSource({[this.storagePath(level)]: updates});
  1592. }
  1593. /* -------------------------------------------- */
  1594. /** @inheritdoc */
  1595. reverse(level) {
  1596. const items = [];
  1597. const keyPath = this.storagePath(level);
  1598. for ( const id of Object.keys(foundry.utils.getProperty(this, keyPath) ?? {}) ) {
  1599. const item = this.actor.items.get(id);
  1600. if ( item ) items.push(item.toObject());
  1601. this.actor.items.delete(id);
  1602. }
  1603. this.updateSource({[keyPath.replace(/\.([\w\d]+)$/, ".-=$1")]: null});
  1604. return { items };
  1605. }
  1606. /* -------------------------------------------- */
  1607. /**
  1608. * Verify that the provided item can be used with this advancement based on the configuration.
  1609. * @param {Item5e} item Item that needs to be tested.
  1610. * @param {object} config
  1611. * @param {boolean} [config.strict=true] Should an error be thrown when an invalid type is encountered?
  1612. * @returns {boolean} Is this type valid?
  1613. * @throws An error if the item is invalid and strict is `true`.
  1614. */
  1615. _validateItemType(item, { strict=true }={}) {
  1616. if ( this.constructor.VALID_TYPES.has(item.type) ) return true;
  1617. const type = game.i18n.localize(CONFIG.Item.typeLabels[item.type]);
  1618. if ( strict ) throw new Error(game.i18n.format("DND5E.AdvancementItemTypeInvalidWarning", {type}));
  1619. return false;
  1620. }
  1621. }
  1622. /**
  1623. * Configuration application for item choices.
  1624. */
  1625. class ItemChoiceConfig extends AdvancementConfig {
  1626. /** @inheritdoc */
  1627. static get defaultOptions() {
  1628. return foundry.utils.mergeObject(super.defaultOptions, {
  1629. classes: ["dnd5e", "advancement", "item-choice", "two-column"],
  1630. dragDrop: [{ dropSelector: ".drop-target" }],
  1631. dropKeyPath: "pool",
  1632. template: "systems/dnd5e/templates/advancement/item-choice-config.hbs",
  1633. width: 540
  1634. });
  1635. }
  1636. /* -------------------------------------------- */
  1637. /** @inheritdoc */
  1638. getData(options={}) {
  1639. const context = {
  1640. ...super.getData(options),
  1641. showSpellConfig: this.advancement.configuration.type === "spell",
  1642. validTypes: this.advancement.constructor.VALID_TYPES.reduce((obj, type) => {
  1643. obj[type] = game.i18n.localize(CONFIG.Item.typeLabels[type]);
  1644. return obj;
  1645. }, {})
  1646. };
  1647. if ( this.advancement.configuration.type === "feat" ) {
  1648. const selectedType = CONFIG.DND5E.featureTypes[this.advancement.configuration.restriction.type];
  1649. context.typeRestriction = {
  1650. typeLabel: game.i18n.localize("DND5E.ItemFeatureType"),
  1651. typeOptions: CONFIG.DND5E.featureTypes,
  1652. subtypeLabel: game.i18n.format("DND5E.ItemFeatureSubtype", {category: selectedType?.label}),
  1653. subtypeOptions: selectedType?.subtypes
  1654. };
  1655. }
  1656. return context;
  1657. }
  1658. /* -------------------------------------------- */
  1659. /** @inheritdoc */
  1660. async prepareConfigurationUpdate(configuration) {
  1661. if ( configuration.choices ) configuration.choices = this.constructor._cleanedObject(configuration.choices);
  1662. // Ensure items are still valid if type restriction or spell restriction are changed
  1663. const pool = [];
  1664. for ( const uuid of (configuration.pool ?? this.advancement.configuration.pool) ) {
  1665. if ( this.advancement._validateItemType(await fromUuid(uuid), {
  1666. type: configuration.type, restriction: configuration.restriction ?? {}, strict: false
  1667. }) ) pool.push(uuid);
  1668. }
  1669. configuration.pool = pool;
  1670. return configuration;
  1671. }
  1672. /* -------------------------------------------- */
  1673. /** @inheritdoc */
  1674. _validateDroppedItem(event, item) {
  1675. this.advancement._validateItemType(item);
  1676. }
  1677. }
  1678. /**
  1679. * Object describing the proficiency for a specific ability or skill.
  1680. *
  1681. * @param {number} proficiency Actor's flat proficiency bonus based on their current level.
  1682. * @param {number} multiplier Value by which to multiply the actor's base proficiency value.
  1683. * @param {boolean} [roundDown] Should half-values be rounded up or down?
  1684. */
  1685. class Proficiency {
  1686. constructor(proficiency, multiplier, roundDown=true) {
  1687. /**
  1688. * Base proficiency value of the actor.
  1689. * @type {number}
  1690. * @private
  1691. */
  1692. this._baseProficiency = Number(proficiency ?? 0);
  1693. /**
  1694. * Value by which to multiply the actor's base proficiency value.
  1695. * @type {number}
  1696. */
  1697. this.multiplier = Number(multiplier ?? 0);
  1698. /**
  1699. * Direction decimal results should be rounded ("up" or "down").
  1700. * @type {string}
  1701. */
  1702. this.rounding = roundDown ? "down" : "up";
  1703. }
  1704. /* -------------------------------------------- */
  1705. /**
  1706. * Calculate an actor's proficiency modifier based on level or CR.
  1707. * @param {number} level Level or CR To use for calculating proficiency modifier.
  1708. * @returns {number} Proficiency modifier.
  1709. */
  1710. static calculateMod(level) {
  1711. return Math.floor((level + 7) / 4);
  1712. }
  1713. /* -------------------------------------------- */
  1714. /**
  1715. * Flat proficiency value regardless of proficiency mode.
  1716. * @type {number}
  1717. */
  1718. get flat() {
  1719. const roundMethod = (this.rounding === "down") ? Math.floor : Math.ceil;
  1720. return roundMethod(this.multiplier * this._baseProficiency);
  1721. }
  1722. /* -------------------------------------------- */
  1723. /**
  1724. * Dice-based proficiency value regardless of proficiency mode.
  1725. * @type {string}
  1726. */
  1727. get dice() {
  1728. if ( (this._baseProficiency === 0) || (this.multiplier === 0) ) return "0";
  1729. const roundTerm = (this.rounding === "down") ? "floor" : "ceil";
  1730. if ( this.multiplier === 0.5 ) {
  1731. return `${roundTerm}(1d${this._baseProficiency * 2} / 2)`;
  1732. } else {
  1733. return `${this.multiplier}d${this._baseProficiency * 2}`;
  1734. }
  1735. }
  1736. /* -------------------------------------------- */
  1737. /**
  1738. * Either flat or dice proficiency term based on configured setting.
  1739. * @type {string}
  1740. */
  1741. get term() {
  1742. return (game.settings.get("dnd5e", "proficiencyModifier") === "dice") ? this.dice : String(this.flat);
  1743. }
  1744. /* -------------------------------------------- */
  1745. /**
  1746. * Whether the proficiency is greater than zero.
  1747. * @type {boolean}
  1748. */
  1749. get hasProficiency() {
  1750. return (this._baseProficiency > 0) && (this.multiplier > 0);
  1751. }
  1752. /* -------------------------------------------- */
  1753. /**
  1754. * Override the default `toString` method to return flat proficiency for backwards compatibility in formula.
  1755. * @returns {string} Flat proficiency value.
  1756. */
  1757. toString() {
  1758. return this.term;
  1759. }
  1760. }
  1761. /* -------------------------------------------- */
  1762. /* D20 Roll */
  1763. /* -------------------------------------------- */
  1764. /**
  1765. * Configuration data for a D20 roll.
  1766. *
  1767. * @typedef {object} D20RollConfiguration
  1768. *
  1769. * @property {string[]} [parts=[]] The dice roll component parts, excluding the initial d20.
  1770. * @property {object} [data={}] Data that will be used when parsing this roll.
  1771. * @property {Event} [event] The triggering event for this roll.
  1772. *
  1773. * ## D20 Properties
  1774. * @property {boolean} [advantage] Apply advantage to this roll (unless overridden by modifier keys or dialog)?
  1775. * @property {boolean} [disadvantage] Apply disadvantage to this roll (unless overridden by modifier keys or dialog)?
  1776. * @property {number|null} [critical=20] The value of the d20 result which represents a critical success,
  1777. * `null` will prevent critical successes.
  1778. * @property {number|null} [fumble=1] The value of the d20 result which represents a critical failure,
  1779. * `null` will prevent critical failures.
  1780. * @property {number} [targetValue] The value of the d20 result which should represent a successful roll.
  1781. *
  1782. * ## Flags
  1783. * @property {boolean} [elvenAccuracy] Allow Elven Accuracy to modify this roll?
  1784. * @property {boolean} [halflingLucky] Allow Halfling Luck to modify this roll?
  1785. * @property {boolean} [reliableTalent] Allow Reliable Talent to modify this roll?
  1786. *
  1787. * ## Roll Configuration Dialog
  1788. * @property {boolean} [fastForward] Should the roll configuration dialog be skipped?
  1789. * @property {boolean} [chooseModifier=false] If the configuration dialog is shown, should the ability modifier be
  1790. * configurable within that interface?
  1791. * @property {string} [template] The HTML template used to display the roll configuration dialog.
  1792. * @property {string} [title] Title of the roll configuration dialog.
  1793. * @property {object} [dialogOptions] Additional options passed to the roll configuration dialog.
  1794. *
  1795. * ## Chat Message
  1796. * @property {boolean} [chatMessage=true] Should a chat message be created for this roll?
  1797. * @property {object} [messageData={}] Additional data which is applied to the created chat message.
  1798. * @property {string} [rollMode] Value of `CONST.DICE_ROLL_MODES` to apply as default for the chat message.
  1799. * @property {object} [flavor] Flavor text to use in the created chat message.
  1800. */
  1801. /**
  1802. * A standardized helper function for managing core 5e d20 rolls.
  1803. * Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward".
  1804. * This chooses the default options of a normal attack with no bonus, Advantage, or Disadvantage respectively
  1805. *
  1806. * @param {D20RollConfiguration} configuration Configuration data for the D20 roll.
  1807. * @returns {Promise<D20Roll|null>} The evaluated D20Roll, or null if the workflow was cancelled.
  1808. */
  1809. async function d20Roll({
  1810. parts=[], data={}, event,
  1811. advantage, disadvantage, critical=20, fumble=1, targetValue,
  1812. elvenAccuracy, halflingLucky, reliableTalent,
  1813. fastForward, chooseModifier=false, template, title, dialogOptions,
  1814. chatMessage=true, messageData={}, rollMode, flavor
  1815. }={}) {
  1816. // Handle input arguments
  1817. const formula = ["1d20"].concat(parts).join(" + ");
  1818. const {advantageMode, isFF} = CONFIG.Dice.D20Roll.determineAdvantageMode({
  1819. advantage, disadvantage, fastForward, event
  1820. });
  1821. const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
  1822. if ( chooseModifier && !isFF ) {
  1823. data.mod = "@mod";
  1824. if ( "abilityCheckBonus" in data ) data.abilityCheckBonus = "@abilityCheckBonus";
  1825. }
  1826. // Construct the D20Roll instance
  1827. const roll = new CONFIG.Dice.D20Roll(formula, data, {
  1828. flavor: flavor || title,
  1829. advantageMode,
  1830. defaultRollMode,
  1831. rollMode,
  1832. critical,
  1833. fumble,
  1834. targetValue,
  1835. elvenAccuracy,
  1836. halflingLucky,
  1837. reliableTalent
  1838. });
  1839. // Prompt a Dialog to further configure the D20Roll
  1840. if ( !isFF ) {
  1841. const configured = await roll.configureDialog({
  1842. title,
  1843. chooseModifier,
  1844. defaultRollMode,
  1845. defaultAction: advantageMode,
  1846. defaultAbility: data?.item?.ability || data?.defaultAbility,
  1847. template
  1848. }, dialogOptions);
  1849. if ( configured === null ) return null;
  1850. } else roll.options.rollMode ??= defaultRollMode;
  1851. // Evaluate the configured roll
  1852. await roll.evaluate({async: true});
  1853. // Create a Chat Message
  1854. if ( roll && chatMessage ) await roll.toMessage(messageData);
  1855. return roll;
  1856. }
  1857. /* -------------------------------------------- */
  1858. /* Damage Roll */
  1859. /* -------------------------------------------- */
  1860. /**
  1861. * Configuration data for a damage roll.
  1862. *
  1863. * @typedef {object} DamageRollConfiguration
  1864. *
  1865. * @property {string[]} [parts=[]] The dice roll component parts.
  1866. * @property {object} [data={}] Data that will be used when parsing this roll.
  1867. * @property {Event} [event] The triggering event for this roll.
  1868. *
  1869. * ## Critical Handling
  1870. * @property {boolean} [allowCritical=true] Is this damage roll allowed to be rolled as critical?
  1871. * @property {boolean} [critical] Apply critical to this roll (unless overridden by modifier key or dialog)?
  1872. * @property {number} [criticalBonusDice] A number of bonus damage dice that are added for critical hits.
  1873. * @property {number} [criticalMultiplier] Multiplier to use when calculating critical damage.
  1874. * @property {boolean} [multiplyNumeric] Should numeric terms be multiplied when this roll criticals?
  1875. * @property {boolean} [powerfulCritical] Should the critical dice be maximized rather than rolled?
  1876. * @property {string} [criticalBonusDamage] An extra damage term that is applied only on a critical hit.
  1877. *
  1878. * ## Roll Configuration Dialog
  1879. * @property {boolean} [fastForward] Should the roll configuration dialog be skipped?
  1880. * @property {string} [template] The HTML template used to render the roll configuration dialog.
  1881. * @property {string} [title] Title of the roll configuration dialog.
  1882. * @property {object} [dialogOptions] Additional options passed to the roll configuration dialog.
  1883. *
  1884. * ## Chat Message
  1885. * @property {boolean} [chatMessage=true] Should a chat message be created for this roll?
  1886. * @property {object} [messageData={}] Additional data which is applied to the created chat message.
  1887. * @property {string} [rollMode] Value of `CONST.DICE_ROLL_MODES` to apply as default for the chat message.
  1888. * @property {string} [flavor] Flavor text to use in the created chat message.
  1889. */
  1890. /**
  1891. * A standardized helper function for managing core 5e damage rolls.
  1892. * Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward".
  1893. * This chooses the default options of a normal attack with no bonus, Critical, or no bonus respectively
  1894. *
  1895. * @param {DamageRollConfiguration} configuration Configuration data for the Damage roll.
  1896. * @returns {Promise<DamageRoll|null>} The evaluated DamageRoll, or null if the workflow was canceled.
  1897. */
  1898. async function damageRoll({
  1899. parts=[], data={}, event,
  1900. allowCritical=true, critical, criticalBonusDice, criticalMultiplier,
  1901. multiplyNumeric, powerfulCritical, criticalBonusDamage,
  1902. fastForward, template, title, dialogOptions,
  1903. chatMessage=true, messageData={}, rollMode, flavor
  1904. }={}) {
  1905. // Handle input arguments
  1906. const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
  1907. // Construct the DamageRoll instance
  1908. const formula = parts.join(" + ");
  1909. const {isCritical, isFF} = _determineCriticalMode({critical, fastForward, event});
  1910. const roll = new CONFIG.Dice.DamageRoll(formula, data, {
  1911. flavor: flavor || title,
  1912. rollMode,
  1913. critical: isFF ? isCritical : false,
  1914. criticalBonusDice,
  1915. criticalMultiplier,
  1916. criticalBonusDamage,
  1917. multiplyNumeric: multiplyNumeric ?? game.settings.get("dnd5e", "criticalDamageModifiers"),
  1918. powerfulCritical: powerfulCritical ?? game.settings.get("dnd5e", "criticalDamageMaxDice")
  1919. });
  1920. // Prompt a Dialog to further configure the DamageRoll
  1921. if ( !isFF ) {
  1922. const configured = await roll.configureDialog({
  1923. title,
  1924. defaultRollMode: defaultRollMode,
  1925. defaultCritical: isCritical,
  1926. template,
  1927. allowCritical
  1928. }, dialogOptions);
  1929. if ( configured === null ) return null;
  1930. }
  1931. // Evaluate the configured roll
  1932. await roll.evaluate({async: true});
  1933. // Create a Chat Message
  1934. if ( roll && chatMessage ) await roll.toMessage(messageData);
  1935. return roll;
  1936. }
  1937. /* -------------------------------------------- */
  1938. /**
  1939. * Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
  1940. * @param {object} [config]
  1941. * @param {Event} [config.event] Event that triggered the roll.
  1942. * @param {boolean} [config.critical] Is this roll treated as a critical by default?
  1943. * @param {boolean} [config.fastForward] Should the roll dialog be skipped?
  1944. * @returns {{isFF: boolean, isCritical: boolean}} Whether the roll is fast-forward, and whether it is a critical hit
  1945. */
  1946. function _determineCriticalMode({event, critical=false, fastForward}={}) {
  1947. const isFF = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
  1948. if ( event?.altKey ) critical = true;
  1949. return {isFF: !!isFF, isCritical: critical};
  1950. }
  1951. /**
  1952. * A helper Dialog subclass for rolling Hit Dice on short rest.
  1953. *
  1954. * @param {Actor5e} actor Actor that is taking the short rest.
  1955. * @param {object} [dialogData={}] An object of dialog data which configures how the modal window is rendered.
  1956. * @param {object} [options={}] Dialog rendering options.
  1957. */
  1958. class ShortRestDialog extends Dialog {
  1959. constructor(actor, dialogData={}, options={}) {
  1960. super(dialogData, options);
  1961. /**
  1962. * Store a reference to the Actor document which is resting
  1963. * @type {Actor}
  1964. */
  1965. this.actor = actor;
  1966. /**
  1967. * Track the most recently used HD denomination for re-rendering the form
  1968. * @type {string}
  1969. */
  1970. this._denom = null;
  1971. }
  1972. /* -------------------------------------------- */
  1973. /** @inheritDoc */
  1974. static get defaultOptions() {
  1975. return foundry.utils.mergeObject(super.defaultOptions, {
  1976. template: "systems/dnd5e/templates/apps/short-rest.hbs",
  1977. classes: ["dnd5e", "dialog"]
  1978. });
  1979. }
  1980. /* -------------------------------------------- */
  1981. /** @inheritDoc */
  1982. getData() {
  1983. const data = super.getData();
  1984. // Determine Hit Dice
  1985. data.availableHD = this.actor.items.reduce((hd, item) => {
  1986. if ( item.type === "class" ) {
  1987. const {levels, hitDice, hitDiceUsed} = item.system;
  1988. const denom = hitDice ?? "d6";
  1989. const available = parseInt(levels ?? 1) - parseInt(hitDiceUsed ?? 0);
  1990. hd[denom] = denom in hd ? hd[denom] + available : available;
  1991. }
  1992. return hd;
  1993. }, {});
  1994. data.canRoll = this.actor.system.attributes.hd > 0;
  1995. data.denomination = this._denom;
  1996. // Determine rest type
  1997. const variant = game.settings.get("dnd5e", "restVariant");
  1998. data.promptNewDay = variant !== "epic"; // It's never a new day when only resting 1 minute
  1999. data.newDay = false; // It may be a new day, but not by default
  2000. return data;
  2001. }
  2002. /* -------------------------------------------- */
  2003. /** @inheritDoc */
  2004. activateListeners(html) {
  2005. super.activateListeners(html);
  2006. let btn = html.find("#roll-hd");
  2007. btn.click(this._onRollHitDie.bind(this));
  2008. }
  2009. /* -------------------------------------------- */
  2010. /**
  2011. * Handle rolling a Hit Die as part of a Short Rest action
  2012. * @param {Event} event The triggering click event
  2013. * @protected
  2014. */
  2015. async _onRollHitDie(event) {
  2016. event.preventDefault();
  2017. const btn = event.currentTarget;
  2018. this._denom = btn.form.hd.value;
  2019. await this.actor.rollHitDie(this._denom);
  2020. this.render();
  2021. }
  2022. /* -------------------------------------------- */
  2023. /**
  2024. * A helper constructor function which displays the Short Rest dialog and returns a Promise once it's workflow has
  2025. * been resolved.
  2026. * @param {object} [options={}]
  2027. * @param {Actor5e} [options.actor] Actor that is taking the short rest.
  2028. * @returns {Promise} Promise that resolves when the rest is completed or rejects when canceled.
  2029. */
  2030. static async shortRestDialog({ actor }={}) {
  2031. return new Promise((resolve, reject) => {
  2032. const dlg = new this(actor, {
  2033. title: `${game.i18n.localize("DND5E.ShortRest")}: ${actor.name}`,
  2034. buttons: {
  2035. rest: {
  2036. icon: '<i class="fas fa-bed"></i>',
  2037. label: game.i18n.localize("DND5E.Rest"),
  2038. callback: html => {
  2039. let newDay = false;
  2040. if ( game.settings.get("dnd5e", "restVariant") !== "epic" ) {
  2041. newDay = html.find('input[name="newDay"]')[0].checked;
  2042. }
  2043. resolve(newDay);
  2044. }
  2045. },
  2046. cancel: {
  2047. icon: '<i class="fas fa-times"></i>',
  2048. label: game.i18n.localize("Cancel"),
  2049. callback: reject
  2050. }
  2051. },
  2052. close: reject
  2053. });
  2054. dlg.render(true);
  2055. });
  2056. }
  2057. }
  2058. /**
  2059. * A helper Dialog subclass for completing a long rest.
  2060. *
  2061. * @param {Actor5e} actor Actor that is taking the long rest.
  2062. * @param {object} [dialogData={}] An object of dialog data which configures how the modal window is rendered.
  2063. * @param {object} [options={}] Dialog rendering options.
  2064. */
  2065. class LongRestDialog extends Dialog {
  2066. constructor(actor, dialogData={}, options={}) {
  2067. super(dialogData, options);
  2068. this.actor = actor;
  2069. }
  2070. /* -------------------------------------------- */
  2071. /** @inheritDoc */
  2072. static get defaultOptions() {
  2073. return foundry.utils.mergeObject(super.defaultOptions, {
  2074. template: "systems/dnd5e/templates/apps/long-rest.hbs",
  2075. classes: ["dnd5e", "dialog"]
  2076. });
  2077. }
  2078. /* -------------------------------------------- */
  2079. /** @inheritDoc */
  2080. getData() {
  2081. const data = super.getData();
  2082. const variant = game.settings.get("dnd5e", "restVariant");
  2083. data.promptNewDay = variant !== "gritty"; // It's always a new day when resting 1 week
  2084. data.newDay = variant === "normal"; // It's probably a new day when resting normally (8 hours)
  2085. return data;
  2086. }
  2087. /* -------------------------------------------- */
  2088. /**
  2089. * A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's
  2090. * workflow has been resolved.
  2091. * @param {object} [options={}]
  2092. * @param {Actor5e} [options.actor] Actor that is taking the long rest.
  2093. * @returns {Promise} Promise that resolves when the rest is completed or rejects when canceled.
  2094. */
  2095. static async longRestDialog({ actor } = {}) {
  2096. return new Promise((resolve, reject) => {
  2097. const dlg = new this(actor, {
  2098. title: `${game.i18n.localize("DND5E.LongRest")}: ${actor.name}`,
  2099. buttons: {
  2100. rest: {
  2101. icon: '<i class="fas fa-bed"></i>',
  2102. label: game.i18n.localize("DND5E.Rest"),
  2103. callback: html => {
  2104. let newDay = true;
  2105. if (game.settings.get("dnd5e", "restVariant") !== "gritty") {
  2106. newDay = html.find('input[name="newDay"]')[0].checked;
  2107. }
  2108. resolve(newDay);
  2109. }
  2110. },
  2111. cancel: {
  2112. icon: '<i class="fas fa-times"></i>',
  2113. label: game.i18n.localize("Cancel"),
  2114. callback: reject
  2115. }
  2116. },
  2117. default: "rest",
  2118. close: reject
  2119. });
  2120. dlg.render(true);
  2121. });
  2122. }
  2123. }
  2124. /**
  2125. * Cached version of the base items compendia indices with the needed subtype fields.
  2126. * @type {object}
  2127. * @private
  2128. */
  2129. const _cachedIndices = {};
  2130. /* -------------------------------------------- */
  2131. /* Trait Lists */
  2132. /* -------------------------------------------- */
  2133. /**
  2134. * Get the key path to the specified trait on an actor.
  2135. * @param {string} trait Trait as defined in `CONFIG.DND5E.traits`.
  2136. * @returns {string} Key path to this trait's object within an actor's system data.
  2137. */
  2138. function actorKeyPath(trait) {
  2139. const traitConfig = CONFIG.DND5E.traits[trait];
  2140. if ( traitConfig.actorKeyPath ) return traitConfig.actorKeyPath;
  2141. return `traits.${trait}`;
  2142. }
  2143. /* -------------------------------------------- */
  2144. /**
  2145. * Fetch the categories object for the specified trait.
  2146. * @param {string} trait Trait as defined in `CONFIG.DND5E.traits`.
  2147. * @returns {object} Trait categories defined within `CONFIG.DND5E`.
  2148. */
  2149. function categories(trait) {
  2150. const traitConfig = CONFIG.DND5E.traits[trait];
  2151. return CONFIG.DND5E[traitConfig.configKey ?? trait];
  2152. }
  2153. /* -------------------------------------------- */
  2154. /**
  2155. * Get a list of choices for a specific trait.
  2156. * @param {string} trait Trait as defined in `CONFIG.DND5E.traits`.
  2157. * @param {Set<string>} [chosen=[]] Optional list of keys to be marked as chosen.
  2158. * @returns {object} Object mapping proficiency ids to choice objects.
  2159. */
  2160. async function choices(trait, chosen=new Set()) {
  2161. const traitConfig = CONFIG.DND5E.traits[trait];
  2162. if ( foundry.utils.getType(chosen) === "Array" ) chosen = new Set(chosen);
  2163. let data = Object.entries(categories(trait)).reduce((obj, [key, label]) => {
  2164. obj[key] = { label, chosen: chosen.has(key) };
  2165. return obj;
  2166. }, {});
  2167. if ( traitConfig.children ) {
  2168. for ( const [categoryKey, childrenKey] of Object.entries(traitConfig.children) ) {
  2169. const children = CONFIG.DND5E[childrenKey];
  2170. if ( !children || !data[categoryKey] ) continue;
  2171. data[categoryKey].children = Object.entries(children).reduce((obj, [key, label]) => {
  2172. obj[key] = { label, chosen: chosen.has(key) };
  2173. return obj;
  2174. }, {});
  2175. }
  2176. }
  2177. if ( traitConfig.subtypes ) {
  2178. const keyPath = `system.${traitConfig.subtypes.keyPath}`;
  2179. const map = CONFIG.DND5E[`${trait}ProficienciesMap`];
  2180. // Merge all IDs lists together
  2181. const ids = traitConfig.subtypes.ids.reduce((obj, key) => {
  2182. if ( CONFIG.DND5E[key] ) Object.assign(obj, CONFIG.DND5E[key]);
  2183. return obj;
  2184. }, {});
  2185. // Fetch base items for all IDs
  2186. const baseItems = await Promise.all(Object.entries(ids).map(async ([key, id]) => {
  2187. const index = await getBaseItem(id);
  2188. return [key, index];
  2189. }));
  2190. // Sort base items as children of categories based on subtypes
  2191. for ( const [key, index] of baseItems ) {
  2192. if ( !index ) continue;
  2193. // Get the proper subtype, using proficiency map if needed
  2194. let type = foundry.utils.getProperty(index, keyPath);
  2195. if ( map?.[type] ) type = map[type];
  2196. const entry = { label: index.name, chosen: chosen.has(key) };
  2197. // No category for this type, add at top level
  2198. if ( !data[type] ) data[key] = entry;
  2199. // Add as child to appropriate category
  2200. else {
  2201. data[type].children ??= {};
  2202. data[type].children[key] = entry;
  2203. }
  2204. }
  2205. }
  2206. // Sort Categories
  2207. if ( traitConfig.sortCategories ) data = dnd5e.utils.sortObjectEntries(data, "label");
  2208. // Sort Children
  2209. for ( const category of Object.values(data) ) {
  2210. if ( !category.children ) continue;
  2211. category.children = dnd5e.utils.sortObjectEntries(category.children, "label");
  2212. }
  2213. return data;
  2214. }
  2215. /* -------------------------------------------- */
  2216. /**
  2217. * Fetch an item for the provided ID. If the provided ID contains a compendium pack name
  2218. * it will be fetched from that pack, otherwise it will be fetched from the compendium defined
  2219. * in `DND5E.sourcePacks.ITEMS`.
  2220. * @param {string} identifier Simple ID or compendium name and ID separated by a dot.
  2221. * @param {object} [options]
  2222. * @param {boolean} [options.indexOnly] If set to true, only the index data will be fetched (will never return
  2223. * Promise).
  2224. * @param {boolean} [options.fullItem] If set to true, the full item will be returned as long as `indexOnly` is
  2225. * false.
  2226. * @returns {Promise<Item5e>|object} Promise for a `Document` if `indexOnly` is false & `fullItem` is true,
  2227. * otherwise else a simple object containing the minimal index data.
  2228. */
  2229. function getBaseItem(identifier, { indexOnly=false, fullItem=false }={}) {
  2230. let pack = CONFIG.DND5E.sourcePacks.ITEMS;
  2231. let [scope, collection, id] = identifier.split(".");
  2232. if ( scope && collection ) pack = `${scope}.${collection}`;
  2233. if ( !id ) id = identifier;
  2234. const packObject = game.packs.get(pack);
  2235. // Full Item5e document required, always async.
  2236. if ( fullItem && !indexOnly ) return packObject?.getDocument(id);
  2237. const cache = _cachedIndices[pack];
  2238. const loading = cache instanceof Promise;
  2239. // Return extended index if cached, otherwise normal index, guaranteed to never be async.
  2240. if ( indexOnly ) {
  2241. const index = packObject?.index.get(id);
  2242. return loading ? index : cache?.[id] ?? index;
  2243. }
  2244. // Returned cached version of extended index if available.
  2245. if ( loading ) return cache.then(() => _cachedIndices[pack][id]);
  2246. else if ( cache ) return cache[id];
  2247. if ( !packObject ) return;
  2248. // Build the extended index and return a promise for the data
  2249. const promise = packObject.getIndex({ fields: traitIndexFields() }).then(index => {
  2250. const store = index.reduce((obj, entry) => {
  2251. obj[entry._id] = entry;
  2252. return obj;
  2253. }, {});
  2254. _cachedIndices[pack] = store;
  2255. return store[id];
  2256. });
  2257. _cachedIndices[pack] = promise;
  2258. return promise;
  2259. }
  2260. /* -------------------------------------------- */
  2261. /**
  2262. * List of fields on items that should be indexed for retrieving subtypes.
  2263. * @returns {string[]} Index list to pass to `Compendium#getIndex`.
  2264. * @protected
  2265. */
  2266. function traitIndexFields() {
  2267. const fields = [];
  2268. for ( const traitConfig of Object.values(CONFIG.DND5E.traits) ) {
  2269. if ( !traitConfig.subtypes ) continue;
  2270. fields.push(`system.${traitConfig.subtypes.keyPath}`);
  2271. }
  2272. return fields;
  2273. }
  2274. /* -------------------------------------------- */
  2275. /* Localized Formatting Methods */
  2276. /* -------------------------------------------- */
  2277. /**
  2278. * Get the localized label for a specific trait type.
  2279. * @param {string} trait Trait as defined in `CONFIG.DND5E.traits`.
  2280. * @param {number} [count] Count used to determine pluralization. If no count is provided, will default to
  2281. * the 'other' pluralization.
  2282. * @returns {string} Localized label.
  2283. */
  2284. function traitLabel(trait, count) {
  2285. let typeCap;
  2286. if ( trait.length === 2 ) typeCap = trait.toUpperCase();
  2287. else typeCap = trait.capitalize();
  2288. const pluralRule = ( count !== undefined ) ? new Intl.PluralRules(game.i18n.lang).select(count) : "other";
  2289. return game.i18n.localize(`DND5E.Trait${typeCap}Plural.${pluralRule}`);
  2290. }
  2291. /* -------------------------------------------- */
  2292. /**
  2293. * Retrieve the proper display label for the provided key.
  2294. * @param {string} trait Trait as defined in `CONFIG.DND5E.traits`.
  2295. * @param {string} key Key for which to generate the label.
  2296. * @returns {string} Retrieved label.
  2297. */
  2298. function keyLabel(trait, key) {
  2299. const traitConfig = CONFIG.DND5E.traits[trait];
  2300. if ( categories(trait)[key] ) {
  2301. const category = categories(trait)[key];
  2302. if ( !traitConfig.labelKey ) return category;
  2303. return foundry.utils.getProperty(category, traitConfig.labelKey);
  2304. }
  2305. for ( const childrenKey of Object.values(traitConfig.children ?? {}) ) {
  2306. if ( CONFIG.DND5E[childrenKey]?.[key] ) return CONFIG.DND5E[childrenKey]?.[key];
  2307. }
  2308. for ( const idsKey of traitConfig.subtypes?.ids ?? [] ) {
  2309. if ( !CONFIG.DND5E[idsKey]?.[key] ) continue;
  2310. const index = getBaseItem(CONFIG.DND5E[idsKey][key], { indexOnly: true });
  2311. if ( index ) return index.name;
  2312. else break;
  2313. }
  2314. return key;
  2315. }
  2316. /* -------------------------------------------- */
  2317. /**
  2318. * Create a human readable description of the provided choice.
  2319. * @param {string} trait Trait as defined in `CONFIG.DND5E.traits`.
  2320. * @param {TraitChoice} choice Data for a specific choice.
  2321. * @returns {string}
  2322. */
  2323. function choiceLabel(trait, choice) {
  2324. // Select from any trait values
  2325. if ( !choice.pool ) {
  2326. return game.i18n.format("DND5E.TraitConfigChooseAny", {
  2327. count: choice.count,
  2328. type: traitLabel(trait, choice.count).toLowerCase()
  2329. });
  2330. }
  2331. // Select from a list of options
  2332. const choices = choice.pool.map(key => keyLabel(trait, key));
  2333. const listFormatter = new Intl.ListFormat(game.i18n.lang, { type: "disjunction" });
  2334. return game.i18n.format("DND5E.TraitConfigChooseList", {
  2335. count: choice.count,
  2336. list: listFormatter.format(choices)
  2337. });
  2338. }
  2339. var trait = /*#__PURE__*/Object.freeze({
  2340. __proto__: null,
  2341. actorKeyPath: actorKeyPath,
  2342. categories: categories,
  2343. choiceLabel: choiceLabel,
  2344. choices: choices,
  2345. getBaseItem: getBaseItem,
  2346. keyLabel: keyLabel,
  2347. traitIndexFields: traitIndexFields,
  2348. traitLabel: traitLabel
  2349. });
  2350. /**
  2351. * Extend the base Actor class to implement additional system-specific logic.
  2352. */
  2353. class Actor5e extends Actor {
  2354. /**
  2355. * The data source for Actor5e.classes allowing it to be lazily computed.
  2356. * @type {Object<Item5e>}
  2357. * @private
  2358. */
  2359. _classes;
  2360. /* -------------------------------------------- */
  2361. /* Properties */
  2362. /* -------------------------------------------- */
  2363. /**
  2364. * A mapping of classes belonging to this Actor.
  2365. * @type {Object<Item5e>}
  2366. */
  2367. get classes() {
  2368. if ( this._classes !== undefined ) return this._classes;
  2369. if ( !["character", "npc"].includes(this.type) ) return this._classes = {};
  2370. return this._classes = this.items.filter(item => item.type === "class").reduce((obj, cls) => {
  2371. obj[cls.identifier] = cls;
  2372. return obj;
  2373. }, {});
  2374. }
  2375. /* -------------------------------------------- */
  2376. /**
  2377. * Is this Actor currently polymorphed into some other creature?
  2378. * @type {boolean}
  2379. */
  2380. get isPolymorphed() {
  2381. return this.getFlag("dnd5e", "isPolymorphed") || false;
  2382. }
  2383. /* -------------------------------------------- */
  2384. /**
  2385. * The Actor's currently equipped armor, if any.
  2386. * @type {Item5e|null}
  2387. */
  2388. get armor() {
  2389. return this.system.attributes.ac.equippedArmor ?? null;
  2390. }
  2391. /* -------------------------------------------- */
  2392. /**
  2393. * The Actor's currently equipped shield, if any.
  2394. * @type {Item5e|null}
  2395. */
  2396. get shield() {
  2397. return this.system.attributes.ac.equippedShield ?? null;
  2398. }
  2399. /* -------------------------------------------- */
  2400. /* Methods */
  2401. /* -------------------------------------------- */
  2402. /** @inheritdoc */
  2403. _initializeSource(source, options={}) {
  2404. source = super._initializeSource(source, options);
  2405. if ( !source._id || !options.pack || dnd5e.moduleArt.suppressArt ) return source;
  2406. const uuid = `Compendium.${options.pack}.${source._id}`;
  2407. const art = game.dnd5e.moduleArt.map.get(uuid);
  2408. if ( art?.actor || art?.token ) {
  2409. if ( art.actor ) source.img = art.actor;
  2410. if ( typeof art.token === "string" ) source.prototypeToken.texture.src = art.token;
  2411. else if ( art.token ) foundry.utils.mergeObject(source.prototypeToken, art.token);
  2412. const biography = source.system.details?.biography;
  2413. if ( art.credit && biography ) {
  2414. if ( typeof biography.value !== "string" ) biography.value = "";
  2415. biography.value += `<p>${art.credit}</p>`;
  2416. }
  2417. }
  2418. return source;
  2419. }
  2420. /* -------------------------------------------- */
  2421. /** @inheritDoc */
  2422. prepareData() {
  2423. // Do not attempt to prepare non-system types.
  2424. if ( !game.template.Actor.types.includes(this.type) ) return;
  2425. this._classes = undefined;
  2426. this._preparationWarnings = [];
  2427. super.prepareData();
  2428. this.items.forEach(item => item.prepareFinalAttributes());
  2429. }
  2430. /* -------------------------------------------- */
  2431. /** @inheritDoc */
  2432. prepareBaseData() {
  2433. // Delegate preparation to type-subclass
  2434. if ( this.type === "group" ) { // Eventually other types will also support this
  2435. return this.system._prepareBaseData();
  2436. }
  2437. this._prepareBaseArmorClass();
  2438. // Type-specific preparation
  2439. switch ( this.type ) {
  2440. case "character":
  2441. return this._prepareCharacterData();
  2442. case "npc":
  2443. return this._prepareNPCData();
  2444. case "vehicle":
  2445. return this._prepareVehicleData();
  2446. }
  2447. }
  2448. /* --------------------------------------------- */
  2449. /** @inheritDoc */
  2450. applyActiveEffects() {
  2451. this._prepareScaleValues();
  2452. // The Active Effects do not have access to their parent at preparation time, so we wait until this stage to
  2453. // determine whether they are suppressed or not.
  2454. this.effects.forEach(e => e.determineSuppression());
  2455. return super.applyActiveEffects();
  2456. }
  2457. /* -------------------------------------------- */
  2458. /** @inheritDoc */
  2459. prepareDerivedData() {
  2460. // Delegate preparation to type-subclass
  2461. if ( this.type === "group" ) { // Eventually other types will also support this
  2462. return this.system._prepareDerivedData();
  2463. }
  2464. const flags = this.flags.dnd5e || {};
  2465. this.labels = {};
  2466. // Retrieve data for polymorphed actors
  2467. let originalSaves = null;
  2468. let originalSkills = null;
  2469. if ( this.isPolymorphed ) {
  2470. const transformOptions = flags.transformOptions;
  2471. const original = game.actors?.get(flags.originalActor);
  2472. if ( original ) {
  2473. if ( transformOptions.mergeSaves ) originalSaves = original.system.abilities;
  2474. if ( transformOptions.mergeSkills ) originalSkills = original.system.skills;
  2475. }
  2476. }
  2477. // Prepare abilities, skills, & everything else
  2478. const globalBonuses = this.system.bonuses?.abilities ?? {};
  2479. const rollData = this.getRollData();
  2480. const checkBonus = simplifyBonus(globalBonuses?.check, rollData);
  2481. this._prepareAbilities(rollData, globalBonuses, checkBonus, originalSaves);
  2482. this._prepareSkills(rollData, globalBonuses, checkBonus, originalSkills);
  2483. this._prepareTools(rollData, globalBonuses, checkBonus);
  2484. this._prepareArmorClass();
  2485. this._prepareEncumbrance();
  2486. this._prepareHitPoints(rollData);
  2487. this._prepareInitiative(rollData, checkBonus);
  2488. this._prepareSpellcasting();
  2489. }
  2490. /* -------------------------------------------- */
  2491. /**
  2492. * Return the amount of experience required to gain a certain character level.
  2493. * @param {number} level The desired level.
  2494. * @returns {number} The XP required.
  2495. */
  2496. getLevelExp(level) {
  2497. const levels = CONFIG.DND5E.CHARACTER_EXP_LEVELS;
  2498. return levels[Math.min(level, levels.length - 1)];
  2499. }
  2500. /* -------------------------------------------- */
  2501. /**
  2502. * Return the amount of experience granted by killing a creature of a certain CR.
  2503. * @param {number} cr The creature's challenge rating.
  2504. * @returns {number} The amount of experience granted per kill.
  2505. */
  2506. getCRExp(cr) {
  2507. if ( cr < 1.0 ) return Math.max(200 * cr, 10);
  2508. return CONFIG.DND5E.CR_EXP_LEVELS[cr];
  2509. }
  2510. /* -------------------------------------------- */
  2511. /**
  2512. * @inheritdoc
  2513. * @param {object} [options]
  2514. * @param {boolean} [options.deterministic] Whether to force deterministic values for data properties that could be
  2515. * either a die term or a flat term.
  2516. */
  2517. getRollData({ deterministic=false }={}) {
  2518. const data = {...super.getRollData()};
  2519. if ( this.type === "group" ) return data;
  2520. data.prof = new Proficiency(this.system.attributes.prof, 1);
  2521. if ( deterministic ) data.prof = data.prof.flat;
  2522. data.attributes = foundry.utils.deepClone(data.attributes);
  2523. data.attributes.spellmod = data.abilities[data.attributes.spellcasting || "int"]?.mod ?? 0;
  2524. data.classes = {};
  2525. for ( const [identifier, cls] of Object.entries(this.classes) ) {
  2526. data.classes[identifier] = {...cls.system};
  2527. if ( cls.subclass ) data.classes[identifier].subclass = cls.subclass.system;
  2528. }
  2529. return data;
  2530. }
  2531. /* -------------------------------------------- */
  2532. /* Base Data Preparation Helpers */
  2533. /* -------------------------------------------- */
  2534. /**
  2535. * Initialize derived AC fields for Active Effects to target.
  2536. * Mutates the system.attributes.ac object.
  2537. * @protected
  2538. */
  2539. _prepareBaseArmorClass() {
  2540. const ac = this.system.attributes.ac;
  2541. ac.armor = 10;
  2542. ac.shield = ac.bonus = ac.cover = 0;
  2543. }
  2544. /* -------------------------------------------- */
  2545. /**
  2546. * Derive any values that have been scaled by the Advancement system.
  2547. * Mutates the value of the `system.scale` object.
  2548. * @protected
  2549. */
  2550. _prepareScaleValues() {
  2551. this.system.scale = Object.entries(this.classes).reduce((scale, [identifier, cls]) => {
  2552. scale[identifier] = cls.scaleValues;
  2553. if ( cls.subclass ) scale[cls.subclass.identifier] = cls.subclass.scaleValues;
  2554. return scale;
  2555. }, {});
  2556. }
  2557. /* -------------------------------------------- */
  2558. /**
  2559. * Perform any Character specific preparation.
  2560. * Mutates several aspects of the system data object.
  2561. * @protected
  2562. */
  2563. _prepareCharacterData() {
  2564. this.system.details.level = 0;
  2565. this.system.attributes.hd = 0;
  2566. this.system.attributes.attunement.value = 0;
  2567. for ( const item of this.items ) {
  2568. // Class levels & hit dice
  2569. if ( item.type === "class" ) {
  2570. const classLevels = parseInt(item.system.levels) || 1;
  2571. this.system.details.level += classLevels;
  2572. this.system.attributes.hd += classLevels - (parseInt(item.system.hitDiceUsed) || 0);
  2573. }
  2574. // Attuned items
  2575. else if ( item.system.attunement === CONFIG.DND5E.attunementTypes.ATTUNED ) {
  2576. this.system.attributes.attunement.value += 1;
  2577. }
  2578. }
  2579. // Character proficiency bonus
  2580. this.system.attributes.prof = Proficiency.calculateMod(this.system.details.level);
  2581. // Experience required for next level
  2582. const xp = this.system.details.xp;
  2583. xp.max = this.getLevelExp(this.system.details.level || 1);
  2584. const prior = this.getLevelExp(this.system.details.level - 1 || 0);
  2585. const required = xp.max - prior;
  2586. const pct = Math.round((xp.value - prior) * 100 / required);
  2587. xp.pct = Math.clamped(pct, 0, 100);
  2588. }
  2589. /* -------------------------------------------- */
  2590. /**
  2591. * Perform any NPC specific preparation.
  2592. * Mutates several aspects of the system data object.
  2593. * @protected
  2594. */
  2595. _prepareNPCData() {
  2596. const cr = this.system.details.cr;
  2597. // Attuned items
  2598. this.system.attributes.attunement.value = this.items.filter(i => {
  2599. return i.system.attunement === CONFIG.DND5E.attunementTypes.ATTUNED;
  2600. }).length;
  2601. // Kill Experience
  2602. this.system.details.xp ??= {};
  2603. this.system.details.xp.value = this.getCRExp(cr);
  2604. // Proficiency
  2605. this.system.attributes.prof = Proficiency.calculateMod(Math.max(cr, 1));
  2606. // Spellcaster Level
  2607. if ( this.system.attributes.spellcasting && !Number.isNumeric(this.system.details.spellLevel) ) {
  2608. this.system.details.spellLevel = Math.max(cr, 1);
  2609. }
  2610. }
  2611. /* -------------------------------------------- */
  2612. /**
  2613. * Perform any Vehicle specific preparation.
  2614. * Mutates several aspects of the system data object.
  2615. * @protected
  2616. */
  2617. _prepareVehicleData() {
  2618. this.system.attributes.prof = 0;
  2619. }
  2620. /* -------------------------------------------- */
  2621. /* Derived Data Preparation Helpers */
  2622. /* -------------------------------------------- */
  2623. /**
  2624. * Prepare abilities.
  2625. * @param {object} bonusData Data produced by `getRollData` to be applied to bonus formulas.
  2626. * @param {object} globalBonuses Global bonus data.
  2627. * @param {number} checkBonus Global ability check bonus.
  2628. * @param {object} originalSaves A transformed actor's original actor's abilities.
  2629. * @protected
  2630. */
  2631. _prepareAbilities(bonusData, globalBonuses, checkBonus, originalSaves) {
  2632. const flags = this.flags.dnd5e ?? {};
  2633. const dcBonus = simplifyBonus(this.system.bonuses?.spell?.dc, bonusData);
  2634. const saveBonus = simplifyBonus(globalBonuses.save, bonusData);
  2635. for ( const [id, abl] of Object.entries(this.system.abilities) ) {
  2636. if ( flags.diamondSoul ) abl.proficient = 1; // Diamond Soul is proficient in all saves
  2637. abl.mod = Math.floor((abl.value - 10) / 2);
  2638. const isRA = this._isRemarkableAthlete(id);
  2639. abl.checkProf = new Proficiency(this.system.attributes.prof, (isRA || flags.jackOfAllTrades) ? 0.5 : 0, !isRA);
  2640. const saveBonusAbl = simplifyBonus(abl.bonuses?.save, bonusData);
  2641. abl.saveBonus = saveBonusAbl + saveBonus;
  2642. abl.saveProf = new Proficiency(this.system.attributes.prof, abl.proficient);
  2643. const checkBonusAbl = simplifyBonus(abl.bonuses?.check, bonusData);
  2644. abl.checkBonus = checkBonusAbl + checkBonus;
  2645. abl.save = abl.mod + abl.saveBonus;
  2646. if ( Number.isNumeric(abl.saveProf.term) ) abl.save += abl.saveProf.flat;
  2647. abl.dc = 8 + abl.mod + this.system.attributes.prof + dcBonus;
  2648. // If we merged saves when transforming, take the highest bonus here.
  2649. if ( originalSaves && abl.proficient ) abl.save = Math.max(abl.save, originalSaves[id].save);
  2650. }
  2651. }
  2652. /* -------------------------------------------- */
  2653. /**
  2654. * Prepare skill checks. Mutates the values of system.skills.
  2655. * @param {object} bonusData Data produced by `getRollData` to be applied to bonus formulas.
  2656. * @param {object} globalBonuses Global bonus data.
  2657. * @param {number} checkBonus Global ability check bonus.
  2658. * @param {object} originalSkills A transformed actor's original actor's skills.
  2659. * @protected
  2660. */
  2661. _prepareSkills(bonusData, globalBonuses, checkBonus, originalSkills) {
  2662. if ( this.type === "vehicle" ) return;
  2663. const flags = this.flags.dnd5e ?? {};
  2664. // Skill modifiers
  2665. const feats = CONFIG.DND5E.characterFlags;
  2666. const skillBonus = simplifyBonus(globalBonuses.skill, bonusData);
  2667. for ( const [id, skl] of Object.entries(this.system.skills) ) {
  2668. const ability = this.system.abilities[skl.ability];
  2669. const baseBonus = simplifyBonus(skl.bonuses?.check, bonusData);
  2670. let roundDown = true;
  2671. // Remarkable Athlete
  2672. if ( this._isRemarkableAthlete(skl.ability) && (skl.value < 0.5) ) {
  2673. skl.value = 0.5;
  2674. roundDown = false;
  2675. }
  2676. // Jack of All Trades
  2677. else if ( flags.jackOfAllTrades && (skl.value < 0.5) ) {
  2678. skl.value = 0.5;
  2679. }
  2680. // Polymorph Skill Proficiencies
  2681. if ( originalSkills ) {
  2682. skl.value = Math.max(skl.value, originalSkills[id].value);
  2683. }
  2684. // Compute modifier
  2685. const checkBonusAbl = simplifyBonus(ability?.bonuses?.check, bonusData);
  2686. skl.bonus = baseBonus + checkBonus + checkBonusAbl + skillBonus;
  2687. skl.mod = ability?.mod ?? 0;
  2688. skl.prof = new Proficiency(this.system.attributes.prof, skl.value, roundDown);
  2689. skl.proficient = skl.value;
  2690. skl.total = skl.mod + skl.bonus;
  2691. if ( Number.isNumeric(skl.prof.term) ) skl.total += skl.prof.flat;
  2692. // Compute passive bonus
  2693. const passive = flags.observantFeat && (feats.observantFeat.skills.includes(id)) ? 5 : 0;
  2694. const passiveBonus = simplifyBonus(skl.bonuses?.passive, bonusData);
  2695. skl.passive = 10 + skl.mod + skl.bonus + skl.prof.flat + passive + passiveBonus;
  2696. }
  2697. }
  2698. /* -------------------------------------------- */
  2699. /**
  2700. * Prepare tool checks. Mutates the values of system.tools.
  2701. * @param {object} bonusData Data produced by `getRollData` to be applied to bonus formulae.
  2702. * @param {object} globalBonuses Global bonus data.
  2703. * @param {number} checkBonus Global ability check bonus.
  2704. * @protected
  2705. */
  2706. _prepareTools(bonusData, globalBonuses, checkBonus) {
  2707. if ( this.type === "vehicle" ) return;
  2708. const flags = this.flags.dnd5e ?? {};
  2709. for ( const tool of Object.values(this.system.tools) ) {
  2710. const ability = this.system.abilities[tool.ability];
  2711. const baseBonus = simplifyBonus(tool.bonuses.check, bonusData);
  2712. let roundDown = true;
  2713. // Remarkable Athlete.
  2714. if ( this._isRemarkableAthlete(tool.ability) && (tool.value < 0.5) ) {
  2715. tool.value = 0.5;
  2716. roundDown = false;
  2717. }
  2718. // Jack of All Trades.
  2719. else if ( flags.jackOfAllTrades && (tool.value < 0.5) ) tool.value = 0.5;
  2720. const checkBonusAbl = simplifyBonus(ability?.bonuses?.check, bonusData);
  2721. tool.bonus = baseBonus + checkBonus + checkBonusAbl;
  2722. tool.mod = ability?.mod ?? 0;
  2723. tool.prof = new Proficiency(this.system.attributes.prof, tool.value, roundDown);
  2724. tool.total = tool.mod + tool.bonus;
  2725. if ( Number.isNumeric(tool.prof.term) ) tool.total += tool.prof.flat;
  2726. }
  2727. }
  2728. /* -------------------------------------------- */
  2729. /**
  2730. * Prepare a character's AC value from their equipped armor and shield.
  2731. * Mutates the value of the `system.attributes.ac` object.
  2732. */
  2733. _prepareArmorClass() {
  2734. const ac = this.system.attributes.ac;
  2735. // Apply automatic migrations for older data structures
  2736. let cfg = CONFIG.DND5E.armorClasses[ac.calc];
  2737. if ( !cfg ) {
  2738. ac.calc = "flat";
  2739. if ( Number.isNumeric(ac.value) ) ac.flat = Number(ac.value);
  2740. cfg = CONFIG.DND5E.armorClasses.flat;
  2741. }
  2742. // Identify Equipped Items
  2743. const armorTypes = new Set(Object.keys(CONFIG.DND5E.armorTypes));
  2744. const {armors, shields} = this.itemTypes.equipment.reduce((obj, equip) => {
  2745. const armor = equip.system.armor;
  2746. if ( !equip.system.equipped || !armorTypes.has(armor?.type) ) return obj;
  2747. if ( armor.type === "shield" ) obj.shields.push(equip);
  2748. else obj.armors.push(equip);
  2749. return obj;
  2750. }, {armors: [], shields: []});
  2751. // Determine base AC
  2752. switch ( ac.calc ) {
  2753. // Flat AC (no additional bonuses)
  2754. case "flat":
  2755. ac.value = Number(ac.flat);
  2756. return;
  2757. // Natural AC (includes bonuses)
  2758. case "natural":
  2759. ac.base = Number(ac.flat);
  2760. break;
  2761. default:
  2762. let formula = ac.calc === "custom" ? ac.formula : cfg.formula;
  2763. if ( armors.length ) {
  2764. if ( armors.length > 1 ) this._preparationWarnings.push({
  2765. message: game.i18n.localize("DND5E.WarnMultipleArmor"), type: "warning"
  2766. });
  2767. const armorData = armors[0].system.armor;
  2768. const isHeavy = armorData.type === "heavy";
  2769. ac.armor = armorData.value ?? ac.armor;
  2770. ac.dex = isHeavy ? 0 : Math.min(armorData.dex ?? Infinity, this.system.abilities.dex?.mod ?? 0);
  2771. ac.equippedArmor = armors[0];
  2772. }
  2773. else ac.dex = this.system.abilities.dex?.mod ?? 0;
  2774. const rollData = this.getRollData({ deterministic: true });
  2775. rollData.attributes.ac = ac;
  2776. try {
  2777. const replaced = Roll.replaceFormulaData(formula, rollData);
  2778. ac.base = Roll.safeEval(replaced);
  2779. } catch(err) {
  2780. this._preparationWarnings.push({
  2781. message: game.i18n.localize("DND5E.WarnBadACFormula"), link: "armor", type: "error"
  2782. });
  2783. const replaced = Roll.replaceFormulaData(CONFIG.DND5E.armorClasses.default.formula, rollData);
  2784. ac.base = Roll.safeEval(replaced);
  2785. }
  2786. break;
  2787. }
  2788. // Equipped Shield
  2789. if ( shields.length ) {
  2790. if ( shields.length > 1 ) this._preparationWarnings.push({
  2791. message: game.i18n.localize("DND5E.WarnMultipleShields"), type: "warning"
  2792. });
  2793. ac.shield = shields[0].system.armor.value ?? 0;
  2794. ac.equippedShield = shields[0];
  2795. }
  2796. // Compute total AC and return
  2797. ac.value = ac.base + ac.shield + ac.bonus + ac.cover;
  2798. }
  2799. /* -------------------------------------------- */
  2800. /**
  2801. * Prepare the level and percentage of encumbrance for an Actor.
  2802. * Optionally include the weight of carried currency by applying the standard rule from the PHB pg. 143.
  2803. * Mutates the value of the `system.attributes.encumbrance` object.
  2804. * @protected
  2805. */
  2806. _prepareEncumbrance() {
  2807. const encumbrance = this.system.attributes.encumbrance ??= {};
  2808. // Get the total weight from items
  2809. const physicalItems = ["weapon", "equipment", "consumable", "tool", "backpack", "loot"];
  2810. let weight = this.items.reduce((weight, i) => {
  2811. if ( !physicalItems.includes(i.type) ) return weight;
  2812. const q = i.system.quantity || 0;
  2813. const w = i.system.weight || 0;
  2814. return weight + (q * w);
  2815. }, 0);
  2816. // [Optional] add Currency Weight (for non-transformed actors)
  2817. const currency = this.system.currency;
  2818. if ( game.settings.get("dnd5e", "currencyWeight") && currency ) {
  2819. const numCoins = Object.values(currency).reduce((val, denom) => val + Math.max(denom, 0), 0);
  2820. const currencyPerWeight = game.settings.get("dnd5e", "metricWeightUnits")
  2821. ? CONFIG.DND5E.encumbrance.currencyPerWeight.metric
  2822. : CONFIG.DND5E.encumbrance.currencyPerWeight.imperial;
  2823. weight += numCoins / currencyPerWeight;
  2824. }
  2825. // Determine the Encumbrance size class
  2826. let mod = {tiny: 0.5, sm: 1, med: 1, lg: 2, huge: 4, grg: 8}[this.system.traits.size] || 1;
  2827. if ( this.flags.dnd5e?.powerfulBuild ) mod = Math.min(mod * 2, 8);
  2828. const strengthMultiplier = game.settings.get("dnd5e", "metricWeightUnits")
  2829. ? CONFIG.DND5E.encumbrance.strMultiplier.metric
  2830. : CONFIG.DND5E.encumbrance.strMultiplier.imperial;
  2831. // Populate final Encumbrance values
  2832. encumbrance.value = weight.toNearest(0.1);
  2833. encumbrance.max = ((this.system.abilities.str?.value ?? 10) * strengthMultiplier * mod).toNearest(0.1);
  2834. encumbrance.pct = Math.clamped((encumbrance.value * 100) / encumbrance.max, 0, 100);
  2835. encumbrance.encumbered = encumbrance.pct > (200 / 3);
  2836. }
  2837. /* -------------------------------------------- */
  2838. /**
  2839. * Prepare hit points for characters.
  2840. * @param {object} rollData Data produced by `getRollData` to be applied to bonus formulas.
  2841. * @protected
  2842. */
  2843. _prepareHitPoints(rollData) {
  2844. if ( this.type !== "character" || (this.system._source.attributes.hp.max !== null) ) return;
  2845. const hp = this.system.attributes.hp;
  2846. const abilityId = CONFIG.DND5E.hitPointsAbility || "con";
  2847. const abilityMod = (this.system.abilities[abilityId]?.mod ?? 0);
  2848. const base = Object.values(this.classes).reduce((total, item) => {
  2849. const advancement = item.advancement.byType.HitPoints?.[0];
  2850. return total + (advancement?.getAdjustedTotal(abilityMod) ?? 0);
  2851. }, 0);
  2852. const levelBonus = simplifyBonus(hp.bonuses.level, rollData) * this.system.details.level;
  2853. const overallBonus = simplifyBonus(hp.bonuses.overall, rollData);
  2854. hp.max = base + levelBonus + overallBonus;
  2855. }
  2856. /* -------------------------------------------- */
  2857. /**
  2858. * Prepare the initiative data for an actor.
  2859. * Mutates the value of the system.attributes.init object.
  2860. * @param {object} bonusData Data produced by getRollData to be applied to bonus formulas
  2861. * @param {number} globalCheckBonus Global ability check bonus
  2862. * @protected
  2863. */
  2864. _prepareInitiative(bonusData, globalCheckBonus=0) {
  2865. const init = this.system.attributes.init ??= {};
  2866. const flags = this.flags.dnd5e || {};
  2867. // Compute initiative modifier
  2868. const abilityId = init.ability || CONFIG.DND5E.initiativeAbility;
  2869. const ability = this.system.abilities?.[abilityId] || {};
  2870. init.mod = ability.mod ?? 0;
  2871. // Initiative proficiency
  2872. const prof = this.system.attributes.prof ?? 0;
  2873. const ra = flags.remarkableAthlete && ["str", "dex", "con"].includes(abilityId);
  2874. init.prof = new Proficiency(prof, (flags.jackOfAllTrades || ra) ? 0.5 : 0, !ra);
  2875. // Total initiative includes all numeric terms
  2876. const initBonus = simplifyBonus(init.bonus, bonusData);
  2877. const abilityBonus = simplifyBonus(ability.bonuses?.check, bonusData);
  2878. init.total = init.mod + initBonus + abilityBonus + globalCheckBonus
  2879. + (flags.initiativeAlert ? 5 : 0)
  2880. + (Number.isNumeric(init.prof.term) ? init.prof.flat : 0);
  2881. }
  2882. /* -------------------------------------------- */
  2883. /* Spellcasting Preparation */
  2884. /* -------------------------------------------- */
  2885. /**
  2886. * Prepare data related to the spell-casting capabilities of the Actor.
  2887. * Mutates the value of the system.spells object.
  2888. * @protected
  2889. */
  2890. _prepareSpellcasting() {
  2891. if ( !this.system.spells ) return;
  2892. // Spellcasting DC
  2893. const spellcastingAbility = this.system.abilities[this.system.attributes.spellcasting];
  2894. this.system.attributes.spelldc = spellcastingAbility ? spellcastingAbility.dc : 8 + this.system.attributes.prof;
  2895. // Translate the list of classes into spellcasting progression
  2896. const progression = { slot: 0, pact: 0 };
  2897. const types = {};
  2898. // NPCs don't get spell levels from classes
  2899. if ( this.type === "npc" ) {
  2900. progression.slot = this.system.details.spellLevel ?? 0;
  2901. types.leveled = 1;
  2902. }
  2903. else {
  2904. // Grab all classes with spellcasting
  2905. const classes = this.items.filter(cls => {
  2906. if ( cls.type !== "class" ) return false;
  2907. const type = cls.spellcasting.type;
  2908. if ( !type ) return false;
  2909. types[type] ??= 0;
  2910. types[type] += 1;
  2911. return true;
  2912. });
  2913. for ( const cls of classes ) this.constructor.computeClassProgression(
  2914. progression, cls, { actor: this, count: types[cls.spellcasting.type] }
  2915. );
  2916. }
  2917. for ( const type of Object.keys(CONFIG.DND5E.spellcastingTypes) ) {
  2918. this.constructor.prepareSpellcastingSlots(this.system.spells, type, progression, { actor: this });
  2919. }
  2920. }
  2921. /* -------------------------------------------- */
  2922. /**
  2923. * Contribute to the actor's spellcasting progression.
  2924. * @param {object} progression Spellcasting progression data. *Will be mutated.*
  2925. * @param {Item5e} cls Class for whom this progression is being computed.
  2926. * @param {object} [config={}]
  2927. * @param {Actor5e|null} [config.actor] Actor for whom the data is being prepared.
  2928. * @param {SpellcastingDescription} [config.spellcasting] Spellcasting descriptive object.
  2929. * @param {number} [config.count=1] Number of classes with this type of spellcasting.
  2930. */
  2931. static computeClassProgression(progression, cls, {actor, spellcasting, count=1}={}) {
  2932. const type = cls.spellcasting.type;
  2933. spellcasting = spellcasting ?? cls.spellcasting;
  2934. /**
  2935. * A hook event that fires while computing the spellcasting progression for each class on each actor.
  2936. * The actual hook names include the spellcasting type (e.g. `dnd5e.computeLeveledProgression`).
  2937. * @param {object} progression Spellcasting progression data. *Will be mutated.*
  2938. * @param {Actor5e|null} [actor] Actor for whom the data is being prepared.
  2939. * @param {Item5e} cls Class for whom this progression is being computed.
  2940. * @param {SpellcastingDescription} spellcasting Spellcasting descriptive object.
  2941. * @param {number} count Number of classes with this type of spellcasting.
  2942. * @returns {boolean} Explicitly return false to prevent default progression from being calculated.
  2943. * @function dnd5e.computeSpellcastingProgression
  2944. * @memberof hookEvents
  2945. */
  2946. const allowed = Hooks.call(
  2947. `dnd5e.compute${type.capitalize()}Progression`, progression, actor, cls, spellcasting, count
  2948. );
  2949. if ( allowed && (type === "pact") ) {
  2950. this.computePactProgression(progression, actor, cls, spellcasting, count);
  2951. } else if ( allowed && (type === "leveled") ) {
  2952. this.computeLeveledProgression(progression, actor, cls, spellcasting, count);
  2953. }
  2954. }
  2955. /* -------------------------------------------- */
  2956. /**
  2957. * Contribute to the actor's spellcasting progression for a class with leveled spellcasting.
  2958. * @param {object} progression Spellcasting progression data. *Will be mutated.*
  2959. * @param {Actor5e} actor Actor for whom the data is being prepared.
  2960. * @param {Item5e} cls Class for whom this progression is being computed.
  2961. * @param {SpellcastingDescription} spellcasting Spellcasting descriptive object.
  2962. * @param {number} count Number of classes with this type of spellcasting.
  2963. */
  2964. static computeLeveledProgression(progression, actor, cls, spellcasting, count) {
  2965. const prog = CONFIG.DND5E.spellcastingTypes.leveled.progression[spellcasting.progression];
  2966. if ( !prog ) return;
  2967. const rounding = prog.roundUp ? Math.ceil : Math.floor;
  2968. progression.slot += rounding(spellcasting.levels / prog.divisor ?? 1);
  2969. // Single-classed, non-full progression rounds up, rather than down.
  2970. if ( (count === 1) && (prog.divisor > 1) && progression.slot ) {
  2971. progression.slot = Math.ceil(spellcasting.levels / prog.divisor);
  2972. }
  2973. }
  2974. /* -------------------------------------------- */
  2975. /**
  2976. * Contribute to the actor's spellcasting progression for a class with pact spellcasting.
  2977. * @param {object} progression Spellcasting progression data. *Will be mutated.*
  2978. * @param {Actor5e} actor Actor for whom the data is being prepared.
  2979. * @param {Item5e} cls Class for whom this progression is being computed.
  2980. * @param {SpellcastingDescription} spellcasting Spellcasting descriptive object.
  2981. * @param {number} count Number of classes with this type of spellcasting.
  2982. */
  2983. static computePactProgression(progression, actor, cls, spellcasting, count) {
  2984. progression.pact += spellcasting.levels;
  2985. }
  2986. /* -------------------------------------------- */
  2987. /**
  2988. * Prepare actor's spell slots using progression data.
  2989. * @param {object} spells The `data.spells` object within actor's data. *Will be mutated.*
  2990. * @param {string} type Type of spellcasting slots being prepared.
  2991. * @param {object} progression Spellcasting progression data.
  2992. * @param {object} [config]
  2993. * @param {Actor5e} [config.actor] Actor for whom the data is being prepared.
  2994. */
  2995. static prepareSpellcastingSlots(spells, type, progression, {actor}={}) {
  2996. /**
  2997. * A hook event that fires to convert the provided spellcasting progression into spell slots.
  2998. * The actual hook names include the spellcasting type (e.g. `dnd5e.prepareLeveledSlots`).
  2999. * @param {object} spells The `data.spells` object within actor's data. *Will be mutated.*
  3000. * @param {Actor5e} actor Actor for whom the data is being prepared.
  3001. * @param {object} progression Spellcasting progression data.
  3002. * @returns {boolean} Explicitly return false to prevent default preparation from being performed.
  3003. * @function dnd5e.prepareSpellcastingSlots
  3004. * @memberof hookEvents
  3005. */
  3006. const allowed = Hooks.call(`dnd5e.prepare${type.capitalize()}Slots`, spells, actor, progression);
  3007. if ( allowed && (type === "pact") ) this.preparePactSlots(spells, actor, progression);
  3008. else if ( allowed && (type === "leveled") ) this.prepareLeveledSlots(spells, actor, progression);
  3009. }
  3010. /* -------------------------------------------- */
  3011. /**
  3012. * Prepare leveled spell slots using progression data.
  3013. * @param {object} spells The `data.spells` object within actor's data. *Will be mutated.*
  3014. * @param {Actor5e} actor Actor for whom the data is being prepared.
  3015. * @param {object} progression Spellcasting progression data.
  3016. */
  3017. static prepareLeveledSlots(spells, actor, progression) {
  3018. const levels = Math.clamped(progression.slot, 0, CONFIG.DND5E.maxLevel);
  3019. const slots = CONFIG.DND5E.SPELL_SLOT_TABLE[Math.min(levels, CONFIG.DND5E.SPELL_SLOT_TABLE.length) - 1] ?? [];
  3020. for ( const [n, slot] of Object.entries(spells) ) {
  3021. const level = parseInt(n.slice(-1));
  3022. if ( Number.isNaN(level) ) continue;
  3023. slot.max = Number.isNumeric(slot.override) ? Math.max(parseInt(slot.override), 0) : slots[level - 1] ?? 0;
  3024. slot.value = parseInt(slot.value); // TODO: DataModels should remove the need for this
  3025. }
  3026. }
  3027. /* -------------------------------------------- */
  3028. /**
  3029. * Prepare pact spell slots using progression data.
  3030. * @param {object} spells The `data.spells` object within actor's data. *Will be mutated.*
  3031. * @param {Actor5e} actor Actor for whom the data is being prepared.
  3032. * @param {object} progression Spellcasting progression data.
  3033. */
  3034. static preparePactSlots(spells, actor, progression) {
  3035. // Pact spell data:
  3036. // - pact.level: Slot level for pact casting
  3037. // - pact.max: Total number of pact slots
  3038. // - pact.value: Currently available pact slots
  3039. // - pact.override: Override number of available spell slots
  3040. let pactLevel = Math.clamped(progression.pact, 0, CONFIG.DND5E.maxLevel);
  3041. spells.pact ??= {};
  3042. const override = Number.isNumeric(spells.pact.override) ? parseInt(spells.pact.override) : null;
  3043. // Pact slot override
  3044. if ( (pactLevel === 0) && (actor.type === "npc") && (override !== null) ) {
  3045. pactLevel = actor.system.details.spellLevel;
  3046. }
  3047. // TODO: Allow pact level and slot count to be configured
  3048. if ( pactLevel > 0 ) {
  3049. spells.pact.level = Math.ceil(Math.min(10, pactLevel) / 2); // TODO: Allow custom max pact level
  3050. if ( override === null ) {
  3051. spells.pact.max = Math.max(1, Math.min(pactLevel, 2), Math.min(pactLevel - 8, 3), Math.min(pactLevel - 13, 4));
  3052. } else {
  3053. spells.pact.max = Math.max(override, 1);
  3054. }
  3055. spells.pact.value = Math.min(spells.pact.value, spells.pact.max);
  3056. }
  3057. else {
  3058. spells.pact.max = override || 0;
  3059. spells.pact.level = spells.pact.max > 0 ? 1 : 0;
  3060. }
  3061. }
  3062. /* -------------------------------------------- */
  3063. /* Event Handlers */
  3064. /* -------------------------------------------- */
  3065. /** @inheritdoc */
  3066. async _preCreate(data, options, user) {
  3067. await super._preCreate(data, options, user);
  3068. const sourceId = this.getFlag("core", "sourceId");
  3069. if ( sourceId?.startsWith("Compendium.") ) return;
  3070. // Configure prototype token settings
  3071. if ( "size" in (this.system.traits || {}) ) {
  3072. const s = CONFIG.DND5E.tokenSizes[this.system.traits.size || "med"];
  3073. const prototypeToken = {width: s, height: s};
  3074. if ( this.type === "character" ) Object.assign(prototypeToken, {
  3075. sight: { enabled: true }, actorLink: true, disposition: 1
  3076. });
  3077. this.updateSource({prototypeToken});
  3078. }
  3079. }
  3080. /* -------------------------------------------- */
  3081. /** @inheritdoc */
  3082. async _preUpdate(changed, options, user) {
  3083. await super._preUpdate(changed, options, user);
  3084. // Apply changes in Actor size to Token width/height
  3085. if ( "size" in (this.system.traits || {}) ) {
  3086. const newSize = foundry.utils.getProperty(changed, "system.traits.size");
  3087. if ( newSize && (newSize !== this.system.traits?.size) ) {
  3088. let size = CONFIG.DND5E.tokenSizes[newSize];
  3089. if ( !foundry.utils.hasProperty(changed, "prototypeToken.width") ) {
  3090. changed.prototypeToken ||= {};
  3091. changed.prototypeToken.height = size;
  3092. changed.prototypeToken.width = size;
  3093. }
  3094. }
  3095. }
  3096. // Reset death save counters
  3097. if ( "hp" in (this.system.attributes || {}) ) {
  3098. const isDead = this.system.attributes.hp.value <= 0;
  3099. if ( isDead && (foundry.utils.getProperty(changed, "system.attributes.hp.value") > 0) ) {
  3100. foundry.utils.setProperty(changed, "system.attributes.death.success", 0);
  3101. foundry.utils.setProperty(changed, "system.attributes.death.failure", 0);
  3102. }
  3103. }
  3104. }
  3105. /* -------------------------------------------- */
  3106. /**
  3107. * Assign a class item as the original class for the Actor based on which class has the most levels.
  3108. * @returns {Promise<Actor5e>} Instance of the updated actor.
  3109. * @protected
  3110. */
  3111. _assignPrimaryClass() {
  3112. const classes = this.itemTypes.class.sort((a, b) => b.system.levels - a.system.levels);
  3113. const newPC = classes[0]?.id || "";
  3114. return this.update({"system.details.originalClass": newPC});
  3115. }
  3116. /* -------------------------------------------- */
  3117. /* Gameplay Mechanics */
  3118. /* -------------------------------------------- */
  3119. /** @override */
  3120. async modifyTokenAttribute(attribute, value, isDelta, isBar) {
  3121. if ( attribute === "attributes.hp" ) {
  3122. const hp = this.system.attributes.hp;
  3123. const delta = isDelta ? (-1 * value) : (hp.value + hp.temp) - value;
  3124. return this.applyDamage(delta);
  3125. }
  3126. return super.modifyTokenAttribute(attribute, value, isDelta, isBar);
  3127. }
  3128. /* -------------------------------------------- */
  3129. /**
  3130. * Apply a certain amount of damage or healing to the health pool for Actor
  3131. * @param {number} amount An amount of damage (positive) or healing (negative) to sustain
  3132. * @param {number} multiplier A multiplier which allows for resistance, vulnerability, or healing
  3133. * @returns {Promise<Actor5e>} A Promise which resolves once the damage has been applied
  3134. */
  3135. async applyDamage(amount=0, multiplier=1) {
  3136. amount = Math.floor(parseInt(amount) * multiplier);
  3137. const hp = this.system.attributes.hp;
  3138. if ( !hp ) return this; // Group actors don't have HP at the moment
  3139. // Deduct damage from temp HP first
  3140. const tmp = parseInt(hp.temp) || 0;
  3141. const dt = amount > 0 ? Math.min(tmp, amount) : 0;
  3142. // Remaining goes to health
  3143. const tmpMax = parseInt(hp.tempmax) || 0;
  3144. const dh = Math.clamped(hp.value - (amount - dt), 0, Math.max(0, hp.max + tmpMax));
  3145. // Update the Actor
  3146. const updates = {
  3147. "system.attributes.hp.temp": tmp - dt,
  3148. "system.attributes.hp.value": dh
  3149. };
  3150. // Delegate damage application to a hook
  3151. // TODO replace this in the future with a better modifyTokenAttribute function in the core
  3152. const allowed = Hooks.call("modifyTokenAttribute", {
  3153. attribute: "attributes.hp",
  3154. value: amount,
  3155. isDelta: false,
  3156. isBar: true
  3157. }, updates);
  3158. return allowed !== false ? this.update(updates, {dhp: -amount}) : this;
  3159. }
  3160. /* -------------------------------------------- */
  3161. /**
  3162. * Apply a certain amount of temporary hit point, but only if it's more than the actor currently has.
  3163. * @param {number} amount An amount of temporary hit points to set
  3164. * @returns {Promise<Actor5e>} A Promise which resolves once the temp HP has been applied
  3165. */
  3166. async applyTempHP(amount=0) {
  3167. amount = parseInt(amount);
  3168. const hp = this.system.attributes.hp;
  3169. // Update the actor if the new amount is greater than the current
  3170. const tmp = parseInt(hp.temp) || 0;
  3171. return amount > tmp ? this.update({"system.attributes.hp.temp": amount}) : this;
  3172. }
  3173. /* -------------------------------------------- */
  3174. /**
  3175. * Get a color used to represent the current hit points of an Actor.
  3176. * @param {number} current The current HP value
  3177. * @param {number} max The maximum HP value
  3178. * @returns {Color} The color used to represent the HP percentage
  3179. */
  3180. static getHPColor(current, max) {
  3181. const pct = Math.clamped(current, 0, max) / max;
  3182. return Color.fromRGB([(1-(pct/2)), pct, 0]);
  3183. }
  3184. /* -------------------------------------------- */
  3185. /**
  3186. * Determine whether the provided ability is usable for remarkable athlete.
  3187. * @param {string} ability Ability type to check.
  3188. * @returns {boolean} Whether the actor has the remarkable athlete flag and the ability is physical.
  3189. * @private
  3190. */
  3191. _isRemarkableAthlete(ability) {
  3192. return this.getFlag("dnd5e", "remarkableAthlete")
  3193. && CONFIG.DND5E.characterFlags.remarkableAthlete.abilities.includes(ability);
  3194. }
  3195. /* -------------------------------------------- */
  3196. /* Rolling */
  3197. /* -------------------------------------------- */
  3198. /**
  3199. * Roll a Skill Check
  3200. * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus
  3201. * @param {string} skillId The skill id (e.g. "ins")
  3202. * @param {object} options Options which configure how the skill check is rolled
  3203. * @returns {Promise<D20Roll>} A Promise which resolves to the created Roll instance
  3204. */
  3205. async rollSkill(skillId, options={}) {
  3206. const skl = this.system.skills[skillId];
  3207. const abl = this.system.abilities[skl.ability];
  3208. const globalBonuses = this.system.bonuses?.abilities ?? {};
  3209. const parts = ["@mod", "@abilityCheckBonus"];
  3210. const data = this.getRollData();
  3211. // Add ability modifier
  3212. data.mod = skl.mod;
  3213. data.defaultAbility = skl.ability;
  3214. // Include proficiency bonus
  3215. if ( skl.prof.hasProficiency ) {
  3216. parts.push("@prof");
  3217. data.prof = skl.prof.term;
  3218. }
  3219. // Global ability check bonus
  3220. if ( globalBonuses.check ) {
  3221. parts.push("@checkBonus");
  3222. data.checkBonus = Roll.replaceFormulaData(globalBonuses.check, data);
  3223. }
  3224. // Ability-specific check bonus
  3225. if ( abl?.bonuses?.check ) data.abilityCheckBonus = Roll.replaceFormulaData(abl.bonuses.check, data);
  3226. else data.abilityCheckBonus = 0;
  3227. // Skill-specific skill bonus
  3228. if ( skl.bonuses?.check ) {
  3229. const checkBonusKey = `${skillId}CheckBonus`;
  3230. parts.push(`@${checkBonusKey}`);
  3231. data[checkBonusKey] = Roll.replaceFormulaData(skl.bonuses.check, data);
  3232. }
  3233. // Global skill check bonus
  3234. if ( globalBonuses.skill ) {
  3235. parts.push("@skillBonus");
  3236. data.skillBonus = Roll.replaceFormulaData(globalBonuses.skill, data);
  3237. }
  3238. // Reliable Talent applies to any skill check we have full or better proficiency in
  3239. const reliableTalent = (skl.value >= 1 && this.getFlag("dnd5e", "reliableTalent"));
  3240. // Roll and return
  3241. const flavor = game.i18n.format("DND5E.SkillPromptTitle", {skill: CONFIG.DND5E.skills[skillId]?.label ?? ""});
  3242. const rollData = foundry.utils.mergeObject({
  3243. data: data,
  3244. title: `${flavor}: ${this.name}`,
  3245. flavor,
  3246. chooseModifier: true,
  3247. halflingLucky: this.getFlag("dnd5e", "halflingLucky"),
  3248. reliableTalent,
  3249. messageData: {
  3250. speaker: options.speaker || ChatMessage.getSpeaker({actor: this}),
  3251. "flags.dnd5e.roll": {type: "skill", skillId }
  3252. }
  3253. }, options);
  3254. rollData.parts = parts.concat(options.parts ?? []);
  3255. /**
  3256. * A hook event that fires before a skill check is rolled for an Actor.
  3257. * @function dnd5e.preRollSkill
  3258. * @memberof hookEvents
  3259. * @param {Actor5e} actor Actor for which the skill check is being rolled.
  3260. * @param {D20RollConfiguration} config Configuration data for the pending roll.
  3261. * @param {string} skillId ID of the skill being rolled as defined in `DND5E.skills`.
  3262. * @returns {boolean} Explicitly return `false` to prevent skill check from being rolled.
  3263. */
  3264. if ( Hooks.call("dnd5e.preRollSkill", this, rollData, skillId) === false ) return;
  3265. const roll = await d20Roll(rollData);
  3266. /**
  3267. * A hook event that fires after a skill check has been rolled for an Actor.
  3268. * @function dnd5e.rollSkill
  3269. * @memberof hookEvents
  3270. * @param {Actor5e} actor Actor for which the skill check has been rolled.
  3271. * @param {D20Roll} roll The resulting roll.
  3272. * @param {string} skillId ID of the skill that was rolled as defined in `DND5E.skills`.
  3273. */
  3274. if ( roll ) Hooks.callAll("dnd5e.rollSkill", this, roll, skillId);
  3275. return roll;
  3276. }
  3277. /* -------------------------------------------- */
  3278. /**
  3279. * Roll a Tool Check.
  3280. * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonuses.
  3281. * @param {string} toolId The identifier of the tool being rolled.
  3282. * @param {object} options Options which configure how the tool check is rolled.
  3283. * @returns {Promise<D20Roll>} A Promise which resolves to the created Roll instance.
  3284. */
  3285. async rollToolCheck(toolId, options={}) {
  3286. // Prepare roll data.
  3287. const tool = this.system.tools[toolId];
  3288. const ability = this.system.abilities[tool?.ability ?? "int"];
  3289. const globalBonuses = this.system.bonuses?.abilities ?? {};
  3290. const parts = ["@mod", "@abilityCheckBonus"];
  3291. const data = this.getRollData();
  3292. // Add ability modifier.
  3293. data.mod = tool?.mod ?? 0;
  3294. data.defaultAbility = options.ability || (tool?.ability ?? "int");
  3295. // Add proficiency.
  3296. if ( tool?.prof.hasProficiency ) {
  3297. parts.push("@prof");
  3298. data.prof = tool.prof.term;
  3299. }
  3300. // Global ability check bonus.
  3301. if ( globalBonuses.check ) {
  3302. parts.push("@checkBonus");
  3303. data.checkBonus = Roll.replaceFormulaData(globalBonuses.check, data);
  3304. }
  3305. // Ability-specific check bonus.
  3306. if ( ability?.bonuses.check ) data.abilityCheckBonus = Roll.replaceFormulaData(ability.bonuses.check, data);
  3307. else data.abilityCheckBonus = 0;
  3308. // Tool-specific check bonus.
  3309. if ( tool?.bonuses.check || options.bonus ) {
  3310. parts.push("@toolBonus");
  3311. const bonus = [];
  3312. if ( tool?.bonuses.check ) bonus.push(Roll.replaceFormulaData(tool.bonuses.check, data));
  3313. if ( options.bonus ) bonus.push(Roll.replaceFormulaData(options.bonus, data));
  3314. data.toolBonus = bonus.join(" + ");
  3315. }
  3316. const flavor = game.i18n.format("DND5E.ToolPromptTitle", {tool: keyLabel("tool", toolId) ?? ""});
  3317. const rollData = foundry.utils.mergeObject({
  3318. data, flavor,
  3319. title: `${flavor}: ${this.name}`,
  3320. chooseModifier: true,
  3321. halflingLucky: this.getFlag("dnd5e", "halflingLucky"),
  3322. messageData: {
  3323. speaker: options.speaker || ChatMessage.implementation.getSpeaker({actor: this}),
  3324. "flags.dnd5e.roll": {type: "tool", toolId}
  3325. }
  3326. }, options);
  3327. rollData.parts = parts.concat(options.parts ?? []);
  3328. /**
  3329. * A hook event that fires before a tool check is rolled for an Actor.
  3330. * @function dnd5e.preRollRool
  3331. * @memberof hookEvents
  3332. * @param {Actor5e} actor Actor for which the tool check is being rolled.
  3333. * @param {D20RollConfiguration} config Configuration data for the pending roll.
  3334. * @param {string} toolId Identifier of the tool being rolled.
  3335. * @returns {boolean} Explicitly return `false` to prevent skill check from being rolled.
  3336. */
  3337. if ( Hooks.call("dnd5e.preRollToolCheck", this, rollData, toolId) === false ) return;
  3338. const roll = await d20Roll(rollData);
  3339. /**
  3340. * A hook event that fires after a tool check has been rolled for an Actor.
  3341. * @function dnd5e.rollTool
  3342. * @memberof hookEvents
  3343. * @param {Actor5e} actor Actor for which the tool check has been rolled.
  3344. * @param {D20Roll} roll The resulting roll.
  3345. * @param {string} toolId Identifier of the tool that was rolled.
  3346. */
  3347. if ( roll ) Hooks.callAll("dnd5e.rollToolCheck", this, roll, toolId);
  3348. return roll;
  3349. }
  3350. /* -------------------------------------------- */
  3351. /**
  3352. * Roll a generic ability test or saving throw.
  3353. * Prompt the user for input on which variety of roll they want to do.
  3354. * @param {string} abilityId The ability id (e.g. "str")
  3355. * @param {object} options Options which configure how ability tests or saving throws are rolled
  3356. */
  3357. rollAbility(abilityId, options={}) {
  3358. const label = CONFIG.DND5E.abilities[abilityId]?.label ?? "";
  3359. new Dialog({
  3360. title: `${game.i18n.format("DND5E.AbilityPromptTitle", {ability: label})}: ${this.name}`,
  3361. content: `<p>${game.i18n.format("DND5E.AbilityPromptText", {ability: label})}</p>`,
  3362. buttons: {
  3363. test: {
  3364. label: game.i18n.localize("DND5E.ActionAbil"),
  3365. callback: () => this.rollAbilityTest(abilityId, options)
  3366. },
  3367. save: {
  3368. label: game.i18n.localize("DND5E.ActionSave"),
  3369. callback: () => this.rollAbilitySave(abilityId, options)
  3370. }
  3371. }
  3372. }).render(true);
  3373. }
  3374. /* -------------------------------------------- */
  3375. /**
  3376. * Roll an Ability Test
  3377. * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus
  3378. * @param {string} abilityId The ability ID (e.g. "str")
  3379. * @param {object} options Options which configure how ability tests are rolled
  3380. * @returns {Promise<D20Roll>} A Promise which resolves to the created Roll instance
  3381. */
  3382. async rollAbilityTest(abilityId, options={}) {
  3383. const label = CONFIG.DND5E.abilities[abilityId]?.label ?? "";
  3384. const abl = this.system.abilities[abilityId];
  3385. const globalBonuses = this.system.bonuses?.abilities ?? {};
  3386. const parts = [];
  3387. const data = this.getRollData();
  3388. // Add ability modifier
  3389. parts.push("@mod");
  3390. data.mod = abl?.mod ?? 0;
  3391. // Include proficiency bonus
  3392. if ( abl?.checkProf.hasProficiency ) {
  3393. parts.push("@prof");
  3394. data.prof = abl.checkProf.term;
  3395. }
  3396. // Add ability-specific check bonus
  3397. if ( abl?.bonuses?.check ) {
  3398. const checkBonusKey = `${abilityId}CheckBonus`;
  3399. parts.push(`@${checkBonusKey}`);
  3400. data[checkBonusKey] = Roll.replaceFormulaData(abl.bonuses.check, data);
  3401. }
  3402. // Add global actor bonus
  3403. if ( globalBonuses.check ) {
  3404. parts.push("@checkBonus");
  3405. data.checkBonus = Roll.replaceFormulaData(globalBonuses.check, data);
  3406. }
  3407. // Roll and return
  3408. const flavor = game.i18n.format("DND5E.AbilityPromptTitle", {ability: label});
  3409. const rollData = foundry.utils.mergeObject({
  3410. data,
  3411. title: `${flavor}: ${this.name}`,
  3412. flavor,
  3413. halflingLucky: this.getFlag("dnd5e", "halflingLucky"),
  3414. messageData: {
  3415. speaker: options.speaker || ChatMessage.getSpeaker({actor: this}),
  3416. "flags.dnd5e.roll": {type: "ability", abilityId }
  3417. }
  3418. }, options);
  3419. rollData.parts = parts.concat(options.parts ?? []);
  3420. /**
  3421. * A hook event that fires before an ability test is rolled for an Actor.
  3422. * @function dnd5e.preRollAbilityTest
  3423. * @memberof hookEvents
  3424. * @param {Actor5e} actor Actor for which the ability test is being rolled.
  3425. * @param {D20RollConfiguration} config Configuration data for the pending roll.
  3426. * @param {string} abilityId ID of the ability being rolled as defined in `DND5E.abilities`.
  3427. * @returns {boolean} Explicitly return `false` to prevent ability test from being rolled.
  3428. */
  3429. if ( Hooks.call("dnd5e.preRollAbilityTest", this, rollData, abilityId) === false ) return;
  3430. const roll = await d20Roll(rollData);
  3431. /**
  3432. * A hook event that fires after an ability test has been rolled for an Actor.
  3433. * @function dnd5e.rollAbilityTest
  3434. * @memberof hookEvents
  3435. * @param {Actor5e} actor Actor for which the ability test has been rolled.
  3436. * @param {D20Roll} roll The resulting roll.
  3437. * @param {string} abilityId ID of the ability that was rolled as defined in `DND5E.abilities`.
  3438. */
  3439. if ( roll ) Hooks.callAll("dnd5e.rollAbilityTest", this, roll, abilityId);
  3440. return roll;
  3441. }
  3442. /* -------------------------------------------- */
  3443. /**
  3444. * Roll an Ability Saving Throw
  3445. * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus
  3446. * @param {string} abilityId The ability ID (e.g. "str")
  3447. * @param {object} options Options which configure how ability tests are rolled
  3448. * @returns {Promise<D20Roll>} A Promise which resolves to the created Roll instance
  3449. */
  3450. async rollAbilitySave(abilityId, options={}) {
  3451. const label = CONFIG.DND5E.abilities[abilityId]?.label ?? "";
  3452. const abl = this.system.abilities[abilityId];
  3453. const globalBonuses = this.system.bonuses?.abilities ?? {};
  3454. const parts = [];
  3455. const data = this.getRollData();
  3456. // Add ability modifier
  3457. parts.push("@mod");
  3458. data.mod = abl?.mod ?? 0;
  3459. // Include proficiency bonus
  3460. if ( abl?.saveProf.hasProficiency ) {
  3461. parts.push("@prof");
  3462. data.prof = abl.saveProf.term;
  3463. }
  3464. // Include ability-specific saving throw bonus
  3465. if ( abl?.bonuses?.save ) {
  3466. const saveBonusKey = `${abilityId}SaveBonus`;
  3467. parts.push(`@${saveBonusKey}`);
  3468. data[saveBonusKey] = Roll.replaceFormulaData(abl.bonuses.save, data);
  3469. }
  3470. // Include a global actor ability save bonus
  3471. if ( globalBonuses.save ) {
  3472. parts.push("@saveBonus");
  3473. data.saveBonus = Roll.replaceFormulaData(globalBonuses.save, data);
  3474. }
  3475. // Roll and return
  3476. const flavor = game.i18n.format("DND5E.SavePromptTitle", {ability: label});
  3477. const rollData = foundry.utils.mergeObject({
  3478. data,
  3479. title: `${flavor}: ${this.name}`,
  3480. flavor,
  3481. halflingLucky: this.getFlag("dnd5e", "halflingLucky"),
  3482. messageData: {
  3483. speaker: options.speaker || ChatMessage.getSpeaker({actor: this}),
  3484. "flags.dnd5e.roll": {type: "save", abilityId }
  3485. }
  3486. }, options);
  3487. rollData.parts = parts.concat(options.parts ?? []);
  3488. /**
  3489. * A hook event that fires before an ability save is rolled for an Actor.
  3490. * @function dnd5e.preRollAbilitySave
  3491. * @memberof hookEvents
  3492. * @param {Actor5e} actor Actor for which the ability save is being rolled.
  3493. * @param {D20RollConfiguration} config Configuration data for the pending roll.
  3494. * @param {string} abilityId ID of the ability being rolled as defined in `DND5E.abilities`.
  3495. * @returns {boolean} Explicitly return `false` to prevent ability save from being rolled.
  3496. */
  3497. if ( Hooks.call("dnd5e.preRollAbilitySave", this, rollData, abilityId) === false ) return;
  3498. const roll = await d20Roll(rollData);
  3499. /**
  3500. * A hook event that fires after an ability save has been rolled for an Actor.
  3501. * @function dnd5e.rollAbilitySave
  3502. * @memberof hookEvents
  3503. * @param {Actor5e} actor Actor for which the ability save has been rolled.
  3504. * @param {D20Roll} roll The resulting roll.
  3505. * @param {string} abilityId ID of the ability that was rolled as defined in `DND5E.abilities`.
  3506. */
  3507. if ( roll ) Hooks.callAll("dnd5e.rollAbilitySave", this, roll, abilityId);
  3508. return roll;
  3509. }
  3510. /* -------------------------------------------- */
  3511. /**
  3512. * Perform a death saving throw, rolling a d20 plus any global save bonuses
  3513. * @param {object} options Additional options which modify the roll
  3514. * @returns {Promise<D20Roll|null>} A Promise which resolves to the Roll instance
  3515. */
  3516. async rollDeathSave(options={}) {
  3517. const death = this.system.attributes.death;
  3518. // Display a warning if we are not at zero HP or if we already have reached 3
  3519. if ( (this.system.attributes.hp.value > 0) || (death.failure >= 3) || (death.success >= 3) ) {
  3520. ui.notifications.warn(game.i18n.localize("DND5E.DeathSaveUnnecessary"));
  3521. return null;
  3522. }
  3523. // Evaluate a global saving throw bonus
  3524. const speaker = options.speaker || ChatMessage.getSpeaker({actor: this});
  3525. const globalBonuses = this.system.bonuses?.abilities ?? {};
  3526. const parts = [];
  3527. const data = this.getRollData();
  3528. // Diamond Soul adds proficiency
  3529. if ( this.getFlag("dnd5e", "diamondSoul") ) {
  3530. parts.push("@prof");
  3531. data.prof = new Proficiency(this.system.attributes.prof, 1).term;
  3532. }
  3533. // Include a global actor ability save bonus
  3534. if ( globalBonuses.save ) {
  3535. parts.push("@saveBonus");
  3536. data.saveBonus = Roll.replaceFormulaData(globalBonuses.save, data);
  3537. }
  3538. // Evaluate the roll
  3539. const flavor = game.i18n.localize("DND5E.DeathSavingThrow");
  3540. const rollData = foundry.utils.mergeObject({
  3541. data,
  3542. title: `${flavor}: ${this.name}`,
  3543. flavor,
  3544. halflingLucky: this.getFlag("dnd5e", "halflingLucky"),
  3545. targetValue: 10,
  3546. messageData: {
  3547. speaker: speaker,
  3548. "flags.dnd5e.roll": {type: "death"}
  3549. }
  3550. }, options);
  3551. rollData.parts = parts.concat(options.parts ?? []);
  3552. /**
  3553. * A hook event that fires before a death saving throw is rolled for an Actor.
  3554. * @function dnd5e.preRollDeathSave
  3555. * @memberof hookEvents
  3556. * @param {Actor5e} actor Actor for which the death saving throw is being rolled.
  3557. * @param {D20RollConfiguration} config Configuration data for the pending roll.
  3558. * @returns {boolean} Explicitly return `false` to prevent death saving throw from being rolled.
  3559. */
  3560. if ( Hooks.call("dnd5e.preRollDeathSave", this, rollData) === false ) return;
  3561. const roll = await d20Roll(rollData);
  3562. if ( !roll ) return null;
  3563. // Take action depending on the result
  3564. const details = {};
  3565. // Save success
  3566. if ( roll.total >= (roll.options.targetValue ?? 10) ) {
  3567. let successes = (death.success || 0) + 1;
  3568. // Critical Success = revive with 1hp
  3569. if ( roll.isCritical ) {
  3570. details.updates = {
  3571. "system.attributes.death.success": 0,
  3572. "system.attributes.death.failure": 0,
  3573. "system.attributes.hp.value": 1
  3574. };
  3575. details.chatString = "DND5E.DeathSaveCriticalSuccess";
  3576. }
  3577. // 3 Successes = survive and reset checks
  3578. else if ( successes === 3 ) {
  3579. details.updates = {
  3580. "system.attributes.death.success": 0,
  3581. "system.attributes.death.failure": 0
  3582. };
  3583. details.chatString = "DND5E.DeathSaveSuccess";
  3584. }
  3585. // Increment successes
  3586. else details.updates = {"system.attributes.death.success": Math.clamped(successes, 0, 3)};
  3587. }
  3588. // Save failure
  3589. else {
  3590. let failures = (death.failure || 0) + (roll.isFumble ? 2 : 1);
  3591. details.updates = {"system.attributes.death.failure": Math.clamped(failures, 0, 3)};
  3592. if ( failures >= 3 ) { // 3 Failures = death
  3593. details.chatString = "DND5E.DeathSaveFailure";
  3594. }
  3595. }
  3596. /**
  3597. * A hook event that fires after a death saving throw has been rolled for an Actor, but before
  3598. * updates have been performed.
  3599. * @function dnd5e.rollDeathSave
  3600. * @memberof hookEvents
  3601. * @param {Actor5e} actor Actor for which the death saving throw has been rolled.
  3602. * @param {D20Roll} roll The resulting roll.
  3603. * @param {object} details
  3604. * @param {object} details.updates Updates that will be applied to the actor as a result of this save.
  3605. * @param {string} details.chatString Localizable string displayed in the create chat message. If not set, then
  3606. * no chat message will be displayed.
  3607. * @returns {boolean} Explicitly return `false` to prevent updates from being performed.
  3608. */
  3609. if ( Hooks.call("dnd5e.rollDeathSave", this, roll, details) === false ) return roll;
  3610. if ( !foundry.utils.isEmpty(details.updates) ) await this.update(details.updates);
  3611. // Display success/failure chat message
  3612. if ( details.chatString ) {
  3613. let chatData = { content: game.i18n.format(details.chatString, {name: this.name}), speaker };
  3614. ChatMessage.applyRollMode(chatData, roll.options.rollMode);
  3615. await ChatMessage.create(chatData);
  3616. }
  3617. // Return the rolled result
  3618. return roll;
  3619. }
  3620. /* -------------------------------------------- */
  3621. /**
  3622. * Get an un-evaluated D20Roll instance used to roll initiative for this Actor.
  3623. * @param {object} [options] Options which modify the roll
  3624. * @param {D20Roll.ADV_MODE} [options.advantageMode] A specific advantage mode to apply
  3625. * @param {string} [options.flavor] Special flavor text to apply
  3626. * @returns {D20Roll} The constructed but unevaluated D20Roll
  3627. */
  3628. getInitiativeRoll(options={}) {
  3629. // Use a temporarily cached initiative roll
  3630. if ( this._cachedInitiativeRoll ) return this._cachedInitiativeRoll.clone();
  3631. // Obtain required data
  3632. const init = this.system.attributes?.init;
  3633. const abilityId = init?.ability || CONFIG.DND5E.initiativeAbility;
  3634. const data = this.getRollData();
  3635. const flags = this.flags.dnd5e || {};
  3636. if ( flags.initiativeAdv ) options.advantageMode ??= dnd5e.dice.D20Roll.ADV_MODE.ADVANTAGE;
  3637. // Standard initiative formula
  3638. const parts = ["1d20"];
  3639. // Special initiative bonuses
  3640. if ( init ) {
  3641. parts.push(init.mod);
  3642. if ( init.prof.term !== "0" ) {
  3643. parts.push("@prof");
  3644. data.prof = init.prof.term;
  3645. }
  3646. if ( init.bonus ) {
  3647. parts.push("@bonus");
  3648. data.bonus = Roll.replaceFormulaData(init.bonus, data);
  3649. }
  3650. }
  3651. // Ability check bonuses
  3652. if ( "abilities" in this.system ) {
  3653. const abilityBonus = this.system.abilities[abilityId]?.bonuses?.check;
  3654. if ( abilityBonus ) {
  3655. parts.push("@abilityBonus");
  3656. data.abilityBonus = Roll.replaceFormulaData(abilityBonus, data);
  3657. }
  3658. }
  3659. // Global check bonus
  3660. if ( "bonuses" in this.system ) {
  3661. const globalCheckBonus = this.system.bonuses.abilities?.check;
  3662. if ( globalCheckBonus ) {
  3663. parts.push("@globalBonus");
  3664. data.globalBonus = Roll.replaceFormulaData(globalCheckBonus, data);
  3665. }
  3666. }
  3667. // Alert feat
  3668. if ( flags.initiativeAlert ) {
  3669. parts.push("@alertBonus");
  3670. data.alertBonus = 5;
  3671. }
  3672. // Ability score tiebreaker
  3673. const tiebreaker = game.settings.get("dnd5e", "initiativeDexTiebreaker");
  3674. if ( tiebreaker && ("abilities" in this.system) ) {
  3675. const abilityValue = this.system.abilities[abilityId]?.value;
  3676. if ( Number.isNumeric(abilityValue) ) parts.push(String(abilityValue / 100));
  3677. }
  3678. options = foundry.utils.mergeObject({
  3679. flavor: options.flavor ?? game.i18n.localize("DND5E.Initiative"),
  3680. halflingLucky: flags.halflingLucky ?? false,
  3681. critical: null,
  3682. fumble: null
  3683. }, options);
  3684. // Create the d20 roll
  3685. const formula = parts.join(" + ");
  3686. return new CONFIG.Dice.D20Roll(formula, data, options);
  3687. }
  3688. /* -------------------------------------------- */
  3689. /**
  3690. * Roll initiative for this Actor with a dialog that provides an opportunity to elect advantage or other bonuses.
  3691. * @param {object} [rollOptions] Options forwarded to the Actor#getInitiativeRoll method
  3692. * @returns {Promise<void>} A promise which resolves once initiative has been rolled for the Actor
  3693. */
  3694. async rollInitiativeDialog(rollOptions={}) {
  3695. // Create and configure the Initiative roll
  3696. const roll = this.getInitiativeRoll(rollOptions);
  3697. const choice = await roll.configureDialog({
  3698. defaultRollMode: game.settings.get("core", "rollMode"),
  3699. title: `${game.i18n.localize("DND5E.InitiativeRoll")}: ${this.name}`,
  3700. chooseModifier: false,
  3701. defaultAction: rollOptions.advantageMode ?? dnd5e.dice.D20Roll.ADV_MODE.NORMAL
  3702. });
  3703. if ( choice === null ) return; // Closed dialog
  3704. // Temporarily cache the configured roll and use it to roll initiative for the Actor
  3705. this._cachedInitiativeRoll = roll;
  3706. await this.rollInitiative({createCombatants: true});
  3707. delete this._cachedInitiativeRoll;
  3708. }
  3709. /* -------------------------------------------- */
  3710. /** @inheritdoc */
  3711. async rollInitiative(options={}) {
  3712. /**
  3713. * A hook event that fires before initiative is rolled for an Actor.
  3714. * @function dnd5e.preRollInitiative
  3715. * @memberof hookEvents
  3716. * @param {Actor5e} actor The Actor that is rolling initiative.
  3717. * @param {D20Roll} roll The initiative roll.
  3718. */
  3719. if ( Hooks.call("dnd5e.preRollInitiative", this, this._cachedInitiativeRoll) === false ) return;
  3720. const combat = await super.rollInitiative(options);
  3721. const combatants = this.isToken ? this.getActiveTokens(false, true).reduce((arr, t) => {
  3722. const combatant = game.combat.getCombatantByToken(t.id);
  3723. if ( combatant ) arr.push(combatant);
  3724. return arr;
  3725. }, []) : [game.combat.getCombatantByActor(this.id)];
  3726. /**
  3727. * A hook event that fires after an Actor has rolled for initiative.
  3728. * @function dnd5e.rollInitiative
  3729. * @memberof hookEvents
  3730. * @param {Actor5e} actor The Actor that rolled initiative.
  3731. * @param {Combatant[]} combatants The associated Combatants in the Combat.
  3732. */
  3733. Hooks.callAll("dnd5e.rollInitiative", this, combatants);
  3734. return combat;
  3735. }
  3736. /* -------------------------------------------- */
  3737. /**
  3738. * Roll a hit die of the appropriate type, gaining hit points equal to the die roll plus your CON modifier.
  3739. * @param {string} [denomination] The hit denomination of hit die to roll. Example "d8".
  3740. * If no denomination is provided, the first available HD will be used
  3741. * @param {object} options Additional options which modify the roll.
  3742. * @returns {Promise<Roll|null>} The created Roll instance, or null if no hit die was rolled
  3743. */
  3744. async rollHitDie(denomination, options={}) {
  3745. // If no denomination was provided, choose the first available
  3746. let cls = null;
  3747. if ( !denomination ) {
  3748. cls = this.itemTypes.class.find(c => c.system.hitDiceUsed < c.system.levels);
  3749. if ( !cls ) return null;
  3750. denomination = cls.system.hitDice;
  3751. }
  3752. // Otherwise, locate a class (if any) which has an available hit die of the requested denomination
  3753. else cls = this.items.find(i => {
  3754. return (i.system.hitDice === denomination) && ((i.system.hitDiceUsed || 0) < (i.system.levels || 1));
  3755. });
  3756. // If no class is available, display an error notification
  3757. if ( !cls ) {
  3758. ui.notifications.error(game.i18n.format("DND5E.HitDiceWarn", {name: this.name, formula: denomination}));
  3759. return null;
  3760. }
  3761. // Prepare roll data
  3762. const flavor = game.i18n.localize("DND5E.HitDiceRoll");
  3763. const rollConfig = foundry.utils.mergeObject({
  3764. formula: `max(0, 1${denomination} + @abilities.con.mod)`,
  3765. data: this.getRollData(),
  3766. chatMessage: true,
  3767. messageData: {
  3768. speaker: ChatMessage.getSpeaker({actor: this}),
  3769. flavor,
  3770. title: `${flavor}: ${this.name}`,
  3771. rollMode: game.settings.get("core", "rollMode"),
  3772. "flags.dnd5e.roll": {type: "hitDie"}
  3773. }
  3774. }, options);
  3775. /**
  3776. * A hook event that fires before a hit die is rolled for an Actor.
  3777. * @function dnd5e.preRollHitDie
  3778. * @memberof hookEvents
  3779. * @param {Actor5e} actor Actor for which the hit die is to be rolled.
  3780. * @param {object} config Configuration data for the pending roll.
  3781. * @param {string} config.formula Formula that will be rolled.
  3782. * @param {object} config.data Data used when evaluating the roll.
  3783. * @param {boolean} config.chatMessage Should a chat message be created for this roll?
  3784. * @param {object} config.messageData Data used to create the chat message.
  3785. * @param {string} denomination Size of hit die to be rolled.
  3786. * @returns {boolean} Explicitly return `false` to prevent hit die from being rolled.
  3787. */
  3788. if ( Hooks.call("dnd5e.preRollHitDie", this, rollConfig, denomination) === false ) return;
  3789. const roll = await new Roll(rollConfig.formula, rollConfig.data).roll({async: true});
  3790. if ( rollConfig.chatMessage ) roll.toMessage(rollConfig.messageData);
  3791. const hp = this.system.attributes.hp;
  3792. const dhp = Math.min(Math.max(0, hp.max + (hp.tempmax ?? 0)) - hp.value, roll.total);
  3793. const updates = {
  3794. actor: {"system.attributes.hp.value": hp.value + dhp},
  3795. class: {"system.hitDiceUsed": cls.system.hitDiceUsed + 1}
  3796. };
  3797. /**
  3798. * A hook event that fires after a hit die has been rolled for an Actor, but before updates have been performed.
  3799. * @function dnd5e.rollHitDie
  3800. * @memberof hookEvents
  3801. * @param {Actor5e} actor Actor for which the hit die has been rolled.
  3802. * @param {Roll} roll The resulting roll.
  3803. * @param {object} updates
  3804. * @param {object} updates.actor Updates that will be applied to the actor.
  3805. * @param {object} updates.class Updates that will be applied to the class.
  3806. * @returns {boolean} Explicitly return `false` to prevent updates from being performed.
  3807. */
  3808. if ( Hooks.call("dnd5e.rollHitDie", this, roll, updates) === false ) return roll;
  3809. // Re-evaluate dhp in the event that it was changed in the previous hook
  3810. const updateOptions = { dhp: (updates.actor?.["system.attributes.hp.value"] ?? hp.value) - hp.value };
  3811. // Perform updates
  3812. if ( !foundry.utils.isEmpty(updates.actor) ) await this.update(updates.actor, updateOptions);
  3813. if ( !foundry.utils.isEmpty(updates.class) ) await cls.update(updates.class);
  3814. return roll;
  3815. }
  3816. /* -------------------------------------------- */
  3817. /**
  3818. * Roll hit points for a specific class as part of a level-up workflow.
  3819. * @param {Item5e} item The class item whose hit dice to roll.
  3820. * @param {object} options
  3821. * @param {boolean} [options.chatMessage=true] Display the chat message for this roll.
  3822. * @returns {Promise<Roll>} The completed roll.
  3823. * @see {@link dnd5e.preRollClassHitPoints}
  3824. */
  3825. async rollClassHitPoints(item, { chatMessage=true }={}) {
  3826. if ( item.type !== "class" ) throw new Error("Hit points can only be rolled for a class item.");
  3827. const rollData = {
  3828. formula: `1${item.system.hitDice}`,
  3829. data: item.getRollData(),
  3830. chatMessage
  3831. };
  3832. const flavor = game.i18n.format("DND5E.AdvancementHitPointsRollMessage", { class: item.name });
  3833. const messageData = {
  3834. title: `${flavor}: ${this.name}`,
  3835. flavor,
  3836. speaker: ChatMessage.getSpeaker({ actor: this }),
  3837. "flags.dnd5e.roll": { type: "hitPoints" }
  3838. };
  3839. /**
  3840. * A hook event that fires before hit points are rolled for a character's class.
  3841. * @function dnd5e.preRollClassHitPoints
  3842. * @memberof hookEvents
  3843. * @param {Actor5e} actor Actor for which the hit points are being rolled.
  3844. * @param {Item5e} item The class item whose hit dice will be rolled.
  3845. * @param {object} rollData
  3846. * @param {string} rollData.formula The string formula to parse.
  3847. * @param {object} rollData.data The data object against which to parse attributes within the formula.
  3848. * @param {object} messageData The data object to use when creating the message.
  3849. */
  3850. Hooks.callAll("dnd5e.preRollClassHitPoints", this, item, rollData, messageData);
  3851. const roll = new Roll(rollData.formula, rollData.data);
  3852. await roll.evaluate({async: true});
  3853. /**
  3854. * A hook event that fires after hit points haven been rolled for a character's class.
  3855. * @function dnd5e.rollClassHitPoints
  3856. * @memberof hookEvents
  3857. * @param {Actor5e} actor Actor for which the hit points have been rolled.
  3858. * @param {Roll} roll The resulting roll.
  3859. */
  3860. Hooks.callAll("dnd5e.rollClassHitPoints", this, roll);
  3861. if ( rollData.chatMessage ) await roll.toMessage(messageData);
  3862. return roll;
  3863. }
  3864. /* -------------------------------------------- */
  3865. /**
  3866. * Roll hit points for an NPC based on the HP formula.
  3867. * @param {object} options
  3868. * @param {boolean} [options.chatMessage=true] Display the chat message for this roll.
  3869. * @returns {Promise<Roll>} The completed roll.
  3870. * @see {@link dnd5e.preRollNPCHitPoints}
  3871. */
  3872. async rollNPCHitPoints({ chatMessage=true }={}) {
  3873. if ( this.type !== "npc" ) throw new Error("NPC hit points can only be rolled for NPCs");
  3874. const rollData = {
  3875. formula: this.system.attributes.hp.formula,
  3876. data: this.getRollData(),
  3877. chatMessage
  3878. };
  3879. const flavor = game.i18n.format("DND5E.HPFormulaRollMessage");
  3880. const messageData = {
  3881. title: `${flavor}: ${this.name}`,
  3882. flavor,
  3883. speaker: ChatMessage.getSpeaker({ actor: this }),
  3884. "flags.dnd5e.roll": { type: "hitPoints" }
  3885. };
  3886. /**
  3887. * A hook event that fires before hit points are rolled for an NPC.
  3888. * @function dnd5e.preRollNPCHitPoints
  3889. * @memberof hookEvents
  3890. * @param {Actor5e} actor Actor for which the hit points are being rolled.
  3891. * @param {object} rollData
  3892. * @param {string} rollData.formula The string formula to parse.
  3893. * @param {object} rollData.data The data object against which to parse attributes within the formula.
  3894. * @param {object} messageData The data object to use when creating the message.
  3895. */
  3896. Hooks.callAll("dnd5e.preRollNPCHitPoints", this, rollData, messageData);
  3897. const roll = new Roll(rollData.formula, rollData.data);
  3898. await roll.evaluate({async: true});
  3899. /**
  3900. * A hook event that fires after hit points are rolled for an NPC.
  3901. * @function dnd5e.rollNPCHitPoints
  3902. * @memberof hookEvents
  3903. * @param {Actor5e} actor Actor for which the hit points have been rolled.
  3904. * @param {Roll} roll The resulting roll.
  3905. */
  3906. Hooks.callAll("dnd5e.rollNPCHitPoints", this, roll);
  3907. if ( rollData.chatMessage ) await roll.toMessage(messageData);
  3908. return roll;
  3909. }
  3910. /* -------------------------------------------- */
  3911. /* Resting */
  3912. /* -------------------------------------------- */
  3913. /**
  3914. * Configuration options for a rest.
  3915. *
  3916. * @typedef {object} RestConfiguration
  3917. * @property {boolean} dialog Present a dialog window which allows for rolling hit dice as part of the
  3918. * Short Rest and selecting whether a new day has occurred.
  3919. * @property {boolean} chat Should a chat message be created to summarize the results of the rest?
  3920. * @property {boolean} newDay Does this rest carry over to a new day?
  3921. * @property {boolean} [autoHD] Should hit dice be spent automatically during a short rest?
  3922. * @property {number} [autoHDThreshold] How many hit points should be missing before hit dice are
  3923. * automatically spent during a short rest.
  3924. */
  3925. /**
  3926. * Results from a rest operation.
  3927. *
  3928. * @typedef {object} RestResult
  3929. * @property {number} dhp Hit points recovered during the rest.
  3930. * @property {number} dhd Hit dice recovered or spent during the rest.
  3931. * @property {object} updateData Updates applied to the actor.
  3932. * @property {object[]} updateItems Updates applied to actor's items.
  3933. * @property {boolean} longRest Whether the rest type was a long rest.
  3934. * @property {boolean} newDay Whether a new day occurred during the rest.
  3935. * @property {Roll[]} rolls Any rolls that occurred during the rest process, not including hit dice.
  3936. */
  3937. /* -------------------------------------------- */
  3938. /**
  3939. * Take a short rest, possibly spending hit dice and recovering resources, item uses, and pact slots.
  3940. * @param {RestConfiguration} [config] Configuration options for a short rest.
  3941. * @returns {Promise<RestResult>} A Promise which resolves once the short rest workflow has completed.
  3942. */
  3943. async shortRest(config={}) {
  3944. config = foundry.utils.mergeObject({
  3945. dialog: true, chat: true, newDay: false, autoHD: false, autoHDThreshold: 3
  3946. }, config);
  3947. /**
  3948. * A hook event that fires before a short rest is started.
  3949. * @function dnd5e.preShortRest
  3950. * @memberof hookEvents
  3951. * @param {Actor5e} actor The actor that is being rested.
  3952. * @param {RestConfiguration} config Configuration options for the rest.
  3953. * @returns {boolean} Explicitly return `false` to prevent the rest from being started.
  3954. */
  3955. if ( Hooks.call("dnd5e.preShortRest", this, config) === false ) return;
  3956. // Take note of the initial hit points and number of hit dice the Actor has
  3957. const hd0 = this.system.attributes.hd;
  3958. const hp0 = this.system.attributes.hp.value;
  3959. // Display a Dialog for rolling hit dice
  3960. if ( config.dialog ) {
  3961. try { config.newDay = await ShortRestDialog.shortRestDialog({actor: this, canRoll: hd0 > 0});
  3962. } catch(err) { return; }
  3963. }
  3964. // Automatically spend hit dice
  3965. else if ( config.autoHD ) await this.autoSpendHitDice({ threshold: config.autoHDThreshold });
  3966. // Return the rest result
  3967. const dhd = this.system.attributes.hd - hd0;
  3968. const dhp = this.system.attributes.hp.value - hp0;
  3969. return this._rest(config.chat, config.newDay, false, dhd, dhp);
  3970. }
  3971. /* -------------------------------------------- */
  3972. /**
  3973. * Take a long rest, recovering hit points, hit dice, resources, item uses, and spell slots.
  3974. * @param {RestConfiguration} [config] Configuration options for a long rest.
  3975. * @returns {Promise<RestResult>} A Promise which resolves once the long rest workflow has completed.
  3976. */
  3977. async longRest(config={}) {
  3978. config = foundry.utils.mergeObject({
  3979. dialog: true, chat: true, newDay: true
  3980. }, config);
  3981. /**
  3982. * A hook event that fires before a long rest is started.
  3983. * @function dnd5e.preLongRest
  3984. * @memberof hookEvents
  3985. * @param {Actor5e} actor The actor that is being rested.
  3986. * @param {RestConfiguration} config Configuration options for the rest.
  3987. * @returns {boolean} Explicitly return `false` to prevent the rest from being started.
  3988. */
  3989. if ( Hooks.call("dnd5e.preLongRest", this, config) === false ) return;
  3990. if ( config.dialog ) {
  3991. try { config.newDay = await LongRestDialog.longRestDialog({actor: this}); }
  3992. catch(err) { return; }
  3993. }
  3994. return this._rest(config.chat, config.newDay, true);
  3995. }
  3996. /* -------------------------------------------- */
  3997. /**
  3998. * Perform all of the changes needed for a short or long rest.
  3999. *
  4000. * @param {boolean} chat Summarize the results of the rest workflow as a chat message.
  4001. * @param {boolean} newDay Has a new day occurred during this rest?
  4002. * @param {boolean} longRest Is this a long rest?
  4003. * @param {number} [dhd=0] Number of hit dice spent during so far during the rest.
  4004. * @param {number} [dhp=0] Number of hit points recovered so far during the rest.
  4005. * @returns {Promise<RestResult>} Consolidated results of the rest workflow.
  4006. * @private
  4007. */
  4008. async _rest(chat, newDay, longRest, dhd=0, dhp=0) {
  4009. let hitPointsRecovered = 0;
  4010. let hitPointUpdates = {};
  4011. let hitDiceRecovered = 0;
  4012. let hitDiceUpdates = [];
  4013. const rolls = [];
  4014. // Recover hit points & hit dice on long rest
  4015. if ( longRest ) {
  4016. ({ updates: hitPointUpdates, hitPointsRecovered } = this._getRestHitPointRecovery());
  4017. ({ updates: hitDiceUpdates, hitDiceRecovered } = this._getRestHitDiceRecovery());
  4018. }
  4019. // Figure out the rest of the changes
  4020. const result = {
  4021. dhd: dhd + hitDiceRecovered,
  4022. dhp: dhp + hitPointsRecovered,
  4023. updateData: {
  4024. ...hitPointUpdates,
  4025. ...this._getRestResourceRecovery({ recoverShortRestResources: !longRest, recoverLongRestResources: longRest }),
  4026. ...this._getRestSpellRecovery({ recoverSpells: longRest })
  4027. },
  4028. updateItems: [
  4029. ...hitDiceUpdates,
  4030. ...(await this._getRestItemUsesRecovery({ recoverLongRestUses: longRest, recoverDailyUses: newDay, rolls }))
  4031. ],
  4032. longRest,
  4033. newDay
  4034. };
  4035. result.rolls = rolls;
  4036. /**
  4037. * A hook event that fires after rest result is calculated, but before any updates are performed.
  4038. * @function dnd5e.preRestCompleted
  4039. * @memberof hookEvents
  4040. * @param {Actor5e} actor The actor that is being rested.
  4041. * @param {RestResult} result Details on the rest to be completed.
  4042. * @returns {boolean} Explicitly return `false` to prevent the rest updates from being performed.
  4043. */
  4044. if ( Hooks.call("dnd5e.preRestCompleted", this, result) === false ) return result;
  4045. // Perform updates
  4046. await this.update(result.updateData);
  4047. await this.updateEmbeddedDocuments("Item", result.updateItems);
  4048. // Display a Chat Message summarizing the rest effects
  4049. if ( chat ) await this._displayRestResultMessage(result, longRest);
  4050. /**
  4051. * A hook event that fires when the rest process is completed for an actor.
  4052. * @function dnd5e.restCompleted
  4053. * @memberof hookEvents
  4054. * @param {Actor5e} actor The actor that just completed resting.
  4055. * @param {RestResult} result Details on the rest completed.
  4056. */
  4057. Hooks.callAll("dnd5e.restCompleted", this, result);
  4058. // Return data summarizing the rest effects
  4059. return result;
  4060. }
  4061. /* -------------------------------------------- */
  4062. /**
  4063. * Display a chat message with the result of a rest.
  4064. *
  4065. * @param {RestResult} result Result of the rest operation.
  4066. * @param {boolean} [longRest=false] Is this a long rest?
  4067. * @returns {Promise<ChatMessage>} Chat message that was created.
  4068. * @protected
  4069. */
  4070. async _displayRestResultMessage(result, longRest=false) {
  4071. const { dhd, dhp, newDay } = result;
  4072. const diceRestored = dhd !== 0;
  4073. const healthRestored = dhp !== 0;
  4074. const length = longRest ? "Long" : "Short";
  4075. // Summarize the rest duration
  4076. let restFlavor;
  4077. switch (game.settings.get("dnd5e", "restVariant")) {
  4078. case "normal":
  4079. restFlavor = (longRest && newDay) ? "DND5E.LongRestOvernight" : `DND5E.${length}RestNormal`;
  4080. break;
  4081. case "gritty":
  4082. restFlavor = (!longRest && newDay) ? "DND5E.ShortRestOvernight" : `DND5E.${length}RestGritty`;
  4083. break;
  4084. case "epic":
  4085. restFlavor = `DND5E.${length}RestEpic`;
  4086. break;
  4087. }
  4088. // Determine the chat message to display
  4089. let message;
  4090. if ( diceRestored && healthRestored ) message = `DND5E.${length}RestResult`;
  4091. else if ( longRest && !diceRestored && healthRestored ) message = "DND5E.LongRestResultHitPoints";
  4092. else if ( longRest && diceRestored && !healthRestored ) message = "DND5E.LongRestResultHitDice";
  4093. else message = `DND5E.${length}RestResultShort`;
  4094. // Create a chat message
  4095. let chatData = {
  4096. user: game.user.id,
  4097. speaker: {actor: this, alias: this.name},
  4098. flavor: game.i18n.localize(restFlavor),
  4099. rolls: result.rolls,
  4100. content: game.i18n.format(message, {
  4101. name: this.name,
  4102. dice: longRest ? dhd : -dhd,
  4103. health: dhp
  4104. })
  4105. };
  4106. ChatMessage.applyRollMode(chatData, game.settings.get("core", "rollMode"));
  4107. return ChatMessage.create(chatData);
  4108. }
  4109. /* -------------------------------------------- */
  4110. /**
  4111. * Automatically spend hit dice to recover hit points up to a certain threshold.
  4112. * @param {object} [options]
  4113. * @param {number} [options.threshold=3] A number of missing hit points which would trigger an automatic HD roll.
  4114. * @returns {Promise<number>} Number of hit dice spent.
  4115. */
  4116. async autoSpendHitDice({ threshold=3 }={}) {
  4117. const hp = this.system.attributes.hp;
  4118. const max = Math.max(0, hp.max + hp.tempmax);
  4119. let diceRolled = 0;
  4120. while ( (this.system.attributes.hp.value + threshold) <= max ) {
  4121. const r = await this.rollHitDie();
  4122. if ( r === null ) break;
  4123. diceRolled += 1;
  4124. }
  4125. return diceRolled;
  4126. }
  4127. /* -------------------------------------------- */
  4128. /**
  4129. * Recovers actor hit points and eliminates any temp HP.
  4130. * @param {object} [options]
  4131. * @param {boolean} [options.recoverTemp=true] Reset temp HP to zero.
  4132. * @param {boolean} [options.recoverTempMax=true] Reset temp max HP to zero.
  4133. * @returns {object} Updates to the actor and change in hit points.
  4134. * @protected
  4135. */
  4136. _getRestHitPointRecovery({recoverTemp=true, recoverTempMax=true}={}) {
  4137. const hp = this.system.attributes.hp;
  4138. let max = hp.max;
  4139. let updates = {};
  4140. if ( recoverTempMax ) updates["system.attributes.hp.tempmax"] = 0;
  4141. else max = Math.max(0, max + (hp.tempmax || 0));
  4142. updates["system.attributes.hp.value"] = max;
  4143. if ( recoverTemp ) updates["system.attributes.hp.temp"] = 0;
  4144. return { updates, hitPointsRecovered: max - hp.value };
  4145. }
  4146. /* -------------------------------------------- */
  4147. /**
  4148. * Recovers actor resources.
  4149. * @param {object} [options]
  4150. * @param {boolean} [options.recoverShortRestResources=true] Recover resources that recharge on a short rest.
  4151. * @param {boolean} [options.recoverLongRestResources=true] Recover resources that recharge on a long rest.
  4152. * @returns {object} Updates to the actor.
  4153. * @protected
  4154. */
  4155. _getRestResourceRecovery({recoverShortRestResources=true, recoverLongRestResources=true}={}) {
  4156. let updates = {};
  4157. for ( let [k, r] of Object.entries(this.system.resources) ) {
  4158. if ( Number.isNumeric(r.max) && ((recoverShortRestResources && r.sr) || (recoverLongRestResources && r.lr)) ) {
  4159. updates[`system.resources.${k}.value`] = Number(r.max);
  4160. }
  4161. }
  4162. return updates;
  4163. }
  4164. /* -------------------------------------------- */
  4165. /**
  4166. * Recovers spell slots and pact slots.
  4167. * @param {object} [options]
  4168. * @param {boolean} [options.recoverPact=true] Recover all expended pact slots.
  4169. * @param {boolean} [options.recoverSpells=true] Recover all expended spell slots.
  4170. * @returns {object} Updates to the actor.
  4171. * @protected
  4172. */
  4173. _getRestSpellRecovery({recoverPact=true, recoverSpells=true}={}) {
  4174. const spells = this.system.spells;
  4175. let updates = {};
  4176. if ( recoverPact ) {
  4177. const pact = spells.pact;
  4178. updates["system.spells.pact.value"] = pact.override || pact.max;
  4179. }
  4180. if ( recoverSpells ) {
  4181. for ( let [k, v] of Object.entries(spells) ) {
  4182. updates[`system.spells.${k}.value`] = Number.isNumeric(v.override) ? v.override : (v.max ?? 0);
  4183. }
  4184. }
  4185. return updates;
  4186. }
  4187. /* -------------------------------------------- */
  4188. /**
  4189. * Recovers class hit dice during a long rest.
  4190. *
  4191. * @param {object} [options]
  4192. * @param {number} [options.maxHitDice] Maximum number of hit dice to recover.
  4193. * @returns {object} Array of item updates and number of hit dice recovered.
  4194. * @protected
  4195. */
  4196. _getRestHitDiceRecovery({maxHitDice}={}) {
  4197. // Determine the number of hit dice which may be recovered
  4198. if ( maxHitDice === undefined ) maxHitDice = Math.max(Math.floor(this.system.details.level / 2), 1);
  4199. // Sort classes which can recover HD, assuming players prefer recovering larger HD first.
  4200. const sortedClasses = Object.values(this.classes).sort((a, b) => {
  4201. return (parseInt(b.system.hitDice.slice(1)) || 0) - (parseInt(a.system.hitDice.slice(1)) || 0);
  4202. });
  4203. // Update hit dice usage
  4204. let updates = [];
  4205. let hitDiceRecovered = 0;
  4206. for ( let item of sortedClasses ) {
  4207. const hitDiceUsed = item.system.hitDiceUsed;
  4208. if ( (hitDiceRecovered < maxHitDice) && (hitDiceUsed > 0) ) {
  4209. let delta = Math.min(hitDiceUsed || 0, maxHitDice - hitDiceRecovered);
  4210. hitDiceRecovered += delta;
  4211. updates.push({_id: item.id, "system.hitDiceUsed": hitDiceUsed - delta});
  4212. }
  4213. }
  4214. return { updates, hitDiceRecovered };
  4215. }
  4216. /* -------------------------------------------- */
  4217. /**
  4218. * Recovers item uses during short or long rests.
  4219. * @param {object} [options]
  4220. * @param {boolean} [options.recoverShortRestUses=true] Recover uses for items that recharge after a short rest.
  4221. * @param {boolean} [options.recoverLongRestUses=true] Recover uses for items that recharge after a long rest.
  4222. * @param {boolean} [options.recoverDailyUses=true] Recover uses for items that recharge on a new day.
  4223. * @param {Roll[]} [options.rolls] Rolls that have been performed as part of this rest.
  4224. * @returns {Promise<object[]>} Array of item updates.
  4225. * @protected
  4226. */
  4227. async _getRestItemUsesRecovery({recoverShortRestUses=true, recoverLongRestUses=true,
  4228. recoverDailyUses=true, rolls}={}) {
  4229. let recovery = [];
  4230. if ( recoverShortRestUses ) recovery.push("sr");
  4231. if ( recoverLongRestUses ) recovery.push("lr");
  4232. if ( recoverDailyUses ) recovery.push("day");
  4233. let updates = [];
  4234. for ( let item of this.items ) {
  4235. const uses = item.system.uses;
  4236. if ( recovery.includes(uses?.per) ) {
  4237. updates.push({_id: item.id, "system.uses.value": uses.max});
  4238. }
  4239. if ( recoverLongRestUses && item.system.recharge?.value ) {
  4240. updates.push({_id: item.id, "system.recharge.charged": true});
  4241. }
  4242. // Items that roll to gain charges on a new day
  4243. if ( recoverDailyUses && uses?.recovery && (uses?.per === "charges") ) {
  4244. const roll = new Roll(uses.recovery, item.getRollData());
  4245. if ( recoverLongRestUses && (game.settings.get("dnd5e", "restVariant") === "gritty") ) {
  4246. roll.alter(7, 0, {multiplyNumeric: true});
  4247. }
  4248. let total = 0;
  4249. try {
  4250. total = (await roll.evaluate({async: true})).total;
  4251. } catch(err) {
  4252. ui.notifications.warn(game.i18n.format("DND5E.ItemRecoveryFormulaWarning", {
  4253. name: item.name,
  4254. formula: uses.recovery
  4255. }));
  4256. }
  4257. const newValue = Math.clamped(uses.value + total, 0, uses.max);
  4258. if ( newValue !== uses.value ) {
  4259. const diff = newValue - uses.value;
  4260. const isMax = newValue === uses.max;
  4261. const locKey = `DND5E.Item${diff < 0 ? "Loss" : "Recovery"}Roll${isMax ? "Max" : ""}`;
  4262. updates.push({_id: item.id, "system.uses.value": newValue});
  4263. rolls.push(roll);
  4264. await roll.toMessage({
  4265. user: game.user.id,
  4266. speaker: {actor: this, alias: this.name},
  4267. flavor: game.i18n.format(locKey, {name: item.name, count: Math.abs(diff)})
  4268. });
  4269. }
  4270. }
  4271. }
  4272. return updates;
  4273. }
  4274. /* -------------------------------------------- */
  4275. /* Conversion & Transformation */
  4276. /* -------------------------------------------- */
  4277. /**
  4278. * Convert all carried currency to the highest possible denomination using configured conversion rates.
  4279. * See CONFIG.DND5E.currencies for configuration.
  4280. * @returns {Promise<Actor5e>}
  4281. */
  4282. convertCurrency() {
  4283. const currency = foundry.utils.deepClone(this.system.currency);
  4284. const currencies = Object.entries(CONFIG.DND5E.currencies);
  4285. currencies.sort((a, b) => a[1].conversion - b[1].conversion);
  4286. // Count total converted units of the base currency
  4287. let basis = currencies.reduce((change, [denomination, config]) => {
  4288. if ( !config.conversion ) return change;
  4289. return change + (currency[denomination] / config.conversion);
  4290. }, 0);
  4291. // Convert base units into the highest denomination possible
  4292. for ( const [denomination, config] of currencies) {
  4293. if ( !config.conversion ) continue;
  4294. const amount = Math.floor(basis * config.conversion);
  4295. currency[denomination] = amount;
  4296. basis -= (amount / config.conversion);
  4297. }
  4298. // Save the updated currency object
  4299. return this.update({"system.currency": currency});
  4300. }
  4301. /* -------------------------------------------- */
  4302. /**
  4303. * Options that determine what properties of the original actor are kept and which are replaced with
  4304. * the target actor.
  4305. *
  4306. * @typedef {object} TransformationOptions
  4307. * @property {boolean} [keepPhysical=false] Keep physical abilities (str, dex, con)
  4308. * @property {boolean} [keepMental=false] Keep mental abilities (int, wis, cha)
  4309. * @property {boolean} [keepSaves=false] Keep saving throw proficiencies
  4310. * @property {boolean} [keepSkills=false] Keep skill proficiencies
  4311. * @property {boolean} [mergeSaves=false] Take the maximum of the save proficiencies
  4312. * @property {boolean} [mergeSkills=false] Take the maximum of the skill proficiencies
  4313. * @property {boolean} [keepClass=false] Keep proficiency bonus
  4314. * @property {boolean} [keepFeats=false] Keep features
  4315. * @property {boolean} [keepSpells=false] Keep spells
  4316. * @property {boolean} [keepItems=false] Keep items
  4317. * @property {boolean} [keepBio=false] Keep biography
  4318. * @property {boolean} [keepVision=false] Keep vision
  4319. * @property {boolean} [keepSelf=false] Keep self
  4320. * @property {boolean} [keepAE=false] Keep all effects
  4321. * @property {boolean} [keepOriginAE=true] Keep effects which originate on this actor
  4322. * @property {boolean} [keepOtherOriginAE=true] Keep effects which originate on another actor
  4323. * @property {boolean} [keepSpellAE=true] Keep effects which originate from actors spells
  4324. * @property {boolean} [keepFeatAE=true] Keep effects which originate from actors features
  4325. * @property {boolean} [keepEquipmentAE=true] Keep effects which originate on actors equipment
  4326. * @property {boolean} [keepClassAE=true] Keep effects which originate from actors class/subclass
  4327. * @property {boolean} [keepBackgroundAE=true] Keep effects which originate from actors background
  4328. * @property {boolean} [transformTokens=true] Transform linked tokens too
  4329. */
  4330. /**
  4331. * Transform this Actor into another one.
  4332. *
  4333. * @param {Actor5e} target The target Actor.
  4334. * @param {TransformationOptions} [options={}] Options that determine how the transformation is performed.
  4335. * @param {boolean} [options.renderSheet=true] Render the sheet of the transformed actor after the polymorph
  4336. * @returns {Promise<Array<Token>>|null} Updated token if the transformation was performed.
  4337. */
  4338. async transformInto(target, { keepPhysical=false, keepMental=false, keepSaves=false, keepSkills=false,
  4339. mergeSaves=false, mergeSkills=false, keepClass=false, keepFeats=false, keepSpells=false, keepItems=false,
  4340. keepBio=false, keepVision=false, keepSelf=false, keepAE=false, keepOriginAE=true, keepOtherOriginAE=true,
  4341. keepSpellAE=true, keepEquipmentAE=true, keepFeatAE=true, keepClassAE=true, keepBackgroundAE=true,
  4342. transformTokens=true}={}, {renderSheet=true}={}) {
  4343. // Ensure the player is allowed to polymorph
  4344. const allowed = game.settings.get("dnd5e", "allowPolymorphing");
  4345. if ( !allowed && !game.user.isGM ) {
  4346. return ui.notifications.warn(game.i18n.localize("DND5E.PolymorphWarn"));
  4347. }
  4348. // Get the original Actor data and the new source data
  4349. const o = this.toObject();
  4350. o.flags.dnd5e = o.flags.dnd5e || {};
  4351. o.flags.dnd5e.transformOptions = {mergeSkills, mergeSaves};
  4352. const source = target.toObject();
  4353. if ( keepSelf ) {
  4354. o.img = source.img;
  4355. o.name = `${o.name} (${game.i18n.localize("DND5E.PolymorphSelf")})`;
  4356. }
  4357. // Prepare new data to merge from the source
  4358. const d = foundry.utils.mergeObject({
  4359. type: o.type, // Remain the same actor type
  4360. name: `${o.name} (${source.name})`, // Append the new shape to your old name
  4361. system: source.system, // Get the systemdata model of your new form
  4362. items: source.items, // Get the items of your new form
  4363. effects: o.effects.concat(source.effects), // Combine active effects from both forms
  4364. img: source.img, // New appearance
  4365. ownership: o.ownership, // Use the original actor permissions
  4366. folder: o.folder, // Be displayed in the same sidebar folder
  4367. flags: o.flags, // Use the original actor flags
  4368. prototypeToken: { name: `${o.name} (${source.name})`, texture: {}, sight: {}, detectionModes: [] } // Set a new empty token
  4369. }, keepSelf ? o : {}); // Keeps most of original actor
  4370. // Specifically delete some data attributes
  4371. delete d.system.resources; // Don't change your resource pools
  4372. delete d.system.currency; // Don't lose currency
  4373. delete d.system.bonuses; // Don't lose global bonuses
  4374. // Specific additional adjustments
  4375. d.system.details.alignment = o.system.details.alignment; // Don't change alignment
  4376. d.system.attributes.exhaustion = o.system.attributes.exhaustion; // Keep your prior exhaustion level
  4377. d.system.attributes.inspiration = o.system.attributes.inspiration; // Keep inspiration
  4378. d.system.spells = o.system.spells; // Keep spell slots
  4379. d.system.attributes.ac.flat = target.system.attributes.ac.value; // Override AC
  4380. // Token appearance updates
  4381. for ( const k of ["width", "height", "alpha", "lockRotation"] ) {
  4382. d.prototypeToken[k] = source.prototypeToken[k];
  4383. }
  4384. for ( const k of ["offsetX", "offsetY", "scaleX", "scaleY", "src", "tint"] ) {
  4385. d.prototypeToken.texture[k] = source.prototypeToken.texture[k];
  4386. }
  4387. for ( const k of ["bar1", "bar2", "displayBars", "displayName", "disposition", "rotation", "elevation"] ) {
  4388. d.prototypeToken[k] = o.prototypeToken[k];
  4389. }
  4390. if ( !keepSelf ) {
  4391. const sightSource = keepVision ? o.prototypeToken : source.prototypeToken;
  4392. for ( const k of ["range", "angle", "visionMode", "color", "attenuation", "brightness", "saturation", "contrast", "enabled"] ) {
  4393. d.prototypeToken.sight[k] = sightSource.sight[k];
  4394. }
  4395. d.prototypeToken.detectionModes = sightSource.detectionModes;
  4396. // Transfer ability scores
  4397. const abilities = d.system.abilities;
  4398. for ( let k of Object.keys(abilities) ) {
  4399. const oa = o.system.abilities[k];
  4400. const prof = abilities[k].proficient;
  4401. const type = CONFIG.DND5E.abilities[k]?.type;
  4402. if ( keepPhysical && (type === "physical") ) abilities[k] = oa;
  4403. else if ( keepMental && (type === "mental") ) abilities[k] = oa;
  4404. if ( keepSaves ) abilities[k].proficient = oa.proficient;
  4405. else if ( mergeSaves ) abilities[k].proficient = Math.max(prof, oa.proficient);
  4406. }
  4407. // Transfer skills
  4408. if ( keepSkills ) d.system.skills = o.system.skills;
  4409. else if ( mergeSkills ) {
  4410. for ( let [k, s] of Object.entries(d.system.skills) ) {
  4411. s.value = Math.max(s.value, o.system.skills[k].value);
  4412. }
  4413. }
  4414. // Keep specific items from the original data
  4415. d.items = d.items.concat(o.items.filter(i => {
  4416. if ( ["class", "subclass"].includes(i.type) ) return keepClass;
  4417. else if ( i.type === "feat" ) return keepFeats;
  4418. else if ( i.type === "spell" ) return keepSpells;
  4419. else return keepItems;
  4420. }));
  4421. // Transfer classes for NPCs
  4422. if ( !keepClass && d.system.details.cr ) {
  4423. const cls = new dnd5e.dataModels.item.ClassData({levels: d.system.details.cr});
  4424. d.items.push({
  4425. type: "class",
  4426. name: game.i18n.localize("DND5E.PolymorphTmpClass"),
  4427. system: cls.toObject()
  4428. });
  4429. }
  4430. // Keep biography
  4431. if ( keepBio ) d.system.details.biography = o.system.details.biography;
  4432. // Keep senses
  4433. if ( keepVision ) d.system.traits.senses = o.system.traits.senses;
  4434. // Remove active effects
  4435. const oEffects = foundry.utils.deepClone(d.effects);
  4436. const originEffectIds = new Set(oEffects.filter(effect => {
  4437. return !effect.origin || effect.origin === this.uuid;
  4438. }).map(e => e._id));
  4439. d.effects = d.effects.filter(e => {
  4440. if ( keepAE ) return true;
  4441. const origin = e.origin?.startsWith("Actor") || e.origin?.startsWith("Item") ? fromUuidSync(e.origin) : {};
  4442. const originIsSelf = origin?.parent?.uuid === this.uuid;
  4443. const isOriginEffect = originEffectIds.has(e._id);
  4444. if ( isOriginEffect ) return keepOriginAE;
  4445. if ( !isOriginEffect && !originIsSelf ) return keepOtherOriginAE;
  4446. if ( origin.type === "spell" ) return keepSpellAE;
  4447. if ( origin.type === "feat" ) return keepFeatAE;
  4448. if ( origin.type === "background" ) return keepBackgroundAE;
  4449. if ( ["subclass", "class"].includes(origin.type) ) return keepClassAE;
  4450. if ( ["equipment", "weapon", "tool", "loot", "backpack"].includes(origin.type) ) return keepEquipmentAE;
  4451. return true;
  4452. });
  4453. }
  4454. // Set a random image if source is configured that way
  4455. if ( source.prototypeToken.randomImg ) {
  4456. const images = await target.getTokenImages();
  4457. d.prototypeToken.texture.src = images[Math.floor(Math.random() * images.length)];
  4458. }
  4459. // Set new data flags
  4460. if ( !this.isPolymorphed || !d.flags.dnd5e.originalActor ) d.flags.dnd5e.originalActor = this.id;
  4461. d.flags.dnd5e.isPolymorphed = true;
  4462. // Gather previous actor data
  4463. const previousActorIds = this.getFlag("dnd5e", "previousActorIds") || [];
  4464. previousActorIds.push(this._id);
  4465. foundry.utils.setProperty(d.flags, "dnd5e.previousActorIds", previousActorIds);
  4466. // Update unlinked Tokens, and grab a copy of any actorData adjustments to re-apply
  4467. if ( this.isToken ) {
  4468. const tokenData = d.prototypeToken;
  4469. delete d.prototypeToken;
  4470. let previousActorData;
  4471. if ( game.dnd5e.isV10 ) {
  4472. tokenData.actorData = d;
  4473. previousActorData = this.token.toObject().actorData;
  4474. } else {
  4475. tokenData.delta = d;
  4476. previousActorData = this.token.delta.toObject();
  4477. }
  4478. foundry.utils.setProperty(tokenData, "flags.dnd5e.previousActorData", previousActorData);
  4479. await this.sheet?.close();
  4480. const update = await this.token.update(tokenData);
  4481. if ( renderSheet ) this.sheet?.render(true);
  4482. return update;
  4483. }
  4484. // Close sheet for non-transformed Actor
  4485. await this.sheet?.close();
  4486. /**
  4487. * A hook event that fires just before the actor is transformed.
  4488. * @function dnd5e.transformActor
  4489. * @memberof hookEvents
  4490. * @param {Actor5e} actor The original actor before transformation.
  4491. * @param {Actor5e} target The target actor into which to transform.
  4492. * @param {object} data The data that will be used to create the new transformed actor.
  4493. * @param {TransformationOptions} options Options that determine how the transformation is performed.
  4494. * @param {object} [options]
  4495. */
  4496. Hooks.callAll("dnd5e.transformActor", this, target, d, {
  4497. keepPhysical, keepMental, keepSaves, keepSkills, mergeSaves, mergeSkills, keepClass, keepFeats, keepSpells,
  4498. keepItems, keepBio, keepVision, keepSelf, keepAE, keepOriginAE, keepOtherOriginAE, keepSpellAE,
  4499. keepEquipmentAE, keepFeatAE, keepClassAE, keepBackgroundAE, transformTokens
  4500. }, {renderSheet});
  4501. // Create new Actor with transformed data
  4502. const newActor = await this.constructor.create(d, {renderSheet});
  4503. // Update placed Token instances
  4504. if ( !transformTokens ) return;
  4505. const tokens = this.getActiveTokens(true);
  4506. const updates = tokens.map(t => {
  4507. const newTokenData = foundry.utils.deepClone(d.prototypeToken);
  4508. newTokenData._id = t.id;
  4509. newTokenData.actorId = newActor.id;
  4510. newTokenData.actorLink = true;
  4511. const dOriginalActor = foundry.utils.getProperty(d, "flags.dnd5e.originalActor");
  4512. foundry.utils.setProperty(newTokenData, "flags.dnd5e.originalActor", dOriginalActor);
  4513. foundry.utils.setProperty(newTokenData, "flags.dnd5e.isPolymorphed", true);
  4514. return newTokenData;
  4515. });
  4516. return canvas.scene?.updateEmbeddedDocuments("Token", updates);
  4517. }
  4518. /* -------------------------------------------- */
  4519. /**
  4520. * If this actor was transformed with transformTokens enabled, then its
  4521. * active tokens need to be returned to their original state. If not, then
  4522. * we can safely just delete this actor.
  4523. * @param {object} [options]
  4524. * @param {boolean} [options.renderSheet=true] Render Sheet after revert the transformation.
  4525. * @returns {Promise<Actor>|null} Original actor if it was reverted.
  4526. */
  4527. async revertOriginalForm({renderSheet=true}={}) {
  4528. if ( !this.isPolymorphed ) return;
  4529. if ( !this.isOwner ) return ui.notifications.warn(game.i18n.localize("DND5E.PolymorphRevertWarn"));
  4530. /**
  4531. * A hook event that fires just before the actor is reverted to original form.
  4532. * @function dnd5e.revertOriginalForm
  4533. * @memberof hookEvents
  4534. * @param {Actor} this The original actor before transformation.
  4535. * @param {object} [options]
  4536. */
  4537. Hooks.callAll("dnd5e.revertOriginalForm", this, {renderSheet});
  4538. const previousActorIds = this.getFlag("dnd5e", "previousActorIds") ?? [];
  4539. const isOriginalActor = !previousActorIds.length;
  4540. const isRendered = this.sheet.rendered;
  4541. // Obtain a reference to the original actor
  4542. const original = game.actors.get(this.getFlag("dnd5e", "originalActor"));
  4543. // If we are reverting an unlinked token, grab the previous actorData, and create a new token
  4544. if ( this.isToken ) {
  4545. const baseActor = original ? original : game.actors.get(this.token.actorId);
  4546. if ( !baseActor ) {
  4547. ui.notifications.warn(game.i18n.format("DND5E.PolymorphRevertNoOriginalActorWarn", {
  4548. reference: this.getFlag("dnd5e", "originalActor")
  4549. }));
  4550. return;
  4551. }
  4552. const prototypeTokenData = await baseActor.getTokenDocument();
  4553. const actorData = this.token.getFlag("dnd5e", "previousActorData");
  4554. const tokenUpdate = this.token.toObject();
  4555. if ( game.dnd5e.isV10 ) tokenUpdate.actorData = actorData ?? {};
  4556. else {
  4557. actorData._id = tokenUpdate.delta._id;
  4558. tokenUpdate.delta = actorData;
  4559. }
  4560. for ( const k of ["width", "height", "alpha", "lockRotation", "name"] ) {
  4561. tokenUpdate[k] = prototypeTokenData[k];
  4562. }
  4563. for ( const k of ["offsetX", "offsetY", "scaleX", "scaleY", "src", "tint"] ) {
  4564. tokenUpdate.texture[k] = prototypeTokenData.texture[k];
  4565. }
  4566. tokenUpdate.sight = prototypeTokenData.sight;
  4567. tokenUpdate.detectionModes = prototypeTokenData.detectionModes;
  4568. await this.sheet.close();
  4569. await canvas.scene?.deleteEmbeddedDocuments("Token", [this.token._id]);
  4570. const token = await TokenDocument.implementation.create(tokenUpdate, {
  4571. parent: canvas.scene, keepId: true, render: true
  4572. });
  4573. if ( isOriginalActor ) {
  4574. await this.unsetFlag("dnd5e", "isPolymorphed");
  4575. await this.unsetFlag("dnd5e", "previousActorIds");
  4576. await this.token.unsetFlag("dnd5e", "previousActorData");
  4577. }
  4578. if ( isRendered && renderSheet ) token.actor?.sheet?.render(true);
  4579. return token;
  4580. }
  4581. if ( !original ) {
  4582. ui.notifications.warn(game.i18n.format("DND5E.PolymorphRevertNoOriginalActorWarn", {
  4583. reference: this.getFlag("dnd5e", "originalActor")
  4584. }));
  4585. return;
  4586. }
  4587. // Get the Tokens which represent this actor
  4588. if ( canvas.ready ) {
  4589. const tokens = this.getActiveTokens(true);
  4590. const tokenData = await original.getTokenDocument();
  4591. const tokenUpdates = tokens.map(t => {
  4592. const update = duplicate(tokenData);
  4593. update._id = t.id;
  4594. delete update.x;
  4595. delete update.y;
  4596. return update;
  4597. });
  4598. await canvas.scene.updateEmbeddedDocuments("Token", tokenUpdates);
  4599. }
  4600. if ( isOriginalActor ) {
  4601. await this.unsetFlag("dnd5e", "isPolymorphed");
  4602. await this.unsetFlag("dnd5e", "previousActorIds");
  4603. }
  4604. // Delete the polymorphed version(s) of the actor, if possible
  4605. if ( game.user.isGM ) {
  4606. const idsToDelete = previousActorIds.filter(id =>
  4607. id !== original.id // Is not original Actor Id
  4608. && game.actors?.get(id) // Actor still exists
  4609. ).concat([this.id]); // Add this id
  4610. await Actor.implementation.deleteDocuments(idsToDelete);
  4611. } else if ( isRendered ) {
  4612. this.sheet?.close();
  4613. }
  4614. if ( isRendered && renderSheet ) original.sheet?.render(isRendered);
  4615. return original;
  4616. }
  4617. /* -------------------------------------------- */
  4618. /**
  4619. * Add additional system-specific sidebar directory context menu options for Actor documents
  4620. * @param {jQuery} html The sidebar HTML
  4621. * @param {Array} entryOptions The default array of context menu options
  4622. */
  4623. static addDirectoryContextOptions(html, entryOptions) {
  4624. entryOptions.push({
  4625. name: "DND5E.PolymorphRestoreTransformation",
  4626. icon: '<i class="fas fa-backward"></i>',
  4627. callback: li => {
  4628. const actor = game.actors.get(li.data("documentId"));
  4629. return actor.revertOriginalForm();
  4630. },
  4631. condition: li => {
  4632. const allowed = game.settings.get("dnd5e", "allowPolymorphing");
  4633. if ( !allowed && !game.user.isGM ) return false;
  4634. const actor = game.actors.get(li.data("documentId"));
  4635. return actor && actor.isPolymorphed;
  4636. }
  4637. });
  4638. }
  4639. /* -------------------------------------------- */
  4640. /**
  4641. * Format a type object into a string.
  4642. * @param {object} typeData The type data to convert to a string.
  4643. * @returns {string}
  4644. */
  4645. static formatCreatureType(typeData) {
  4646. if ( typeof typeData === "string" ) return typeData; // Backwards compatibility
  4647. let localizedType;
  4648. if ( typeData.value === "custom" ) {
  4649. localizedType = typeData.custom;
  4650. } else {
  4651. let code = CONFIG.DND5E.creatureTypes[typeData.value];
  4652. localizedType = game.i18n.localize(typeData.swarm ? `${code}Pl` : code);
  4653. }
  4654. let type = localizedType;
  4655. if ( typeData.swarm ) {
  4656. type = game.i18n.format("DND5E.CreatureSwarmPhrase", {
  4657. size: game.i18n.localize(CONFIG.DND5E.actorSizes[typeData.swarm]),
  4658. type: localizedType
  4659. });
  4660. }
  4661. if (typeData.subtype) type = `${type} (${typeData.subtype})`;
  4662. return type;
  4663. }
  4664. /* -------------------------------------------- */
  4665. /* Event Listeners and Handlers */
  4666. /* -------------------------------------------- */
  4667. /** @inheritdoc */
  4668. _onUpdate(data, options, userId) {
  4669. super._onUpdate(data, options, userId);
  4670. this._displayScrollingDamage(options.dhp);
  4671. }
  4672. /* -------------------------------------------- */
  4673. /**
  4674. * Display changes to health as scrolling combat text.
  4675. * Adapt the font size relative to the Actor's HP total to emphasize more significant blows.
  4676. * @param {number} dhp The change in hit points that was applied
  4677. * @private
  4678. */
  4679. _displayScrollingDamage(dhp) {
  4680. if ( !dhp ) return;
  4681. dhp = Number(dhp);
  4682. const tokens = this.isToken ? [this.token?.object] : this.getActiveTokens(true);
  4683. for ( const t of tokens ) {
  4684. const pct = Math.clamped(Math.abs(dhp) / this.system.attributes.hp.max, 0, 1);
  4685. canvas.interface.createScrollingText(t.center, dhp.signedString(), {
  4686. anchor: CONST.TEXT_ANCHOR_POINTS.TOP,
  4687. fontSize: 16 + (32 * pct), // Range between [16, 48]
  4688. fill: CONFIG.DND5E.tokenHPColors[dhp < 0 ? "damage" : "healing"],
  4689. stroke: 0x000000,
  4690. strokeThickness: 4,
  4691. jitter: 0.25
  4692. });
  4693. }
  4694. }
  4695. }
  4696. /**
  4697. * Inline application that presents the player with a choice of items.
  4698. */
  4699. class ItemChoiceFlow extends ItemGrantFlow {
  4700. /**
  4701. * Set of selected UUIDs.
  4702. * @type {Set<string>}
  4703. */
  4704. selected;
  4705. /**
  4706. * Cached items from the advancement's pool.
  4707. * @type {Item5e[]}
  4708. */
  4709. pool;
  4710. /**
  4711. * List of dropped items.
  4712. * @type {Item5e[]}
  4713. */
  4714. dropped;
  4715. /* -------------------------------------------- */
  4716. /** @inheritdoc */
  4717. static get defaultOptions() {
  4718. return foundry.utils.mergeObject(super.defaultOptions, {
  4719. dragDrop: [{ dropSelector: ".drop-target" }],
  4720. template: "systems/dnd5e/templates/advancement/item-choice-flow.hbs"
  4721. });
  4722. }
  4723. /* -------------------------------------------- */
  4724. /** @inheritdoc */
  4725. async getContext() {
  4726. this.selected ??= new Set(
  4727. this.retainedData?.items.map(i => foundry.utils.getProperty(i, "flags.dnd5e.sourceId"))
  4728. ?? Object.values(this.advancement.value[this.level] ?? {})
  4729. );
  4730. this.pool ??= await Promise.all(this.advancement.configuration.pool.map(uuid => fromUuid(uuid)));
  4731. if ( !this.dropped ) {
  4732. this.dropped = [];
  4733. for ( const data of this.retainedData?.items ?? [] ) {
  4734. const uuid = foundry.utils.getProperty(data, "flags.dnd5e.sourceId");
  4735. if ( this.pool.find(i => uuid === i.uuid) ) continue;
  4736. const item = await fromUuid(uuid);
  4737. item.dropped = true;
  4738. this.dropped.push(item);
  4739. }
  4740. }
  4741. const max = this.advancement.configuration.choices[this.level];
  4742. const choices = { max, current: this.selected.size, full: this.selected.size >= max };
  4743. const previousLevels = {};
  4744. const previouslySelected = new Set();
  4745. for ( const [level, data] of Object.entries(this.advancement.value.added ?? {}) ) {
  4746. if ( level > this.level ) continue;
  4747. previousLevels[level] = await Promise.all(Object.values(data).map(uuid => fromUuid(uuid)));
  4748. Object.values(data).forEach(uuid => previouslySelected.add(uuid));
  4749. }
  4750. const items = [...this.pool, ...this.dropped].reduce((items, i) => {
  4751. i.checked = this.selected.has(i.uuid);
  4752. i.disabled = !i.checked && choices.full;
  4753. if ( !previouslySelected.has(i.uuid) ) items.push(i);
  4754. return items;
  4755. }, []);
  4756. return { choices, items, previousLevels };
  4757. }
  4758. /* -------------------------------------------- */
  4759. /** @inheritdoc */
  4760. activateListeners(html) {
  4761. super.activateListeners(html);
  4762. html.find(".item-delete").click(this._onItemDelete.bind(this));
  4763. }
  4764. /* -------------------------------------------- */
  4765. /** @inheritdoc */
  4766. _onChangeInput(event) {
  4767. if ( event.target.checked ) this.selected.add(event.target.name);
  4768. else this.selected.delete(event.target.name);
  4769. this.render();
  4770. }
  4771. /* -------------------------------------------- */
  4772. /**
  4773. * Handle deleting a dropped item.
  4774. * @param {Event} event The originating click event.
  4775. * @protected
  4776. */
  4777. async _onItemDelete(event) {
  4778. event.preventDefault();
  4779. const uuidToDelete = event.currentTarget.closest(".item-name")?.querySelector("input")?.name;
  4780. if ( !uuidToDelete ) return;
  4781. this.dropped.findSplice(i => i.uuid === uuidToDelete);
  4782. this.selected.delete(uuidToDelete);
  4783. this.render();
  4784. }
  4785. /* -------------------------------------------- */
  4786. /** @inheritdoc */
  4787. async _onDrop(event) {
  4788. if ( this.selected.size >= this.advancement.configuration.choices[this.level] ) return false;
  4789. // Try to extract the data
  4790. let data;
  4791. try {
  4792. data = JSON.parse(event.dataTransfer.getData("text/plain"));
  4793. } catch(err) {
  4794. return false;
  4795. }
  4796. if ( data.type !== "Item" ) return false;
  4797. const item = await Item.implementation.fromDropData(data);
  4798. try {
  4799. this.advancement._validateItemType(item);
  4800. } catch(err) {
  4801. return ui.notifications.error(err.message);
  4802. }
  4803. // If the item is already been marked as selected, no need to go further
  4804. if ( this.selected.has(item.uuid) ) return false;
  4805. // Check to ensure the dropped item hasn't been selected at a lower level
  4806. for ( const [level, data] of Object.entries(this.advancement.value.added ?? {}) ) {
  4807. if ( level >= this.level ) continue;
  4808. if ( Object.values(data).includes(item.uuid) ) {
  4809. return ui.notifications.error(game.i18n.localize("DND5E.AdvancementItemChoicePreviouslyChosenWarning"));
  4810. }
  4811. }
  4812. // If spell level is restricted to available level, ensure the spell is of the appropriate level
  4813. const spellLevel = this.advancement.configuration.restriction.level;
  4814. if ( (this.advancement.configuration.type === "spell") && spellLevel === "available" ) {
  4815. const maxSlot = this._maxSpellSlotLevel();
  4816. if ( item.system.level > maxSlot ) return ui.notifications.error(game.i18n.format(
  4817. "DND5E.AdvancementItemChoiceSpellLevelAvailableWarning", { level: CONFIG.DND5E.spellLevels[maxSlot] }
  4818. ));
  4819. }
  4820. // Mark the item as selected
  4821. this.selected.add(item.uuid);
  4822. // If the item doesn't already exist in the pool, add it
  4823. if ( !this.pool.find(i => i.uuid === item.uuid) ) {
  4824. this.dropped.push(item);
  4825. item.dropped = true;
  4826. }
  4827. this.render();
  4828. }
  4829. /* -------------------------------------------- */
  4830. /**
  4831. * Determine the maximum spell slot level for the actor to which this advancement is being applied.
  4832. * @returns {number}
  4833. */
  4834. _maxSpellSlotLevel() {
  4835. const spellcasting = this.advancement.item.spellcasting;
  4836. let spells;
  4837. // For advancements on classes or subclasses, use the largest slot available for that class
  4838. if ( spellcasting ) {
  4839. const progression = { slot: 0, pact: {} };
  4840. const maxSpellLevel = CONFIG.DND5E.SPELL_SLOT_TABLE[CONFIG.DND5E.SPELL_SLOT_TABLE.length - 1].length;
  4841. spells = Object.fromEntries(Array.fromRange(maxSpellLevel, 1).map(l => [`spell${l}`, {}]));
  4842. Actor5e.computeClassProgression(progression, this.advancement.item, { spellcasting });
  4843. Actor5e.prepareSpellcastingSlots(spells, spellcasting.type, progression);
  4844. }
  4845. // For all other items, use the largest slot possible
  4846. else spells = this.advancement.actor.system.spells;
  4847. const largestSlot = Object.entries(spells).reduce((slot, [key, data]) => {
  4848. if ( data.max === 0 ) return slot;
  4849. const level = parseInt(key.replace("spell", ""));
  4850. if ( !Number.isNaN(level) && level > slot ) return level;
  4851. return slot;
  4852. }, -1);
  4853. return Math.max(spells.pact?.level ?? 0, largestSlot);
  4854. }
  4855. }
  4856. class ItemChoiceConfigurationData extends foundry.abstract.DataModel {
  4857. static defineSchema() {
  4858. return {
  4859. hint: new foundry.data.fields.StringField({label: "DND5E.AdvancementHint"}),
  4860. choices: new MappingField(new foundry.data.fields.NumberField(), {
  4861. hint: "DND5E.AdvancementItemChoiceLevelsHint"
  4862. }),
  4863. allowDrops: new foundry.data.fields.BooleanField({
  4864. initial: true, label: "DND5E.AdvancementConfigureAllowDrops",
  4865. hint: "DND5E.AdvancementConfigureAllowDropsHint"
  4866. }),
  4867. type: new foundry.data.fields.StringField({
  4868. blank: false, nullable: true, initial: null,
  4869. label: "DND5E.AdvancementItemChoiceType", hint: "DND5E.AdvancementItemChoiceTypeHint"
  4870. }),
  4871. pool: new foundry.data.fields.ArrayField(new foundry.data.fields.StringField(), {label: "DOCUMENT.Items"}),
  4872. spell: new foundry.data.fields.EmbeddedDataField(SpellConfigurationData, {nullable: true, initial: null}),
  4873. restriction: new foundry.data.fields.SchemaField({
  4874. type: new foundry.data.fields.StringField({label: "DND5E.Type"}),
  4875. subtype: new foundry.data.fields.StringField({label: "DND5E.Subtype"}),
  4876. level: new foundry.data.fields.StringField({label: "DND5E.SpellLevel"})
  4877. })
  4878. };
  4879. }
  4880. }
  4881. /**
  4882. * Advancement that presents the player with a choice of multiple items that they can take. Keeps track of which
  4883. * items were selected at which levels.
  4884. */
  4885. class ItemChoiceAdvancement extends ItemGrantAdvancement {
  4886. /** @inheritdoc */
  4887. static get metadata() {
  4888. return foundry.utils.mergeObject(super.metadata, {
  4889. dataModels: {
  4890. configuration: ItemChoiceConfigurationData
  4891. },
  4892. order: 50,
  4893. icon: "systems/dnd5e/icons/svg/item-choice.svg",
  4894. title: game.i18n.localize("DND5E.AdvancementItemChoiceTitle"),
  4895. hint: game.i18n.localize("DND5E.AdvancementItemChoiceHint"),
  4896. multiLevel: true,
  4897. apps: {
  4898. config: ItemChoiceConfig,
  4899. flow: ItemChoiceFlow
  4900. }
  4901. });
  4902. }
  4903. /* -------------------------------------------- */
  4904. /* Instance Properties */
  4905. /* -------------------------------------------- */
  4906. /** @inheritdoc */
  4907. get levels() {
  4908. return Array.from(Object.keys(this.configuration.choices));
  4909. }
  4910. /* -------------------------------------------- */
  4911. /* Display Methods */
  4912. /* -------------------------------------------- */
  4913. /** @inheritdoc */
  4914. configuredForLevel(level) {
  4915. return this.value.added?.[level] !== undefined;
  4916. }
  4917. /* -------------------------------------------- */
  4918. /** @inheritdoc */
  4919. titleForLevel(level, { configMode=false }={}) {
  4920. return `${this.title} <em>(${game.i18n.localize("DND5E.AdvancementChoices")})</em>`;
  4921. }
  4922. /* -------------------------------------------- */
  4923. /** @inheritdoc */
  4924. summaryForLevel(level, { configMode=false }={}) {
  4925. const items = this.value.added?.[level];
  4926. if ( !items || configMode ) return "";
  4927. return Object.values(items).reduce((html, uuid) => html + game.dnd5e.utils.linkForUuid(uuid), "");
  4928. }
  4929. /* -------------------------------------------- */
  4930. /* Application Methods */
  4931. /* -------------------------------------------- */
  4932. /** @inheritdoc */
  4933. storagePath(level) {
  4934. return `value.added.${level}`;
  4935. }
  4936. /* -------------------------------------------- */
  4937. /**
  4938. * Verify that the provided item can be used with this advancement based on the configuration.
  4939. * @param {Item5e} item Item that needs to be tested.
  4940. * @param {object} config
  4941. * @param {string} config.type Type restriction on this advancement.
  4942. * @param {object} config.restriction Additional restrictions to be applied.
  4943. * @param {boolean} [config.strict=true] Should an error be thrown when an invalid type is encountered?
  4944. * @returns {boolean} Is this type valid?
  4945. * @throws An error if the item is invalid and strict is `true`.
  4946. */
  4947. _validateItemType(item, { type, restriction, strict=true }={}) {
  4948. super._validateItemType(item, { strict });
  4949. type ??= this.configuration.type;
  4950. restriction ??= this.configuration.restriction;
  4951. // Type restriction is set and the item type does not match the selected type
  4952. if ( type && (type !== item.type) ) {
  4953. const typeLabel = game.i18n.localize(CONFIG.Item.typeLabels[restriction]);
  4954. if ( strict ) throw new Error(game.i18n.format("DND5E.AdvancementItemChoiceTypeWarning", {type: typeLabel}));
  4955. return false;
  4956. }
  4957. // If additional type restrictions applied, make sure they are valid
  4958. if ( (type === "feat") && restriction.type ) {
  4959. const typeConfig = CONFIG.DND5E.featureTypes[restriction.type];
  4960. const subtype = typeConfig.subtypes?.[restriction.subtype];
  4961. let errorLabel;
  4962. if ( restriction.type !== item.system.type.value ) errorLabel = typeConfig.label;
  4963. else if ( subtype && (restriction.subtype !== item.system.type.subtype) ) errorLabel = subtype;
  4964. if ( errorLabel ) {
  4965. if ( strict ) throw new Error(game.i18n.format("DND5E.AdvancementItemChoiceTypeWarning", {type: errorLabel}));
  4966. return false;
  4967. }
  4968. }
  4969. // If spell level is restricted, ensure the spell is of the appropriate level
  4970. const l = parseInt(restriction.level);
  4971. if ( (type === "spell") && !Number.isNaN(l) && (item.system.level !== l) ) {
  4972. const level = CONFIG.DND5E.spellLevels[l];
  4973. if ( strict ) throw new Error(game.i18n.format("DND5E.AdvancementItemChoiceSpellLevelSpecificWarning", {level}));
  4974. return false;
  4975. }
  4976. return true;
  4977. }
  4978. }
  4979. /**
  4980. * Data model for the Scale Value advancement type.
  4981. *
  4982. * @property {string} identifier Identifier used to select this scale value in roll formulas.
  4983. * @property {string} type Type of data represented by this scale value.
  4984. * @property {object} [distance]
  4985. * @property {string} [distance.units] If distance type is selected, the units each value uses.
  4986. * @property {Object<string, *>} scale Scale values for each level. Value format is determined by type.
  4987. */
  4988. class ScaleValueConfigurationData extends foundry.abstract.DataModel {
  4989. /** @inheritdoc */
  4990. static defineSchema() {
  4991. return {
  4992. identifier: new IdentifierField({required: true, label: "DND5E.Identifier"}),
  4993. type: new foundry.data.fields.StringField({
  4994. required: true, initial: "string", choices: TYPES, label: "DND5E.AdvancementScaleValueTypeLabel"
  4995. }),
  4996. distance: new foundry.data.fields.SchemaField({
  4997. units: new foundry.data.fields.StringField({required: true, label: "DND5E.MovementUnits"})
  4998. }),
  4999. scale: new MappingField(new ScaleValueEntryField(), {required: true})
  5000. };
  5001. }
  5002. /* -------------------------------------------- */
  5003. /** @inheritdoc */
  5004. static migrateData(source) {
  5005. super.migrateData(source);
  5006. if ( source.type === "numeric" ) source.type = "number";
  5007. Object.values(source.scale ?? {}).forEach(v => TYPES[source.type].migrateData(v));
  5008. }
  5009. }
  5010. /**
  5011. * Data field that automatically selects the appropriate ScaleValueType based on the selected type.
  5012. */
  5013. class ScaleValueEntryField extends foundry.data.fields.ObjectField {
  5014. /** @override */
  5015. _cleanType(value, options) {
  5016. if ( !(typeof value === "object") ) value = {};
  5017. // Use a defined DataModel
  5018. const cls = TYPES[options.source?.type];
  5019. if ( cls ) return cls.cleanData(value, options);
  5020. return value;
  5021. }
  5022. /* -------------------------------------------- */
  5023. /** @override */
  5024. initialize(value, model) {
  5025. const cls = TYPES[model.type];
  5026. if ( !value || !cls ) return value;
  5027. return new cls(value, {parent: model});
  5028. }
  5029. /* -------------------------------------------- */
  5030. /** @override */
  5031. toObject(value) {
  5032. return value.toObject(false);
  5033. }
  5034. }
  5035. /**
  5036. * Base scale value data type that stores generic string values.
  5037. *
  5038. * @property {string} value String value.
  5039. */
  5040. class ScaleValueType extends foundry.abstract.DataModel {
  5041. /** @inheritdoc */
  5042. static defineSchema() {
  5043. return {
  5044. value: new foundry.data.fields.StringField({required: true})
  5045. };
  5046. }
  5047. /* -------------------------------------------- */
  5048. /**
  5049. * Information on how a scale value of this type is configured.
  5050. *
  5051. * @typedef {object} ScaleValueTypeMetadata
  5052. * @property {string} label Name of this type.
  5053. * @property {string} hint Hint for this type shown in the scale value configuration.
  5054. * @property {boolean} isNumeric When using the default editing interface, should numeric inputs be used?
  5055. */
  5056. /**
  5057. * Configuration information for this scale value type.
  5058. * @type {ScaleValueTypeMetadata}
  5059. */
  5060. static get metadata() {
  5061. return {
  5062. label: "DND5E.AdvancementScaleValueTypeString",
  5063. hint: "DND5E.AdvancementScaleValueTypeHintString",
  5064. isNumeric: false
  5065. };
  5066. }
  5067. /* -------------------------------------------- */
  5068. /**
  5069. * Attempt to convert another scale value type to this one.
  5070. * @param {ScaleValueType} original Original type to attempt to convert.
  5071. * @param {object} [options] Options which affect DataModel construction.
  5072. * @returns {ScaleValueType|null}
  5073. */
  5074. static convertFrom(original, options) {
  5075. return new this({value: original.formula}, options);
  5076. }
  5077. /* -------------------------------------------- */
  5078. /**
  5079. * This scale value prepared to be used in roll formulas.
  5080. * @type {string|null}
  5081. */
  5082. get formula() { return this.value; }
  5083. /* -------------------------------------------- */
  5084. /**
  5085. * This scale value formatted for display.
  5086. * @type {string|null}
  5087. */
  5088. get display() { return this.formula; }
  5089. /* -------------------------------------------- */
  5090. /**
  5091. * Shortcut to the prepared value when used in roll formulas.
  5092. * @returns {string}
  5093. */
  5094. toString() {
  5095. return this.formula;
  5096. }
  5097. }
  5098. /**
  5099. * Scale value data type that stores numeric values.
  5100. *
  5101. * @property {number} value Numeric value.
  5102. */
  5103. class ScaleValueTypeNumber extends ScaleValueType {
  5104. /** @inheritdoc */
  5105. static defineSchema() {
  5106. return {
  5107. value: new foundry.data.fields.NumberField({required: true})
  5108. };
  5109. }
  5110. /* -------------------------------------------- */
  5111. /** @inheritdoc */
  5112. static get metadata() {
  5113. return foundry.utils.mergeObject(super.metadata, {
  5114. label: "DND5E.AdvancementScaleValueTypeNumber",
  5115. hint: "DND5E.AdvancementScaleValueTypeHintNumber",
  5116. isNumeric: true
  5117. });
  5118. }
  5119. /* -------------------------------------------- */
  5120. /** @inheritdoc */
  5121. static convertFrom(original, options) {
  5122. const value = Number(original.formula);
  5123. if ( Number.isNaN(value) ) return null;
  5124. return new this({value}, options);
  5125. }
  5126. }
  5127. /**
  5128. * Scale value data type that stores challenge ratings.
  5129. *
  5130. * @property {number} value CR value.
  5131. */
  5132. class ScaleValueTypeCR extends ScaleValueTypeNumber {
  5133. /** @inheritdoc */
  5134. static defineSchema() {
  5135. return {
  5136. value: new foundry.data.fields.NumberField({required: true, min: 0})
  5137. // TODO: Add CR validator
  5138. };
  5139. }
  5140. /* -------------------------------------------- */
  5141. /** @inheritdoc */
  5142. static get metadata() {
  5143. return foundry.utils.mergeObject(super.metadata, {
  5144. label: "DND5E.AdvancementScaleValueTypeCR",
  5145. hint: "DND5E.AdvancementScaleValueTypeHintCR"
  5146. });
  5147. }
  5148. /* -------------------------------------------- */
  5149. /** @inheritdoc */
  5150. get display() {
  5151. switch ( this.value ) {
  5152. case 0.125: return "&frac18;";
  5153. case 0.25: return "&frac14;";
  5154. case 0.5: return "&frac12;";
  5155. default: return super.display;
  5156. }
  5157. }
  5158. }
  5159. /**
  5160. * Scale value data type that stores dice values.
  5161. *
  5162. * @property {number} number Number of dice.
  5163. * @property {number} faces Die faces.
  5164. */
  5165. class ScaleValueTypeDice extends ScaleValueType {
  5166. /** @inheritdoc */
  5167. static defineSchema() {
  5168. return {
  5169. number: new foundry.data.fields.NumberField({nullable: true, integer: true, positive: true}),
  5170. faces: new foundry.data.fields.NumberField({required: true, integer: true, positive: true})
  5171. };
  5172. }
  5173. /* -------------------------------------------- */
  5174. /** @inheritdoc */
  5175. static get metadata() {
  5176. return foundry.utils.mergeObject(super.metadata, {
  5177. label: "DND5E.AdvancementScaleValueTypeDice",
  5178. hint: "DND5E.AdvancementScaleValueTypeHintDice"
  5179. });
  5180. }
  5181. /* -------------------------------------------- */
  5182. /**
  5183. * List of die faces that can be chosen.
  5184. * @type {number[]}
  5185. */
  5186. static FACES = [2, 3, 4, 6, 8, 10, 12, 20, 100];
  5187. /* -------------------------------------------- */
  5188. /** @inheritdoc */
  5189. static convertFrom(original, options) {
  5190. const [number, faces] = (original.formula ?? "").split("d");
  5191. if ( !faces || !Number.isNumeric(number) || !Number.isNumeric(faces) ) return null;
  5192. return new this({number: Number(number) || null, faces: Number(faces)}, options);
  5193. }
  5194. /* -------------------------------------------- */
  5195. /** @inheritdoc */
  5196. get formula() {
  5197. if ( !this.faces ) return null;
  5198. return `${this.number ?? ""}${this.die}`;
  5199. }
  5200. /* -------------------------------------------- */
  5201. /**
  5202. * The die value to be rolled with the leading "d" (e.g. "d4").
  5203. * @type {string}
  5204. */
  5205. get die() {
  5206. if ( !this.faces ) return "";
  5207. return `d${this.faces}`;
  5208. }
  5209. /* -------------------------------------------- */
  5210. /** @inheritdoc */
  5211. static migrateData(source) {
  5212. if ( source.n ) source.number = source.n;
  5213. if ( source.die ) source.faces = source.die;
  5214. }
  5215. }
  5216. /**
  5217. * Scale value data type that stores distance values.
  5218. *
  5219. * @property {number} value Numeric value.
  5220. */
  5221. class ScaleValueTypeDistance extends ScaleValueTypeNumber {
  5222. /** @inheritdoc */
  5223. static get metadata() {
  5224. return foundry.utils.mergeObject(super.metadata, {
  5225. label: "DND5E.AdvancementScaleValueTypeDistance",
  5226. hint: "DND5E.AdvancementScaleValueTypeHintDistance"
  5227. });
  5228. }
  5229. /* -------------------------------------------- */
  5230. /** @inheritdoc */
  5231. get display() {
  5232. return `${this.value} ${CONFIG.DND5E.movementUnits[this.parent.configuration.distance?.units ?? "ft"]}`;
  5233. }
  5234. }
  5235. /**
  5236. * The available types of scaling value.
  5237. * @enum {ScaleValueType}
  5238. */
  5239. const TYPES = {
  5240. string: ScaleValueType,
  5241. number: ScaleValueTypeNumber,
  5242. cr: ScaleValueTypeCR,
  5243. dice: ScaleValueTypeDice,
  5244. distance: ScaleValueTypeDistance
  5245. };
  5246. var scaleValue = /*#__PURE__*/Object.freeze({
  5247. __proto__: null,
  5248. ScaleValueConfigurationData: ScaleValueConfigurationData,
  5249. ScaleValueEntryField: ScaleValueEntryField,
  5250. ScaleValueType: ScaleValueType,
  5251. ScaleValueTypeCR: ScaleValueTypeCR,
  5252. ScaleValueTypeDice: ScaleValueTypeDice,
  5253. ScaleValueTypeDistance: ScaleValueTypeDistance,
  5254. ScaleValueTypeNumber: ScaleValueTypeNumber,
  5255. TYPES: TYPES
  5256. });
  5257. /**
  5258. * Configuration application for scale values.
  5259. */
  5260. class ScaleValueConfig extends AdvancementConfig {
  5261. /** @inheritdoc */
  5262. static get defaultOptions() {
  5263. return foundry.utils.mergeObject(super.defaultOptions, {
  5264. classes: ["dnd5e", "advancement", "scale-value", "two-column"],
  5265. template: "systems/dnd5e/templates/advancement/scale-value-config.hbs",
  5266. width: 540
  5267. });
  5268. }
  5269. /* -------------------------------------------- */
  5270. /** @inheritdoc */
  5271. getData() {
  5272. const config = this.advancement.configuration;
  5273. const type = TYPES[config.type];
  5274. return foundry.utils.mergeObject(super.getData(), {
  5275. classIdentifier: this.item.identifier,
  5276. previewIdentifier: config.identifier || this.advancement.title?.slugify()
  5277. || this.advancement.constructor.metadata.title.slugify(),
  5278. type: type.metadata,
  5279. types: Object.fromEntries(
  5280. Object.entries(TYPES).map(([key, d]) => [key, game.i18n.localize(d.metadata.label)])
  5281. ),
  5282. faces: Object.fromEntries(TYPES.dice.FACES.map(die => [die, `d${die}`])),
  5283. levels: this._prepareLevelData(),
  5284. movementUnits: CONFIG.DND5E.movementUnits
  5285. });
  5286. }
  5287. /* -------------------------------------------- */
  5288. /**
  5289. * Prepare the data to display at each of the scale levels.
  5290. * @returns {object}
  5291. * @protected
  5292. */
  5293. _prepareLevelData() {
  5294. let lastValue = null;
  5295. return Array.fromRange(CONFIG.DND5E.maxLevel + 1).slice(1).reduce((obj, level) => {
  5296. obj[level] = { placeholder: this._formatPlaceholder(lastValue), value: null };
  5297. const value = this.advancement.configuration.scale[level];
  5298. if ( value ) {
  5299. this._mergeScaleValues(value, lastValue);
  5300. obj[level].className = "new-scale-value";
  5301. obj[level].value = value;
  5302. lastValue = value;
  5303. }
  5304. return obj;
  5305. }, {});
  5306. }
  5307. /* -------------------------------------------- */
  5308. /**
  5309. * Formats the placeholder for this scale value.
  5310. * @param {*} placeholder
  5311. * @returns {object}
  5312. * @protected
  5313. */
  5314. _formatPlaceholder(placeholder) {
  5315. if ( this.advancement.configuration.type === "dice" ) {
  5316. return { number: placeholder?.number ?? "", faces: placeholder?.faces ? `d${placeholder.faces}` : "" };
  5317. }
  5318. return { value: placeholder?.value ?? "" };
  5319. }
  5320. /* -------------------------------------------- */
  5321. /**
  5322. * For scale values with multiple properties, have missing properties inherit from earlier filled-in values.
  5323. * @param {*} value The primary value.
  5324. * @param {*} lastValue The previous value.
  5325. */
  5326. _mergeScaleValues(value, lastValue) {
  5327. for ( const k of Object.keys(lastValue ?? {}) ) {
  5328. if ( value[k] == null ) value[k] = lastValue[k];
  5329. }
  5330. }
  5331. /* -------------------------------------------- */
  5332. /** @inheritdoc */
  5333. static _cleanedObject(object) {
  5334. return Object.entries(object).reduce((obj, [key, value]) => {
  5335. if ( Object.keys(value ?? {}).some(k => value[k]) ) obj[key] = value;
  5336. else obj[`-=${key}`] = null;
  5337. return obj;
  5338. }, {});
  5339. }
  5340. /* -------------------------------------------- */
  5341. /** @inheritdoc */
  5342. prepareConfigurationUpdate(configuration) {
  5343. // Ensure multiple values in a row are not the same
  5344. let lastValue = null;
  5345. for ( const [lvl, value] of Object.entries(configuration.scale) ) {
  5346. if ( this.advancement.testEquality(lastValue, value) ) configuration.scale[lvl] = null;
  5347. else if ( Object.keys(value ?? {}).some(k => value[k]) ) {
  5348. this._mergeScaleValues(value, lastValue);
  5349. lastValue = value;
  5350. }
  5351. }
  5352. configuration.scale = this.constructor._cleanedObject(configuration.scale);
  5353. return configuration;
  5354. }
  5355. /* -------------------------------------------- */
  5356. /** @inheritdoc */
  5357. activateListeners(html) {
  5358. super.activateListeners(html);
  5359. this.form.querySelector("input[name='title']").addEventListener("input", this._onChangeTitle.bind(this));
  5360. this.form.querySelector(".identifier-hint-copy").addEventListener("click", this._onIdentifierHintCopy.bind(this));
  5361. }
  5362. /* -------------------------------------------- */
  5363. /**
  5364. * Copies the full scale identifier hint to the clipboard.
  5365. * @param {Event} event The triggering click event.
  5366. * @protected
  5367. */
  5368. _onIdentifierHintCopy(event) {
  5369. const data = this.getData();
  5370. game.clipboard.copyPlainText(`@scale.${data.classIdentifier}.${data.previewIdentifier}`);
  5371. game.tooltip.activate(event.target, {text: game.i18n.localize("DND5E.IdentifierCopied"), direction: "UP"});
  5372. }
  5373. /* -------------------------------------------- */
  5374. /**
  5375. * If no identifier is manually entered, slugify the custom title and display as placeholder.
  5376. * @param {Event} event Change event to the title input.
  5377. */
  5378. _onChangeTitle(event) {
  5379. const slug = (event.target.value || this.advancement.constructor.metadata.title).slugify();
  5380. this.form.querySelector("input[name='configuration.identifier']").placeholder = slug;
  5381. }
  5382. /* -------------------------------------------- */
  5383. /** @inheritdoc */
  5384. async _updateObject(event, formData) {
  5385. const updates = foundry.utils.expandObject(formData);
  5386. const typeChange = "configuration.type" in formData;
  5387. if ( typeChange && (updates.configuration.type !== this.advancement.configuration.type) ) {
  5388. // Clear existing scale value data to prevent error during type update
  5389. await this.advancement.update(Array.fromRange(CONFIG.DND5E.maxLevel, 1).reduce((obj, lvl) => {
  5390. obj[`configuration.scale.-=${lvl}`] = null;
  5391. return obj;
  5392. }, {}));
  5393. updates.configuration.scale ??= {};
  5394. const OriginalType = TYPES[this.advancement.configuration.type];
  5395. const NewType = TYPES[updates.configuration.type];
  5396. for ( const [lvl, data] of Object.entries(updates.configuration.scale) ) {
  5397. const original = new OriginalType(data, { parent: this.advancement });
  5398. updates.configuration.scale[lvl] = NewType.convertFrom(original)?.toObject();
  5399. }
  5400. }
  5401. return super._updateObject(event, foundry.utils.flattenObject(updates));
  5402. }
  5403. }
  5404. /**
  5405. * Inline application that displays any changes to a scale value.
  5406. */
  5407. class ScaleValueFlow extends AdvancementFlow {
  5408. /** @inheritdoc */
  5409. static get defaultOptions() {
  5410. return foundry.utils.mergeObject(super.defaultOptions, {
  5411. template: "systems/dnd5e/templates/advancement/scale-value-flow.hbs"
  5412. });
  5413. }
  5414. /* -------------------------------------------- */
  5415. /** @inheritdoc */
  5416. getData() {
  5417. return foundry.utils.mergeObject(super.getData(), {
  5418. initial: this.advancement.valueForLevel(this.level - 1)?.display,
  5419. final: this.advancement.valueForLevel(this.level).display
  5420. });
  5421. }
  5422. }
  5423. /**
  5424. * Advancement that represents a value that scales with class level. **Can only be added to classes or subclasses.**
  5425. */
  5426. class ScaleValueAdvancement extends Advancement {
  5427. /** @inheritdoc */
  5428. static get metadata() {
  5429. return foundry.utils.mergeObject(super.metadata, {
  5430. dataModels: {
  5431. configuration: ScaleValueConfigurationData
  5432. },
  5433. order: 60,
  5434. icon: "systems/dnd5e/icons/svg/scale-value.svg",
  5435. title: game.i18n.localize("DND5E.AdvancementScaleValueTitle"),
  5436. hint: game.i18n.localize("DND5E.AdvancementScaleValueHint"),
  5437. multiLevel: true,
  5438. validItemTypes: new Set(["class", "subclass"]),
  5439. apps: {
  5440. config: ScaleValueConfig,
  5441. flow: ScaleValueFlow
  5442. }
  5443. });
  5444. }
  5445. /* -------------------------------------------- */
  5446. /**
  5447. * The available types of scaling value.
  5448. * @enum {ScaleValueType}
  5449. */
  5450. static TYPES = TYPES;
  5451. /* -------------------------------------------- */
  5452. /* Instance Properties */
  5453. /* -------------------------------------------- */
  5454. /** @inheritdoc */
  5455. get levels() {
  5456. return Array.from(Object.keys(this.configuration.scale).map(l => Number(l)));
  5457. }
  5458. /* -------------------------------------------- */
  5459. /**
  5460. * Identifier for this scale value, either manual value or the slugified title.
  5461. * @type {string}
  5462. */
  5463. get identifier() {
  5464. return this.configuration.identifier || this.title.slugify();
  5465. }
  5466. /* -------------------------------------------- */
  5467. /* Display Methods */
  5468. /* -------------------------------------------- */
  5469. /** @inheritdoc */
  5470. titleForLevel(level, { configMode=false }={}) {
  5471. const value = this.valueForLevel(level)?.display;
  5472. if ( !value ) return this.title;
  5473. return `${this.title}: <strong>${value}</strong>`;
  5474. }
  5475. /* -------------------------------------------- */
  5476. /**
  5477. * Scale value for the given level.
  5478. * @param {number} level Level for which to get the scale value.
  5479. * @returns {ScaleValueType} Scale value at the given level or null if none exists.
  5480. */
  5481. valueForLevel(level) {
  5482. const key = Object.keys(this.configuration.scale).reverse().find(l => Number(l) <= level);
  5483. const data = this.configuration.scale[key];
  5484. const TypeClass = this.constructor.TYPES[this.configuration.type];
  5485. if ( !data || !TypeClass ) return null;
  5486. return new TypeClass(data, { parent: this });
  5487. }
  5488. /* -------------------------------------------- */
  5489. /**
  5490. * Compare two scaling values and determine if they are equal.
  5491. * @param {*} a
  5492. * @param {*} b
  5493. * @returns {boolean}
  5494. */
  5495. testEquality(a, b) {
  5496. const keys = Object.keys(a ?? {});
  5497. if ( keys.length !== Object.keys(b ?? {}).length ) return false;
  5498. for ( const k of keys ) {
  5499. if ( a[k] !== b[k] ) return false;
  5500. }
  5501. return true;
  5502. }
  5503. }
  5504. var _module$a = /*#__PURE__*/Object.freeze({
  5505. __proto__: null,
  5506. Advancement: Advancement,
  5507. HitPointsAdvancement: HitPointsAdvancement,
  5508. ItemChoiceAdvancement: ItemChoiceAdvancement,
  5509. ItemGrantAdvancement: ItemGrantAdvancement,
  5510. ScaleValueAdvancement: ScaleValueAdvancement
  5511. });
  5512. // Namespace Configuration Values
  5513. const DND5E = {};
  5514. // ASCII Artwork
  5515. DND5E.ASCII = `_______________________________
  5516. ______ ______ _____ _____
  5517. | _ \\___ | _ \\ ___| ___|
  5518. | | | ( _ ) | | | |___ \\| |__
  5519. | | | / _ \\/\\ | | | \\ \\ __|
  5520. | |/ / (_> < |/ //\\__/ / |___
  5521. |___/ \\___/\\/___/ \\____/\\____/
  5522. _______________________________`;
  5523. /**
  5524. * Configuration data for abilities.
  5525. *
  5526. * @typedef {object} AbilityConfiguration
  5527. * @property {string} label Localized label.
  5528. * @property {string} abbreviation Localized abbreviation.
  5529. * @property {string} [type] Whether this is a "physical" or "mental" ability.
  5530. * @property {Object<string, number|string>} [defaults] Default values for this ability based on actor type.
  5531. * If a string is used, the system will attempt to fetch.
  5532. * the value of the specified ability.
  5533. */
  5534. /**
  5535. * The set of Ability Scores used within the system.
  5536. * @enum {AbilityConfiguration}
  5537. */
  5538. DND5E.abilities = {
  5539. str: {
  5540. label: "DND5E.AbilityStr",
  5541. abbreviation: "DND5E.AbilityStrAbbr",
  5542. type: "physical"
  5543. },
  5544. dex: {
  5545. label: "DND5E.AbilityDex",
  5546. abbreviation: "DND5E.AbilityDexAbbr",
  5547. type: "physical"
  5548. },
  5549. con: {
  5550. label: "DND5E.AbilityCon",
  5551. abbreviation: "DND5E.AbilityConAbbr",
  5552. type: "physical"
  5553. },
  5554. int: {
  5555. label: "DND5E.AbilityInt",
  5556. abbreviation: "DND5E.AbilityIntAbbr",
  5557. type: "mental",
  5558. defaults: { vehicle: 0 }
  5559. },
  5560. wis: {
  5561. label: "DND5E.AbilityWis",
  5562. abbreviation: "DND5E.AbilityWisAbbr",
  5563. type: "mental",
  5564. defaults: { vehicle: 0 }
  5565. },
  5566. cha: {
  5567. label: "DND5E.AbilityCha",
  5568. abbreviation: "DND5E.AbilityChaAbbr",
  5569. type: "mental",
  5570. defaults: { vehicle: 0 }
  5571. },
  5572. hon: {
  5573. label: "DND5E.AbilityHon",
  5574. abbreviation: "DND5E.AbilityHonAbbr",
  5575. type: "mental",
  5576. defaults: { npc: "cha", vehicle: 0 }
  5577. },
  5578. san: {
  5579. label: "DND5E.AbilitySan",
  5580. abbreviation: "DND5E.AbilitySanAbbr",
  5581. type: "mental",
  5582. defaults: { npc: "wis", vehicle: 0 }
  5583. }
  5584. };
  5585. preLocalize("abilities", { keys: ["label", "abbreviation"] });
  5586. patchConfig("abilities", "label", { since: 2.2, until: 2.4 });
  5587. Object.defineProperty(DND5E, "abilityAbbreviations", {
  5588. get() {
  5589. foundry.utils.logCompatibilityWarning(
  5590. "The `abilityAbbreviations` configuration object has been merged with `abilities`.",
  5591. { since: "DnD5e 2.2", until: "DnD5e 2.4" }
  5592. );
  5593. return Object.fromEntries(Object.entries(DND5E.abilities).map(([k, v]) => [k, v.abbreviation]));
  5594. }
  5595. });
  5596. /**
  5597. * Configure which ability score is used as the default modifier for initiative rolls.
  5598. * @type {string}
  5599. */
  5600. DND5E.initiativeAbility = "dex";
  5601. /**
  5602. * Configure which ability score is used when calculating hit points per level.
  5603. * @type {string}
  5604. */
  5605. DND5E.hitPointsAbility = "con";
  5606. /* -------------------------------------------- */
  5607. /**
  5608. * Configuration data for skills.
  5609. *
  5610. * @typedef {object} SkillConfiguration
  5611. * @property {string} label Localized label.
  5612. * @property {string} ability Key for the default ability used by this skill.
  5613. */
  5614. /**
  5615. * The set of skill which can be trained with their default ability scores.
  5616. * @enum {SkillConfiguration}
  5617. */
  5618. DND5E.skills = {
  5619. acr: { label: "DND5E.SkillAcr", ability: "dex" },
  5620. ani: { label: "DND5E.SkillAni", ability: "wis" },
  5621. arc: { label: "DND5E.SkillArc", ability: "int" },
  5622. ath: { label: "DND5E.SkillAth", ability: "str" },
  5623. dec: { label: "DND5E.SkillDec", ability: "cha" },
  5624. his: { label: "DND5E.SkillHis", ability: "int" },
  5625. ins: { label: "DND5E.SkillIns", ability: "wis" },
  5626. itm: { label: "DND5E.SkillItm", ability: "cha" },
  5627. inv: { label: "DND5E.SkillInv", ability: "int" },
  5628. med: { label: "DND5E.SkillMed", ability: "wis" },
  5629. nat: { label: "DND5E.SkillNat", ability: "int" },
  5630. prc: { label: "DND5E.SkillPrc", ability: "wis" },
  5631. prf: { label: "DND5E.SkillPrf", ability: "cha" },
  5632. per: { label: "DND5E.SkillPer", ability: "cha" },
  5633. rel: { label: "DND5E.SkillRel", ability: "int" },
  5634. slt: { label: "DND5E.SkillSlt", ability: "dex" },
  5635. ste: { label: "DND5E.SkillSte", ability: "dex" },
  5636. sur: { label: "DND5E.SkillSur", ability: "wis" }
  5637. };
  5638. preLocalize("skills", { key: "label", sort: true });
  5639. /* -------------------------------------------- */
  5640. /**
  5641. * Character alignment options.
  5642. * @enum {string}
  5643. */
  5644. DND5E.alignments = {
  5645. lg: "DND5E.AlignmentLG",
  5646. ng: "DND5E.AlignmentNG",
  5647. cg: "DND5E.AlignmentCG",
  5648. ln: "DND5E.AlignmentLN",
  5649. tn: "DND5E.AlignmentTN",
  5650. cn: "DND5E.AlignmentCN",
  5651. le: "DND5E.AlignmentLE",
  5652. ne: "DND5E.AlignmentNE",
  5653. ce: "DND5E.AlignmentCE"
  5654. };
  5655. preLocalize("alignments");
  5656. /* -------------------------------------------- */
  5657. /**
  5658. * An enumeration of item attunement types.
  5659. * @enum {number}
  5660. */
  5661. DND5E.attunementTypes = {
  5662. NONE: 0,
  5663. REQUIRED: 1,
  5664. ATTUNED: 2
  5665. };
  5666. /**
  5667. * An enumeration of item attunement states.
  5668. * @type {{"0": string, "1": string, "2": string}}
  5669. */
  5670. DND5E.attunements = {
  5671. 0: "DND5E.AttunementNone",
  5672. 1: "DND5E.AttunementRequired",
  5673. 2: "DND5E.AttunementAttuned"
  5674. };
  5675. preLocalize("attunements");
  5676. /* -------------------------------------------- */
  5677. /**
  5678. * General weapon categories.
  5679. * @enum {string}
  5680. */
  5681. DND5E.weaponProficiencies = {
  5682. sim: "DND5E.WeaponSimpleProficiency",
  5683. mar: "DND5E.WeaponMartialProficiency"
  5684. };
  5685. preLocalize("weaponProficiencies");
  5686. /**
  5687. * A mapping between `DND5E.weaponTypes` and `DND5E.weaponProficiencies` that
  5688. * is used to determine if character has proficiency when adding an item.
  5689. * @enum {(boolean|string)}
  5690. */
  5691. DND5E.weaponProficienciesMap = {
  5692. natural: true,
  5693. simpleM: "sim",
  5694. simpleR: "sim",
  5695. martialM: "mar",
  5696. martialR: "mar"
  5697. };
  5698. /**
  5699. * The basic weapon types in 5e. This enables specific weapon proficiencies or
  5700. * starting equipment provided by classes and backgrounds.
  5701. * @enum {string}
  5702. */
  5703. DND5E.weaponIds = {
  5704. battleaxe: "I0WocDSuNpGJayPb",
  5705. blowgun: "wNWK6yJMHG9ANqQV",
  5706. club: "nfIRTECQIG81CvM4",
  5707. dagger: "0E565kQUBmndJ1a2",
  5708. dart: "3rCO8MTIdPGSW6IJ",
  5709. flail: "UrH3sMdnUDckIHJ6",
  5710. glaive: "rOG1OM2ihgPjOvFW",
  5711. greataxe: "1Lxk6kmoRhG8qQ0u",
  5712. greatclub: "QRCsxkCwWNwswL9o",
  5713. greatsword: "xMkP8BmFzElcsMaR",
  5714. halberd: "DMejWAc8r8YvDPP1",
  5715. handaxe: "eO7Fbv5WBk5zvGOc",
  5716. handcrossbow: "qaSro7kFhxD6INbZ",
  5717. heavycrossbow: "RmP0mYRn2J7K26rX",
  5718. javelin: "DWLMnODrnHn8IbAG",
  5719. lance: "RnuxdHUAIgxccVwj",
  5720. lightcrossbow: "ddWvQRLmnnIS0eLF",
  5721. lighthammer: "XVK6TOL4sGItssAE",
  5722. longbow: "3cymOVja8jXbzrdT",
  5723. longsword: "10ZP2Bu3vnCuYMIB",
  5724. mace: "Ajyq6nGwF7FtLhDQ",
  5725. maul: "DizirD7eqjh8n95A",
  5726. morningstar: "dX8AxCh9o0A9CkT3",
  5727. net: "aEiM49V8vWpWw7rU",
  5728. pike: "tC0kcqZT9HHAO0PD",
  5729. quarterstaff: "g2dWN7PQiMRYWzyk",
  5730. rapier: "Tobce1hexTnDk4sV",
  5731. scimitar: "fbC0Mg1a73wdFbqO",
  5732. shortsword: "osLzOwQdPtrK3rQH",
  5733. sickle: "i4NeNZ30ycwPDHMx",
  5734. spear: "OG4nBBydvmfWYXIk",
  5735. shortbow: "GJv6WkD7D2J6rP6M",
  5736. sling: "3gynWO9sN4OLGMWD",
  5737. trident: "F65ANO66ckP8FDMa",
  5738. warpick: "2YdfjN1PIIrSHZii",
  5739. warhammer: "F0Df164Xv1gWcYt0",
  5740. whip: "QKTyxoO0YDnAsbYe"
  5741. };
  5742. /* -------------------------------------------- */
  5743. /**
  5744. * The categories into which Tool items can be grouped.
  5745. *
  5746. * @enum {string}
  5747. */
  5748. DND5E.toolTypes = {
  5749. art: "DND5E.ToolArtisans",
  5750. game: "DND5E.ToolGamingSet",
  5751. music: "DND5E.ToolMusicalInstrument"
  5752. };
  5753. preLocalize("toolTypes", { sort: true });
  5754. /**
  5755. * The categories of tool proficiencies that a character can gain.
  5756. *
  5757. * @enum {string}
  5758. */
  5759. DND5E.toolProficiencies = {
  5760. ...DND5E.toolTypes,
  5761. vehicle: "DND5E.ToolVehicle"
  5762. };
  5763. preLocalize("toolProficiencies", { sort: true });
  5764. /**
  5765. * The basic tool types in 5e. This enables specific tool proficiencies or
  5766. * starting equipment provided by classes and backgrounds.
  5767. * @enum {string}
  5768. */
  5769. DND5E.toolIds = {
  5770. alchemist: "SztwZhbhZeCqyAes",
  5771. bagpipes: "yxHi57T5mmVt0oDr",
  5772. brewer: "Y9S75go1hLMXUD48",
  5773. calligrapher: "jhjo20QoiD5exf09",
  5774. card: "YwlHI3BVJapz4a3E",
  5775. carpenter: "8NS6MSOdXtUqD7Ib",
  5776. cartographer: "fC0lFK8P4RuhpfaU",
  5777. chess: "23y8FvWKf9YLcnBL",
  5778. cobbler: "hM84pZnpCqKfi8XH",
  5779. cook: "Gflnp29aEv5Lc1ZM",
  5780. dice: "iBuTM09KD9IoM5L8",
  5781. disg: "IBhDAr7WkhWPYLVn",
  5782. drum: "69Dpr25pf4BjkHKb",
  5783. dulcimer: "NtdDkjmpdIMiX7I2",
  5784. flute: "eJOrPcAz9EcquyRQ",
  5785. forg: "cG3m4YlHfbQlLEOx",
  5786. glassblower: "rTbVrNcwApnuTz5E",
  5787. herb: "i89okN7GFTWHsvPy",
  5788. horn: "aa9KuBy4dst7WIW9",
  5789. jeweler: "YfBwELTgPFHmQdHh",
  5790. leatherworker: "PUMfwyVUbtyxgYbD",
  5791. lute: "qBydtUUIkv520DT7",
  5792. lyre: "EwG1EtmbgR3bM68U",
  5793. mason: "skUih6tBvcBbORzA",
  5794. navg: "YHCmjsiXxZ9UdUhU",
  5795. painter: "ccm5xlWhx74d6lsK",
  5796. panflute: "G5m5gYIx9VAUWC3J",
  5797. pois: "il2GNi8C0DvGLL9P",
  5798. potter: "hJS8yEVkqgJjwfWa",
  5799. shawm: "G3cqbejJpfB91VhP",
  5800. smith: "KndVe2insuctjIaj",
  5801. thief: "woWZ1sO5IUVGzo58",
  5802. tinker: "0d08g1i5WXnNrCNA",
  5803. viol: "baoe3U5BfMMMxhCU",
  5804. weaver: "ap9prThUB2y9lDyj",
  5805. woodcarver: "xKErqkLo4ASYr5EP"
  5806. };
  5807. /* -------------------------------------------- */
  5808. /**
  5809. * Time periods that accept a numeric value.
  5810. * @enum {string}
  5811. */
  5812. DND5E.scalarTimePeriods = {
  5813. turn: "DND5E.TimeTurn",
  5814. round: "DND5E.TimeRound",
  5815. minute: "DND5E.TimeMinute",
  5816. hour: "DND5E.TimeHour",
  5817. day: "DND5E.TimeDay",
  5818. month: "DND5E.TimeMonth",
  5819. year: "DND5E.TimeYear"
  5820. };
  5821. preLocalize("scalarTimePeriods");
  5822. /* -------------------------------------------- */
  5823. /**
  5824. * Time periods for spells that don't have a defined ending.
  5825. * @enum {string}
  5826. */
  5827. DND5E.permanentTimePeriods = {
  5828. disp: "DND5E.TimeDisp",
  5829. dstr: "DND5E.TimeDispTrig",
  5830. perm: "DND5E.TimePerm"
  5831. };
  5832. preLocalize("permanentTimePeriods");
  5833. /* -------------------------------------------- */
  5834. /**
  5835. * Time periods that don't accept a numeric value.
  5836. * @enum {string}
  5837. */
  5838. DND5E.specialTimePeriods = {
  5839. inst: "DND5E.TimeInst",
  5840. spec: "DND5E.Special"
  5841. };
  5842. preLocalize("specialTimePeriods");
  5843. /* -------------------------------------------- */
  5844. /**
  5845. * The various lengths of time over which effects can occur.
  5846. * @enum {string}
  5847. */
  5848. DND5E.timePeriods = {
  5849. ...DND5E.specialTimePeriods,
  5850. ...DND5E.permanentTimePeriods,
  5851. ...DND5E.scalarTimePeriods
  5852. };
  5853. preLocalize("timePeriods");
  5854. /* -------------------------------------------- */
  5855. /**
  5856. * Various ways in which an item or ability can be activated.
  5857. * @enum {string}
  5858. */
  5859. DND5E.abilityActivationTypes = {
  5860. action: "DND5E.Action",
  5861. bonus: "DND5E.BonusAction",
  5862. reaction: "DND5E.Reaction",
  5863. minute: DND5E.timePeriods.minute,
  5864. hour: DND5E.timePeriods.hour,
  5865. day: DND5E.timePeriods.day,
  5866. special: DND5E.timePeriods.spec,
  5867. legendary: "DND5E.LegendaryActionLabel",
  5868. mythic: "DND5E.MythicActionLabel",
  5869. lair: "DND5E.LairActionLabel",
  5870. crew: "DND5E.VehicleCrewAction"
  5871. };
  5872. preLocalize("abilityActivationTypes");
  5873. /* -------------------------------------------- */
  5874. /**
  5875. * Different things that an ability can consume upon use.
  5876. * @enum {string}
  5877. */
  5878. DND5E.abilityConsumptionTypes = {
  5879. ammo: "DND5E.ConsumeAmmunition",
  5880. attribute: "DND5E.ConsumeAttribute",
  5881. hitDice: "DND5E.ConsumeHitDice",
  5882. material: "DND5E.ConsumeMaterial",
  5883. charges: "DND5E.ConsumeCharges"
  5884. };
  5885. preLocalize("abilityConsumptionTypes", { sort: true });
  5886. /* -------------------------------------------- */
  5887. /**
  5888. * Creature sizes.
  5889. * @enum {string}
  5890. */
  5891. DND5E.actorSizes = {
  5892. tiny: "DND5E.SizeTiny",
  5893. sm: "DND5E.SizeSmall",
  5894. med: "DND5E.SizeMedium",
  5895. lg: "DND5E.SizeLarge",
  5896. huge: "DND5E.SizeHuge",
  5897. grg: "DND5E.SizeGargantuan"
  5898. };
  5899. preLocalize("actorSizes");
  5900. /**
  5901. * Default token image size for the values of `DND5E.actorSizes`.
  5902. * @enum {number}
  5903. */
  5904. DND5E.tokenSizes = {
  5905. tiny: 0.5,
  5906. sm: 1,
  5907. med: 1,
  5908. lg: 2,
  5909. huge: 3,
  5910. grg: 4
  5911. };
  5912. /**
  5913. * Colors used to visualize temporary and temporary maximum HP in token health bars.
  5914. * @enum {number}
  5915. */
  5916. DND5E.tokenHPColors = {
  5917. damage: 0xFF0000,
  5918. healing: 0x00FF00,
  5919. temp: 0x66CCFF,
  5920. tempmax: 0x440066,
  5921. negmax: 0x550000
  5922. };
  5923. /* -------------------------------------------- */
  5924. /**
  5925. * Default types of creatures.
  5926. * *Note: Not pre-localized to allow for easy fetching of pluralized forms.*
  5927. * @enum {string}
  5928. */
  5929. DND5E.creatureTypes = {
  5930. aberration: "DND5E.CreatureAberration",
  5931. beast: "DND5E.CreatureBeast",
  5932. celestial: "DND5E.CreatureCelestial",
  5933. construct: "DND5E.CreatureConstruct",
  5934. dragon: "DND5E.CreatureDragon",
  5935. elemental: "DND5E.CreatureElemental",
  5936. fey: "DND5E.CreatureFey",
  5937. fiend: "DND5E.CreatureFiend",
  5938. giant: "DND5E.CreatureGiant",
  5939. humanoid: "DND5E.CreatureHumanoid",
  5940. monstrosity: "DND5E.CreatureMonstrosity",
  5941. ooze: "DND5E.CreatureOoze",
  5942. plant: "DND5E.CreaturePlant",
  5943. undead: "DND5E.CreatureUndead"
  5944. };
  5945. /* -------------------------------------------- */
  5946. /**
  5947. * Classification types for item action types.
  5948. * @enum {string}
  5949. */
  5950. DND5E.itemActionTypes = {
  5951. mwak: "DND5E.ActionMWAK",
  5952. rwak: "DND5E.ActionRWAK",
  5953. msak: "DND5E.ActionMSAK",
  5954. rsak: "DND5E.ActionRSAK",
  5955. save: "DND5E.ActionSave",
  5956. heal: "DND5E.ActionHeal",
  5957. abil: "DND5E.ActionAbil",
  5958. util: "DND5E.ActionUtil",
  5959. other: "DND5E.ActionOther"
  5960. };
  5961. preLocalize("itemActionTypes");
  5962. /* -------------------------------------------- */
  5963. /**
  5964. * Different ways in which item capacity can be limited.
  5965. * @enum {string}
  5966. */
  5967. DND5E.itemCapacityTypes = {
  5968. items: "DND5E.ItemContainerCapacityItems",
  5969. weight: "DND5E.ItemContainerCapacityWeight"
  5970. };
  5971. preLocalize("itemCapacityTypes", { sort: true });
  5972. /* -------------------------------------------- */
  5973. /**
  5974. * List of various item rarities.
  5975. * @enum {string}
  5976. */
  5977. DND5E.itemRarity = {
  5978. common: "DND5E.ItemRarityCommon",
  5979. uncommon: "DND5E.ItemRarityUncommon",
  5980. rare: "DND5E.ItemRarityRare",
  5981. veryRare: "DND5E.ItemRarityVeryRare",
  5982. legendary: "DND5E.ItemRarityLegendary",
  5983. artifact: "DND5E.ItemRarityArtifact"
  5984. };
  5985. preLocalize("itemRarity");
  5986. /* -------------------------------------------- */
  5987. /**
  5988. * Enumerate the lengths of time over which an item can have limited use ability.
  5989. * @enum {string}
  5990. */
  5991. DND5E.limitedUsePeriods = {
  5992. sr: "DND5E.ShortRest",
  5993. lr: "DND5E.LongRest",
  5994. day: "DND5E.Day",
  5995. charges: "DND5E.Charges"
  5996. };
  5997. preLocalize("limitedUsePeriods");
  5998. /* -------------------------------------------- */
  5999. /**
  6000. * Specific equipment types that modify base AC.
  6001. * @enum {string}
  6002. */
  6003. DND5E.armorTypes = {
  6004. light: "DND5E.EquipmentLight",
  6005. medium: "DND5E.EquipmentMedium",
  6006. heavy: "DND5E.EquipmentHeavy",
  6007. natural: "DND5E.EquipmentNatural",
  6008. shield: "DND5E.EquipmentShield"
  6009. };
  6010. preLocalize("armorTypes");
  6011. /* -------------------------------------------- */
  6012. /**
  6013. * Equipment types that aren't armor.
  6014. * @enum {string}
  6015. */
  6016. DND5E.miscEquipmentTypes = {
  6017. clothing: "DND5E.EquipmentClothing",
  6018. trinket: "DND5E.EquipmentTrinket",
  6019. vehicle: "DND5E.EquipmentVehicle"
  6020. };
  6021. preLocalize("miscEquipmentTypes", { sort: true });
  6022. /* -------------------------------------------- */
  6023. /**
  6024. * The set of equipment types for armor, clothing, and other objects which can be worn by the character.
  6025. * @enum {string}
  6026. */
  6027. DND5E.equipmentTypes = {
  6028. ...DND5E.miscEquipmentTypes,
  6029. ...DND5E.armorTypes
  6030. };
  6031. preLocalize("equipmentTypes", { sort: true });
  6032. /* -------------------------------------------- */
  6033. /**
  6034. * The various types of vehicles in which characters can be proficient.
  6035. * @enum {string}
  6036. */
  6037. DND5E.vehicleTypes = {
  6038. air: "DND5E.VehicleTypeAir",
  6039. land: "DND5E.VehicleTypeLand",
  6040. space: "DND5E.VehicleTypeSpace",
  6041. water: "DND5E.VehicleTypeWater"
  6042. };
  6043. preLocalize("vehicleTypes", { sort: true });
  6044. /* -------------------------------------------- */
  6045. /**
  6046. * The set of Armor Proficiencies which a character may have.
  6047. * @type {object}
  6048. */
  6049. DND5E.armorProficiencies = {
  6050. lgt: DND5E.equipmentTypes.light,
  6051. med: DND5E.equipmentTypes.medium,
  6052. hvy: DND5E.equipmentTypes.heavy,
  6053. shl: "DND5E.EquipmentShieldProficiency"
  6054. };
  6055. preLocalize("armorProficiencies");
  6056. /**
  6057. * A mapping between `DND5E.equipmentTypes` and `DND5E.armorProficiencies` that
  6058. * is used to determine if character has proficiency when adding an item.
  6059. * @enum {(boolean|string)}
  6060. */
  6061. DND5E.armorProficienciesMap = {
  6062. natural: true,
  6063. clothing: true,
  6064. light: "lgt",
  6065. medium: "med",
  6066. heavy: "hvy",
  6067. shield: "shl"
  6068. };
  6069. /**
  6070. * The basic armor types in 5e. This enables specific armor proficiencies,
  6071. * automated AC calculation in NPCs, and starting equipment.
  6072. * @enum {string}
  6073. */
  6074. DND5E.armorIds = {
  6075. breastplate: "SK2HATQ4abKUlV8i",
  6076. chainmail: "rLMflzmxpe8JGTOA",
  6077. chainshirt: "p2zChy24ZJdVqMSH",
  6078. halfplate: "vsgmACFYINloIdPm",
  6079. hide: "n1V07puo0RQxPGuF",
  6080. leather: "WwdpHLXGX5r8uZu5",
  6081. padded: "GtKV1b5uqFQqpEni",
  6082. plate: "OjkIqlW2UpgFcjZa",
  6083. ringmail: "nsXZejlmgalj4he9",
  6084. scalemail: "XmnlF5fgIO3tg6TG",
  6085. splint: "cKpJmsJmU8YaiuqG",
  6086. studded: "TIV3B1vbrVHIhQAm"
  6087. };
  6088. /**
  6089. * The basic shield in 5e.
  6090. * @enum {string}
  6091. */
  6092. DND5E.shieldIds = {
  6093. shield: "sSs3hSzkKBMNBgTs"
  6094. };
  6095. /**
  6096. * Common armor class calculations.
  6097. * @enum {{ label: string, [formula]: string }}
  6098. */
  6099. DND5E.armorClasses = {
  6100. flat: {
  6101. label: "DND5E.ArmorClassFlat",
  6102. formula: "@attributes.ac.flat"
  6103. },
  6104. natural: {
  6105. label: "DND5E.ArmorClassNatural",
  6106. formula: "@attributes.ac.flat"
  6107. },
  6108. default: {
  6109. label: "DND5E.ArmorClassEquipment",
  6110. formula: "@attributes.ac.armor + @attributes.ac.dex"
  6111. },
  6112. mage: {
  6113. label: "DND5E.ArmorClassMage",
  6114. formula: "13 + @abilities.dex.mod"
  6115. },
  6116. draconic: {
  6117. label: "DND5E.ArmorClassDraconic",
  6118. formula: "13 + @abilities.dex.mod"
  6119. },
  6120. unarmoredMonk: {
  6121. label: "DND5E.ArmorClassUnarmoredMonk",
  6122. formula: "10 + @abilities.dex.mod + @abilities.wis.mod"
  6123. },
  6124. unarmoredBarb: {
  6125. label: "DND5E.ArmorClassUnarmoredBarbarian",
  6126. formula: "10 + @abilities.dex.mod + @abilities.con.mod"
  6127. },
  6128. custom: {
  6129. label: "DND5E.ArmorClassCustom"
  6130. }
  6131. };
  6132. preLocalize("armorClasses", { key: "label" });
  6133. /* -------------------------------------------- */
  6134. /**
  6135. * Enumerate the valid consumable types which are recognized by the system.
  6136. * @enum {string}
  6137. */
  6138. DND5E.consumableTypes = {
  6139. ammo: "DND5E.ConsumableAmmo",
  6140. potion: "DND5E.ConsumablePotion",
  6141. poison: "DND5E.ConsumablePoison",
  6142. food: "DND5E.ConsumableFood",
  6143. scroll: "DND5E.ConsumableScroll",
  6144. wand: "DND5E.ConsumableWand",
  6145. rod: "DND5E.ConsumableRod",
  6146. trinket: "DND5E.ConsumableTrinket"
  6147. };
  6148. preLocalize("consumableTypes", { sort: true });
  6149. /* -------------------------------------------- */
  6150. /**
  6151. * Configuration data for an item with the "feature" type.
  6152. *
  6153. * @typedef {object} FeatureTypeConfiguration
  6154. * @property {string} label Localized label for this type.
  6155. * @property {Object<string, string>} [subtypes] Enum containing localized labels for subtypes.
  6156. */
  6157. /**
  6158. * Types of "features" items.
  6159. * @enum {FeatureTypeConfiguration}
  6160. */
  6161. DND5E.featureTypes = {
  6162. background: {
  6163. label: "DND5E.Feature.Background"
  6164. },
  6165. class: {
  6166. label: "DND5E.Feature.Class",
  6167. subtypes: {
  6168. artificerInfusion: "DND5E.ClassFeature.ArtificerInfusion",
  6169. channelDivinity: "DND5E.ClassFeature.ChannelDivinity",
  6170. defensiveTactic: "DND5E.ClassFeature.DefensiveTactic",
  6171. eldritchInvocation: "DND5E.ClassFeature.EldritchInvocation",
  6172. elementalDiscipline: "DND5E.ClassFeature.ElementalDiscipline",
  6173. fightingStyle: "DND5E.ClassFeature.FightingStyle",
  6174. huntersPrey: "DND5E.ClassFeature.HuntersPrey",
  6175. ki: "DND5E.ClassFeature.Ki",
  6176. maneuver: "DND5E.ClassFeature.Maneuver",
  6177. metamagic: "DND5E.ClassFeature.Metamagic",
  6178. multiattack: "DND5E.ClassFeature.Multiattack",
  6179. pact: "DND5E.ClassFeature.PactBoon",
  6180. psionicPower: "DND5E.ClassFeature.PsionicPower",
  6181. rune: "DND5E.ClassFeature.Rune",
  6182. superiorHuntersDefense: "DND5E.ClassFeature.SuperiorHuntersDefense"
  6183. }
  6184. },
  6185. monster: {
  6186. label: "DND5E.Feature.Monster"
  6187. },
  6188. race: {
  6189. label: "DND5E.Feature.Race"
  6190. },
  6191. feat: {
  6192. label: "DND5E.Feature.Feat"
  6193. }
  6194. };
  6195. preLocalize("featureTypes", { key: "label" });
  6196. preLocalize("featureTypes.class.subtypes", { sort: true });
  6197. /* -------------------------------------------- */
  6198. /**
  6199. * @typedef {object} CurrencyConfiguration
  6200. * @property {string} label Localized label for the currency.
  6201. * @property {string} abbreviation Localized abbreviation for the currency.
  6202. * @property {number} conversion Number by which this currency should be multiplied to arrive at a standard value.
  6203. */
  6204. /**
  6205. * The valid currency denominations with localized labels, abbreviations, and conversions.
  6206. * The conversion number defines how many of that currency are equal to one GP.
  6207. * @enum {CurrencyConfiguration}
  6208. */
  6209. DND5E.currencies = {
  6210. pp: {
  6211. label: "DND5E.CurrencyPP",
  6212. abbreviation: "DND5E.CurrencyAbbrPP",
  6213. conversion: 0.1
  6214. },
  6215. gp: {
  6216. label: "DND5E.CurrencyGP",
  6217. abbreviation: "DND5E.CurrencyAbbrGP",
  6218. conversion: 1
  6219. },
  6220. ep: {
  6221. label: "DND5E.CurrencyEP",
  6222. abbreviation: "DND5E.CurrencyAbbrEP",
  6223. conversion: 2
  6224. },
  6225. sp: {
  6226. label: "DND5E.CurrencySP",
  6227. abbreviation: "DND5E.CurrencyAbbrSP",
  6228. conversion: 10
  6229. },
  6230. cp: {
  6231. label: "DND5E.CurrencyCP",
  6232. abbreviation: "DND5E.CurrencyAbbrCP",
  6233. conversion: 100
  6234. }
  6235. };
  6236. preLocalize("currencies", { keys: ["label", "abbreviation"] });
  6237. /* -------------------------------------------- */
  6238. /* Damage Types */
  6239. /* -------------------------------------------- */
  6240. /**
  6241. * Types of damage that are considered physical.
  6242. * @enum {string}
  6243. */
  6244. DND5E.physicalDamageTypes = {
  6245. bludgeoning: "DND5E.DamageBludgeoning",
  6246. piercing: "DND5E.DamagePiercing",
  6247. slashing: "DND5E.DamageSlashing"
  6248. };
  6249. preLocalize("physicalDamageTypes", { sort: true });
  6250. /* -------------------------------------------- */
  6251. /**
  6252. * Types of damage the can be caused by abilities.
  6253. * @enum {string}
  6254. */
  6255. DND5E.damageTypes = {
  6256. ...DND5E.physicalDamageTypes,
  6257. acid: "DND5E.DamageAcid",
  6258. cold: "DND5E.DamageCold",
  6259. fire: "DND5E.DamageFire",
  6260. force: "DND5E.DamageForce",
  6261. lightning: "DND5E.DamageLightning",
  6262. necrotic: "DND5E.DamageNecrotic",
  6263. poison: "DND5E.DamagePoison",
  6264. psychic: "DND5E.DamagePsychic",
  6265. radiant: "DND5E.DamageRadiant",
  6266. thunder: "DND5E.DamageThunder"
  6267. };
  6268. preLocalize("damageTypes", { sort: true });
  6269. /* -------------------------------------------- */
  6270. /**
  6271. * Types of damage to which an actor can possess resistance, immunity, or vulnerability.
  6272. * @enum {string}
  6273. * @deprecated
  6274. */
  6275. DND5E.damageResistanceTypes = {
  6276. ...DND5E.damageTypes,
  6277. physical: "DND5E.DamagePhysical"
  6278. };
  6279. preLocalize("damageResistanceTypes", { sort: true });
  6280. /* -------------------------------------------- */
  6281. /* Movement */
  6282. /* -------------------------------------------- */
  6283. /**
  6284. * Different types of healing that can be applied using abilities.
  6285. * @enum {string}
  6286. */
  6287. DND5E.healingTypes = {
  6288. healing: "DND5E.Healing",
  6289. temphp: "DND5E.HealingTemp"
  6290. };
  6291. preLocalize("healingTypes");
  6292. /* -------------------------------------------- */
  6293. /**
  6294. * The valid units of measure for movement distances in the game system.
  6295. * By default this uses the imperial units of feet and miles.
  6296. * @enum {string}
  6297. */
  6298. DND5E.movementTypes = {
  6299. burrow: "DND5E.MovementBurrow",
  6300. climb: "DND5E.MovementClimb",
  6301. fly: "DND5E.MovementFly",
  6302. swim: "DND5E.MovementSwim",
  6303. walk: "DND5E.MovementWalk"
  6304. };
  6305. preLocalize("movementTypes", { sort: true });
  6306. /* -------------------------------------------- */
  6307. /* Measurement */
  6308. /* -------------------------------------------- */
  6309. /**
  6310. * The valid units of measure for movement distances in the game system.
  6311. * By default this uses the imperial units of feet and miles.
  6312. * @enum {string}
  6313. */
  6314. DND5E.movementUnits = {
  6315. ft: "DND5E.DistFt",
  6316. mi: "DND5E.DistMi",
  6317. m: "DND5E.DistM",
  6318. km: "DND5E.DistKm"
  6319. };
  6320. preLocalize("movementUnits");
  6321. /* -------------------------------------------- */
  6322. /**
  6323. * The types of range that are used for measuring actions and effects.
  6324. * @enum {string}
  6325. */
  6326. DND5E.rangeTypes = {
  6327. self: "DND5E.DistSelf",
  6328. touch: "DND5E.DistTouch",
  6329. spec: "DND5E.Special",
  6330. any: "DND5E.DistAny"
  6331. };
  6332. preLocalize("rangeTypes");
  6333. /* -------------------------------------------- */
  6334. /**
  6335. * The valid units of measure for the range of an action or effect. A combination of `DND5E.movementUnits` and
  6336. * `DND5E.rangeUnits`.
  6337. * @enum {string}
  6338. */
  6339. DND5E.distanceUnits = {
  6340. ...DND5E.movementUnits,
  6341. ...DND5E.rangeTypes
  6342. };
  6343. preLocalize("distanceUnits");
  6344. /* -------------------------------------------- */
  6345. /**
  6346. * Configure aspects of encumbrance calculation so that it could be configured by modules.
  6347. * @enum {{ imperial: number, metric: number }}
  6348. */
  6349. DND5E.encumbrance = {
  6350. currencyPerWeight: {
  6351. imperial: 50,
  6352. metric: 110
  6353. },
  6354. strMultiplier: {
  6355. imperial: 15,
  6356. metric: 6.8
  6357. },
  6358. vehicleWeightMultiplier: {
  6359. imperial: 2000, // 2000 lbs in an imperial ton
  6360. metric: 1000 // 1000 kg in a metric ton
  6361. }
  6362. };
  6363. /* -------------------------------------------- */
  6364. /* Targeting */
  6365. /* -------------------------------------------- */
  6366. /**
  6367. * Targeting types that apply to one or more distinct targets.
  6368. * @enum {string}
  6369. */
  6370. DND5E.individualTargetTypes = {
  6371. self: "DND5E.TargetSelf",
  6372. ally: "DND5E.TargetAlly",
  6373. enemy: "DND5E.TargetEnemy",
  6374. creature: "DND5E.TargetCreature",
  6375. object: "DND5E.TargetObject",
  6376. space: "DND5E.TargetSpace"
  6377. };
  6378. preLocalize("individualTargetTypes");
  6379. /* -------------------------------------------- */
  6380. /**
  6381. * Information needed to represent different area of effect target types.
  6382. *
  6383. * @typedef {object} AreaTargetDefinition
  6384. * @property {string} label Localized label for this type.
  6385. * @property {string} template Type of `MeasuredTemplate` create for this target type.
  6386. */
  6387. /**
  6388. * Targeting types that cover an area.
  6389. * @enum {AreaTargetDefinition}
  6390. */
  6391. DND5E.areaTargetTypes = {
  6392. radius: {
  6393. label: "DND5E.TargetRadius",
  6394. template: "circle"
  6395. },
  6396. sphere: {
  6397. label: "DND5E.TargetSphere",
  6398. template: "circle"
  6399. },
  6400. cylinder: {
  6401. label: "DND5E.TargetCylinder",
  6402. template: "circle"
  6403. },
  6404. cone: {
  6405. label: "DND5E.TargetCone",
  6406. template: "cone"
  6407. },
  6408. square: {
  6409. label: "DND5E.TargetSquare",
  6410. template: "rect"
  6411. },
  6412. cube: {
  6413. label: "DND5E.TargetCube",
  6414. template: "rect"
  6415. },
  6416. line: {
  6417. label: "DND5E.TargetLine",
  6418. template: "ray"
  6419. },
  6420. wall: {
  6421. label: "DND5E.TargetWall",
  6422. template: "ray"
  6423. }
  6424. };
  6425. preLocalize("areaTargetTypes", { key: "label", sort: true });
  6426. /* -------------------------------------------- */
  6427. /**
  6428. * The types of single or area targets which can be applied to abilities.
  6429. * @enum {string}
  6430. */
  6431. DND5E.targetTypes = {
  6432. ...DND5E.individualTargetTypes,
  6433. ...Object.fromEntries(Object.entries(DND5E.areaTargetTypes).map(([k, v]) => [k, v.label]))
  6434. };
  6435. preLocalize("targetTypes", { sort: true });
  6436. /* -------------------------------------------- */
  6437. /**
  6438. * Denominations of hit dice which can apply to classes.
  6439. * @type {string[]}
  6440. */
  6441. DND5E.hitDieTypes = ["d4", "d6", "d8", "d10", "d12"];
  6442. /* -------------------------------------------- */
  6443. /**
  6444. * The set of possible sensory perception types which an Actor may have.
  6445. * @enum {string}
  6446. */
  6447. DND5E.senses = {
  6448. blindsight: "DND5E.SenseBlindsight",
  6449. darkvision: "DND5E.SenseDarkvision",
  6450. tremorsense: "DND5E.SenseTremorsense",
  6451. truesight: "DND5E.SenseTruesight"
  6452. };
  6453. preLocalize("senses", { sort: true });
  6454. /* -------------------------------------------- */
  6455. /* Spellcasting */
  6456. /* -------------------------------------------- */
  6457. /**
  6458. * Define the standard slot progression by character level.
  6459. * The entries of this array represent the spell slot progression for a full spell-caster.
  6460. * @type {number[][]}
  6461. */
  6462. DND5E.SPELL_SLOT_TABLE = [
  6463. [2],
  6464. [3],
  6465. [4, 2],
  6466. [4, 3],
  6467. [4, 3, 2],
  6468. [4, 3, 3],
  6469. [4, 3, 3, 1],
  6470. [4, 3, 3, 2],
  6471. [4, 3, 3, 3, 1],
  6472. [4, 3, 3, 3, 2],
  6473. [4, 3, 3, 3, 2, 1],
  6474. [4, 3, 3, 3, 2, 1],
  6475. [4, 3, 3, 3, 2, 1, 1],
  6476. [4, 3, 3, 3, 2, 1, 1],
  6477. [4, 3, 3, 3, 2, 1, 1, 1],
  6478. [4, 3, 3, 3, 2, 1, 1, 1],
  6479. [4, 3, 3, 3, 2, 1, 1, 1, 1],
  6480. [4, 3, 3, 3, 3, 1, 1, 1, 1],
  6481. [4, 3, 3, 3, 3, 2, 1, 1, 1],
  6482. [4, 3, 3, 3, 3, 2, 2, 1, 1]
  6483. ];
  6484. /* -------------------------------------------- */
  6485. /**
  6486. * Various different ways a spell can be prepared.
  6487. */
  6488. DND5E.spellPreparationModes = {
  6489. prepared: "DND5E.SpellPrepPrepared",
  6490. pact: "DND5E.PactMagic",
  6491. always: "DND5E.SpellPrepAlways",
  6492. atwill: "DND5E.SpellPrepAtWill",
  6493. innate: "DND5E.SpellPrepInnate"
  6494. };
  6495. preLocalize("spellPreparationModes");
  6496. /* -------------------------------------------- */
  6497. /**
  6498. * Subset of `DND5E.spellPreparationModes` that consume spell slots.
  6499. * @type {boolean[]}
  6500. */
  6501. DND5E.spellUpcastModes = ["always", "pact", "prepared"];
  6502. /* -------------------------------------------- */
  6503. /**
  6504. * Configuration data for different types of spellcasting supported.
  6505. *
  6506. * @typedef {object} SpellcastingTypeConfiguration
  6507. * @property {string} label Localized label.
  6508. * @property {Object<string, SpellcastingProgressionConfiguration>} [progression] Any progression modes for this type.
  6509. */
  6510. /**
  6511. * Configuration data for a spellcasting progression mode.
  6512. *
  6513. * @typedef {object} SpellcastingProgressionConfiguration
  6514. * @property {string} label Localized label.
  6515. * @property {number} [divisor=1] Value by which the class levels are divided to determine spellcasting level.
  6516. * @property {boolean} [roundUp=false] Should fractional values should be rounded up by default?
  6517. */
  6518. /**
  6519. * Different spellcasting types and their progression.
  6520. * @type {SpellcastingTypeConfiguration}
  6521. */
  6522. DND5E.spellcastingTypes = {
  6523. leveled: {
  6524. label: "DND5E.SpellProgLeveled",
  6525. progression: {
  6526. full: {
  6527. label: "DND5E.SpellProgFull",
  6528. divisor: 1
  6529. },
  6530. half: {
  6531. label: "DND5E.SpellProgHalf",
  6532. divisor: 2
  6533. },
  6534. third: {
  6535. label: "DND5E.SpellProgThird",
  6536. divisor: 3
  6537. },
  6538. artificer: {
  6539. label: "DND5E.SpellProgArt",
  6540. divisor: 2,
  6541. roundUp: true
  6542. }
  6543. }
  6544. },
  6545. pact: {
  6546. label: "DND5E.SpellProgPact"
  6547. }
  6548. };
  6549. preLocalize("spellcastingTypes", { key: "label", sort: true });
  6550. preLocalize("spellcastingTypes.leveled.progression", { key: "label" });
  6551. /* -------------------------------------------- */
  6552. /**
  6553. * Ways in which a class can contribute to spellcasting levels.
  6554. * @enum {string}
  6555. */
  6556. DND5E.spellProgression = {
  6557. none: "DND5E.SpellNone",
  6558. full: "DND5E.SpellProgFull",
  6559. half: "DND5E.SpellProgHalf",
  6560. third: "DND5E.SpellProgThird",
  6561. pact: "DND5E.SpellProgPact",
  6562. artificer: "DND5E.SpellProgArt"
  6563. };
  6564. preLocalize("spellProgression", { key: "label" });
  6565. /* -------------------------------------------- */
  6566. /**
  6567. * Valid spell levels.
  6568. * @enum {string}
  6569. */
  6570. DND5E.spellLevels = {
  6571. 0: "DND5E.SpellLevel0",
  6572. 1: "DND5E.SpellLevel1",
  6573. 2: "DND5E.SpellLevel2",
  6574. 3: "DND5E.SpellLevel3",
  6575. 4: "DND5E.SpellLevel4",
  6576. 5: "DND5E.SpellLevel5",
  6577. 6: "DND5E.SpellLevel6",
  6578. 7: "DND5E.SpellLevel7",
  6579. 8: "DND5E.SpellLevel8",
  6580. 9: "DND5E.SpellLevel9"
  6581. };
  6582. preLocalize("spellLevels");
  6583. /* -------------------------------------------- */
  6584. /**
  6585. * The available choices for how spell damage scaling may be computed.
  6586. * @enum {string}
  6587. */
  6588. DND5E.spellScalingModes = {
  6589. none: "DND5E.SpellNone",
  6590. cantrip: "DND5E.SpellCantrip",
  6591. level: "DND5E.SpellLevel"
  6592. };
  6593. preLocalize("spellScalingModes", { sort: true });
  6594. /* -------------------------------------------- */
  6595. /**
  6596. * Types of components that can be required when casting a spell.
  6597. * @enum {object}
  6598. */
  6599. DND5E.spellComponents = {
  6600. vocal: {
  6601. label: "DND5E.ComponentVerbal",
  6602. abbr: "DND5E.ComponentVerbalAbbr"
  6603. },
  6604. somatic: {
  6605. label: "DND5E.ComponentSomatic",
  6606. abbr: "DND5E.ComponentSomaticAbbr"
  6607. },
  6608. material: {
  6609. label: "DND5E.ComponentMaterial",
  6610. abbr: "DND5E.ComponentMaterialAbbr"
  6611. }
  6612. };
  6613. preLocalize("spellComponents", {keys: ["label", "abbr"]});
  6614. /* -------------------------------------------- */
  6615. /**
  6616. * Supplementary rules keywords that inform a spell's use.
  6617. * @enum {object}
  6618. */
  6619. DND5E.spellTags = {
  6620. concentration: {
  6621. label: "DND5E.Concentration",
  6622. abbr: "DND5E.ConcentrationAbbr"
  6623. },
  6624. ritual: {
  6625. label: "DND5E.Ritual",
  6626. abbr: "DND5E.RitualAbbr"
  6627. }
  6628. };
  6629. preLocalize("spellTags", {keys: ["label", "abbr"]});
  6630. /* -------------------------------------------- */
  6631. /**
  6632. * Schools to which a spell can belong.
  6633. * @enum {string}
  6634. */
  6635. DND5E.spellSchools = {
  6636. abj: "DND5E.SchoolAbj",
  6637. con: "DND5E.SchoolCon",
  6638. div: "DND5E.SchoolDiv",
  6639. enc: "DND5E.SchoolEnc",
  6640. evo: "DND5E.SchoolEvo",
  6641. ill: "DND5E.SchoolIll",
  6642. nec: "DND5E.SchoolNec",
  6643. trs: "DND5E.SchoolTrs"
  6644. };
  6645. preLocalize("spellSchools", { sort: true });
  6646. /* -------------------------------------------- */
  6647. /**
  6648. * Spell scroll item ID within the `DND5E.sourcePacks` compendium for each level.
  6649. * @enum {string}
  6650. */
  6651. DND5E.spellScrollIds = {
  6652. 0: "rQ6sO7HDWzqMhSI3",
  6653. 1: "9GSfMg0VOA2b4uFN",
  6654. 2: "XdDp6CKh9qEvPTuS",
  6655. 3: "hqVKZie7x9w3Kqds",
  6656. 4: "DM7hzgL836ZyUFB1",
  6657. 5: "wa1VF8TXHmkrrR35",
  6658. 6: "tI3rWx4bxefNCexS",
  6659. 7: "mtyw4NS1s7j2EJaD",
  6660. 8: "aOrinPg7yuDZEuWr",
  6661. 9: "O4YbkJkLlnsgUszZ"
  6662. };
  6663. /* -------------------------------------------- */
  6664. /* Weapon Details */
  6665. /* -------------------------------------------- */
  6666. /**
  6667. * The set of types which a weapon item can take.
  6668. * @enum {string}
  6669. */
  6670. DND5E.weaponTypes = {
  6671. simpleM: "DND5E.WeaponSimpleM",
  6672. simpleR: "DND5E.WeaponSimpleR",
  6673. martialM: "DND5E.WeaponMartialM",
  6674. martialR: "DND5E.WeaponMartialR",
  6675. natural: "DND5E.WeaponNatural",
  6676. improv: "DND5E.WeaponImprov",
  6677. siege: "DND5E.WeaponSiege"
  6678. };
  6679. preLocalize("weaponTypes");
  6680. /* -------------------------------------------- */
  6681. /**
  6682. * A subset of weapon properties that determine the physical characteristics of the weapon.
  6683. * These properties are used for determining physical resistance bypasses.
  6684. * @enum {string}
  6685. */
  6686. DND5E.physicalWeaponProperties = {
  6687. ada: "DND5E.WeaponPropertiesAda",
  6688. mgc: "DND5E.WeaponPropertiesMgc",
  6689. sil: "DND5E.WeaponPropertiesSil"
  6690. };
  6691. preLocalize("physicalWeaponProperties", { sort: true });
  6692. /* -------------------------------------------- */
  6693. /**
  6694. * The set of weapon property flags which can exist on a weapon.
  6695. * @enum {string}
  6696. */
  6697. DND5E.weaponProperties = {
  6698. ...DND5E.physicalWeaponProperties,
  6699. amm: "DND5E.WeaponPropertiesAmm",
  6700. fin: "DND5E.WeaponPropertiesFin",
  6701. fir: "DND5E.WeaponPropertiesFir",
  6702. foc: "DND5E.WeaponPropertiesFoc",
  6703. hvy: "DND5E.WeaponPropertiesHvy",
  6704. lgt: "DND5E.WeaponPropertiesLgt",
  6705. lod: "DND5E.WeaponPropertiesLod",
  6706. rch: "DND5E.WeaponPropertiesRch",
  6707. rel: "DND5E.WeaponPropertiesRel",
  6708. ret: "DND5E.WeaponPropertiesRet",
  6709. spc: "DND5E.WeaponPropertiesSpc",
  6710. thr: "DND5E.WeaponPropertiesThr",
  6711. two: "DND5E.WeaponPropertiesTwo",
  6712. ver: "DND5E.WeaponPropertiesVer"
  6713. };
  6714. preLocalize("weaponProperties", { sort: true });
  6715. /* -------------------------------------------- */
  6716. /**
  6717. * Compendium packs used for localized items.
  6718. * @enum {string}
  6719. */
  6720. DND5E.sourcePacks = {
  6721. ITEMS: "dnd5e.items"
  6722. };
  6723. /* -------------------------------------------- */
  6724. /**
  6725. * Settings to configure how actors are merged when polymorphing is applied.
  6726. * @enum {string}
  6727. */
  6728. DND5E.polymorphSettings = {
  6729. keepPhysical: "DND5E.PolymorphKeepPhysical",
  6730. keepMental: "DND5E.PolymorphKeepMental",
  6731. keepSaves: "DND5E.PolymorphKeepSaves",
  6732. keepSkills: "DND5E.PolymorphKeepSkills",
  6733. mergeSaves: "DND5E.PolymorphMergeSaves",
  6734. mergeSkills: "DND5E.PolymorphMergeSkills",
  6735. keepClass: "DND5E.PolymorphKeepClass",
  6736. keepFeats: "DND5E.PolymorphKeepFeats",
  6737. keepSpells: "DND5E.PolymorphKeepSpells",
  6738. keepItems: "DND5E.PolymorphKeepItems",
  6739. keepBio: "DND5E.PolymorphKeepBio",
  6740. keepVision: "DND5E.PolymorphKeepVision",
  6741. keepSelf: "DND5E.PolymorphKeepSelf"
  6742. };
  6743. preLocalize("polymorphSettings", { sort: true });
  6744. /**
  6745. * Settings to configure how actors are effects are merged when polymorphing is applied.
  6746. * @enum {string}
  6747. */
  6748. DND5E.polymorphEffectSettings = {
  6749. keepAE: "DND5E.PolymorphKeepAE",
  6750. keepOtherOriginAE: "DND5E.PolymorphKeepOtherOriginAE",
  6751. keepOriginAE: "DND5E.PolymorphKeepOriginAE",
  6752. keepEquipmentAE: "DND5E.PolymorphKeepEquipmentAE",
  6753. keepFeatAE: "DND5E.PolymorphKeepFeatureAE",
  6754. keepSpellAE: "DND5E.PolymorphKeepSpellAE",
  6755. keepClassAE: "DND5E.PolymorphKeepClassAE",
  6756. keepBackgroundAE: "DND5E.PolymorphKeepBackgroundAE"
  6757. };
  6758. preLocalize("polymorphEffectSettings", { sort: true });
  6759. /**
  6760. * Settings to configure how actors are merged when preset polymorphing is applied.
  6761. * @enum {object}
  6762. */
  6763. DND5E.transformationPresets = {
  6764. wildshape: {
  6765. icon: '<i class="fas fa-paw"></i>',
  6766. label: "DND5E.PolymorphWildShape",
  6767. options: {
  6768. keepBio: true,
  6769. keepClass: true,
  6770. keepMental: true,
  6771. mergeSaves: true,
  6772. mergeSkills: true,
  6773. keepEquipmentAE: false
  6774. }
  6775. },
  6776. polymorph: {
  6777. icon: '<i class="fas fa-pastafarianism"></i>',
  6778. label: "DND5E.Polymorph",
  6779. options: {
  6780. keepEquipmentAE: false,
  6781. keepClassAE: false,
  6782. keepFeatAE: false,
  6783. keepBackgroundAE: false
  6784. }
  6785. },
  6786. polymorphSelf: {
  6787. icon: '<i class="fas fa-eye"></i>',
  6788. label: "DND5E.PolymorphSelf",
  6789. options: {
  6790. keepSelf: true
  6791. }
  6792. }
  6793. };
  6794. preLocalize("transformationPresets", { sort: true, keys: ["label"] });
  6795. /* -------------------------------------------- */
  6796. /**
  6797. * Skill, ability, and tool proficiency levels.
  6798. * The key for each level represents its proficiency multiplier.
  6799. * @enum {string}
  6800. */
  6801. DND5E.proficiencyLevels = {
  6802. 0: "DND5E.NotProficient",
  6803. 1: "DND5E.Proficient",
  6804. 0.5: "DND5E.HalfProficient",
  6805. 2: "DND5E.Expertise"
  6806. };
  6807. preLocalize("proficiencyLevels");
  6808. /* -------------------------------------------- */
  6809. /**
  6810. * The amount of cover provided by an object. In cases where multiple pieces
  6811. * of cover are in play, we take the highest value.
  6812. * @enum {string}
  6813. */
  6814. DND5E.cover = {
  6815. 0: "DND5E.None",
  6816. .5: "DND5E.CoverHalf",
  6817. .75: "DND5E.CoverThreeQuarters",
  6818. 1: "DND5E.CoverTotal"
  6819. };
  6820. preLocalize("cover");
  6821. /* -------------------------------------------- */
  6822. /**
  6823. * A selection of actor attributes that can be tracked on token resource bars.
  6824. * @type {string[]}
  6825. * @deprecated since v10
  6826. */
  6827. DND5E.trackableAttributes = [
  6828. "attributes.ac.value", "attributes.init.bonus", "attributes.movement", "attributes.senses", "attributes.spelldc",
  6829. "attributes.spellLevel", "details.cr", "details.spellLevel", "details.xp.value", "skills.*.passive",
  6830. "abilities.*.value"
  6831. ];
  6832. /* -------------------------------------------- */
  6833. /**
  6834. * A selection of actor and item attributes that are valid targets for item resource consumption.
  6835. * @type {string[]}
  6836. */
  6837. DND5E.consumableResources = [
  6838. "item.quantity", "item.weight", "item.duration.value", "currency", "details.xp.value", "abilities.*.value",
  6839. "attributes.senses", "attributes.movement", "attributes.ac.flat", "item.armor.value", "item.target", "item.range",
  6840. "item.save.dc"
  6841. ];
  6842. /* -------------------------------------------- */
  6843. /**
  6844. * Conditions that can affect an actor.
  6845. * @enum {string}
  6846. */
  6847. DND5E.conditionTypes = {
  6848. blinded: "DND5E.ConBlinded",
  6849. charmed: "DND5E.ConCharmed",
  6850. deafened: "DND5E.ConDeafened",
  6851. diseased: "DND5E.ConDiseased",
  6852. exhaustion: "DND5E.ConExhaustion",
  6853. frightened: "DND5E.ConFrightened",
  6854. grappled: "DND5E.ConGrappled",
  6855. incapacitated: "DND5E.ConIncapacitated",
  6856. invisible: "DND5E.ConInvisible",
  6857. paralyzed: "DND5E.ConParalyzed",
  6858. petrified: "DND5E.ConPetrified",
  6859. poisoned: "DND5E.ConPoisoned",
  6860. prone: "DND5E.ConProne",
  6861. restrained: "DND5E.ConRestrained",
  6862. stunned: "DND5E.ConStunned",
  6863. unconscious: "DND5E.ConUnconscious"
  6864. };
  6865. preLocalize("conditionTypes", { sort: true });
  6866. /**
  6867. * Languages a character can learn.
  6868. * @enum {string}
  6869. */
  6870. DND5E.languages = {
  6871. common: "DND5E.LanguagesCommon",
  6872. aarakocra: "DND5E.LanguagesAarakocra",
  6873. abyssal: "DND5E.LanguagesAbyssal",
  6874. aquan: "DND5E.LanguagesAquan",
  6875. auran: "DND5E.LanguagesAuran",
  6876. celestial: "DND5E.LanguagesCelestial",
  6877. deep: "DND5E.LanguagesDeepSpeech",
  6878. draconic: "DND5E.LanguagesDraconic",
  6879. druidic: "DND5E.LanguagesDruidic",
  6880. dwarvish: "DND5E.LanguagesDwarvish",
  6881. elvish: "DND5E.LanguagesElvish",
  6882. giant: "DND5E.LanguagesGiant",
  6883. gith: "DND5E.LanguagesGith",
  6884. gnomish: "DND5E.LanguagesGnomish",
  6885. goblin: "DND5E.LanguagesGoblin",
  6886. gnoll: "DND5E.LanguagesGnoll",
  6887. halfling: "DND5E.LanguagesHalfling",
  6888. ignan: "DND5E.LanguagesIgnan",
  6889. infernal: "DND5E.LanguagesInfernal",
  6890. orc: "DND5E.LanguagesOrc",
  6891. primordial: "DND5E.LanguagesPrimordial",
  6892. sylvan: "DND5E.LanguagesSylvan",
  6893. terran: "DND5E.LanguagesTerran",
  6894. cant: "DND5E.LanguagesThievesCant",
  6895. undercommon: "DND5E.LanguagesUndercommon"
  6896. };
  6897. preLocalize("languages", { sort: true });
  6898. /**
  6899. * Maximum allowed character level.
  6900. * @type {number}
  6901. */
  6902. DND5E.maxLevel = 20;
  6903. /**
  6904. * XP required to achieve each character level.
  6905. * @type {number[]}
  6906. */
  6907. DND5E.CHARACTER_EXP_LEVELS = [
  6908. 0, 300, 900, 2700, 6500, 14000, 23000, 34000, 48000, 64000, 85000, 100000,
  6909. 120000, 140000, 165000, 195000, 225000, 265000, 305000, 355000
  6910. ];
  6911. /**
  6912. * XP granted for each challenge rating.
  6913. * @type {number[]}
  6914. */
  6915. DND5E.CR_EXP_LEVELS = [
  6916. 10, 200, 450, 700, 1100, 1800, 2300, 2900, 3900, 5000, 5900, 7200, 8400, 10000, 11500, 13000, 15000, 18000,
  6917. 20000, 22000, 25000, 33000, 41000, 50000, 62000, 75000, 90000, 105000, 120000, 135000, 155000
  6918. ];
  6919. /**
  6920. * @typedef {object} CharacterFlagConfig
  6921. * @property {string} name
  6922. * @property {string} hint
  6923. * @property {string} section
  6924. * @property {typeof boolean|string|number} type
  6925. * @property {string} placeholder
  6926. * @property {string[]} [abilities]
  6927. * @property {Object<string, string>} [choices]
  6928. * @property {string[]} [skills]
  6929. */
  6930. /* -------------------------------------------- */
  6931. /**
  6932. * Trait configuration information.
  6933. *
  6934. * @typedef {object} TraitConfiguration
  6935. * @property {string} label Localization key for the trait name.
  6936. * @property {string} [actorKeyPath] If the trait doesn't directly map to an entry as `traits.[key]`, where is
  6937. * this trait's data stored on the actor?
  6938. * @property {string} [configKey] If the list of trait options doesn't match the name of the trait, where can
  6939. * the options be found within `CONFIG.DND5E`?
  6940. * @property {string} [labelKey] If config is an enum of objects, where can the label be found?
  6941. * @property {object} [subtypes] Configuration for traits that take some sort of base item.
  6942. * @property {string} [subtypes.keyPath] Path to subtype value on base items, should match a category key.
  6943. * @property {string[]} [subtypes.ids] Key for base item ID objects within `CONFIG.DND5E`.
  6944. * @property {object} [children] Mapping of category key to an object defining its children.
  6945. * @property {boolean} [sortCategories] Whether top-level categories should be sorted.
  6946. */
  6947. /**
  6948. * Configurable traits on actors.
  6949. * @enum {TraitConfiguration}
  6950. */
  6951. DND5E.traits = {
  6952. saves: {
  6953. label: "DND5E.ClassSaves",
  6954. configKey: "abilities",
  6955. labelKey: "label"
  6956. },
  6957. skills: {
  6958. label: "DND5E.TraitSkillProf",
  6959. labelKey: "label"
  6960. },
  6961. languages: {
  6962. label: "DND5E.Languages"
  6963. },
  6964. di: {
  6965. label: "DND5E.DamImm",
  6966. configKey: "damageTypes"
  6967. },
  6968. dr: {
  6969. label: "DND5E.DamRes",
  6970. configKey: "damageTypes"
  6971. },
  6972. dv: {
  6973. label: "DND5E.DamVuln",
  6974. configKey: "damageTypes"
  6975. },
  6976. ci: {
  6977. label: "DND5E.ConImm",
  6978. configKey: "conditionTypes"
  6979. },
  6980. weapon: {
  6981. label: "DND5E.TraitWeaponProf",
  6982. actorKeyPath: "traits.weaponProf",
  6983. configKey: "weaponProficiencies",
  6984. subtypes: { keyPath: "weaponType", ids: ["weaponIds"] }
  6985. },
  6986. armor: {
  6987. label: "DND5E.TraitArmorProf",
  6988. actorKeyPath: "traits.armorProf",
  6989. configKey: "armorProficiencies",
  6990. subtypes: { keyPath: "armor.type", ids: ["armorIds", "shieldIds"] }
  6991. },
  6992. tool: {
  6993. label: "DND5E.TraitToolProf",
  6994. actorKeyPath: "tools",
  6995. configKey: "toolProficiencies",
  6996. subtypes: { keyPath: "toolType", ids: ["toolIds"] },
  6997. children: { vehicle: "vehicleTypes" },
  6998. sortCategories: true
  6999. }
  7000. };
  7001. preLocalize("traits", { key: "label" });
  7002. /* -------------------------------------------- */
  7003. /**
  7004. * Special character flags.
  7005. * @enum {CharacterFlagConfig}
  7006. */
  7007. DND5E.characterFlags = {
  7008. diamondSoul: {
  7009. name: "DND5E.FlagsDiamondSoul",
  7010. hint: "DND5E.FlagsDiamondSoulHint",
  7011. section: "DND5E.Feats",
  7012. type: Boolean
  7013. },
  7014. elvenAccuracy: {
  7015. name: "DND5E.FlagsElvenAccuracy",
  7016. hint: "DND5E.FlagsElvenAccuracyHint",
  7017. section: "DND5E.RacialTraits",
  7018. abilities: ["dex", "int", "wis", "cha"],
  7019. type: Boolean
  7020. },
  7021. halflingLucky: {
  7022. name: "DND5E.FlagsHalflingLucky",
  7023. hint: "DND5E.FlagsHalflingLuckyHint",
  7024. section: "DND5E.RacialTraits",
  7025. type: Boolean
  7026. },
  7027. initiativeAdv: {
  7028. name: "DND5E.FlagsInitiativeAdv",
  7029. hint: "DND5E.FlagsInitiativeAdvHint",
  7030. section: "DND5E.Feats",
  7031. type: Boolean
  7032. },
  7033. initiativeAlert: {
  7034. name: "DND5E.FlagsAlert",
  7035. hint: "DND5E.FlagsAlertHint",
  7036. section: "DND5E.Feats",
  7037. type: Boolean
  7038. },
  7039. jackOfAllTrades: {
  7040. name: "DND5E.FlagsJOAT",
  7041. hint: "DND5E.FlagsJOATHint",
  7042. section: "DND5E.Feats",
  7043. type: Boolean
  7044. },
  7045. observantFeat: {
  7046. name: "DND5E.FlagsObservant",
  7047. hint: "DND5E.FlagsObservantHint",
  7048. skills: ["prc", "inv"],
  7049. section: "DND5E.Feats",
  7050. type: Boolean
  7051. },
  7052. powerfulBuild: {
  7053. name: "DND5E.FlagsPowerfulBuild",
  7054. hint: "DND5E.FlagsPowerfulBuildHint",
  7055. section: "DND5E.RacialTraits",
  7056. type: Boolean
  7057. },
  7058. reliableTalent: {
  7059. name: "DND5E.FlagsReliableTalent",
  7060. hint: "DND5E.FlagsReliableTalentHint",
  7061. section: "DND5E.Feats",
  7062. type: Boolean
  7063. },
  7064. remarkableAthlete: {
  7065. name: "DND5E.FlagsRemarkableAthlete",
  7066. hint: "DND5E.FlagsRemarkableAthleteHint",
  7067. abilities: ["str", "dex", "con"],
  7068. section: "DND5E.Feats",
  7069. type: Boolean
  7070. },
  7071. weaponCriticalThreshold: {
  7072. name: "DND5E.FlagsWeaponCritThreshold",
  7073. hint: "DND5E.FlagsWeaponCritThresholdHint",
  7074. section: "DND5E.Feats",
  7075. type: Number,
  7076. placeholder: 20
  7077. },
  7078. spellCriticalThreshold: {
  7079. name: "DND5E.FlagsSpellCritThreshold",
  7080. hint: "DND5E.FlagsSpellCritThresholdHint",
  7081. section: "DND5E.Feats",
  7082. type: Number,
  7083. placeholder: 20
  7084. },
  7085. meleeCriticalDamageDice: {
  7086. name: "DND5E.FlagsMeleeCriticalDice",
  7087. hint: "DND5E.FlagsMeleeCriticalDiceHint",
  7088. section: "DND5E.Feats",
  7089. type: Number,
  7090. placeholder: 0
  7091. }
  7092. };
  7093. preLocalize("characterFlags", { keys: ["name", "hint", "section"] });
  7094. /**
  7095. * Flags allowed on actors. Any flags not in the list may be deleted during a migration.
  7096. * @type {string[]}
  7097. */
  7098. DND5E.allowedActorFlags = ["isPolymorphed", "originalActor"].concat(Object.keys(DND5E.characterFlags));
  7099. /* -------------------------------------------- */
  7100. /**
  7101. * Advancement types that can be added to items.
  7102. * @enum {*}
  7103. */
  7104. DND5E.advancementTypes = {
  7105. HitPoints: HitPointsAdvancement,
  7106. ItemChoice: ItemChoiceAdvancement,
  7107. ItemGrant: ItemGrantAdvancement,
  7108. ScaleValue: ScaleValueAdvancement
  7109. };
  7110. /* -------------------------------------------- */
  7111. /**
  7112. * Patch an existing config enum to allow conversion from string values to object values without
  7113. * breaking existing modules that are expecting strings.
  7114. * @param {string} key Key within DND5E that has been replaced with an enum of objects.
  7115. * @param {string} fallbackKey Key within the new config object from which to get the fallback value.
  7116. * @param {object} [options] Additional options passed through to logCompatibilityWarning.
  7117. */
  7118. function patchConfig(key, fallbackKey, options) {
  7119. /** @override */
  7120. function toString() {
  7121. const message = `The value of CONFIG.DND5E.${key} has been changed to an object.`
  7122. +` The former value can be acccessed from .${fallbackKey}.`;
  7123. foundry.utils.logCompatibilityWarning(message, options);
  7124. return this[fallbackKey];
  7125. }
  7126. Object.values(DND5E[key]).forEach(o => o.toString = toString);
  7127. }
  7128. /**
  7129. * @typedef {object} ModuleArtInfo
  7130. * @property {string} actor The path to the actor's portrait image.
  7131. * @property {string|object} token The path to the token image, or a richer object specifying additional token
  7132. * adjustments.
  7133. */
  7134. /**
  7135. * A class responsible for managing module-provided art in compendia.
  7136. */
  7137. class ModuleArt {
  7138. constructor() {
  7139. /**
  7140. * The stored map of actor UUIDs to their art information.
  7141. * @type {Map<string, ModuleArtInfo>}
  7142. */
  7143. Object.defineProperty(this, "map", {value: new Map(), writable: false});
  7144. }
  7145. /* -------------------------------------------- */
  7146. /**
  7147. * Set to true to temporarily prevent actors from loading module art.
  7148. * @type {boolean}
  7149. */
  7150. suppressArt = false;
  7151. /* -------------------------------------------- */
  7152. /**
  7153. * Register any art mapping information included in active modules.
  7154. * @returns {Promise<void>}
  7155. */
  7156. async registerModuleArt() {
  7157. this.map.clear();
  7158. for ( const module of game.modules ) {
  7159. const flags = module.flags?.[module.id];
  7160. const artPath = this.constructor.getModuleArtPath(module);
  7161. if ( !artPath ) continue;
  7162. try {
  7163. const mapping = await foundry.utils.fetchJsonWithTimeout(artPath);
  7164. await this.#parseArtMapping(module.id, mapping, flags["dnd5e-art-credit"]);
  7165. } catch( e ) {
  7166. console.error(e);
  7167. }
  7168. }
  7169. // Load system mapping.
  7170. try {
  7171. const mapping = await foundry.utils.fetchJsonWithTimeout("systems/dnd5e/json/fa-token-mapping.json");
  7172. const credit = `
  7173. <em>
  7174. Token artwork by
  7175. <a href="https://www.forgotten-adventures.net/" target="_blank" rel="noopener">Forgotten Adventures</a>.
  7176. </em>
  7177. `;
  7178. await this.#parseArtMapping(game.system.id, mapping, credit);
  7179. } catch( e ) {
  7180. console.error(e);
  7181. }
  7182. }
  7183. /* -------------------------------------------- */
  7184. /**
  7185. * Parse a provided module art mapping and store it for reference later.
  7186. * @param {string} moduleId The module ID.
  7187. * @param {object} mapping A mapping containing pack names, a list of actor IDs, and paths to the art provided by
  7188. * the module for them.
  7189. * @param {string} [credit] An optional credit line to attach to the Actor's biography.
  7190. * @returns {Promise<void>}
  7191. */
  7192. async #parseArtMapping(moduleId, mapping, credit) {
  7193. let settings = game.settings.get("dnd5e", "moduleArtConfiguration")?.[moduleId];
  7194. settings ??= {portraits: true, tokens: true};
  7195. for ( const [packName, actors] of Object.entries(mapping) ) {
  7196. const pack = game.packs.get(packName);
  7197. if ( !pack ) continue;
  7198. for ( let [actorId, info] of Object.entries(actors) ) {
  7199. const entry = pack.index.get(actorId);
  7200. if ( !entry || !(settings.portraits || settings.tokens) ) continue;
  7201. if ( settings.portraits ) entry.img = info.actor;
  7202. else delete info.actor;
  7203. if ( !settings.tokens ) delete info.token;
  7204. if ( credit ) info.credit = credit;
  7205. const uuid = `Compendium.${packName}.${actorId}`;
  7206. info = foundry.utils.mergeObject(this.map.get(uuid) ?? {}, info, {inplace: false});
  7207. this.map.set(`Compendium.${packName}.${actorId}`, info);
  7208. }
  7209. }
  7210. }
  7211. /* -------------------------------------------- */
  7212. /**
  7213. * If a module provides art, return the path to is JSON mapping.
  7214. * @param {Module} module The module.
  7215. * @returns {string|null}
  7216. */
  7217. static getModuleArtPath(module) {
  7218. const flags = module.flags?.[module.id];
  7219. const artPath = flags?.["dnd5e-art"];
  7220. if ( !artPath || !module.active ) return null;
  7221. return artPath;
  7222. }
  7223. }
  7224. /**
  7225. * A class responsible for allowing GMs to configure art provided by installed modules.
  7226. */
  7227. class ModuleArtConfig extends FormApplication {
  7228. /** @inheritdoc */
  7229. constructor(object={}, options={}) {
  7230. object = foundry.utils.mergeObject(game.settings.get("dnd5e", "moduleArtConfiguration"), object, {inplace: false});
  7231. super(object, options);
  7232. }
  7233. /* -------------------------------------------- */
  7234. /** @inheritdoc */
  7235. static get defaultOptions() {
  7236. return foundry.utils.mergeObject(super.defaultOptions, {
  7237. title: game.i18n.localize("DND5E.ModuleArtConfigL"),
  7238. id: "module-art-config",
  7239. template: "systems/dnd5e/templates/apps/module-art-config.html",
  7240. popOut: true,
  7241. width: 600,
  7242. height: "auto"
  7243. });
  7244. }
  7245. /* -------------------------------------------- */
  7246. /** @inheritdoc */
  7247. getData(options={}) {
  7248. const context = super.getData(options);
  7249. context.config = [];
  7250. for ( const module of game.modules ) {
  7251. if ( !ModuleArt.getModuleArtPath(module) ) continue;
  7252. const settings = this.object[module.id] ?? {portraits: true, tokens: true};
  7253. context.config.push({label: module.title, id: module.id, ...settings});
  7254. }
  7255. context.config.sort((a, b) => a.label.localeCompare(b.label, game.i18n.lang));
  7256. context.config.unshift({label: game.system.title, id: game.system.id, ...this.object.dnd5e});
  7257. return context;
  7258. }
  7259. /* -------------------------------------------- */
  7260. /** @inheritdoc */
  7261. async _updateObject(event, formData) {
  7262. await game.settings.set("dnd5e", "moduleArtConfiguration", foundry.utils.expandObject(formData));
  7263. return SettingsConfig.reloadConfirm({world: true});
  7264. }
  7265. }
  7266. /**
  7267. * Register all of the system's settings.
  7268. */
  7269. function registerSystemSettings() {
  7270. // Internal System Migration Version
  7271. game.settings.register("dnd5e", "systemMigrationVersion", {
  7272. name: "System Migration Version",
  7273. scope: "world",
  7274. config: false,
  7275. type: String,
  7276. default: ""
  7277. });
  7278. // Rest Recovery Rules
  7279. game.settings.register("dnd5e", "restVariant", {
  7280. name: "SETTINGS.5eRestN",
  7281. hint: "SETTINGS.5eRestL",
  7282. scope: "world",
  7283. config: true,
  7284. default: "normal",
  7285. type: String,
  7286. choices: {
  7287. normal: "SETTINGS.5eRestPHB",
  7288. gritty: "SETTINGS.5eRestGritty",
  7289. epic: "SETTINGS.5eRestEpic"
  7290. }
  7291. });
  7292. // Diagonal Movement Rule
  7293. game.settings.register("dnd5e", "diagonalMovement", {
  7294. name: "SETTINGS.5eDiagN",
  7295. hint: "SETTINGS.5eDiagL",
  7296. scope: "world",
  7297. config: true,
  7298. default: "555",
  7299. type: String,
  7300. choices: {
  7301. 555: "SETTINGS.5eDiagPHB",
  7302. 5105: "SETTINGS.5eDiagDMG",
  7303. EUCL: "SETTINGS.5eDiagEuclidean"
  7304. },
  7305. onChange: rule => canvas.grid.diagonalRule = rule
  7306. });
  7307. // Proficiency modifier type
  7308. game.settings.register("dnd5e", "proficiencyModifier", {
  7309. name: "SETTINGS.5eProfN",
  7310. hint: "SETTINGS.5eProfL",
  7311. scope: "world",
  7312. config: true,
  7313. default: "bonus",
  7314. type: String,
  7315. choices: {
  7316. bonus: "SETTINGS.5eProfBonus",
  7317. dice: "SETTINGS.5eProfDice"
  7318. }
  7319. });
  7320. // Use Honor ability score
  7321. game.settings.register("dnd5e", "honorScore", {
  7322. name: "SETTINGS.5eHonorN",
  7323. hint: "SETTINGS.5eHonorL",
  7324. scope: "world",
  7325. config: true,
  7326. default: false,
  7327. type: Boolean,
  7328. requiresReload: true
  7329. });
  7330. // Use Sanity ability score
  7331. game.settings.register("dnd5e", "sanityScore", {
  7332. name: "SETTINGS.5eSanityN",
  7333. hint: "SETTINGS.5eSanityL",
  7334. scope: "world",
  7335. config: true,
  7336. default: false,
  7337. type: Boolean,
  7338. requiresReload: true
  7339. });
  7340. // Apply Dexterity as Initiative Tiebreaker
  7341. game.settings.register("dnd5e", "initiativeDexTiebreaker", {
  7342. name: "SETTINGS.5eInitTBN",
  7343. hint: "SETTINGS.5eInitTBL",
  7344. scope: "world",
  7345. config: true,
  7346. default: false,
  7347. type: Boolean
  7348. });
  7349. // Record Currency Weight
  7350. game.settings.register("dnd5e", "currencyWeight", {
  7351. name: "SETTINGS.5eCurWtN",
  7352. hint: "SETTINGS.5eCurWtL",
  7353. scope: "world",
  7354. config: true,
  7355. default: true,
  7356. type: Boolean
  7357. });
  7358. // Disable Experience Tracking
  7359. game.settings.register("dnd5e", "disableExperienceTracking", {
  7360. name: "SETTINGS.5eNoExpN",
  7361. hint: "SETTINGS.5eNoExpL",
  7362. scope: "world",
  7363. config: true,
  7364. default: false,
  7365. type: Boolean
  7366. });
  7367. // Disable Advancements
  7368. game.settings.register("dnd5e", "disableAdvancements", {
  7369. name: "SETTINGS.5eNoAdvancementsN",
  7370. hint: "SETTINGS.5eNoAdvancementsL",
  7371. scope: "world",
  7372. config: true,
  7373. default: false,
  7374. type: Boolean
  7375. });
  7376. // Collapse Item Cards (by default)
  7377. game.settings.register("dnd5e", "autoCollapseItemCards", {
  7378. name: "SETTINGS.5eAutoCollapseCardN",
  7379. hint: "SETTINGS.5eAutoCollapseCardL",
  7380. scope: "client",
  7381. config: true,
  7382. default: false,
  7383. type: Boolean,
  7384. onChange: s => {
  7385. ui.chat.render();
  7386. }
  7387. });
  7388. // Allow Polymorphing
  7389. game.settings.register("dnd5e", "allowPolymorphing", {
  7390. name: "SETTINGS.5eAllowPolymorphingN",
  7391. hint: "SETTINGS.5eAllowPolymorphingL",
  7392. scope: "world",
  7393. config: true,
  7394. default: false,
  7395. type: Boolean
  7396. });
  7397. // Polymorph Settings
  7398. game.settings.register("dnd5e", "polymorphSettings", {
  7399. scope: "client",
  7400. default: {
  7401. keepPhysical: false,
  7402. keepMental: false,
  7403. keepSaves: false,
  7404. keepSkills: false,
  7405. mergeSaves: false,
  7406. mergeSkills: false,
  7407. keepClass: false,
  7408. keepFeats: false,
  7409. keepSpells: false,
  7410. keepItems: false,
  7411. keepBio: false,
  7412. keepVision: true,
  7413. keepSelf: false,
  7414. keepAE: false,
  7415. keepOriginAE: true,
  7416. keepOtherOriginAE: true,
  7417. keepFeatAE: true,
  7418. keepSpellAE: true,
  7419. keepEquipmentAE: true,
  7420. keepClassAE: true,
  7421. keepBackgroundAE: true,
  7422. transformTokens: true
  7423. }
  7424. });
  7425. // Metric Unit Weights
  7426. game.settings.register("dnd5e", "metricWeightUnits", {
  7427. name: "SETTINGS.5eMetricN",
  7428. hint: "SETTINGS.5eMetricL",
  7429. scope: "world",
  7430. config: true,
  7431. type: Boolean,
  7432. default: false
  7433. });
  7434. // Critical Damage Modifiers
  7435. game.settings.register("dnd5e", "criticalDamageModifiers", {
  7436. name: "SETTINGS.5eCriticalModifiersN",
  7437. hint: "SETTINGS.5eCriticalModifiersL",
  7438. scope: "world",
  7439. config: true,
  7440. type: Boolean,
  7441. default: false
  7442. });
  7443. // Critical Damage Maximize
  7444. game.settings.register("dnd5e", "criticalDamageMaxDice", {
  7445. name: "SETTINGS.5eCriticalMaxDiceN",
  7446. hint: "SETTINGS.5eCriticalMaxDiceL",
  7447. scope: "world",
  7448. config: true,
  7449. type: Boolean,
  7450. default: false
  7451. });
  7452. // Strict validation
  7453. game.settings.register("dnd5e", "strictValidation", {
  7454. scope: "world",
  7455. config: false,
  7456. type: Boolean,
  7457. default: true
  7458. });
  7459. // Dynamic art.
  7460. game.settings.registerMenu("dnd5e", "moduleArtConfiguration", {
  7461. name: "DND5E.ModuleArtConfigN",
  7462. label: "DND5E.ModuleArtConfigL",
  7463. hint: "DND5E.ModuleArtConfigH",
  7464. icon: "fa-solid fa-palette",
  7465. type: ModuleArtConfig,
  7466. restricted: true
  7467. });
  7468. game.settings.register("dnd5e", "moduleArtConfiguration", {
  7469. name: "Module Art Configuration",
  7470. scope: "world",
  7471. config: false,
  7472. type: Object,
  7473. default: {
  7474. dnd5e: {
  7475. portraits: true,
  7476. tokens: true
  7477. }
  7478. }
  7479. });
  7480. }
  7481. /**
  7482. * Extend the base ActiveEffect class to implement system-specific logic.
  7483. */
  7484. class ActiveEffect5e extends ActiveEffect {
  7485. /**
  7486. * Is this active effect currently suppressed?
  7487. * @type {boolean}
  7488. */
  7489. isSuppressed = false;
  7490. /* --------------------------------------------- */
  7491. /** @inheritdoc */
  7492. apply(actor, change) {
  7493. if ( this.isSuppressed ) return null;
  7494. if ( change.key.startsWith("flags.dnd5e.") ) change = this._prepareFlagChange(actor, change);
  7495. return super.apply(actor, change);
  7496. }
  7497. /* -------------------------------------------- */
  7498. /** @inheritdoc */
  7499. _applyAdd(actor, change, current, delta, changes) {
  7500. if ( current instanceof Set ) {
  7501. if ( Array.isArray(delta) ) delta.forEach(item => current.add(item));
  7502. else current.add(delta);
  7503. return;
  7504. }
  7505. super._applyAdd(actor, change, current, delta, changes);
  7506. }
  7507. /* -------------------------------------------- */
  7508. /** @inheritdoc */
  7509. _applyOverride(actor, change, current, delta, changes) {
  7510. if ( current instanceof Set ) {
  7511. current.clear();
  7512. if ( Array.isArray(delta) ) delta.forEach(item => current.add(item));
  7513. else current.add(delta);
  7514. return;
  7515. }
  7516. return super._applyOverride(actor, change, current, delta, changes);
  7517. }
  7518. /* --------------------------------------------- */
  7519. /**
  7520. * Transform the data type of the change to match the type expected for flags.
  7521. * @param {Actor5e} actor The Actor to whom this effect should be applied.
  7522. * @param {EffectChangeData} change The change being applied.
  7523. * @returns {EffectChangeData} The change with altered types if necessary.
  7524. */
  7525. _prepareFlagChange(actor, change) {
  7526. const { key, value } = change;
  7527. const data = CONFIG.DND5E.characterFlags[key.replace("flags.dnd5e.", "")];
  7528. if ( !data ) return change;
  7529. // Set flag to initial value if it isn't present
  7530. const current = foundry.utils.getProperty(actor, key) ?? null;
  7531. if ( current === null ) {
  7532. let initialValue = null;
  7533. if ( data.placeholder ) initialValue = data.placeholder;
  7534. else if ( data.type === Boolean ) initialValue = false;
  7535. else if ( data.type === Number ) initialValue = 0;
  7536. foundry.utils.setProperty(actor, key, initialValue);
  7537. }
  7538. // Coerce change data into the correct type
  7539. if ( data.type === Boolean ) {
  7540. if ( value === "false" ) change.value = false;
  7541. else change.value = Boolean(value);
  7542. }
  7543. return change;
  7544. }
  7545. /* --------------------------------------------- */
  7546. /**
  7547. * Determine whether this Active Effect is suppressed or not.
  7548. */
  7549. determineSuppression() {
  7550. this.isSuppressed = false;
  7551. if ( this.disabled || (this.parent.documentName !== "Actor") ) return;
  7552. const parts = this.origin?.split(".") ?? [];
  7553. const [parentType, parentId, documentType, documentId, syntheticItem, syntheticItemId] = parts;
  7554. let item;
  7555. // Case 1: This is a linked or sidebar actor
  7556. if ( parentType === "Actor" ) {
  7557. if ( (parentId !== this.parent.id) || (documentType !== "Item") ) return;
  7558. item = this.parent.items.get(documentId);
  7559. }
  7560. // Case 2: This is a synthetic actor on the scene
  7561. else if ( parentType === "Scene" ) {
  7562. if ( (documentId !== this.parent.token?.id) || (syntheticItem !== "Item") ) return;
  7563. item = this.parent.items.get(syntheticItemId);
  7564. }
  7565. if ( !item ) return;
  7566. this.isSuppressed = item.areEffectsSuppressed;
  7567. }
  7568. /* --------------------------------------------- */
  7569. /**
  7570. * Manage Active Effect instances through the Actor Sheet via effect control buttons.
  7571. * @param {MouseEvent} event The left-click event on the effect control
  7572. * @param {Actor5e|Item5e} owner The owning document which manages this effect
  7573. * @returns {Promise|null} Promise that resolves when the changes are complete.
  7574. */
  7575. static onManageActiveEffect(event, owner) {
  7576. event.preventDefault();
  7577. const a = event.currentTarget;
  7578. const li = a.closest("li");
  7579. const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null;
  7580. switch ( a.dataset.action ) {
  7581. case "create":
  7582. return owner.createEmbeddedDocuments("ActiveEffect", [{
  7583. label: game.i18n.localize("DND5E.EffectNew"),
  7584. icon: "icons/svg/aura.svg",
  7585. origin: owner.uuid,
  7586. "duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined,
  7587. disabled: li.dataset.effectType === "inactive"
  7588. }]);
  7589. case "edit":
  7590. return effect.sheet.render(true);
  7591. case "delete":
  7592. return effect.delete();
  7593. case "toggle":
  7594. return effect.update({disabled: !effect.disabled});
  7595. }
  7596. }
  7597. /* --------------------------------------------- */
  7598. /**
  7599. * Prepare the data structure for Active Effects which are currently applied to an Actor or Item.
  7600. * @param {ActiveEffect5e[]} effects The array of Active Effect instances to prepare sheet data for
  7601. * @returns {object} Data for rendering
  7602. */
  7603. static prepareActiveEffectCategories(effects) {
  7604. // Define effect header categories
  7605. const categories = {
  7606. temporary: {
  7607. type: "temporary",
  7608. label: game.i18n.localize("DND5E.EffectTemporary"),
  7609. effects: []
  7610. },
  7611. passive: {
  7612. type: "passive",
  7613. label: game.i18n.localize("DND5E.EffectPassive"),
  7614. effects: []
  7615. },
  7616. inactive: {
  7617. type: "inactive",
  7618. label: game.i18n.localize("DND5E.EffectInactive"),
  7619. effects: []
  7620. },
  7621. suppressed: {
  7622. type: "suppressed",
  7623. label: game.i18n.localize("DND5E.EffectUnavailable"),
  7624. effects: [],
  7625. info: [game.i18n.localize("DND5E.EffectUnavailableInfo")]
  7626. }
  7627. };
  7628. // Iterate over active effects, classifying them into categories
  7629. for ( let e of effects ) {
  7630. if ( game.dnd5e.isV10 ) e._getSourceName(); // Trigger a lookup for the source name
  7631. if ( e.isSuppressed ) categories.suppressed.effects.push(e);
  7632. else if ( e.disabled ) categories.inactive.effects.push(e);
  7633. else if ( e.isTemporary ) categories.temporary.effects.push(e);
  7634. else categories.passive.effects.push(e);
  7635. }
  7636. categories.suppressed.hidden = !categories.suppressed.effects.length;
  7637. return categories;
  7638. }
  7639. }
  7640. /**
  7641. * A standardized helper function for simplifying the constant parts of a multipart roll formula.
  7642. *
  7643. * @param {string} formula The original roll formula.
  7644. * @param {object} [options] Formatting options.
  7645. * @param {boolean} [options.preserveFlavor=false] Preserve flavor text in the simplified formula.
  7646. *
  7647. * @returns {string} The resulting simplified formula.
  7648. */
  7649. function simplifyRollFormula(formula, { preserveFlavor=false } = {}) {
  7650. // Create a new roll and verify that the formula is valid before attempting simplification.
  7651. let roll;
  7652. try { roll = new Roll(formula); }
  7653. catch(err) { console.warn(`Unable to simplify formula '${formula}': ${err}`); }
  7654. Roll.validate(roll.formula);
  7655. // Optionally strip flavor annotations.
  7656. if ( !preserveFlavor ) roll.terms = Roll.parse(roll.formula.replace(RollTerm.FLAVOR_REGEXP, ""));
  7657. // Perform arithmetic simplification on the existing roll terms.
  7658. roll.terms = _simplifyOperatorTerms(roll.terms);
  7659. // If the formula contains multiplication or division we cannot easily simplify
  7660. if ( /[*/]/.test(roll.formula) ) {
  7661. if ( roll.isDeterministic && !/d\(/.test(roll.formula) && (!/\[/.test(roll.formula) || !preserveFlavor) ) {
  7662. return Roll.safeEval(roll.formula).toString();
  7663. }
  7664. else return roll.constructor.getFormula(roll.terms);
  7665. }
  7666. // Flatten the roll formula and eliminate string terms.
  7667. roll.terms = _expandParentheticalTerms(roll.terms);
  7668. roll.terms = Roll.simplifyTerms(roll.terms);
  7669. // Group terms by type and perform simplifications on various types of roll term.
  7670. let { poolTerms, diceTerms, mathTerms, numericTerms } = _groupTermsByType(roll.terms);
  7671. numericTerms = _simplifyNumericTerms(numericTerms ?? []);
  7672. diceTerms = _simplifyDiceTerms(diceTerms ?? []);
  7673. // Recombine the terms into a single term array and remove an initial + operator if present.
  7674. const simplifiedTerms = [diceTerms, poolTerms, mathTerms, numericTerms].flat().filter(Boolean);
  7675. if ( simplifiedTerms[0]?.operator === "+" ) simplifiedTerms.shift();
  7676. return roll.constructor.getFormula(simplifiedTerms);
  7677. }
  7678. /* -------------------------------------------- */
  7679. /**
  7680. * A helper function to perform arithmetic simplification and remove redundant operator terms.
  7681. * @param {RollTerm[]} terms An array of roll terms.
  7682. * @returns {RollTerm[]} A new array of roll terms with redundant operators removed.
  7683. */
  7684. function _simplifyOperatorTerms(terms) {
  7685. return terms.reduce((acc, term) => {
  7686. const prior = acc[acc.length - 1];
  7687. const ops = new Set([prior?.operator, term.operator]);
  7688. // If one of the terms is not an operator, add the current term as is.
  7689. if ( ops.has(undefined) ) acc.push(term);
  7690. // Replace consecutive "+ -" operators with a "-" operator.
  7691. else if ( (ops.has("+")) && (ops.has("-")) ) acc.splice(-1, 1, new OperatorTerm({ operator: "-" }));
  7692. // Replace double "-" operators with a "+" operator.
  7693. else if ( (ops.has("-")) && (ops.size === 1) ) acc.splice(-1, 1, new OperatorTerm({ operator: "+" }));
  7694. // Don't include "+" operators that directly follow "+", "*", or "/". Otherwise, add the term as is.
  7695. else if ( !ops.has("+") ) acc.push(term);
  7696. return acc;
  7697. }, []);
  7698. }
  7699. /* -------------------------------------------- */
  7700. /**
  7701. * A helper function for combining unannotated numeric terms in an array into a single numeric term.
  7702. * @param {object[]} terms An array of roll terms.
  7703. * @returns {object[]} A new array of terms with unannotated numeric terms combined into one.
  7704. */
  7705. function _simplifyNumericTerms(terms) {
  7706. const simplified = [];
  7707. const { annotated, unannotated } = _separateAnnotatedTerms(terms);
  7708. // Combine the unannotated numerical bonuses into a single new NumericTerm.
  7709. if ( unannotated.length ) {
  7710. const staticBonus = Roll.safeEval(Roll.getFormula(unannotated));
  7711. if ( staticBonus === 0 ) return [...annotated];
  7712. // If the staticBonus is greater than 0, add a "+" operator so the formula remains valid.
  7713. if ( staticBonus > 0 ) simplified.push(new OperatorTerm({ operator: "+"}));
  7714. simplified.push(new NumericTerm({ number: staticBonus} ));
  7715. }
  7716. return [...simplified, ...annotated];
  7717. }
  7718. /* -------------------------------------------- */
  7719. /**
  7720. * A helper function to group dice of the same size and sign into single dice terms.
  7721. * @param {object[]} terms An array of DiceTerms and associated OperatorTerms.
  7722. * @returns {object[]} A new array of simplified dice terms.
  7723. */
  7724. function _simplifyDiceTerms(terms) {
  7725. const { annotated, unannotated } = _separateAnnotatedTerms(terms);
  7726. // Split the unannotated terms into different die sizes and signs
  7727. const diceQuantities = unannotated.reduce((obj, curr, i) => {
  7728. if ( curr instanceof OperatorTerm ) return obj;
  7729. const key = `${unannotated[i - 1].operator}${curr.faces}`;
  7730. obj[key] = (obj[key] ?? 0) + curr.number;
  7731. return obj;
  7732. }, {});
  7733. // Add new die and operator terms to simplified for each die size and sign
  7734. const simplified = Object.entries(diceQuantities).flatMap(([key, number]) => ([
  7735. new OperatorTerm({ operator: key.charAt(0) }),
  7736. new Die({ number, faces: parseInt(key.slice(1)) })
  7737. ]));
  7738. return [...simplified, ...annotated];
  7739. }
  7740. /* -------------------------------------------- */
  7741. /**
  7742. * A helper function to extract the contents of parenthetical terms into their own terms.
  7743. * @param {object[]} terms An array of roll terms.
  7744. * @returns {object[]} A new array of terms with no parenthetical terms.
  7745. */
  7746. function _expandParentheticalTerms(terms) {
  7747. terms = terms.reduce((acc, term) => {
  7748. if ( term instanceof ParentheticalTerm ) {
  7749. if ( term.isDeterministic ) term = new NumericTerm({ number: Roll.safeEval(term.term) });
  7750. else {
  7751. const subterms = new Roll(term.term).terms;
  7752. term = _expandParentheticalTerms(subterms);
  7753. }
  7754. }
  7755. acc.push(term);
  7756. return acc;
  7757. }, []);
  7758. return _simplifyOperatorTerms(terms.flat());
  7759. }
  7760. /* -------------------------------------------- */
  7761. /**
  7762. * A helper function to group terms into PoolTerms, DiceTerms, MathTerms, and NumericTerms.
  7763. * MathTerms are included as NumericTerms if they are deterministic.
  7764. * @param {RollTerm[]} terms An array of roll terms.
  7765. * @returns {object} An object mapping term types to arrays containing roll terms of that type.
  7766. */
  7767. function _groupTermsByType(terms) {
  7768. // Add an initial operator so that terms can be rearranged arbitrarily.
  7769. if ( !(terms[0] instanceof OperatorTerm) ) terms.unshift(new OperatorTerm({ operator: "+" }));
  7770. return terms.reduce((obj, term, i) => {
  7771. let type;
  7772. if ( term instanceof DiceTerm ) type = DiceTerm;
  7773. else if ( (term instanceof MathTerm) && (term.isDeterministic) ) type = NumericTerm;
  7774. else type = term.constructor;
  7775. const key = `${type.name.charAt(0).toLowerCase()}${type.name.substring(1)}s`;
  7776. // Push the term and the preceding OperatorTerm.
  7777. (obj[key] = obj[key] ?? []).push(terms[i - 1], term);
  7778. return obj;
  7779. }, {});
  7780. }
  7781. /* -------------------------------------------- */
  7782. /**
  7783. * A helper function to separate annotated terms from unannotated terms.
  7784. * @param {object[]} terms An array of DiceTerms and associated OperatorTerms.
  7785. * @returns {Array | Array[]} A pair of term arrays, one containing annotated terms.
  7786. */
  7787. function _separateAnnotatedTerms(terms) {
  7788. return terms.reduce((obj, curr, i) => {
  7789. if ( curr instanceof OperatorTerm ) return obj;
  7790. obj[curr.flavor ? "annotated" : "unannotated"].push(terms[i - 1], curr);
  7791. return obj;
  7792. }, { annotated: [], unannotated: [] });
  7793. }
  7794. /**
  7795. * A specialized Dialog subclass for ability usage.
  7796. *
  7797. * @param {Item5e} item Item that is being used.
  7798. * @param {object} [dialogData={}] An object of dialog data which configures how the modal window is rendered.
  7799. * @param {object} [options={}] Dialog rendering options.
  7800. */
  7801. class AbilityUseDialog extends Dialog {
  7802. constructor(item, dialogData={}, options={}) {
  7803. super(dialogData, options);
  7804. this.options.classes = ["dnd5e", "dialog"];
  7805. /**
  7806. * Store a reference to the Item document being used
  7807. * @type {Item5e}
  7808. */
  7809. this.item = item;
  7810. }
  7811. /* -------------------------------------------- */
  7812. /* Rendering */
  7813. /* -------------------------------------------- */
  7814. /**
  7815. * A constructor function which displays the Spell Cast Dialog app for a given Actor and Item.
  7816. * Returns a Promise which resolves to the dialog FormData once the workflow has been completed.
  7817. * @param {Item5e} item Item being used.
  7818. * @returns {Promise} Promise that is resolved when the use dialog is acted upon.
  7819. */
  7820. static async create(item) {
  7821. if ( !item.isOwned ) throw new Error("You cannot display an ability usage dialog for an unowned item");
  7822. // Prepare data
  7823. const uses = item.system.uses ?? {};
  7824. const resource = item.system.consume ?? {};
  7825. const quantity = item.system.quantity ?? 0;
  7826. const recharge = item.system.recharge ?? {};
  7827. const recharges = !!recharge.value;
  7828. const sufficientUses = (quantity > 0 && !uses.value) || uses.value > 0;
  7829. // Prepare dialog form data
  7830. const data = {
  7831. item: item,
  7832. title: game.i18n.format("DND5E.AbilityUseHint", {type: game.i18n.localize(CONFIG.Item.typeLabels[item.type]), name: item.name}),
  7833. note: this._getAbilityUseNote(item, uses, recharge),
  7834. consumeSpellSlot: false,
  7835. consumeRecharge: recharges,
  7836. consumeResource: resource.target && (!item.hasAttack || (resource.type !== "ammo")),
  7837. consumeUses: uses.per && (uses.max > 0),
  7838. canUse: recharges ? recharge.charged : sufficientUses,
  7839. createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
  7840. errors: []
  7841. };
  7842. if ( item.type === "spell" ) this._getSpellData(item.actor.system, item.system, data);
  7843. // Render the ability usage template
  7844. const html = await renderTemplate("systems/dnd5e/templates/apps/ability-use.hbs", data);
  7845. // Create the Dialog and return data as a Promise
  7846. const icon = data.isSpell ? "fa-magic" : "fa-fist-raised";
  7847. const label = game.i18n.localize(`DND5E.AbilityUse${data.isSpell ? "Cast" : "Use"}`);
  7848. return new Promise(resolve => {
  7849. const dlg = new this(item, {
  7850. title: `${item.name}: ${game.i18n.localize("DND5E.AbilityUseConfig")}`,
  7851. content: html,
  7852. buttons: {
  7853. use: {
  7854. icon: `<i class="fas ${icon}"></i>`,
  7855. label: label,
  7856. callback: html => {
  7857. const fd = new FormDataExtended(html[0].querySelector("form"));
  7858. resolve(fd.object);
  7859. }
  7860. }
  7861. },
  7862. default: "use",
  7863. close: () => resolve(null)
  7864. });
  7865. dlg.render(true);
  7866. });
  7867. }
  7868. /* -------------------------------------------- */
  7869. /* Helpers */
  7870. /* -------------------------------------------- */
  7871. /**
  7872. * Get dialog data related to limited spell slots.
  7873. * @param {object} actorData System data from the actor using the spell.
  7874. * @param {object} itemData System data from the spell being used.
  7875. * @param {object} data Data for the dialog being presented.
  7876. * @returns {object} Modified dialog data.
  7877. * @private
  7878. */
  7879. static _getSpellData(actorData, itemData, data) {
  7880. // Determine whether the spell may be up-cast
  7881. const lvl = itemData.level;
  7882. const consumeSpellSlot = (lvl > 0) && CONFIG.DND5E.spellUpcastModes.includes(itemData.preparation.mode);
  7883. // If can't upcast, return early and don't bother calculating available spell slots
  7884. if (!consumeSpellSlot) {
  7885. return foundry.utils.mergeObject(data, { isSpell: true, consumeSpellSlot });
  7886. }
  7887. // Determine the levels which are feasible
  7888. let lmax = 0;
  7889. const spellLevels = Array.fromRange(10).reduce((arr, i) => {
  7890. if ( i < lvl ) return arr;
  7891. const label = CONFIG.DND5E.spellLevels[i];
  7892. const l = actorData.spells[`spell${i}`] || {max: 0, override: null};
  7893. let max = parseInt(l.override || l.max || 0);
  7894. let slots = Math.clamped(parseInt(l.value || 0), 0, max);
  7895. if ( max > 0 ) lmax = i;
  7896. arr.push({
  7897. level: i,
  7898. label: i > 0 ? game.i18n.format("DND5E.SpellLevelSlot", {level: label, n: slots}) : label,
  7899. canCast: max > 0,
  7900. hasSlots: slots > 0
  7901. });
  7902. return arr;
  7903. }, []).filter(sl => sl.level <= lmax);
  7904. // If this character has pact slots, present them as an option for casting the spell.
  7905. const pact = actorData.spells.pact;
  7906. if (pact.level >= lvl) {
  7907. spellLevels.push({
  7908. level: "pact",
  7909. label: `${game.i18n.format("DND5E.SpellLevelPact", {level: pact.level, n: pact.value})}`,
  7910. canCast: true,
  7911. hasSlots: pact.value > 0
  7912. });
  7913. }
  7914. const canCast = spellLevels.some(l => l.hasSlots);
  7915. if ( !canCast ) data.errors.push(game.i18n.format("DND5E.SpellCastNoSlots", {
  7916. level: CONFIG.DND5E.spellLevels[lvl],
  7917. name: data.item.name
  7918. }));
  7919. // Merge spell casting data
  7920. return foundry.utils.mergeObject(data, { isSpell: true, consumeSpellSlot, spellLevels });
  7921. }
  7922. /* -------------------------------------------- */
  7923. /**
  7924. * Get the ability usage note that is displayed.
  7925. * @param {object} item Data for the item being used.
  7926. * @param {{value: number, max: number, per: string}} uses Object uses and recovery configuration.
  7927. * @param {{charged: boolean, value: string}} recharge Object recharge configuration.
  7928. * @returns {string} Localized string indicating available uses.
  7929. * @private
  7930. */
  7931. static _getAbilityUseNote(item, uses, recharge) {
  7932. // Zero quantity
  7933. const quantity = item.system.quantity;
  7934. if ( quantity <= 0 ) return game.i18n.localize("DND5E.AbilityUseUnavailableHint");
  7935. // Abilities which use Recharge
  7936. if ( recharge.value ) {
  7937. return game.i18n.format(recharge.charged ? "DND5E.AbilityUseChargedHint" : "DND5E.AbilityUseRechargeHint", {
  7938. type: game.i18n.localize(CONFIG.Item.typeLabels[item.type])
  7939. });
  7940. }
  7941. // Does not use any resource
  7942. if ( !uses.per || !uses.max ) return "";
  7943. // Consumables
  7944. if ( item.type === "consumable" ) {
  7945. let str = "DND5E.AbilityUseNormalHint";
  7946. if ( uses.value > 1 ) str = "DND5E.AbilityUseConsumableChargeHint";
  7947. else if ( item.system.quantity === 1 && uses.autoDestroy ) str = "DND5E.AbilityUseConsumableDestroyHint";
  7948. else if ( item.system.quantity > 1 ) str = "DND5E.AbilityUseConsumableQuantityHint";
  7949. return game.i18n.format(str, {
  7950. type: game.i18n.localize(`DND5E.Consumable${item.system.consumableType.capitalize()}`),
  7951. value: uses.value,
  7952. quantity: item.system.quantity,
  7953. max: uses.max,
  7954. per: CONFIG.DND5E.limitedUsePeriods[uses.per]
  7955. });
  7956. }
  7957. // Other Items
  7958. else {
  7959. return game.i18n.format("DND5E.AbilityUseNormalHint", {
  7960. type: game.i18n.localize(CONFIG.Item.typeLabels[item.type]),
  7961. value: uses.value,
  7962. max: uses.max,
  7963. per: CONFIG.DND5E.limitedUsePeriods[uses.per]
  7964. });
  7965. }
  7966. }
  7967. }
  7968. /**
  7969. * Override and extend the basic Item implementation.
  7970. */
  7971. class Item5e extends Item {
  7972. /**
  7973. * Caches an item linked to this one, such as a subclass associated with a class.
  7974. * @type {Item5e}
  7975. * @private
  7976. */
  7977. _classLink;
  7978. /* -------------------------------------------- */
  7979. /* Item Properties */
  7980. /* -------------------------------------------- */
  7981. /**
  7982. * Which ability score modifier is used by this item?
  7983. * @type {string|null}
  7984. * @see {@link ActionTemplate#abilityMod}
  7985. */
  7986. get abilityMod() {
  7987. return this.system.abilityMod ?? null;
  7988. }
  7989. /* --------------------------------------------- */
  7990. /**
  7991. * What is the critical hit threshold for this item, if applicable?
  7992. * @type {number|null}
  7993. * @see {@link ActionTemplate#criticalThreshold}
  7994. */
  7995. get criticalThreshold() {
  7996. return this.system.criticalThreshold ?? null;
  7997. }
  7998. /* --------------------------------------------- */
  7999. /**
  8000. * Does the Item implement an ability check as part of its usage?
  8001. * @type {boolean}
  8002. * @see {@link ActionTemplate#hasAbilityCheck}
  8003. */
  8004. get hasAbilityCheck() {
  8005. return this.system.hasAbilityCheck ?? false;
  8006. }
  8007. /* -------------------------------------------- */
  8008. /**
  8009. * Does this item support advancement and have advancements defined?
  8010. * @type {boolean}
  8011. */
  8012. get hasAdvancement() {
  8013. return !!this.system.advancement?.length;
  8014. }
  8015. /* -------------------------------------------- */
  8016. /**
  8017. * Does the Item have an area of effect target?
  8018. * @type {boolean}
  8019. * @see {@link ActivatedEffectTemplate#hasAreaTarget}
  8020. */
  8021. get hasAreaTarget() {
  8022. return this.system.hasAreaTarget ?? false;
  8023. }
  8024. /* -------------------------------------------- */
  8025. /**
  8026. * Does the Item implement an attack roll as part of its usage?
  8027. * @type {boolean}
  8028. * @see {@link ActionTemplate#hasAttack}
  8029. */
  8030. get hasAttack() {
  8031. return this.system.hasAttack ?? false;
  8032. }
  8033. /* -------------------------------------------- */
  8034. /**
  8035. * Does the Item implement a damage roll as part of its usage?
  8036. * @type {boolean}
  8037. * @see {@link ActionTemplate#hasDamage}
  8038. */
  8039. get hasDamage() {
  8040. return this.system.hasDamage ?? false;
  8041. }
  8042. /* -------------------------------------------- */
  8043. /**
  8044. * Does the Item target one or more distinct targets?
  8045. * @type {boolean}
  8046. * @see {@link ActivatedEffectTemplate#hasIndividualTarget}
  8047. */
  8048. get hasIndividualTarget() {
  8049. return this.system.hasIndividualTarget ?? false;
  8050. }
  8051. /* -------------------------------------------- */
  8052. /**
  8053. * Is this Item limited in its ability to be used by charges or by recharge?
  8054. * @type {boolean}
  8055. * @see {@link ActivatedEffectTemplate#hasLimitedUses}
  8056. * @see {@link FeatData#hasLimitedUses}
  8057. */
  8058. get hasLimitedUses() {
  8059. return this.system.hasLimitedUses ?? false;
  8060. }
  8061. /* -------------------------------------------- */
  8062. /**
  8063. * Does the Item implement a saving throw as part of its usage?
  8064. * @type {boolean}
  8065. * @see {@link ActionTemplate#hasSave}
  8066. */
  8067. get hasSave() {
  8068. return this.system.hasSave ?? false;
  8069. }
  8070. /* -------------------------------------------- */
  8071. /**
  8072. * Does the Item have a target?
  8073. * @type {boolean}
  8074. * @see {@link ActivatedEffectTemplate#hasTarget}
  8075. */
  8076. get hasTarget() {
  8077. return this.system.hasTarget ?? false;
  8078. }
  8079. /* -------------------------------------------- */
  8080. /**
  8081. * Return an item's identifier.
  8082. * @type {string}
  8083. */
  8084. get identifier() {
  8085. return this.system.identifier || this.name.slugify({strict: true});
  8086. }
  8087. /* -------------------------------------------- */
  8088. /**
  8089. * Is this item any of the armor subtypes?
  8090. * @type {boolean}
  8091. * @see {@link EquipmentTemplate#isArmor}
  8092. */
  8093. get isArmor() {
  8094. return this.system.isArmor ?? false;
  8095. }
  8096. /* -------------------------------------------- */
  8097. /**
  8098. * Does the item provide an amount of healing instead of conventional damage?
  8099. * @type {boolean}
  8100. * @see {@link ActionTemplate#isHealing}
  8101. */
  8102. get isHealing() {
  8103. return this.system.isHealing ?? false;
  8104. }
  8105. /* -------------------------------------------- */
  8106. /**
  8107. * Is this item a separate large object like a siege engine or vehicle component that is
  8108. * usually mounted on fixtures rather than equipped, and has its own AC and HP?
  8109. * @type {boolean}
  8110. * @see {@link EquipmentData#isMountable}
  8111. * @see {@link WeaponData#isMountable}
  8112. */
  8113. get isMountable() {
  8114. return this.system.isMountable ?? false;
  8115. }
  8116. /* -------------------------------------------- */
  8117. /**
  8118. * Is this class item the original class for the containing actor? If the item is not a class or it is not
  8119. * embedded in an actor then this will return `null`.
  8120. * @type {boolean|null}
  8121. */
  8122. get isOriginalClass() {
  8123. if ( this.type !== "class" || !this.isEmbedded ) return null;
  8124. return this.id === this.parent.system.details.originalClass;
  8125. }
  8126. /* -------------------------------------------- */
  8127. /**
  8128. * Does the Item implement a versatile damage roll as part of its usage?
  8129. * @type {boolean}
  8130. * @see {@link ActionTemplate#isVersatile}
  8131. */
  8132. get isVersatile() {
  8133. return this.system.isVersatile ?? false;
  8134. }
  8135. /* -------------------------------------------- */
  8136. /**
  8137. * Class associated with this subclass. Always returns null on non-subclass or non-embedded items.
  8138. * @type {Item5e|null}
  8139. */
  8140. get class() {
  8141. if ( !this.isEmbedded || (this.type !== "subclass") ) return null;
  8142. const cid = this.system.classIdentifier;
  8143. return this._classLink ??= this.parent.items.find(i => (i.type === "class") && (i.identifier === cid));
  8144. }
  8145. /* -------------------------------------------- */
  8146. /**
  8147. * Subclass associated with this class. Always returns null on non-class or non-embedded items.
  8148. * @type {Item5e|null}
  8149. */
  8150. get subclass() {
  8151. if ( !this.isEmbedded || (this.type !== "class") ) return null;
  8152. const items = this.parent.items;
  8153. const cid = this.identifier;
  8154. return this._classLink ??= items.find(i => (i.type === "subclass") && (i.system.classIdentifier === cid));
  8155. }
  8156. /* -------------------------------------------- */
  8157. /**
  8158. * Retrieve scale values for current level from advancement data.
  8159. * @type {object}
  8160. */
  8161. get scaleValues() {
  8162. if ( !["class", "subclass"].includes(this.type) || !this.advancement.byType.ScaleValue ) return {};
  8163. const level = this.type === "class" ? this.system.levels : this.class?.system.levels ?? 0;
  8164. return this.advancement.byType.ScaleValue.reduce((obj, advancement) => {
  8165. obj[advancement.identifier] = advancement.valueForLevel(level);
  8166. return obj;
  8167. }, {});
  8168. }
  8169. /* -------------------------------------------- */
  8170. /**
  8171. * Spellcasting details for a class or subclass.
  8172. *
  8173. * @typedef {object} SpellcastingDescription
  8174. * @property {string} type Spellcasting type as defined in ``CONFIG.DND5E.spellcastingTypes`.
  8175. * @property {string|null} progression Progression within the specified spellcasting type if supported.
  8176. * @property {string} ability Ability used when casting spells from this class or subclass.
  8177. * @property {number|null} levels Number of levels of this class or subclass's class if embedded.
  8178. */
  8179. /**
  8180. * Retrieve the spellcasting for a class or subclass. For classes, this will return the spellcasting
  8181. * of the subclass if it overrides the class. For subclasses, this will return the class's spellcasting
  8182. * if no spellcasting is defined on the subclass.
  8183. * @type {SpellcastingDescription|null} Spellcasting object containing progression & ability.
  8184. */
  8185. get spellcasting() {
  8186. const spellcasting = this.system.spellcasting;
  8187. if ( !spellcasting ) return null;
  8188. const isSubclass = this.type === "subclass";
  8189. const classSC = isSubclass ? this.class?.system.spellcasting : spellcasting;
  8190. const subclassSC = isSubclass ? spellcasting : this.subclass?.system.spellcasting;
  8191. const finalSC = foundry.utils.deepClone(
  8192. ( subclassSC && (subclassSC.progression !== "none") ) ? subclassSC : classSC
  8193. );
  8194. if ( !finalSC ) return null;
  8195. finalSC.levels = this.isEmbedded ? (this.system.levels ?? this.class?.system.levels) : null;
  8196. // Temp method for determining spellcasting type until this data is available directly using advancement
  8197. if ( CONFIG.DND5E.spellcastingTypes[finalSC.progression] ) finalSC.type = finalSC.progression;
  8198. else finalSC.type = Object.entries(CONFIG.DND5E.spellcastingTypes).find(([type, data]) => {
  8199. return !!data.progression?.[finalSC.progression];
  8200. })?.[0];
  8201. return finalSC;
  8202. }
  8203. /* -------------------------------------------- */
  8204. /**
  8205. * Should this item's active effects be suppressed.
  8206. * @type {boolean}
  8207. */
  8208. get areEffectsSuppressed() {
  8209. const requireEquipped = (this.type !== "consumable")
  8210. || ["rod", "trinket", "wand"].includes(this.system.consumableType);
  8211. if ( requireEquipped && (this.system.equipped === false) ) return true;
  8212. return this.system.attunement === CONFIG.DND5E.attunementTypes.REQUIRED;
  8213. }
  8214. /* -------------------------------------------- */
  8215. /* Data Preparation */
  8216. /* -------------------------------------------- */
  8217. /** @inheritDoc */
  8218. prepareDerivedData() {
  8219. super.prepareDerivedData();
  8220. this.labels = {};
  8221. // Clear out linked item cache
  8222. this._classLink = undefined;
  8223. // Advancement
  8224. this._prepareAdvancement();
  8225. // Specialized preparation per Item type
  8226. switch ( this.type ) {
  8227. case "equipment":
  8228. this._prepareEquipment(); break;
  8229. case "feat":
  8230. this._prepareFeat(); break;
  8231. case "spell":
  8232. this._prepareSpell(); break;
  8233. }
  8234. // Activated Items
  8235. this._prepareActivation();
  8236. this._prepareAction();
  8237. // Un-owned items can have their final preparation done here, otherwise this needs to happen in the owning Actor
  8238. if ( !this.isOwned ) this.prepareFinalAttributes();
  8239. }
  8240. /* -------------------------------------------- */
  8241. /**
  8242. * Prepare derived data for an equipment-type item and define labels.
  8243. * @protected
  8244. */
  8245. _prepareEquipment() {
  8246. this.labels.armor = this.system.armor.value ? `${this.system.armor.value} ${game.i18n.localize("DND5E.AC")}` : "";
  8247. }
  8248. /* -------------------------------------------- */
  8249. /**
  8250. * Prepare derived data for a feat-type item and define labels.
  8251. * @protected
  8252. */
  8253. _prepareFeat() {
  8254. const act = this.system.activation;
  8255. const types = CONFIG.DND5E.abilityActivationTypes;
  8256. if ( act?.type === types.legendary ) this.labels.featType = game.i18n.localize("DND5E.LegendaryActionLabel");
  8257. else if ( act?.type === types.lair ) this.labels.featType = game.i18n.localize("DND5E.LairActionLabel");
  8258. else if ( act?.type ) {
  8259. this.labels.featType = game.i18n.localize(this.system.damage.length ? "DND5E.Attack" : "DND5E.Action");
  8260. }
  8261. else this.labels.featType = game.i18n.localize("DND5E.Passive");
  8262. }
  8263. /* -------------------------------------------- */
  8264. /**
  8265. * Prepare derived data for a spell-type item and define labels.
  8266. * @protected
  8267. */
  8268. _prepareSpell() {
  8269. const tags = Object.fromEntries(Object.entries(CONFIG.DND5E.spellTags).map(([k, v]) => {
  8270. v.tag = true;
  8271. return [k, v];
  8272. }));
  8273. const attributes = {...CONFIG.DND5E.spellComponents, ...tags};
  8274. this.system.preparation.mode ||= "prepared";
  8275. this.labels.level = CONFIG.DND5E.spellLevels[this.system.level];
  8276. this.labels.school = CONFIG.DND5E.spellSchools[this.system.school];
  8277. this.labels.components = Object.entries(this.system.components).reduce((obj, [c, active]) => {
  8278. const config = attributes[c];
  8279. if ( !config || (active !== true) ) return obj;
  8280. obj.all.push({abbr: config.abbr, tag: config.tag});
  8281. if ( config.tag ) obj.tags.push(config.label);
  8282. else obj.vsm.push(config.abbr);
  8283. return obj;
  8284. }, {all: [], vsm: [], tags: []});
  8285. this.labels.components.vsm = new Intl.ListFormat(game.i18n.lang, { style: "narrow", type: "conjunction" })
  8286. .format(this.labels.components.vsm);
  8287. this.labels.materials = this.system?.materials?.value ?? null;
  8288. }
  8289. /* -------------------------------------------- */
  8290. /**
  8291. * Prepare derived data for activated items and define labels.
  8292. * @protected
  8293. */
  8294. _prepareActivation() {
  8295. if ( !("activation" in this.system) ) return;
  8296. const C = CONFIG.DND5E;
  8297. // Ability Activation Label
  8298. const act = this.system.activation ?? {};
  8299. if ( ["none", ""].includes(act.type) ) act.type = null; // Backwards compatibility
  8300. this.labels.activation = act.type ? [act.cost, C.abilityActivationTypes[act.type]].filterJoin(" ") : "";
  8301. // Target Label
  8302. let tgt = this.system.target ?? {};
  8303. if ( ["none", ""].includes(tgt.type) ) tgt.type = null; // Backwards compatibility
  8304. if ( [null, "self"].includes(tgt.type) ) tgt.value = tgt.units = null;
  8305. else if ( tgt.units === "touch" ) tgt.value = null;
  8306. this.labels.target = tgt.type
  8307. ? [tgt.value, C.distanceUnits[tgt.units], C.targetTypes[tgt.type]].filterJoin(" ") : "";
  8308. // Range Label
  8309. let rng = this.system.range ?? {};
  8310. if ( ["none", ""].includes(rng.units) ) rng.units = null; // Backwards compatibility
  8311. if ( [null, "touch", "self"].includes(rng.units) ) rng.value = rng.long = null;
  8312. this.labels.range = rng.units
  8313. ? [rng.value, rng.long ? `/ ${rng.long}` : null, C.distanceUnits[rng.units]].filterJoin(" ") : "";
  8314. // Recharge Label
  8315. let chg = this.system.recharge ?? {};
  8316. const chgSuffix = `${chg.value}${parseInt(chg.value) < 6 ? "+" : ""}`;
  8317. this.labels.recharge = `${game.i18n.localize("DND5E.Recharge")} [${chgSuffix}]`;
  8318. }
  8319. /* -------------------------------------------- */
  8320. /**
  8321. * Prepare derived data and labels for items which have an action which deals damage.
  8322. * @protected
  8323. */
  8324. _prepareAction() {
  8325. if ( !("actionType" in this.system) ) return;
  8326. let dmg = this.system.damage || {};
  8327. if ( dmg.parts ) {
  8328. const types = CONFIG.DND5E.damageTypes;
  8329. this.labels.damage = dmg.parts.map(d => d[0]).join(" + ").replace(/\+ -/g, "- ");
  8330. this.labels.damageTypes = dmg.parts.map(d => types[d[1]]).join(", ");
  8331. }
  8332. }
  8333. /* -------------------------------------------- */
  8334. /**
  8335. * Prepare advancement objects from stored advancement data.
  8336. * @protected
  8337. */
  8338. _prepareAdvancement() {
  8339. const minAdvancementLevel = ["class", "subclass"].includes(this.type) ? 1 : 0;
  8340. this.advancement = {
  8341. byId: {},
  8342. byLevel: Object.fromEntries(
  8343. Array.fromRange(CONFIG.DND5E.maxLevel + 1).slice(minAdvancementLevel).map(l => [l, []])
  8344. ),
  8345. byType: {},
  8346. needingConfiguration: []
  8347. };
  8348. for ( const advancement of this.system.advancement ?? [] ) {
  8349. if ( !(advancement instanceof Advancement) ) continue;
  8350. this.advancement.byId[advancement.id] = advancement;
  8351. this.advancement.byType[advancement.type] ??= [];
  8352. this.advancement.byType[advancement.type].push(advancement);
  8353. advancement.levels.forEach(l => this.advancement.byLevel[l].push(advancement));
  8354. if ( !advancement.levels.length ) this.advancement.needingConfiguration.push(advancement);
  8355. }
  8356. Object.entries(this.advancement.byLevel).forEach(([lvl, data]) => data.sort((a, b) => {
  8357. return a.sortingValueForLevel(lvl).localeCompare(b.sortingValueForLevel(lvl));
  8358. }));
  8359. }
  8360. /* -------------------------------------------- */
  8361. /**
  8362. * Compute item attributes which might depend on prepared actor data. If this item is embedded this method will
  8363. * be called after the actor's data is prepared.
  8364. * Otherwise, it will be called at the end of `Item5e#prepareDerivedData`.
  8365. */
  8366. prepareFinalAttributes() {
  8367. // Proficiency
  8368. if ( this.actor?.system.attributes?.prof ) {
  8369. const isProficient = (this.type === "spell") || this.system.proficient; // Always proficient in spell attacks.
  8370. this.system.prof = new Proficiency(this.actor?.system.attributes.prof, isProficient);
  8371. }
  8372. // Class data
  8373. if ( this.type === "class" ) this.system.isOriginalClass = this.isOriginalClass;
  8374. // Action usage
  8375. if ( "actionType" in this.system ) {
  8376. this.labels.abilityCheck = game.i18n.format("DND5E.AbilityPromptTitle", {
  8377. ability: CONFIG.DND5E.abilities[this.system.ability]?.label ?? ""
  8378. });
  8379. // Saving throws
  8380. this.getSaveDC();
  8381. // To Hit
  8382. this.getAttackToHit();
  8383. // Limited Uses
  8384. this.prepareMaxUses();
  8385. // Duration
  8386. this.prepareDurationValue();
  8387. // Damage Label
  8388. this.getDerivedDamageLabel();
  8389. }
  8390. }
  8391. /* -------------------------------------------- */
  8392. /**
  8393. * Populate a label with the compiled and simplified damage formula based on owned item
  8394. * actor data. This is only used for display purposes and is not related to `Item5e#rollDamage`.
  8395. * @returns {{damageType: string, formula: string, label: string}[]}
  8396. */
  8397. getDerivedDamageLabel() {
  8398. if ( !this.hasDamage || !this.isOwned ) return [];
  8399. const rollData = this.getRollData();
  8400. const damageLabels = { ...CONFIG.DND5E.damageTypes, ...CONFIG.DND5E.healingTypes };
  8401. const derivedDamage = this.system.damage?.parts?.map(damagePart => {
  8402. let formula;
  8403. try {
  8404. const roll = new Roll(damagePart[0], rollData);
  8405. formula = simplifyRollFormula(roll.formula, { preserveFlavor: true });
  8406. }
  8407. catch(err) {
  8408. console.warn(`Unable to simplify formula for ${this.name}: ${err}`);
  8409. }
  8410. const damageType = damagePart[1];
  8411. return { formula, damageType, label: `${formula} ${damageLabels[damageType] ?? ""}` };
  8412. });
  8413. return this.labels.derivedDamage = derivedDamage;
  8414. }
  8415. /* -------------------------------------------- */
  8416. /**
  8417. * Update the derived spell DC for an item that requires a saving throw.
  8418. * @returns {number|null}
  8419. */
  8420. getSaveDC() {
  8421. if ( !this.hasSave ) return null;
  8422. const save = this.system.save;
  8423. // Actor spell-DC based scaling
  8424. if ( save.scaling === "spell" ) {
  8425. save.dc = this.isOwned ? this.actor.system.attributes.spelldc : null;
  8426. }
  8427. // Ability-score based scaling
  8428. else if ( save.scaling !== "flat" ) {
  8429. save.dc = this.isOwned ? this.actor.system.abilities[save.scaling].dc : null;
  8430. }
  8431. // Update labels
  8432. const abl = CONFIG.DND5E.abilities[save.ability]?.label ?? "";
  8433. this.labels.save = game.i18n.format("DND5E.SaveDC", {dc: save.dc || "", ability: abl});
  8434. return save.dc;
  8435. }
  8436. /* -------------------------------------------- */
  8437. /**
  8438. * Update a label to the Item detailing its total to hit bonus from the following sources:
  8439. * - item document's innate attack bonus
  8440. * - item's actor's proficiency bonus if applicable
  8441. * - item's actor's global bonuses to the given item type
  8442. * - item's ammunition if applicable
  8443. * @returns {{rollData: object, parts: string[]}|null} Data used in the item's Attack roll.
  8444. */
  8445. getAttackToHit() {
  8446. if ( !this.hasAttack ) return null;
  8447. const rollData = this.getRollData();
  8448. const parts = [];
  8449. // Include the item's innate attack bonus as the initial value and label
  8450. const ab = this.system.attackBonus;
  8451. if ( ab ) {
  8452. parts.push(ab);
  8453. this.labels.toHit = !/^[+-]/.test(ab) ? `+ ${ab}` : ab;
  8454. }
  8455. // Take no further action for un-owned items
  8456. if ( !this.isOwned ) return {rollData, parts};
  8457. // Ability score modifier
  8458. parts.push("@mod");
  8459. // Add proficiency bonus if an explicit proficiency flag is present or for non-item features
  8460. if ( !["weapon", "consumable"].includes(this.type) || this.system.proficient ) {
  8461. parts.push("@prof");
  8462. if ( this.system.prof?.hasProficiency ) rollData.prof = this.system.prof.term;
  8463. }
  8464. // Actor-level global bonus to attack rolls
  8465. const actorBonus = this.actor.system.bonuses?.[this.system.actionType] || {};
  8466. if ( actorBonus.attack ) parts.push(actorBonus.attack);
  8467. // One-time bonus provided by consumed ammunition
  8468. if ( (this.system.consume?.type === "ammo") && this.actor.items ) {
  8469. const ammoItem = this.actor.items.get(this.system.consume.target);
  8470. if ( ammoItem ) {
  8471. const ammoItemQuantity = ammoItem.system.quantity;
  8472. const ammoCanBeConsumed = ammoItemQuantity && (ammoItemQuantity - (this.system.consume.amount ?? 0) >= 0);
  8473. const ammoItemAttackBonus = ammoItem.system.attackBonus;
  8474. const ammoIsTypeConsumable = (ammoItem.type === "consumable") && (ammoItem.system.consumableType === "ammo");
  8475. if ( ammoCanBeConsumed && ammoItemAttackBonus && ammoIsTypeConsumable ) {
  8476. parts.push("@ammo");
  8477. rollData.ammo = ammoItemAttackBonus;
  8478. }
  8479. }
  8480. }
  8481. // Condense the resulting attack bonus formula into a simplified label
  8482. const roll = new Roll(parts.join("+"), rollData);
  8483. const formula = simplifyRollFormula(roll.formula) || "0";
  8484. this.labels.toHit = !/^[+-]/.test(formula) ? `+ ${formula}` : formula;
  8485. return {rollData, parts};
  8486. }
  8487. /* -------------------------------------------- */
  8488. /**
  8489. * Populates the max uses of an item.
  8490. * If the item is an owned item and the `max` is not numeric, calculate based on actor data.
  8491. */
  8492. prepareMaxUses() {
  8493. const uses = this.system.uses;
  8494. if ( !uses?.max ) return;
  8495. let max = uses.max;
  8496. if ( this.isOwned && !Number.isNumeric(max) ) {
  8497. const property = game.i18n.localize("DND5E.UsesMax");
  8498. try {
  8499. const rollData = this.getRollData({ deterministic: true });
  8500. max = Roll.safeEval(this.replaceFormulaData(max, rollData, { property }));
  8501. } catch(e) {
  8502. const message = game.i18n.format("DND5E.FormulaMalformedError", { property, name: this.name });
  8503. this.actor._preparationWarnings.push({ message, link: this.uuid, type: "error" });
  8504. console.error(message, e);
  8505. return;
  8506. }
  8507. }
  8508. uses.max = Number(max);
  8509. }
  8510. /* -------------------------------------------- */
  8511. /**
  8512. * Populate the duration value of an item. If the item is an owned item and the
  8513. * duration value is not numeric, calculate based on actor data.
  8514. */
  8515. prepareDurationValue() {
  8516. const duration = this.system.duration;
  8517. if ( !duration?.value ) return;
  8518. let value = duration.value;
  8519. // If this is an owned item and the value is not numeric, we need to calculate it
  8520. if ( this.isOwned && !Number.isNumeric(value) ) {
  8521. const property = game.i18n.localize("DND5E.Duration");
  8522. try {
  8523. const rollData = this.getRollData({ deterministic: true });
  8524. value = Roll.safeEval(this.replaceFormulaData(value, rollData, { property }));
  8525. } catch(e) {
  8526. const message = game.i18n.format("DND5E.FormulaMalformedError", { property, name: this.name });
  8527. this.actor._preparationWarnings.push({ message, link: this.uuid, type: "error" });
  8528. console.error(message, e);
  8529. return;
  8530. }
  8531. }
  8532. duration.value = Number(value);
  8533. // Now that duration value is a number, set the label
  8534. if ( ["inst", "perm"].includes(duration.units) ) duration.value = null;
  8535. this.labels.duration = [duration.value, CONFIG.DND5E.timePeriods[duration.units]].filterJoin(" ");
  8536. }
  8537. /* -------------------------------------------- */
  8538. /**
  8539. * Replace referenced data attributes in the roll formula with values from the provided data.
  8540. * If the attribute is not found in the provided data, display a warning on the actor.
  8541. * @param {string} formula The original formula within which to replace.
  8542. * @param {object} data The data object which provides replacements.
  8543. * @param {object} options
  8544. * @param {string} options.property Name of the property to which this formula belongs.
  8545. * @returns {string} Formula with replaced data.
  8546. */
  8547. replaceFormulaData(formula, data, { property }) {
  8548. const dataRgx = new RegExp(/@([a-z.0-9_-]+)/gi);
  8549. const missingReferences = new Set();
  8550. formula = formula.replace(dataRgx, (match, term) => {
  8551. let value = foundry.utils.getProperty(data, term);
  8552. if ( value == null ) {
  8553. missingReferences.add(match);
  8554. return "0";
  8555. }
  8556. return String(value).trim();
  8557. });
  8558. if ( (missingReferences.size > 0) && this.actor ) {
  8559. const listFormatter = new Intl.ListFormat(game.i18n.lang, { style: "long", type: "conjunction" });
  8560. const message = game.i18n.format("DND5E.FormulaMissingReferenceWarn", {
  8561. property, name: this.name, references: listFormatter.format(missingReferences)
  8562. });
  8563. this.actor._preparationWarnings.push({ message, link: this.uuid, type: "warning" });
  8564. }
  8565. return formula;
  8566. }
  8567. /* -------------------------------------------- */
  8568. /**
  8569. * Configuration data for an item usage being prepared.
  8570. *
  8571. * @typedef {object} ItemUseConfiguration
  8572. * @property {boolean} createMeasuredTemplate Trigger a template creation
  8573. * @property {boolean} consumeQuantity Should the item's quantity be consumed?
  8574. * @property {boolean} consumeRecharge Should a recharge be consumed?
  8575. * @property {boolean} consumeResource Should a linked (non-ammo) resource be consumed?
  8576. * @property {number|string|null} consumeSpellLevel Specific spell level to consume, or "pact" for pact level.
  8577. * @property {boolean} consumeSpellSlot Should any spell slot be consumed?
  8578. * @property {boolean} consumeUsage Should limited uses be consumed?
  8579. * @property {boolean} needsConfiguration Is user-configuration needed?
  8580. */
  8581. /**
  8582. * Additional options used for configuring item usage.
  8583. *
  8584. * @typedef {object} ItemUseOptions
  8585. * @property {boolean} configureDialog Display a configuration dialog for the item usage, if applicable?
  8586. * @property {string} rollMode The roll display mode with which to display (or not) the card.
  8587. * @property {boolean} createMessage Whether to automatically create a chat message (if true) or simply return
  8588. * the prepared chat message data (if false).
  8589. * @property {object} flags Additional flags added to the chat message.
  8590. * @property {Event} event The browser event which triggered the item usage, if any.
  8591. */
  8592. /**
  8593. * Trigger an item usage, optionally creating a chat message with followup actions.
  8594. * @param {ItemUseOptions} [options] Options used for configuring item usage.
  8595. * @returns {Promise<ChatMessage|object|void>} Chat message if options.createMessage is true, message data if it is
  8596. * false, and nothing if the roll wasn't performed.
  8597. * @deprecated since 2.0 in favor of `Item5e#use`, targeted for removal in 2.4
  8598. */
  8599. async roll(options={}) {
  8600. foundry.utils.logCompatibilityWarning(
  8601. "Item5e#roll has been renamed Item5e#use. Support for the old name will be removed in future versions.",
  8602. { since: "DnD5e 2.0", until: "DnD5e 2.4" }
  8603. );
  8604. return this.use(undefined, options);
  8605. }
  8606. /**
  8607. * Trigger an item usage, optionally creating a chat message with followup actions.
  8608. * @param {ItemUseConfiguration} [config] Initial configuration data for the usage.
  8609. * @param {ItemUseOptions} [options] Options used for configuring item usage.
  8610. * @returns {Promise<ChatMessage|object|void>} Chat message if options.createMessage is true, message data if it is
  8611. * false, and nothing if the roll wasn't performed.
  8612. */
  8613. async use(config={}, options={}) {
  8614. let item = this;
  8615. const is = item.system;
  8616. const as = item.actor.system;
  8617. // Ensure the options object is ready
  8618. options = foundry.utils.mergeObject({
  8619. configureDialog: true,
  8620. createMessage: true,
  8621. "flags.dnd5e.use": {type: this.type, itemId: this.id, itemUuid: this.uuid}
  8622. }, options);
  8623. // Reference aspects of the item data necessary for usage
  8624. const resource = is.consume || {}; // Resource consumption
  8625. const isSpell = item.type === "spell"; // Does the item require a spell slot?
  8626. const requireSpellSlot = isSpell && (is.level > 0) && CONFIG.DND5E.spellUpcastModes.includes(is.preparation.mode);
  8627. // Define follow-up actions resulting from the item usage
  8628. config = foundry.utils.mergeObject({
  8629. createMeasuredTemplate: item.hasAreaTarget,
  8630. consumeQuantity: is.uses?.autoDestroy ?? false,
  8631. consumeRecharge: !!is.recharge?.value,
  8632. consumeResource: !!resource.target && (!item.hasAttack || (resource.type !== "ammo")),
  8633. consumeSpellLevel: requireSpellSlot ? is.preparation.mode === "pact" ? "pact" : is.level : null,
  8634. consumeSpellSlot: requireSpellSlot,
  8635. consumeUsage: !!is.uses?.per && (is.uses?.max > 0)
  8636. }, config);
  8637. // Display a configuration dialog to customize the usage
  8638. if ( config.needsConfiguration === undefined ) config.needsConfiguration = config.createMeasuredTemplate
  8639. || config.consumeRecharge || config.consumeResource || config.consumeSpellSlot || config.consumeUsage;
  8640. /**
  8641. * A hook event that fires before an item usage is configured.
  8642. * @function dnd5e.preUseItem
  8643. * @memberof hookEvents
  8644. * @param {Item5e} item Item being used.
  8645. * @param {ItemUseConfiguration} config Configuration data for the item usage being prepared.
  8646. * @param {ItemUseOptions} options Additional options used for configuring item usage.
  8647. * @returns {boolean} Explicitly return `false` to prevent item from being used.
  8648. */
  8649. if ( Hooks.call("dnd5e.preUseItem", item, config, options) === false ) return;
  8650. // Display configuration dialog
  8651. if ( (options.configureDialog !== false) && config.needsConfiguration ) {
  8652. const configuration = await AbilityUseDialog.create(item);
  8653. if ( !configuration ) return;
  8654. foundry.utils.mergeObject(config, configuration);
  8655. }
  8656. // Handle spell upcasting
  8657. if ( isSpell && (config.consumeSpellSlot || config.consumeSpellLevel) ) {
  8658. const upcastLevel = config.consumeSpellLevel === "pact" ? as.spells.pact.level
  8659. : parseInt(config.consumeSpellLevel);
  8660. if ( upcastLevel && (upcastLevel !== is.level) ) {
  8661. item = item.clone({"system.level": upcastLevel}, {keepId: true});
  8662. item.prepareData();
  8663. item.prepareFinalAttributes();
  8664. }
  8665. }
  8666. if ( isSpell ) foundry.utils.mergeObject(options.flags, {"dnd5e.use.spellLevel": item.system.level});
  8667. /**
  8668. * A hook event that fires before an item's resource consumption has been calculated.
  8669. * @function dnd5e.preItemUsageConsumption
  8670. * @memberof hookEvents
  8671. * @param {Item5e} item Item being used.
  8672. * @param {ItemUseConfiguration} config Configuration data for the item usage being prepared.
  8673. * @param {ItemUseOptions} options Additional options used for configuring item usage.
  8674. * @returns {boolean} Explicitly return `false` to prevent item from being used.
  8675. */
  8676. if ( Hooks.call("dnd5e.preItemUsageConsumption", item, config, options) === false ) return;
  8677. // Determine whether the item can be used by testing for resource consumption
  8678. const usage = item._getUsageUpdates(config);
  8679. if ( !usage ) return;
  8680. /**
  8681. * A hook event that fires after an item's resource consumption has been calculated but before any
  8682. * changes have been made.
  8683. * @function dnd5e.itemUsageConsumption
  8684. * @memberof hookEvents
  8685. * @param {Item5e} item Item being used.
  8686. * @param {ItemUseConfiguration} config Configuration data for the item usage being prepared.
  8687. * @param {ItemUseOptions} options Additional options used for configuring item usage.
  8688. * @param {object} usage
  8689. * @param {object} usage.actorUpdates Updates that will be applied to the actor.
  8690. * @param {object} usage.itemUpdates Updates that will be applied to the item being used.
  8691. * @param {object[]} usage.resourceUpdates Updates that will be applied to other items on the actor.
  8692. * @returns {boolean} Explicitly return `false` to prevent item from being used.
  8693. */
  8694. if ( Hooks.call("dnd5e.itemUsageConsumption", item, config, options, usage) === false ) return;
  8695. // Commit pending data updates
  8696. const { actorUpdates, itemUpdates, resourceUpdates } = usage;
  8697. if ( !foundry.utils.isEmpty(itemUpdates) ) await item.update(itemUpdates);
  8698. if ( config.consumeQuantity && (item.system.quantity === 0) ) await item.delete();
  8699. if ( !foundry.utils.isEmpty(actorUpdates) ) await this.actor.update(actorUpdates);
  8700. if ( resourceUpdates.length ) await this.actor.updateEmbeddedDocuments("Item", resourceUpdates);
  8701. // Prepare card data & display it if options.createMessage is true
  8702. const cardData = await item.displayCard(options);
  8703. // Initiate measured template creation
  8704. let templates;
  8705. if ( config.createMeasuredTemplate ) {
  8706. try {
  8707. templates = await (dnd5e.canvas.AbilityTemplate.fromItem(item))?.drawPreview();
  8708. } catch(err) {}
  8709. }
  8710. /**
  8711. * A hook event that fires when an item is used, after the measured template has been created if one is needed.
  8712. * @function dnd5e.useItem
  8713. * @memberof hookEvents
  8714. * @param {Item5e} item Item being used.
  8715. * @param {ItemUseConfiguration} config Configuration data for the roll.
  8716. * @param {ItemUseOptions} options Additional options for configuring item usage.
  8717. * @param {MeasuredTemplateDocument[]|null} templates The measured templates if they were created.
  8718. */
  8719. Hooks.callAll("dnd5e.useItem", item, config, options, templates ?? null);
  8720. return cardData;
  8721. }
  8722. /* -------------------------------------------- */
  8723. /**
  8724. * Verify that the consumed resources used by an Item are available and prepare the updates that should
  8725. * be performed. If required resources are not available, display an error and return false.
  8726. * @param {ItemUseConfiguration} config Configuration data for an item usage being prepared.
  8727. * @returns {object|boolean} A set of data changes to apply when the item is used, or false.
  8728. * @protected
  8729. */
  8730. _getUsageUpdates({
  8731. consumeQuantity, consumeRecharge, consumeResource, consumeSpellSlot,
  8732. consumeSpellLevel, consumeUsage}) {
  8733. const actorUpdates = {};
  8734. const itemUpdates = {};
  8735. const resourceUpdates = [];
  8736. // Consume Recharge
  8737. if ( consumeRecharge ) {
  8738. const recharge = this.system.recharge || {};
  8739. if ( recharge.charged === false ) {
  8740. ui.notifications.warn(game.i18n.format("DND5E.ItemNoUses", {name: this.name}));
  8741. return false;
  8742. }
  8743. itemUpdates["system.recharge.charged"] = false;
  8744. }
  8745. // Consume Limited Resource
  8746. if ( consumeResource ) {
  8747. const canConsume = this._handleConsumeResource(itemUpdates, actorUpdates, resourceUpdates);
  8748. if ( canConsume === false ) return false;
  8749. }
  8750. // Consume Spell Slots
  8751. if ( consumeSpellSlot && consumeSpellLevel ) {
  8752. if ( Number.isNumeric(consumeSpellLevel) ) consumeSpellLevel = `spell${consumeSpellLevel}`;
  8753. const level = this.actor?.system.spells[consumeSpellLevel];
  8754. const spells = Number(level?.value ?? 0);
  8755. if ( spells === 0 ) {
  8756. const labelKey = consumeSpellLevel === "pact" ? "DND5E.SpellProgPact" : `DND5E.SpellLevel${this.system.level}`;
  8757. const label = game.i18n.localize(labelKey);
  8758. ui.notifications.warn(game.i18n.format("DND5E.SpellCastNoSlots", {name: this.name, level: label}));
  8759. return false;
  8760. }
  8761. actorUpdates[`system.spells.${consumeSpellLevel}.value`] = Math.max(spells - 1, 0);
  8762. }
  8763. // Consume Limited Usage
  8764. if ( consumeUsage ) {
  8765. const uses = this.system.uses || {};
  8766. const available = Number(uses.value ?? 0);
  8767. let used = false;
  8768. const remaining = Math.max(available - 1, 0);
  8769. if ( available >= 1 ) {
  8770. used = true;
  8771. itemUpdates["system.uses.value"] = remaining;
  8772. }
  8773. // Reduce quantity if not reducing usages or if usages hit zero, and we are set to consumeQuantity
  8774. if ( consumeQuantity && (!used || (remaining === 0)) ) {
  8775. const q = Number(this.system.quantity ?? 1);
  8776. if ( q >= 1 ) {
  8777. used = true;
  8778. itemUpdates["system.quantity"] = Math.max(q - 1, 0);
  8779. itemUpdates["system.uses.value"] = uses.max ?? 1;
  8780. }
  8781. }
  8782. // If the item was not used, return a warning
  8783. if ( !used ) {
  8784. ui.notifications.warn(game.i18n.format("DND5E.ItemNoUses", {name: this.name}));
  8785. return false;
  8786. }
  8787. }
  8788. // Return the configured usage
  8789. return {itemUpdates, actorUpdates, resourceUpdates};
  8790. }
  8791. /* -------------------------------------------- */
  8792. /**
  8793. * Handle update actions required when consuming an external resource
  8794. * @param {object} itemUpdates An object of data updates applied to this item
  8795. * @param {object} actorUpdates An object of data updates applied to the item owner (Actor)
  8796. * @param {object[]} resourceUpdates An array of updates to apply to other items owned by the actor
  8797. * @returns {boolean|void} Return false to block further progress, or return nothing to continue
  8798. * @protected
  8799. */
  8800. _handleConsumeResource(itemUpdates, actorUpdates, resourceUpdates) {
  8801. const consume = this.system.consume || {};
  8802. if ( !consume.type ) return;
  8803. // No consumed target
  8804. const typeLabel = CONFIG.DND5E.abilityConsumptionTypes[consume.type];
  8805. if ( !consume.target ) {
  8806. ui.notifications.warn(game.i18n.format("DND5E.ConsumeWarningNoResource", {name: this.name, type: typeLabel}));
  8807. return false;
  8808. }
  8809. // Identify the consumed resource and its current quantity
  8810. let resource = null;
  8811. let amount = Number(consume.amount ?? 1);
  8812. let quantity = 0;
  8813. switch ( consume.type ) {
  8814. case "attribute":
  8815. resource = foundry.utils.getProperty(this.actor.system, consume.target);
  8816. quantity = resource || 0;
  8817. break;
  8818. case "ammo":
  8819. case "material":
  8820. resource = this.actor.items.get(consume.target);
  8821. quantity = resource ? resource.system.quantity : 0;
  8822. break;
  8823. case "hitDice":
  8824. const denom = !["smallest", "largest"].includes(consume.target) ? consume.target : false;
  8825. resource = Object.values(this.actor.classes).filter(cls => !denom || (cls.system.hitDice === denom));
  8826. quantity = resource.reduce((count, cls) => count + cls.system.levels - cls.system.hitDiceUsed, 0);
  8827. break;
  8828. case "charges":
  8829. resource = this.actor.items.get(consume.target);
  8830. if ( !resource ) break;
  8831. const uses = resource.system.uses;
  8832. if ( uses.per && uses.max ) quantity = uses.value;
  8833. else if ( resource.system.recharge?.value ) {
  8834. quantity = resource.system.recharge.charged ? 1 : 0;
  8835. amount = 1;
  8836. }
  8837. break;
  8838. }
  8839. // Verify that a consumed resource is available
  8840. if ( resource === undefined ) {
  8841. ui.notifications.warn(game.i18n.format("DND5E.ConsumeWarningNoSource", {name: this.name, type: typeLabel}));
  8842. return false;
  8843. }
  8844. // Verify that the required quantity is available
  8845. let remaining = quantity - amount;
  8846. if ( remaining < 0 ) {
  8847. ui.notifications.warn(game.i18n.format("DND5E.ConsumeWarningNoQuantity", {name: this.name, type: typeLabel}));
  8848. return false;
  8849. }
  8850. // Define updates to provided data objects
  8851. switch ( consume.type ) {
  8852. case "attribute":
  8853. actorUpdates[`system.${consume.target}`] = remaining;
  8854. break;
  8855. case "ammo":
  8856. case "material":
  8857. resourceUpdates.push({_id: consume.target, "system.quantity": remaining});
  8858. break;
  8859. case "hitDice":
  8860. if ( ["smallest", "largest"].includes(consume.target) ) resource = resource.sort((lhs, rhs) => {
  8861. let sort = lhs.system.hitDice.localeCompare(rhs.system.hitDice, "en", {numeric: true});
  8862. if ( consume.target === "largest" ) sort *= -1;
  8863. return sort;
  8864. });
  8865. let toConsume = consume.amount;
  8866. for ( const cls of resource ) {
  8867. const available = (toConsume > 0 ? cls.system.levels : 0) - cls.system.hitDiceUsed;
  8868. const delta = toConsume > 0 ? Math.min(toConsume, available) : Math.max(toConsume, available);
  8869. if ( delta !== 0 ) {
  8870. resourceUpdates.push({_id: cls.id, "system.hitDiceUsed": cls.system.hitDiceUsed + delta});
  8871. toConsume -= delta;
  8872. if ( toConsume === 0 ) break;
  8873. }
  8874. }
  8875. break;
  8876. case "charges":
  8877. const uses = resource.system.uses || {};
  8878. const recharge = resource.system.recharge || {};
  8879. const update = {_id: consume.target};
  8880. if ( uses.per && uses.max ) update["system.uses.value"] = remaining;
  8881. else if ( recharge.value ) update["system.recharge.charged"] = false;
  8882. resourceUpdates.push(update);
  8883. break;
  8884. }
  8885. }
  8886. /* -------------------------------------------- */
  8887. /**
  8888. * Display the chat card for an Item as a Chat Message
  8889. * @param {ItemUseOptions} [options] Options which configure the display of the item chat card.
  8890. * @returns {ChatMessage|object} Chat message if `createMessage` is true, otherwise an object containing
  8891. * message data.
  8892. */
  8893. async displayCard(options={}) {
  8894. // Render the chat card template
  8895. const token = this.actor.token;
  8896. const templateData = {
  8897. actor: this.actor,
  8898. tokenId: token?.uuid || null,
  8899. item: this,
  8900. data: await this.getChatData(),
  8901. labels: this.labels,
  8902. hasAttack: this.hasAttack,
  8903. isHealing: this.isHealing,
  8904. hasDamage: this.hasDamage,
  8905. isVersatile: this.isVersatile,
  8906. isSpell: this.type === "spell",
  8907. hasSave: this.hasSave,
  8908. hasAreaTarget: this.hasAreaTarget,
  8909. isTool: this.type === "tool",
  8910. hasAbilityCheck: this.hasAbilityCheck
  8911. };
  8912. const html = await renderTemplate("systems/dnd5e/templates/chat/item-card.hbs", templateData);
  8913. // Create the ChatMessage data object
  8914. const chatData = {
  8915. user: game.user.id,
  8916. type: CONST.CHAT_MESSAGE_TYPES.OTHER,
  8917. content: html,
  8918. flavor: this.system.chatFlavor || this.name,
  8919. speaker: ChatMessage.getSpeaker({actor: this.actor, token}),
  8920. flags: {"core.canPopout": true}
  8921. };
  8922. // If the Item was destroyed in the process of displaying its card - embed the item data in the chat message
  8923. if ( (this.type === "consumable") && !this.actor.items.has(this.id) ) {
  8924. chatData.flags["dnd5e.itemData"] = templateData.item.toObject();
  8925. }
  8926. // Merge in the flags from options
  8927. chatData.flags = foundry.utils.mergeObject(chatData.flags, options.flags);
  8928. /**
  8929. * A hook event that fires before an item chat card is created.
  8930. * @function dnd5e.preDisplayCard
  8931. * @memberof hookEvents
  8932. * @param {Item5e} item Item for which the chat card is being displayed.
  8933. * @param {object} chatData Data used to create the chat message.
  8934. * @param {ItemUseOptions} options Options which configure the display of the item chat card.
  8935. */
  8936. Hooks.callAll("dnd5e.preDisplayCard", this, chatData, options);
  8937. // Apply the roll mode to adjust message visibility
  8938. ChatMessage.applyRollMode(chatData, options.rollMode ?? game.settings.get("core", "rollMode"));
  8939. // Create the Chat Message or return its data
  8940. const card = (options.createMessage !== false) ? await ChatMessage.create(chatData) : chatData;
  8941. /**
  8942. * A hook event that fires after an item chat card is created.
  8943. * @function dnd5e.displayCard
  8944. * @memberof hookEvents
  8945. * @param {Item5e} item Item for which the chat card is being displayed.
  8946. * @param {ChatMessage|object} card The created ChatMessage instance or ChatMessageData depending on whether
  8947. * options.createMessage was set to `true`.
  8948. */
  8949. Hooks.callAll("dnd5e.displayCard", this, card);
  8950. return card;
  8951. }
  8952. /* -------------------------------------------- */
  8953. /* Chat Cards */
  8954. /* -------------------------------------------- */
  8955. /**
  8956. * Prepare an object of chat data used to display a card for the Item in the chat log.
  8957. * @param {object} htmlOptions Options used by the TextEditor.enrichHTML function.
  8958. * @returns {object} An object of chat data to render.
  8959. */
  8960. async getChatData(htmlOptions={}) {
  8961. const data = this.toObject().system;
  8962. // Rich text description
  8963. data.description.value = await TextEditor.enrichHTML(data.description.value, {
  8964. async: true,
  8965. relativeTo: this,
  8966. rollData: this.getRollData(),
  8967. ...htmlOptions
  8968. });
  8969. // Type specific properties
  8970. data.properties = [
  8971. ...this.system.chatProperties ?? [],
  8972. ...this.system.equippableItemChatProperties ?? [],
  8973. ...this.system.activatedEffectChatProperties ?? []
  8974. ].filter(p => p);
  8975. return data;
  8976. }
  8977. /* -------------------------------------------- */
  8978. /* Item Rolls - Attack, Damage, Saves, Checks */
  8979. /* -------------------------------------------- */
  8980. /**
  8981. * Place an attack roll using an item (weapon, feat, spell, or equipment)
  8982. * Rely upon the d20Roll logic for the core implementation
  8983. *
  8984. * @param {D20RollConfiguration} options Roll options which are configured and provided to the d20Roll function
  8985. * @returns {Promise<D20Roll|null>} A Promise which resolves to the created Roll instance
  8986. */
  8987. async rollAttack(options={}) {
  8988. const flags = this.actor.flags.dnd5e ?? {};
  8989. if ( !this.hasAttack ) throw new Error("You may not place an Attack Roll with this Item.");
  8990. let title = `${this.name} - ${game.i18n.localize("DND5E.AttackRoll")}`;
  8991. // Get the parts and rollData for this item's attack
  8992. const {parts, rollData} = this.getAttackToHit();
  8993. if ( options.spellLevel ) rollData.item.level = options.spellLevel;
  8994. // Handle ammunition consumption
  8995. delete this._ammo;
  8996. let ammo = null;
  8997. let ammoUpdate = [];
  8998. const consume = this.system.consume;
  8999. if ( consume?.type === "ammo" ) {
  9000. ammo = this.actor.items.get(consume.target);
  9001. if ( ammo?.system ) {
  9002. const q = ammo.system.quantity;
  9003. const consumeAmount = consume.amount ?? 0;
  9004. if ( q && (q - consumeAmount >= 0) ) {
  9005. this._ammo = ammo;
  9006. title += ` [${ammo.name}]`;
  9007. }
  9008. }
  9009. // Get pending ammunition update
  9010. const usage = this._getUsageUpdates({consumeResource: true});
  9011. if ( usage === false ) return null;
  9012. ammoUpdate = usage.resourceUpdates ?? [];
  9013. }
  9014. // Flags
  9015. const elvenAccuracy = (flags.elvenAccuracy
  9016. && CONFIG.DND5E.characterFlags.elvenAccuracy.abilities.includes(this.abilityMod)) || undefined;
  9017. // Compose roll options
  9018. const rollConfig = foundry.utils.mergeObject({
  9019. actor: this.actor,
  9020. data: rollData,
  9021. critical: this.criticalThreshold,
  9022. title,
  9023. flavor: title,
  9024. elvenAccuracy,
  9025. halflingLucky: flags.halflingLucky,
  9026. dialogOptions: {
  9027. width: 400,
  9028. top: options.event ? options.event.clientY - 80 : null,
  9029. left: window.innerWidth - 710
  9030. },
  9031. messageData: {
  9032. "flags.dnd5e.roll": {type: "attack", itemId: this.id, itemUuid: this.uuid},
  9033. speaker: ChatMessage.getSpeaker({actor: this.actor})
  9034. }
  9035. }, options);
  9036. rollConfig.parts = parts.concat(options.parts ?? []);
  9037. /**
  9038. * A hook event that fires before an attack is rolled for an Item.
  9039. * @function dnd5e.preRollAttack
  9040. * @memberof hookEvents
  9041. * @param {Item5e} item Item for which the roll is being performed.
  9042. * @param {D20RollConfiguration} config Configuration data for the pending roll.
  9043. * @returns {boolean} Explicitly return false to prevent the roll from being performed.
  9044. */
  9045. if ( Hooks.call("dnd5e.preRollAttack", this, rollConfig) === false ) return;
  9046. const roll = await d20Roll(rollConfig);
  9047. if ( roll === null ) return null;
  9048. /**
  9049. * A hook event that fires after an attack has been rolled for an Item.
  9050. * @function dnd5e.rollAttack
  9051. * @memberof hookEvents
  9052. * @param {Item5e} item Item for which the roll was performed.
  9053. * @param {D20Roll} roll The resulting roll.
  9054. * @param {object[]} ammoUpdate Updates that will be applied to ammo Items as a result of this attack.
  9055. */
  9056. Hooks.callAll("dnd5e.rollAttack", this, roll, ammoUpdate);
  9057. // Commit ammunition consumption on attack rolls resource consumption if the attack roll was made
  9058. if ( ammoUpdate.length ) await this.actor?.updateEmbeddedDocuments("Item", ammoUpdate);
  9059. return roll;
  9060. }
  9061. /* -------------------------------------------- */
  9062. /**
  9063. * Place a damage roll using an item (weapon, feat, spell, or equipment)
  9064. * Rely upon the damageRoll logic for the core implementation.
  9065. * @param {object} [config]
  9066. * @param {MouseEvent} [config.event] An event which triggered this roll, if any
  9067. * @param {boolean} [config.critical] Should damage be rolled as a critical hit?
  9068. * @param {number} [config.spellLevel] If the item is a spell, override the level for damage scaling
  9069. * @param {boolean} [config.versatile] If the item is a weapon, roll damage using the versatile formula
  9070. * @param {DamageRollConfiguration} [config.options] Additional options passed to the damageRoll function
  9071. * @returns {Promise<DamageRoll>} A Promise which resolves to the created Roll instance, or null if the action
  9072. * cannot be performed.
  9073. */
  9074. async rollDamage({critical, event=null, spellLevel=null, versatile=false, options={}}={}) {
  9075. if ( !this.hasDamage ) throw new Error("You may not make a Damage Roll with this Item.");
  9076. const messageData = {
  9077. "flags.dnd5e.roll": {type: "damage", itemId: this.id, itemUuid: this.uuid},
  9078. speaker: ChatMessage.getSpeaker({actor: this.actor})
  9079. };
  9080. // Get roll data
  9081. const dmg = this.system.damage;
  9082. const parts = dmg.parts.map(d => d[0]);
  9083. const rollData = this.getRollData();
  9084. if ( spellLevel ) rollData.item.level = spellLevel;
  9085. // Configure the damage roll
  9086. const actionFlavor = game.i18n.localize(this.system.actionType === "heal" ? "DND5E.Healing" : "DND5E.DamageRoll");
  9087. const title = `${this.name} - ${actionFlavor}`;
  9088. const rollConfig = {
  9089. actor: this.actor,
  9090. critical,
  9091. data: rollData,
  9092. event,
  9093. title: title,
  9094. flavor: this.labels.damageTypes.length ? `${title} (${this.labels.damageTypes})` : title,
  9095. dialogOptions: {
  9096. width: 400,
  9097. top: event ? event.clientY - 80 : null,
  9098. left: window.innerWidth - 710
  9099. },
  9100. messageData
  9101. };
  9102. // Adjust damage from versatile usage
  9103. if ( versatile && dmg.versatile ) {
  9104. parts[0] = dmg.versatile;
  9105. messageData["flags.dnd5e.roll"].versatile = true;
  9106. }
  9107. // Scale damage from up-casting spells
  9108. const scaling = this.system.scaling;
  9109. if ( (this.type === "spell") ) {
  9110. if ( scaling.mode === "cantrip" ) {
  9111. let level;
  9112. if ( this.actor.type === "character" ) level = this.actor.system.details.level;
  9113. else if ( this.system.preparation.mode === "innate" ) level = Math.ceil(this.actor.system.details.cr);
  9114. else level = this.actor.system.details.spellLevel;
  9115. this._scaleCantripDamage(parts, scaling.formula, level, rollData);
  9116. }
  9117. else if ( spellLevel && (scaling.mode === "level") && scaling.formula ) {
  9118. this._scaleSpellDamage(parts, this.system.level, spellLevel, scaling.formula, rollData);
  9119. }
  9120. }
  9121. // Add damage bonus formula
  9122. const actorBonus = foundry.utils.getProperty(this.actor.system, `bonuses.${this.system.actionType}`) || {};
  9123. if ( actorBonus.damage && (parseInt(actorBonus.damage) !== 0) ) {
  9124. parts.push(actorBonus.damage);
  9125. }
  9126. // Only add the ammunition damage if the ammunition is a consumable with type 'ammo'
  9127. if ( this._ammo && (this._ammo.type === "consumable") && (this._ammo.system.consumableType === "ammo") ) {
  9128. parts.push("@ammo");
  9129. rollData.ammo = this._ammo.system.damage.parts.map(p => p[0]).join("+");
  9130. rollConfig.flavor += ` [${this._ammo.name}]`;
  9131. delete this._ammo;
  9132. }
  9133. // Factor in extra critical damage dice from the Barbarian's "Brutal Critical"
  9134. if ( this.system.actionType === "mwak" ) {
  9135. rollConfig.criticalBonusDice = this.actor.getFlag("dnd5e", "meleeCriticalDamageDice") ?? 0;
  9136. }
  9137. // Factor in extra weapon-specific critical damage
  9138. if ( this.system.critical?.damage ) rollConfig.criticalBonusDamage = this.system.critical.damage;
  9139. foundry.utils.mergeObject(rollConfig, options);
  9140. rollConfig.parts = parts.concat(options.parts ?? []);
  9141. /**
  9142. * A hook event that fires before a damage is rolled for an Item.
  9143. * @function dnd5e.preRollDamage
  9144. * @memberof hookEvents
  9145. * @param {Item5e} item Item for which the roll is being performed.
  9146. * @param {DamageRollConfiguration} config Configuration data for the pending roll.
  9147. * @returns {boolean} Explicitly return false to prevent the roll from being performed.
  9148. */
  9149. if ( Hooks.call("dnd5e.preRollDamage", this, rollConfig) === false ) return;
  9150. const roll = await damageRoll(rollConfig);
  9151. /**
  9152. * A hook event that fires after a damage has been rolled for an Item.
  9153. * @function dnd5e.rollDamage
  9154. * @memberof hookEvents
  9155. * @param {Item5e} item Item for which the roll was performed.
  9156. * @param {DamageRoll} roll The resulting roll.
  9157. */
  9158. if ( roll ) Hooks.callAll("dnd5e.rollDamage", this, roll);
  9159. // Call the roll helper utility
  9160. return roll;
  9161. }
  9162. /* -------------------------------------------- */
  9163. /**
  9164. * Adjust a cantrip damage formula to scale it for higher level characters and monsters.
  9165. * @param {string[]} parts The original parts of the damage formula.
  9166. * @param {string} scale The scaling formula.
  9167. * @param {number} level Level at which the spell is being cast.
  9168. * @param {object} rollData A data object that should be applied to the scaled damage roll.
  9169. * @returns {string[]} The parts of the damage formula with the scaling applied.
  9170. * @private
  9171. */
  9172. _scaleCantripDamage(parts, scale, level, rollData) {
  9173. const add = Math.floor((level + 1) / 6);
  9174. if ( add === 0 ) return [];
  9175. return this._scaleDamage(parts, scale || parts.join(" + "), add, rollData);
  9176. }
  9177. /* -------------------------------------------- */
  9178. /**
  9179. * Adjust the spell damage formula to scale it for spell level up-casting.
  9180. * @param {string[]} parts The original parts of the damage formula.
  9181. * @param {number} baseLevel Default level for the spell.
  9182. * @param {number} spellLevel Level at which the spell is being cast.
  9183. * @param {string} formula The scaling formula.
  9184. * @param {object} rollData A data object that should be applied to the scaled damage roll.
  9185. * @returns {string[]} The parts of the damage formula with the scaling applied.
  9186. * @private
  9187. */
  9188. _scaleSpellDamage(parts, baseLevel, spellLevel, formula, rollData) {
  9189. const upcastLevels = Math.max(spellLevel - baseLevel, 0);
  9190. if ( upcastLevels === 0 ) return parts;
  9191. return this._scaleDamage(parts, formula, upcastLevels, rollData);
  9192. }
  9193. /* -------------------------------------------- */
  9194. /**
  9195. * Scale an array of damage parts according to a provided scaling formula and scaling multiplier.
  9196. * @param {string[]} parts The original parts of the damage formula.
  9197. * @param {string} scaling The scaling formula.
  9198. * @param {number} times A number of times to apply the scaling formula.
  9199. * @param {object} rollData A data object that should be applied to the scaled damage roll
  9200. * @returns {string[]} The parts of the damage formula with the scaling applied.
  9201. * @private
  9202. */
  9203. _scaleDamage(parts, scaling, times, rollData) {
  9204. if ( times <= 0 ) return parts;
  9205. const p0 = new Roll(parts[0], rollData);
  9206. const s = new Roll(scaling, rollData).alter(times);
  9207. // Attempt to simplify by combining like dice terms
  9208. let simplified = false;
  9209. if ( (s.terms[0] instanceof Die) && (s.terms.length === 1) ) {
  9210. const d0 = p0.terms[0];
  9211. const s0 = s.terms[0];
  9212. if ( (d0 instanceof Die) && (d0.faces === s0.faces) && d0.modifiers.equals(s0.modifiers) ) {
  9213. d0.number += s0.number;
  9214. parts[0] = p0.formula;
  9215. simplified = true;
  9216. }
  9217. }
  9218. // Otherwise, add to the first part
  9219. if ( !simplified ) parts[0] = `${parts[0]} + ${s.formula}`;
  9220. return parts;
  9221. }
  9222. /* -------------------------------------------- */
  9223. /**
  9224. * Prepare data needed to roll an attack using an item (weapon, feat, spell, or equipment)
  9225. * and then pass it off to `d20Roll`.
  9226. * @param {object} [options]
  9227. * @param {boolean} [options.spellLevel] Level at which a spell is cast.
  9228. * @returns {Promise<Roll>} A Promise which resolves to the created Roll instance.
  9229. */
  9230. async rollFormula({spellLevel}={}) {
  9231. if ( !this.system.formula ) throw new Error("This Item does not have a formula to roll!");
  9232. const rollConfig = {
  9233. formula: this.system.formula,
  9234. data: this.getRollData(),
  9235. chatMessage: true
  9236. };
  9237. if ( spellLevel ) rollConfig.data.item.level = spellLevel;
  9238. /**
  9239. * A hook event that fires before a formula is rolled for an Item.
  9240. * @function dnd5e.preRollFormula
  9241. * @memberof hookEvents
  9242. * @param {Item5e} item Item for which the roll is being performed.
  9243. * @param {object} config Configuration data for the pending roll.
  9244. * @param {string} config.formula Formula that will be rolled.
  9245. * @param {object} config.data Data used when evaluating the roll.
  9246. * @param {boolean} config.chatMessage Should a chat message be created for this roll?
  9247. * @returns {boolean} Explicitly return false to prevent the roll from being performed.
  9248. */
  9249. if ( Hooks.call("dnd5e.preRollFormula", this, rollConfig) === false ) return;
  9250. const roll = await new Roll(rollConfig.formula, rollConfig.data).roll({async: true});
  9251. if ( rollConfig.chatMessage ) {
  9252. roll.toMessage({
  9253. speaker: ChatMessage.getSpeaker({actor: this.actor}),
  9254. flavor: `${this.name} - ${game.i18n.localize("DND5E.OtherFormula")}`,
  9255. rollMode: game.settings.get("core", "rollMode"),
  9256. messageData: {"flags.dnd5e.roll": {type: "other", itemId: this.id, itemUuid: this.uuid}}
  9257. });
  9258. }
  9259. /**
  9260. * A hook event that fires after a formula has been rolled for an Item.
  9261. * @function dnd5e.rollFormula
  9262. * @memberof hookEvents
  9263. * @param {Item5e} item Item for which the roll was performed.
  9264. * @param {Roll} roll The resulting roll.
  9265. */
  9266. Hooks.callAll("dnd5e.rollFormula", this, roll);
  9267. return roll;
  9268. }
  9269. /* -------------------------------------------- */
  9270. /**
  9271. * Perform an ability recharge test for an item which uses the d6 recharge mechanic.
  9272. * @returns {Promise<Roll>} A Promise which resolves to the created Roll instance
  9273. */
  9274. async rollRecharge() {
  9275. const recharge = this.system.recharge ?? {};
  9276. if ( !recharge.value ) return;
  9277. const rollConfig = {
  9278. formula: "1d6",
  9279. data: this.getRollData(),
  9280. target: parseInt(recharge.value),
  9281. chatMessage: true
  9282. };
  9283. /**
  9284. * A hook event that fires before the Item is rolled to recharge.
  9285. * @function dnd5e.preRollRecharge
  9286. * @memberof hookEvents
  9287. * @param {Item5e} item Item for which the roll is being performed.
  9288. * @param {object} config Configuration data for the pending roll.
  9289. * @param {string} config.formula Formula that will be used to roll the recharge.
  9290. * @param {object} config.data Data used when evaluating the roll.
  9291. * @param {number} config.target Total required to be considered recharged.
  9292. * @param {boolean} config.chatMessage Should a chat message be created for this roll?
  9293. * @returns {boolean} Explicitly return false to prevent the roll from being performed.
  9294. */
  9295. if ( Hooks.call("dnd5e.preRollRecharge", this, rollConfig) === false ) return;
  9296. const roll = await new Roll(rollConfig.formula, rollConfig.data).roll({async: true});
  9297. const success = roll.total >= rollConfig.target;
  9298. if ( rollConfig.chatMessage ) {
  9299. const resultMessage = game.i18n.localize(`DND5E.ItemRecharge${success ? "Success" : "Failure"}`);
  9300. roll.toMessage({
  9301. flavor: `${game.i18n.format("DND5E.ItemRechargeCheck", {name: this.name})} - ${resultMessage}`,
  9302. speaker: ChatMessage.getSpeaker({actor: this.actor, token: this.actor.token})
  9303. });
  9304. }
  9305. /**
  9306. * A hook event that fires after the Item has rolled to recharge, but before any changes have been performed.
  9307. * @function dnd5e.rollRecharge
  9308. * @memberof hookEvents
  9309. * @param {Item5e} item Item for which the roll was performed.
  9310. * @param {Roll} roll The resulting roll.
  9311. * @returns {boolean} Explicitly return false to prevent the item from being recharged.
  9312. */
  9313. if ( Hooks.call("dnd5e.rollRecharge", this, roll) === false ) return roll;
  9314. // Update the Item data
  9315. if ( success ) this.update({"system.recharge.charged": true});
  9316. return roll;
  9317. }
  9318. /* -------------------------------------------- */
  9319. /**
  9320. * Prepare data needed to roll a tool check and then pass it off to `d20Roll`.
  9321. * @param {D20RollConfiguration} [options] Roll configuration options provided to the d20Roll function.
  9322. * @returns {Promise<Roll>} A Promise which resolves to the created Roll instance.
  9323. */
  9324. async rollToolCheck(options={}) {
  9325. if ( this.type !== "tool" ) throw new Error("Wrong item type!");
  9326. return this.actor?.rollToolCheck(this.system.baseItem, {
  9327. ability: this.system.ability,
  9328. bonus: this.system.bonus,
  9329. ...options
  9330. });
  9331. }
  9332. /* -------------------------------------------- */
  9333. /**
  9334. * @inheritdoc
  9335. * @param {object} [options]
  9336. * @param {boolean} [options.deterministic] Whether to force deterministic values for data properties that could be
  9337. * either a die term or a flat term.
  9338. */
  9339. getRollData({ deterministic=false }={}) {
  9340. if ( !this.actor ) return null;
  9341. const actorRollData = this.actor.getRollData({ deterministic });
  9342. const rollData = {
  9343. ...actorRollData,
  9344. item: this.toObject().system
  9345. };
  9346. // Include an ability score modifier if one exists
  9347. const abl = this.abilityMod;
  9348. if ( abl && ("abilities" in rollData) ) {
  9349. const ability = rollData.abilities[abl];
  9350. if ( !ability ) {
  9351. console.warn(`Item ${this.name} in Actor ${this.actor.name} has an invalid item ability modifier of ${abl} defined`);
  9352. }
  9353. rollData.mod = ability?.mod ?? 0;
  9354. }
  9355. return rollData;
  9356. }
  9357. /* -------------------------------------------- */
  9358. /* Chat Message Helpers */
  9359. /* -------------------------------------------- */
  9360. /**
  9361. * Apply listeners to chat messages.
  9362. * @param {HTML} html Rendered chat message.
  9363. */
  9364. static chatListeners(html) {
  9365. html.on("click", ".card-buttons button", this._onChatCardAction.bind(this));
  9366. html.on("click", ".item-name", this._onChatCardToggleContent.bind(this));
  9367. }
  9368. /* -------------------------------------------- */
  9369. /**
  9370. * Handle execution of a chat card action via a click event on one of the card buttons
  9371. * @param {Event} event The originating click event
  9372. * @returns {Promise} A promise which resolves once the handler workflow is complete
  9373. * @private
  9374. */
  9375. static async _onChatCardAction(event) {
  9376. event.preventDefault();
  9377. // Extract card data
  9378. const button = event.currentTarget;
  9379. button.disabled = true;
  9380. const card = button.closest(".chat-card");
  9381. const messageId = card.closest(".message").dataset.messageId;
  9382. const message = game.messages.get(messageId);
  9383. const action = button.dataset.action;
  9384. // Recover the actor for the chat card
  9385. const actor = await this._getChatCardActor(card);
  9386. if ( !actor ) return;
  9387. // Validate permission to proceed with the roll
  9388. const isTargetted = action === "save";
  9389. if ( !( isTargetted || game.user.isGM || actor.isOwner ) ) return;
  9390. // Get the Item from stored flag data or by the item ID on the Actor
  9391. const storedData = message.getFlag("dnd5e", "itemData");
  9392. const item = storedData ? new this(storedData, {parent: actor}) : actor.items.get(card.dataset.itemId);
  9393. if ( !item ) {
  9394. const err = game.i18n.format("DND5E.ActionWarningNoItem", {item: card.dataset.itemId, name: actor.name});
  9395. return ui.notifications.error(err);
  9396. }
  9397. const spellLevel = parseInt(card.dataset.spellLevel) || null;
  9398. // Handle different actions
  9399. let targets;
  9400. switch ( action ) {
  9401. case "attack":
  9402. await item.rollAttack({
  9403. event: event,
  9404. spellLevel: spellLevel
  9405. });
  9406. break;
  9407. case "damage":
  9408. case "versatile":
  9409. await item.rollDamage({
  9410. event: event,
  9411. spellLevel: spellLevel,
  9412. versatile: action === "versatile"
  9413. });
  9414. break;
  9415. case "formula":
  9416. await item.rollFormula({event, spellLevel}); break;
  9417. case "save":
  9418. targets = this._getChatCardTargets(card);
  9419. for ( let token of targets ) {
  9420. const speaker = ChatMessage.getSpeaker({scene: canvas.scene, token: token.document});
  9421. await token.actor.rollAbilitySave(button.dataset.ability, { event, speaker });
  9422. }
  9423. break;
  9424. case "toolCheck":
  9425. await item.rollToolCheck({event}); break;
  9426. case "placeTemplate":
  9427. try {
  9428. await dnd5e.canvas.AbilityTemplate.fromItem(item)?.drawPreview();
  9429. } catch(err) {}
  9430. break;
  9431. case "abilityCheck":
  9432. targets = this._getChatCardTargets(card);
  9433. for ( let token of targets ) {
  9434. const speaker = ChatMessage.getSpeaker({scene: canvas.scene, token: token.document});
  9435. await token.actor.rollAbilityTest(button.dataset.ability, { event, speaker });
  9436. }
  9437. break;
  9438. }
  9439. // Re-enable the button
  9440. button.disabled = false;
  9441. }
  9442. /* -------------------------------------------- */
  9443. /**
  9444. * Handle toggling the visibility of chat card content when the name is clicked
  9445. * @param {Event} event The originating click event
  9446. * @private
  9447. */
  9448. static _onChatCardToggleContent(event) {
  9449. event.preventDefault();
  9450. const header = event.currentTarget;
  9451. const card = header.closest(".chat-card");
  9452. const content = card.querySelector(".card-content");
  9453. content.style.display = content.style.display === "none" ? "block" : "none";
  9454. }
  9455. /* -------------------------------------------- */
  9456. /**
  9457. * Get the Actor which is the author of a chat card
  9458. * @param {HTMLElement} card The chat card being used
  9459. * @returns {Actor|null} The Actor document or null
  9460. * @private
  9461. */
  9462. static async _getChatCardActor(card) {
  9463. // Case 1 - a synthetic actor from a Token
  9464. if ( card.dataset.tokenId ) {
  9465. const token = await fromUuid(card.dataset.tokenId);
  9466. if ( !token ) return null;
  9467. return token.actor;
  9468. }
  9469. // Case 2 - use Actor ID directory
  9470. const actorId = card.dataset.actorId;
  9471. return game.actors.get(actorId) || null;
  9472. }
  9473. /* -------------------------------------------- */
  9474. /**
  9475. * Get the Actor which is the author of a chat card
  9476. * @param {HTMLElement} card The chat card being used
  9477. * @returns {Actor[]} An Array of Actor documents, if any
  9478. * @private
  9479. */
  9480. static _getChatCardTargets(card) {
  9481. let targets = canvas.tokens.controlled.filter(t => !!t.actor);
  9482. if ( !targets.length && game.user.character ) targets = targets.concat(game.user.character.getActiveTokens());
  9483. if ( !targets.length ) ui.notifications.warn(game.i18n.localize("DND5E.ActionWarningNoToken"));
  9484. return targets;
  9485. }
  9486. /* -------------------------------------------- */
  9487. /* Advancements */
  9488. /* -------------------------------------------- */
  9489. /**
  9490. * Create a new advancement of the specified type.
  9491. * @param {string} type Type of advancement to create.
  9492. * @param {object} [data] Data to use when creating the advancement.
  9493. * @param {object} [options]
  9494. * @param {boolean} [options.showConfig=true] Should the new advancement's configuration application be shown?
  9495. * @param {boolean} [options.source=false] Should a source-only update be performed?
  9496. * @returns {Promise<AdvancementConfig>|Item5e} Promise for advancement config for new advancement if local
  9497. * is `false`, or item with newly added advancement.
  9498. */
  9499. createAdvancement(type, data={}, { showConfig=true, source=false }={}) {
  9500. if ( !this.system.advancement ) return this;
  9501. const Advancement = CONFIG.DND5E.advancementTypes[type];
  9502. if ( !Advancement ) throw new Error(`${type} not found in CONFIG.DND5E.advancementTypes`);
  9503. if ( !Advancement.metadata.validItemTypes.has(this.type) || !Advancement.availableForItem(this) ) {
  9504. throw new Error(`${type} advancement cannot be added to ${this.name}`);
  9505. }
  9506. const advancement = new Advancement(data, {parent: this});
  9507. const advancementCollection = this.toObject().system.advancement;
  9508. advancementCollection.push(advancement.toObject());
  9509. if ( source ) return this.updateSource({"system.advancement": advancementCollection});
  9510. return this.update({"system.advancement": advancementCollection}).then(() => {
  9511. if ( !showConfig ) return this;
  9512. const config = new Advancement.metadata.apps.config(this.advancement.byId[advancement.id]);
  9513. return config.render(true);
  9514. });
  9515. }
  9516. /* -------------------------------------------- */
  9517. /**
  9518. * Update an advancement belonging to this item.
  9519. * @param {string} id ID of the advancement to update.
  9520. * @param {object} updates Updates to apply to this advancement.
  9521. * @param {object} [options={}]
  9522. * @param {boolean} [options.source=false] Should a source-only update be performed?
  9523. * @returns {Promise<Item5e>|Item5e} This item with the changes applied, promised if source is `false`.
  9524. */
  9525. updateAdvancement(id, updates, { source=false }={}) {
  9526. if ( !this.system.advancement ) return this;
  9527. const idx = this.system.advancement.findIndex(a => a._id === id);
  9528. if ( idx === -1 ) throw new Error(`Advancement of ID ${id} could not be found to update`);
  9529. const advancement = this.advancement.byId[id];
  9530. advancement.updateSource(updates);
  9531. if ( source ) {
  9532. advancement.render();
  9533. return this;
  9534. }
  9535. const advancementCollection = this.toObject().system.advancement;
  9536. advancementCollection[idx] = advancement.toObject();
  9537. return this.update({"system.advancement": advancementCollection}).then(r => {
  9538. advancement.render();
  9539. return r;
  9540. });
  9541. }
  9542. /* -------------------------------------------- */
  9543. /**
  9544. * Remove an advancement from this item.
  9545. * @param {string} id ID of the advancement to remove.
  9546. * @param {object} [options={}]
  9547. * @param {boolean} [options.source=false] Should a source-only update be performed?
  9548. * @returns {Promise<Item5e>|Item5e} This item with the changes applied.
  9549. */
  9550. deleteAdvancement(id, { source=false }={}) {
  9551. if ( !this.system.advancement ) return this;
  9552. const advancementCollection = this.toObject().system.advancement.filter(a => a._id !== id);
  9553. if ( source ) return this.updateSource({"system.advancement": advancementCollection});
  9554. return this.update({"system.advancement": advancementCollection});
  9555. }
  9556. /* -------------------------------------------- */
  9557. /**
  9558. * Duplicate an advancement, resetting its value to default and giving it a new ID.
  9559. * @param {string} id ID of the advancement to duplicate.
  9560. * @param {object} [options]
  9561. * @param {boolean} [options.showConfig=true] Should the new advancement's configuration application be shown?
  9562. * @param {boolean} [options.source=false] Should a source-only update be performed?
  9563. * @returns {Promise<AdvancementConfig>|Item5e} Promise for advancement config for duplicate advancement if source
  9564. * is `false`, or item with newly duplicated advancement.
  9565. */
  9566. duplicateAdvancement(id, options) {
  9567. const original = this.advancement.byId[id];
  9568. if ( !original ) return this;
  9569. const duplicate = original.toObject();
  9570. delete duplicate._id;
  9571. if ( original.constructor.metadata.dataModels?.value ) {
  9572. duplicate.value = (new original.constructor.metadata.dataModels.value()).toObject();
  9573. } else {
  9574. duplicate.value = original.constructor.metadata.defaults?.value ?? {};
  9575. }
  9576. return this.createAdvancement(original.constructor.typeName, duplicate, options);
  9577. }
  9578. /* -------------------------------------------- */
  9579. /** @inheritdoc */
  9580. getEmbeddedDocument(embeddedName, id, options) {
  9581. if ( embeddedName !== "Advancement" ) return super.getEmbeddedDocument(embeddedName, id, options);
  9582. const advancement = this.advancement.byId[id];
  9583. if ( options?.strict && (advancement === undefined) ) {
  9584. throw new Error(`The key ${id} does not exist in the ${embeddedName} Collection`);
  9585. }
  9586. return advancement;
  9587. }
  9588. /* -------------------------------------------- */
  9589. /* Event Handlers */
  9590. /* -------------------------------------------- */
  9591. /** @inheritdoc */
  9592. async _preCreate(data, options, user) {
  9593. await super._preCreate(data, options, user);
  9594. // Create class identifier based on name
  9595. if ( ["class", "subclass"].includes(this.type) && !this.system.identifier ) {
  9596. await this.updateSource({ "system.identifier": data.name.slugify({strict: true}) });
  9597. }
  9598. if ( !this.isEmbedded || (this.parent.type === "vehicle") ) return;
  9599. const isNPC = this.parent.type === "npc";
  9600. let updates;
  9601. switch (data.type) {
  9602. case "equipment":
  9603. updates = this._onCreateOwnedEquipment(data, isNPC);
  9604. break;
  9605. case "spell":
  9606. updates = this._onCreateOwnedSpell(data, isNPC);
  9607. break;
  9608. case "tool":
  9609. updates = this._onCreateOwnedTool(data, isNPC);
  9610. break;
  9611. case "weapon":
  9612. updates = this._onCreateOwnedWeapon(data, isNPC);
  9613. break;
  9614. }
  9615. if ( updates ) return this.updateSource(updates);
  9616. }
  9617. /* -------------------------------------------- */
  9618. /** @inheritdoc */
  9619. async _onCreate(data, options, userId) {
  9620. super._onCreate(data, options, userId);
  9621. if ( (userId !== game.user.id) || !this.parent ) return;
  9622. // Assign a new original class
  9623. if ( (this.parent.type === "character") && (this.type === "class") ) {
  9624. const pc = this.parent.items.get(this.parent.system.details.originalClass);
  9625. if ( !pc ) await this.parent._assignPrimaryClass();
  9626. }
  9627. }
  9628. /* -------------------------------------------- */
  9629. /** @inheritdoc */
  9630. async _preUpdate(changed, options, user) {
  9631. await super._preUpdate(changed, options, user);
  9632. if ( (this.type !== "class") || !("levels" in (changed.system || {})) ) return;
  9633. // Check to make sure the updated class level isn't below zero
  9634. if ( changed.system.levels <= 0 ) {
  9635. ui.notifications.warn(game.i18n.localize("DND5E.MaxClassLevelMinimumWarn"));
  9636. changed.system.levels = 1;
  9637. }
  9638. // Check to make sure the updated class level doesn't exceed level cap
  9639. if ( changed.system.levels > CONFIG.DND5E.maxLevel ) {
  9640. ui.notifications.warn(game.i18n.format("DND5E.MaxClassLevelExceededWarn", {max: CONFIG.DND5E.maxLevel}));
  9641. changed.system.levels = CONFIG.DND5E.maxLevel;
  9642. }
  9643. if ( !this.isEmbedded || (this.parent.type !== "character") ) return;
  9644. // Check to ensure the updated character doesn't exceed level cap
  9645. const newCharacterLevel = this.actor.system.details.level + (changed.system.levels - this.system.levels);
  9646. if ( newCharacterLevel > CONFIG.DND5E.maxLevel ) {
  9647. ui.notifications.warn(game.i18n.format("DND5E.MaxCharacterLevelExceededWarn", {max: CONFIG.DND5E.maxLevel}));
  9648. changed.system.levels -= newCharacterLevel - CONFIG.DND5E.maxLevel;
  9649. }
  9650. }
  9651. /* -------------------------------------------- */
  9652. /** @inheritdoc */
  9653. _onDelete(options, userId) {
  9654. super._onDelete(options, userId);
  9655. if ( (userId !== game.user.id) || !this.parent ) return;
  9656. // Assign a new original class
  9657. if ( (this.type === "class") && (this.id === this.parent.system.details.originalClass) ) {
  9658. this.parent._assignPrimaryClass();
  9659. }
  9660. }
  9661. /* -------------------------------------------- */
  9662. /**
  9663. * Pre-creation logic for the automatic configuration of owned equipment type Items.
  9664. *
  9665. * @param {object} data Data for the newly created item.
  9666. * @param {boolean} isNPC Is this actor an NPC?
  9667. * @returns {object} Updates to apply to the item data.
  9668. * @private
  9669. */
  9670. _onCreateOwnedEquipment(data, isNPC) {
  9671. const updates = {};
  9672. if ( foundry.utils.getProperty(data, "system.equipped") === undefined ) {
  9673. updates["system.equipped"] = isNPC; // NPCs automatically equip equipment
  9674. }
  9675. if ( foundry.utils.getProperty(data, "system.proficient") === undefined ) {
  9676. if ( isNPC ) {
  9677. updates["system.proficient"] = true; // NPCs automatically have equipment proficiency
  9678. } else {
  9679. const armorProf = CONFIG.DND5E.armorProficienciesMap[this.system.armor?.type]; // Player characters check proficiency
  9680. const actorArmorProfs = this.parent.system.traits?.armorProf?.value || new Set();
  9681. updates["system.proficient"] = (armorProf === true) || actorArmorProfs.has(armorProf)
  9682. || actorArmorProfs.has(this.system.baseItem);
  9683. }
  9684. }
  9685. return updates;
  9686. }
  9687. /* -------------------------------------------- */
  9688. /**
  9689. * Pre-creation logic for the automatic configuration of owned spell type Items.
  9690. *
  9691. * @param {object} data Data for the newly created item.
  9692. * @param {boolean} isNPC Is this actor an NPC?
  9693. * @returns {object} Updates to apply to the item data.
  9694. * @private
  9695. */
  9696. _onCreateOwnedSpell(data, isNPC) {
  9697. const updates = {};
  9698. if ( foundry.utils.getProperty(data, "system.preparation.prepared") === undefined ) {
  9699. updates["system.preparation.prepared"] = isNPC; // NPCs automatically prepare spells
  9700. }
  9701. return updates;
  9702. }
  9703. /* -------------------------------------------- */
  9704. /**
  9705. * Pre-creation logic for the automatic configuration of owned tool type Items.
  9706. * @param {object} data Data for the newly created item.
  9707. * @param {boolean} isNPC Is this actor an NPC?
  9708. * @returns {object} Updates to apply to the item data.
  9709. * @private
  9710. */
  9711. _onCreateOwnedTool(data, isNPC) {
  9712. const updates = {};
  9713. if ( data.system?.proficient === undefined ) {
  9714. if ( isNPC ) updates["system.proficient"] = 1;
  9715. else {
  9716. const actorToolProfs = this.parent.system.tools || {};
  9717. const toolProf = actorToolProfs[this.system.baseItem]?.value;
  9718. const generalProf = actorToolProfs[this.system.toolType]?.value;
  9719. updates["system.proficient"] = toolProf ?? generalProf ?? 0;
  9720. }
  9721. }
  9722. return updates;
  9723. }
  9724. /* -------------------------------------------- */
  9725. /**
  9726. * Pre-creation logic for the automatic configuration of owned weapon type Items.
  9727. * @param {object} data Data for the newly created item.
  9728. * @param {boolean} isNPC Is this actor an NPC?
  9729. * @returns {object} Updates to apply to the item data.
  9730. * @private
  9731. */
  9732. _onCreateOwnedWeapon(data, isNPC) {
  9733. // NPCs automatically equip items and are proficient with them
  9734. if ( isNPC ) {
  9735. const updates = {};
  9736. if ( !foundry.utils.hasProperty(data, "system.equipped") ) updates["system.equipped"] = true;
  9737. if ( !foundry.utils.hasProperty(data, "system.proficient") ) updates["system.proficient"] = true;
  9738. return updates;
  9739. }
  9740. if ( data.system?.proficient !== undefined ) return {};
  9741. // Some weapon types are always proficient
  9742. const weaponProf = CONFIG.DND5E.weaponProficienciesMap[this.system.weaponType];
  9743. const updates = {};
  9744. if ( weaponProf === true ) updates["system.proficient"] = true;
  9745. // Characters may have proficiency in this weapon type (or specific base weapon)
  9746. else {
  9747. const actorProfs = this.parent.system.traits?.weaponProf?.value || new Set();
  9748. updates["system.proficient"] = actorProfs.has(weaponProf) || actorProfs.has(this.system.baseItem);
  9749. }
  9750. return updates;
  9751. }
  9752. /* -------------------------------------------- */
  9753. /* Factory Methods */
  9754. /* -------------------------------------------- */
  9755. /**
  9756. * Create a consumable spell scroll Item from a spell Item.
  9757. * @param {Item5e} spell The spell to be made into a scroll
  9758. * @returns {Item5e} The created scroll consumable item
  9759. */
  9760. static async createScrollFromSpell(spell) {
  9761. // Get spell data
  9762. const itemData = (spell instanceof Item5e) ? spell.toObject() : spell;
  9763. let {
  9764. actionType, description, source, activation, duration, target, range, damage, formula, save, level, attackBonus
  9765. } = itemData.system;
  9766. // Get scroll data
  9767. const scrollUuid = `Compendium.${CONFIG.DND5E.sourcePacks.ITEMS}.${CONFIG.DND5E.spellScrollIds[level]}`;
  9768. const scrollItem = await fromUuid(scrollUuid);
  9769. const scrollData = scrollItem.toObject();
  9770. delete scrollData._id;
  9771. // Split the scroll description into an intro paragraph and the remaining details
  9772. const scrollDescription = scrollData.system.description.value;
  9773. const pdel = "</p>";
  9774. const scrollIntroEnd = scrollDescription.indexOf(pdel);
  9775. const scrollIntro = scrollDescription.slice(0, scrollIntroEnd + pdel.length);
  9776. const scrollDetails = scrollDescription.slice(scrollIntroEnd + pdel.length);
  9777. // Create a composite description from the scroll description and the spell details
  9778. const desc = `${scrollIntro}<hr/><h3>${itemData.name} (Level ${level})</h3><hr/>${description.value}<hr/><h3>Scroll Details</h3><hr/>${scrollDetails}`;
  9779. // Used a fixed attack modifier and saving throw according to the level of spell scroll.
  9780. if ( ["mwak", "rwak", "msak", "rsak"].includes(actionType) ) {
  9781. attackBonus = `${scrollData.system.attackBonus} - @mod`;
  9782. }
  9783. if ( save.ability ) {
  9784. save.scaling = "flat";
  9785. save.dc = scrollData.system.save.dc;
  9786. }
  9787. // Create the spell scroll data
  9788. const spellScrollData = foundry.utils.mergeObject(scrollData, {
  9789. name: `${game.i18n.localize("DND5E.SpellScroll")}: ${itemData.name}`,
  9790. img: itemData.img,
  9791. system: {
  9792. description: {value: desc.trim()}, source, actionType, activation, duration, target, range, damage, formula,
  9793. save, level, attackBonus
  9794. }
  9795. });
  9796. return new this(spellScrollData);
  9797. }
  9798. /* -------------------------------------------- */
  9799. /* Deprecations */
  9800. /* -------------------------------------------- */
  9801. /**
  9802. * Retrieve an item's critical hit threshold. Uses the smallest value from among the following sources:
  9803. * - item document
  9804. * - item document's actor (if it has one)
  9805. * - item document's ammunition (if it has any)
  9806. * - the constant '20'
  9807. * @returns {number|null} The minimum value that must be rolled to be considered a critical hit.
  9808. * @deprecated since dnd5e 2.2, targeted for removal in 2.4
  9809. */
  9810. getCriticalThreshold() {
  9811. foundry.utils.logCompatibilityWarning(
  9812. "Item5e#getCriticalThreshold has been replaced with the Item5e#criticalThreshold getter.",
  9813. { since: "DnD5e 2.2", until: "DnD5e 2.4" }
  9814. );
  9815. return this.criticalThreshold;
  9816. }
  9817. }
  9818. /**
  9819. * An abstract class containing common functionality between actor sheet configuration apps.
  9820. * @extends {DocumentSheet}
  9821. * @abstract
  9822. */
  9823. class BaseConfigSheet extends DocumentSheet {
  9824. /** @inheritdoc */
  9825. activateListeners(html) {
  9826. super.activateListeners(html);
  9827. if ( this.isEditable ) {
  9828. for ( const override of this._getActorOverrides() ) {
  9829. html.find(`input[name="${override}"],select[name="${override}"]`).each((i, el) => {
  9830. el.disabled = true;
  9831. el.dataset.tooltip = "DND5E.ActiveEffectOverrideWarning";
  9832. });
  9833. }
  9834. }
  9835. }
  9836. /* -------------------------------------------- */
  9837. /**
  9838. * Retrieve the list of fields that are currently modified by Active Effects on the Actor.
  9839. * @returns {string[]}
  9840. * @protected
  9841. */
  9842. _getActorOverrides() {
  9843. return Object.keys(foundry.utils.flattenObject(this.object.overrides || {}));
  9844. }
  9845. }
  9846. /**
  9847. * A simple form to set save throw configuration for a given ability score.
  9848. *
  9849. * @param {Actor5e} actor The Actor instance being displayed within the sheet.
  9850. * @param {ApplicationOptions} options Additional application configuration options.
  9851. * @param {string} abilityId The ability key as defined in CONFIG.DND5E.abilities.
  9852. */
  9853. class ActorAbilityConfig extends BaseConfigSheet {
  9854. constructor(actor, options, abilityId) {
  9855. super(actor, options);
  9856. this._abilityId = abilityId;
  9857. }
  9858. /* -------------------------------------------- */
  9859. /** @override */
  9860. static get defaultOptions() {
  9861. return foundry.utils.mergeObject(super.defaultOptions, {
  9862. classes: ["dnd5e"],
  9863. template: "systems/dnd5e/templates/apps/ability-config.hbs",
  9864. width: 500,
  9865. height: "auto"
  9866. });
  9867. }
  9868. /* -------------------------------------------- */
  9869. /** @override */
  9870. get title() {
  9871. return `${game.i18n.format("DND5E.AbilityConfigureTitle", {
  9872. ability: CONFIG.DND5E.abilities[this._abilityId].label})}: ${this.document.name}`;
  9873. }
  9874. /* -------------------------------------------- */
  9875. /** @override */
  9876. getData(options) {
  9877. const src = this.document.toObject();
  9878. const ability = CONFIG.DND5E.abilities[this._abilityId].label;
  9879. return {
  9880. ability: src.system.abilities[this._abilityId] ?? this.document.system.abilities[this._abilityId] ?? {},
  9881. labelSaves: game.i18n.format("DND5E.AbilitySaveConfigure", {ability}),
  9882. labelChecks: game.i18n.format("DND5E.AbilityCheckConfigure", {ability}),
  9883. abilityId: this._abilityId,
  9884. proficiencyLevels: {
  9885. 0: CONFIG.DND5E.proficiencyLevels[0],
  9886. 1: CONFIG.DND5E.proficiencyLevels[1]
  9887. },
  9888. bonusGlobalSave: src.system.bonuses?.abilities?.save,
  9889. bonusGlobalCheck: src.system.bonuses?.abilities?.check
  9890. };
  9891. }
  9892. }
  9893. /**
  9894. * Interface for managing a character's armor calculation.
  9895. */
  9896. class ActorArmorConfig extends BaseConfigSheet {
  9897. constructor(...args) {
  9898. super(...args);
  9899. /**
  9900. * Cloned copy of the actor for previewing changes.
  9901. * @type {Actor5e}
  9902. */
  9903. this.clone = this.document.clone();
  9904. }
  9905. /* -------------------------------------------- */
  9906. /** @inheritdoc */
  9907. static get defaultOptions() {
  9908. return foundry.utils.mergeObject(super.defaultOptions, {
  9909. classes: ["dnd5e", "actor-armor-config"],
  9910. template: "systems/dnd5e/templates/apps/actor-armor.hbs",
  9911. width: 320,
  9912. height: "auto",
  9913. sheetConfig: false
  9914. });
  9915. }
  9916. /* -------------------------------------------- */
  9917. /** @inheritdoc */
  9918. get title() {
  9919. return `${game.i18n.localize("DND5E.ArmorConfig")}: ${this.document.name}`;
  9920. }
  9921. /* -------------------------------------------- */
  9922. /** @inheritdoc */
  9923. async getData() {
  9924. const ac = this.clone.system.attributes.ac;
  9925. const isFlat = ["flat", "natural"].includes(ac.calc);
  9926. // Get configuration data for the calculation mode, reset to flat if configuration is unavailable
  9927. let cfg = CONFIG.DND5E.armorClasses[ac.calc];
  9928. if ( !cfg ) {
  9929. ac.calc = "flat";
  9930. cfg = CONFIG.DND5E.armorClasses.flat;
  9931. this.clone.updateSource({ "system.attributes.ac.calc": "flat" });
  9932. }
  9933. return {
  9934. ac, isFlat,
  9935. calculations: CONFIG.DND5E.armorClasses,
  9936. valueDisabled: !isFlat,
  9937. formula: ac.calc === "custom" ? ac.formula : cfg.formula,
  9938. formulaDisabled: ac.calc !== "custom"
  9939. };
  9940. }
  9941. /* -------------------------------------------- */
  9942. /** @inheritdoc */
  9943. _getActorOverrides() {
  9944. return Object.keys(foundry.utils.flattenObject(this.object.overrides?.system?.attributes || {}));
  9945. }
  9946. /* -------------------------------------------- */
  9947. /** @inheritdoc */
  9948. async _updateObject(event, formData) {
  9949. const ac = foundry.utils.expandObject(formData).ac;
  9950. return this.document.update({"system.attributes.ac": ac});
  9951. }
  9952. /* -------------------------------------------- */
  9953. /* Event Listeners and Handlers */
  9954. /* -------------------------------------------- */
  9955. /** @inheritdoc */
  9956. async _onChangeInput(event) {
  9957. await super._onChangeInput(event);
  9958. // Update clone with new data & re-render
  9959. this.clone.updateSource({ [`system.attributes.${event.currentTarget.name}`]: event.currentTarget.value });
  9960. this.render();
  9961. }
  9962. }
  9963. /**
  9964. * A simple form to set actor hit dice amounts.
  9965. */
  9966. class ActorHitDiceConfig extends BaseConfigSheet {
  9967. /** @inheritDoc */
  9968. static get defaultOptions() {
  9969. return foundry.utils.mergeObject(super.defaultOptions, {
  9970. classes: ["dnd5e", "hd-config", "dialog"],
  9971. template: "systems/dnd5e/templates/apps/hit-dice-config.hbs",
  9972. width: 360,
  9973. height: "auto"
  9974. });
  9975. }
  9976. /* -------------------------------------------- */
  9977. /** @inheritDoc */
  9978. get title() {
  9979. return `${game.i18n.localize("DND5E.HitDiceConfig")}: ${this.object.name}`;
  9980. }
  9981. /* -------------------------------------------- */
  9982. /** @inheritDoc */
  9983. getData(options) {
  9984. return {
  9985. classes: this.object.items.reduce((classes, item) => {
  9986. if (item.type === "class") {
  9987. classes.push({
  9988. classItemId: item.id,
  9989. name: item.name,
  9990. diceDenom: item.system.hitDice,
  9991. currentHitDice: item.system.levels - item.system.hitDiceUsed,
  9992. maxHitDice: item.system.levels,
  9993. canRoll: (item.system.levels - item.system.hitDiceUsed) > 0
  9994. });
  9995. }
  9996. return classes;
  9997. }, []).sort((a, b) => parseInt(b.diceDenom.slice(1)) - parseInt(a.diceDenom.slice(1)))
  9998. };
  9999. }
  10000. /* -------------------------------------------- */
  10001. /** @inheritDoc */
  10002. activateListeners(html) {
  10003. super.activateListeners(html);
  10004. // Hook up -/+ buttons to adjust the current value in the form
  10005. html.find("button.increment,button.decrement").click(event => {
  10006. const button = event.currentTarget;
  10007. const current = button.parentElement.querySelector(".current");
  10008. const max = button.parentElement.querySelector(".max");
  10009. const direction = button.classList.contains("increment") ? 1 : -1;
  10010. current.value = Math.clamped(parseInt(current.value) + direction, 0, parseInt(max.value));
  10011. });
  10012. html.find("button.roll-hd").click(this._onRollHitDie.bind(this));
  10013. }
  10014. /* -------------------------------------------- */
  10015. /** @inheritDoc */
  10016. async _updateObject(event, formData) {
  10017. const actorItems = this.object.items;
  10018. const classUpdates = Object.entries(formData).map(([id, hd]) => ({
  10019. _id: id,
  10020. "system.hitDiceUsed": actorItems.get(id).system.levels - hd
  10021. }));
  10022. return this.object.updateEmbeddedDocuments("Item", classUpdates);
  10023. }
  10024. /* -------------------------------------------- */
  10025. /**
  10026. * Rolls the hit die corresponding with the class row containing the event's target button.
  10027. * @param {MouseEvent} event Triggering click event.
  10028. * @protected
  10029. */
  10030. async _onRollHitDie(event) {
  10031. event.preventDefault();
  10032. const button = event.currentTarget;
  10033. await this.object.rollHitDie(button.dataset.hdDenom);
  10034. // Re-render dialog to reflect changed hit dice quantities
  10035. this.render();
  10036. }
  10037. }
  10038. /**
  10039. * A form for configuring actor hit points and bonuses.
  10040. */
  10041. class ActorHitPointsConfig extends BaseConfigSheet {
  10042. constructor(...args) {
  10043. super(...args);
  10044. /**
  10045. * Cloned copy of the actor for previewing changes.
  10046. * @type {Actor5e}
  10047. */
  10048. this.clone = this.object.clone();
  10049. }
  10050. /* -------------------------------------------- */
  10051. /** @override */
  10052. static get defaultOptions() {
  10053. return foundry.utils.mergeObject(super.defaultOptions, {
  10054. classes: ["dnd5e", "actor-hit-points-config"],
  10055. template: "systems/dnd5e/templates/apps/hit-points-config.hbs",
  10056. width: 320,
  10057. height: "auto",
  10058. sheetConfig: false
  10059. });
  10060. }
  10061. /* -------------------------------------------- */
  10062. /** @inheritdoc */
  10063. get title() {
  10064. return `${game.i18n.localize("DND5E.HitPointsConfig")}: ${this.document.name}`;
  10065. }
  10066. /* -------------------------------------------- */
  10067. /** @inheritdoc */
  10068. getData(options) {
  10069. return {
  10070. hp: this.clone.system.attributes.hp,
  10071. source: this.clone.toObject().system.attributes.hp,
  10072. isCharacter: this.document.type === "character"
  10073. };
  10074. }
  10075. /* -------------------------------------------- */
  10076. /** @inheritdoc */
  10077. _getActorOverrides() {
  10078. return Object.keys(foundry.utils.flattenObject(this.object.overrides?.system?.attributes || {}));
  10079. }
  10080. /* -------------------------------------------- */
  10081. /** @inheritdoc */
  10082. async _updateObject(event, formData) {
  10083. const hp = foundry.utils.expandObject(formData).hp;
  10084. this.clone.updateSource({"system.attributes.hp": hp});
  10085. const maxDelta = this.clone.system.attributes.hp.max - this.document.system.attributes.hp.max;
  10086. hp.value = Math.max(this.document.system.attributes.hp.value + maxDelta, 0);
  10087. return this.document.update({"system.attributes.hp": hp});
  10088. }
  10089. /* -------------------------------------------- */
  10090. /* Event Listeners and Handlers */
  10091. /* -------------------------------------------- */
  10092. /** @inheritDoc */
  10093. activateListeners(html) {
  10094. super.activateListeners(html);
  10095. html.find(".roll-hit-points").click(this._onRollHPFormula.bind(this));
  10096. }
  10097. /* -------------------------------------------- */
  10098. /** @inheritdoc */
  10099. async _onChangeInput(event) {
  10100. await super._onChangeInput(event);
  10101. const t = event.currentTarget;
  10102. // Update clone with new data & re-render
  10103. this.clone.updateSource({ [`system.attributes.${t.name}`]: t.value || null });
  10104. if ( t.name !== "hp.formula" ) this.render();
  10105. }
  10106. /* -------------------------------------------- */
  10107. /**
  10108. * Handle rolling NPC health values using the provided formula.
  10109. * @param {Event} event The original click event.
  10110. * @protected
  10111. */
  10112. async _onRollHPFormula(event) {
  10113. event.preventDefault();
  10114. try {
  10115. const roll = await this.clone.rollNPCHitPoints();
  10116. this.clone.updateSource({"system.attributes.hp.max": roll.total});
  10117. this.render();
  10118. } catch(error) {
  10119. ui.notifications.error(game.i18n.localize("DND5E.HPFormulaError"));
  10120. throw error;
  10121. }
  10122. }
  10123. }
  10124. /**
  10125. * A simple sub-application of the ActorSheet which is used to configure properties related to initiative.
  10126. */
  10127. class ActorInitiativeConfig extends BaseConfigSheet {
  10128. /** @override */
  10129. static get defaultOptions() {
  10130. return foundry.utils.mergeObject(super.defaultOptions, {
  10131. classes: ["dnd5e"],
  10132. template: "systems/dnd5e/templates/apps/initiative-config.hbs",
  10133. width: 360,
  10134. height: "auto"
  10135. });
  10136. }
  10137. /* -------------------------------------------- */
  10138. /** @override */
  10139. get title() {
  10140. return `${game.i18n.localize("DND5E.InitiativeConfig")}: ${this.document.name}`;
  10141. }
  10142. /* -------------------------------------------- */
  10143. /** @override */
  10144. getData(options={}) {
  10145. const source = this.document.toObject();
  10146. const init = source.system.attributes.init || {};
  10147. const flags = source.flags.dnd5e || {};
  10148. return {
  10149. ability: init.ability,
  10150. abilities: CONFIG.DND5E.abilities,
  10151. bonus: init.bonus,
  10152. initiativeAlert: flags.initiativeAlert,
  10153. initiativeAdv: flags.initiativeAdv
  10154. };
  10155. }
  10156. /* -------------------------------------------- */
  10157. /** @inheritDoc */
  10158. _getSubmitData(updateData={}) {
  10159. const formData = super._getSubmitData(updateData);
  10160. formData.flags = {dnd5e: {}};
  10161. for ( const flag of ["initiativeAlert", "initiativeAdv"] ) {
  10162. const k = `flags.dnd5e.${flag}`;
  10163. if ( formData[k] ) formData.flags.dnd5e[flag] = true;
  10164. else formData.flags.dnd5e[`-=${flag}`] = null;
  10165. delete formData[k];
  10166. }
  10167. return formData;
  10168. }
  10169. }
  10170. /**
  10171. * A simple form to set actor movement speeds.
  10172. */
  10173. class ActorMovementConfig extends BaseConfigSheet {
  10174. /** @override */
  10175. static get defaultOptions() {
  10176. return foundry.utils.mergeObject(super.defaultOptions, {
  10177. classes: ["dnd5e"],
  10178. template: "systems/dnd5e/templates/apps/movement-config.hbs",
  10179. width: 300,
  10180. height: "auto"
  10181. });
  10182. }
  10183. /* -------------------------------------------- */
  10184. /** @override */
  10185. get title() {
  10186. return `${game.i18n.localize("DND5E.MovementConfig")}: ${this.document.name}`;
  10187. }
  10188. /* -------------------------------------------- */
  10189. /** @override */
  10190. getData(options={}) {
  10191. const source = this.document.toObject();
  10192. // Current movement values
  10193. const movement = source.system.attributes?.movement || {};
  10194. for ( let [k, v] of Object.entries(movement) ) {
  10195. if ( ["units", "hover"].includes(k) ) continue;
  10196. movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : 0;
  10197. }
  10198. // Allowed speeds
  10199. const speeds = source.type === "group" ? {
  10200. land: "DND5E.MovementLand",
  10201. water: "DND5E.MovementWater",
  10202. air: "DND5E.MovementAir"
  10203. } : {
  10204. walk: "DND5E.MovementWalk",
  10205. burrow: "DND5E.MovementBurrow",
  10206. climb: "DND5E.MovementClimb",
  10207. fly: "DND5E.MovementFly",
  10208. swim: "DND5E.MovementSwim"
  10209. };
  10210. // Return rendering context
  10211. return {
  10212. speeds,
  10213. movement,
  10214. selectUnits: source.type !== "group",
  10215. canHover: source.type !== "group",
  10216. units: CONFIG.DND5E.movementUnits
  10217. };
  10218. }
  10219. }
  10220. /**
  10221. * A simple form to configure Actor senses.
  10222. */
  10223. class ActorSensesConfig extends BaseConfigSheet {
  10224. /** @inheritdoc */
  10225. static get defaultOptions() {
  10226. return foundry.utils.mergeObject(super.defaultOptions, {
  10227. classes: ["dnd5e"],
  10228. template: "systems/dnd5e/templates/apps/senses-config.hbs",
  10229. width: 300,
  10230. height: "auto"
  10231. });
  10232. }
  10233. /* -------------------------------------------- */
  10234. /** @inheritdoc */
  10235. get title() {
  10236. return `${game.i18n.localize("DND5E.SensesConfig")}: ${this.document.name}`;
  10237. }
  10238. /* -------------------------------------------- */
  10239. /** @inheritdoc */
  10240. getData(options) {
  10241. const source = this.document.toObject().system.attributes?.senses || {};
  10242. const data = {
  10243. senses: {},
  10244. special: source.special ?? "",
  10245. units: source.units, movementUnits: CONFIG.DND5E.movementUnits
  10246. };
  10247. for ( let [name, label] of Object.entries(CONFIG.DND5E.senses) ) {
  10248. const v = Number(source[name]);
  10249. data.senses[name] = {
  10250. label: game.i18n.localize(label),
  10251. value: Number.isNumeric(v) ? v.toNearest(0.1) : 0
  10252. };
  10253. }
  10254. return data;
  10255. }
  10256. }
  10257. /**
  10258. * An application class which provides advanced configuration for special character flags which modify an Actor.
  10259. */
  10260. class ActorSheetFlags extends BaseConfigSheet {
  10261. /** @inheritDoc */
  10262. static get defaultOptions() {
  10263. return foundry.utils.mergeObject(super.defaultOptions, {
  10264. id: "actor-flags",
  10265. classes: ["dnd5e"],
  10266. template: "systems/dnd5e/templates/apps/actor-flags.hbs",
  10267. width: 500,
  10268. closeOnSubmit: true
  10269. });
  10270. }
  10271. /* -------------------------------------------- */
  10272. /** @inheritDoc */
  10273. get title() {
  10274. return `${game.i18n.localize("DND5E.FlagsTitle")}: ${this.object.name}`;
  10275. }
  10276. /* -------------------------------------------- */
  10277. /** @inheritDoc */
  10278. getData() {
  10279. const data = {};
  10280. data.actor = this.object;
  10281. data.classes = this._getClasses();
  10282. data.flags = this._getFlags();
  10283. data.bonuses = this._getBonuses();
  10284. return data;
  10285. }
  10286. /* -------------------------------------------- */
  10287. /**
  10288. * Prepare an object of sorted classes.
  10289. * @returns {object}
  10290. * @private
  10291. */
  10292. _getClasses() {
  10293. const classes = this.object.items.filter(i => i.type === "class");
  10294. return classes.sort((a, b) => a.name.localeCompare(b.name)).reduce((obj, i) => {
  10295. obj[i.id] = i.name;
  10296. return obj;
  10297. }, {});
  10298. }
  10299. /* -------------------------------------------- */
  10300. /**
  10301. * Prepare an object of flags data which groups flags by section
  10302. * Add some additional data for rendering
  10303. * @returns {object}
  10304. * @private
  10305. */
  10306. _getFlags() {
  10307. const flags = {};
  10308. const baseData = this.document.toJSON();
  10309. for ( let [k, v] of Object.entries(CONFIG.DND5E.characterFlags) ) {
  10310. if ( !flags.hasOwnProperty(v.section) ) flags[v.section] = {};
  10311. let flag = foundry.utils.deepClone(v);
  10312. flag.type = v.type.name;
  10313. flag.isCheckbox = v.type === Boolean;
  10314. flag.isSelect = v.hasOwnProperty("choices");
  10315. flag.value = foundry.utils.getProperty(baseData.flags, `dnd5e.${k}`);
  10316. flags[v.section][`flags.dnd5e.${k}`] = flag;
  10317. }
  10318. return flags;
  10319. }
  10320. /* -------------------------------------------- */
  10321. /**
  10322. * Get the bonuses fields and their localization strings
  10323. * @returns {Array<object>}
  10324. * @private
  10325. */
  10326. _getBonuses() {
  10327. const src = this.object.toObject();
  10328. const bonuses = [
  10329. {name: "system.bonuses.mwak.attack", label: "DND5E.BonusMWAttack"},
  10330. {name: "system.bonuses.mwak.damage", label: "DND5E.BonusMWDamage"},
  10331. {name: "system.bonuses.rwak.attack", label: "DND5E.BonusRWAttack"},
  10332. {name: "system.bonuses.rwak.damage", label: "DND5E.BonusRWDamage"},
  10333. {name: "system.bonuses.msak.attack", label: "DND5E.BonusMSAttack"},
  10334. {name: "system.bonuses.msak.damage", label: "DND5E.BonusMSDamage"},
  10335. {name: "system.bonuses.rsak.attack", label: "DND5E.BonusRSAttack"},
  10336. {name: "system.bonuses.rsak.damage", label: "DND5E.BonusRSDamage"},
  10337. {name: "system.bonuses.abilities.check", label: "DND5E.BonusAbilityCheck"},
  10338. {name: "system.bonuses.abilities.save", label: "DND5E.BonusAbilitySave"},
  10339. {name: "system.bonuses.abilities.skill", label: "DND5E.BonusAbilitySkill"},
  10340. {name: "system.bonuses.spell.dc", label: "DND5E.BonusSpellDC"}
  10341. ];
  10342. for ( let b of bonuses ) {
  10343. b.value = foundry.utils.getProperty(src, b.name) || "";
  10344. }
  10345. return bonuses;
  10346. }
  10347. /* -------------------------------------------- */
  10348. /** @inheritDoc */
  10349. async _updateObject(event, formData) {
  10350. const actor = this.object;
  10351. let updateData = foundry.utils.expandObject(formData);
  10352. const src = actor.toObject();
  10353. // Unset any flags which are "false"
  10354. const flags = updateData.flags.dnd5e;
  10355. for ( let [k, v] of Object.entries(flags) ) {
  10356. if ( [undefined, null, "", false, 0].includes(v) ) {
  10357. delete flags[k];
  10358. if ( foundry.utils.hasProperty(src.flags, `dnd5e.${k}`) ) flags[`-=${k}`] = null;
  10359. }
  10360. }
  10361. // Clear any bonuses which are whitespace only
  10362. for ( let b of Object.values(updateData.system.bonuses ) ) {
  10363. for ( let [k, v] of Object.entries(b) ) {
  10364. b[k] = v.trim();
  10365. }
  10366. }
  10367. // Diff the data against any applied overrides and apply
  10368. await actor.update(updateData, {diff: false});
  10369. }
  10370. }
  10371. /**
  10372. * A specialized form used to select from a checklist of attributes, traits, or properties
  10373. */
  10374. class ActorTypeConfig extends FormApplication {
  10375. /** @inheritDoc */
  10376. static get defaultOptions() {
  10377. return foundry.utils.mergeObject(super.defaultOptions, {
  10378. classes: ["dnd5e", "actor-type", "trait-selector"],
  10379. template: "systems/dnd5e/templates/apps/actor-type.hbs",
  10380. width: 280,
  10381. height: "auto",
  10382. choices: {},
  10383. allowCustom: true,
  10384. minimum: 0,
  10385. maximum: null
  10386. });
  10387. }
  10388. /* -------------------------------------------- */
  10389. /** @inheritDoc */
  10390. get title() {
  10391. return `${game.i18n.localize("DND5E.CreatureTypeTitle")}: ${this.object.name}`;
  10392. }
  10393. /* -------------------------------------------- */
  10394. /** @override */
  10395. get id() {
  10396. return `actor-type-${this.object.id}`;
  10397. }
  10398. /* -------------------------------------------- */
  10399. /** @override */
  10400. getData(options={}) {
  10401. // Get current value or new default
  10402. let attr = foundry.utils.getProperty(this.object.system, "details.type");
  10403. if ( foundry.utils.getType(attr) !== "Object" ) attr = {
  10404. value: (attr in CONFIG.DND5E.creatureTypes) ? attr : "humanoid",
  10405. subtype: "",
  10406. swarm: "",
  10407. custom: ""
  10408. };
  10409. // Populate choices
  10410. const types = {};
  10411. for ( let [k, v] of Object.entries(CONFIG.DND5E.creatureTypes) ) {
  10412. types[k] = {
  10413. label: game.i18n.localize(v),
  10414. chosen: attr.value === k
  10415. };
  10416. }
  10417. // Return data for rendering
  10418. return {
  10419. types: types,
  10420. custom: {
  10421. value: attr.custom,
  10422. label: game.i18n.localize("DND5E.CreatureTypeSelectorCustom"),
  10423. chosen: attr.value === "custom"
  10424. },
  10425. subtype: attr.subtype,
  10426. swarm: attr.swarm,
  10427. sizes: Array.from(Object.entries(CONFIG.DND5E.actorSizes)).reverse().reduce((obj, e) => {
  10428. obj[e[0]] = e[1];
  10429. return obj;
  10430. }, {}),
  10431. preview: Actor5e.formatCreatureType(attr) || "–"
  10432. };
  10433. }
  10434. /* -------------------------------------------- */
  10435. /** @override */
  10436. async _updateObject(event, formData) {
  10437. const typeObject = foundry.utils.expandObject(formData);
  10438. return this.object.update({"system.details.type": typeObject});
  10439. }
  10440. /* -------------------------------------------- */
  10441. /* Event Listeners and Handlers */
  10442. /* -------------------------------------------- */
  10443. /** @inheritdoc */
  10444. activateListeners(html) {
  10445. super.activateListeners(html);
  10446. html.find("input[name='custom']").focusin(this._onCustomFieldFocused.bind(this));
  10447. }
  10448. /* -------------------------------------------- */
  10449. /** @inheritdoc */
  10450. _onChangeInput(event) {
  10451. super._onChangeInput(event);
  10452. const typeObject = foundry.utils.expandObject(this._getSubmitData());
  10453. this.form.preview.value = Actor5e.formatCreatureType(typeObject) || "—";
  10454. }
  10455. /* -------------------------------------------- */
  10456. /**
  10457. * Select the custom radio button when the custom text field is focused.
  10458. * @param {FocusEvent} event The original focusin event
  10459. * @private
  10460. */
  10461. _onCustomFieldFocused(event) {
  10462. this.form.querySelector("input[name='value'][value='custom']").checked = true;
  10463. this._onChangeInput(event);
  10464. }
  10465. }
  10466. /**
  10467. * Dialog to confirm the deletion of an embedded item with advancement or decreasing a class level.
  10468. */
  10469. class AdvancementConfirmationDialog extends Dialog {
  10470. /** @inheritdoc */
  10471. static get defaultOptions() {
  10472. return foundry.utils.mergeObject(super.defaultOptions, {
  10473. template: "systems/dnd5e/templates/advancement/advancement-confirmation-dialog.hbs",
  10474. jQuery: false
  10475. });
  10476. }
  10477. /* -------------------------------------------- */
  10478. /**
  10479. * A helper function that displays the dialog prompting for an item deletion.
  10480. * @param {Item5e} item Item to be deleted.
  10481. * @returns {Promise<boolean|null>} Resolves with whether advancements should be unapplied. Rejects with null.
  10482. */
  10483. static forDelete(item) {
  10484. return this.createDialog(
  10485. item,
  10486. game.i18n.localize("DND5E.AdvancementDeleteConfirmationTitle"),
  10487. game.i18n.localize("DND5E.AdvancementDeleteConfirmationMessage"),
  10488. {
  10489. icon: '<i class="fas fa-trash"></i>',
  10490. label: game.i18n.localize("Delete")
  10491. }
  10492. );
  10493. }
  10494. /* -------------------------------------------- */
  10495. /**
  10496. * A helper function that displays the dialog prompting for leveling down.
  10497. * @param {Item5e} item The class whose level is being changed.
  10498. * @returns {Promise<boolean|null>} Resolves with whether advancements should be unapplied. Rejects with null.
  10499. */
  10500. static forLevelDown(item) {
  10501. return this.createDialog(
  10502. item,
  10503. game.i18n.localize("DND5E.AdvancementLevelDownConfirmationTitle"),
  10504. game.i18n.localize("DND5E.AdvancementLevelDownConfirmationMessage"),
  10505. {
  10506. icon: '<i class="fas fa-sort-numeric-down-alt"></i>',
  10507. label: game.i18n.localize("DND5E.LevelActionDecrease")
  10508. }
  10509. );
  10510. }
  10511. /* -------------------------------------------- */
  10512. /**
  10513. * A helper constructor function which displays the confirmation dialog.
  10514. * @param {Item5e} item Item to be changed.
  10515. * @param {string} title Localized dialog title.
  10516. * @param {string} message Localized dialog message.
  10517. * @param {object} continueButton Object containing label and icon for the action button.
  10518. * @returns {Promise<boolean|null>} Resolves with whether advancements should be unapplied. Rejects with null.
  10519. */
  10520. static createDialog(item, title, message, continueButton) {
  10521. return new Promise((resolve, reject) => {
  10522. const dialog = new this({
  10523. title: `${title}: ${item.name}`,
  10524. content: message,
  10525. buttons: {
  10526. continue: foundry.utils.mergeObject(continueButton, {
  10527. callback: html => {
  10528. const checkbox = html.querySelector('input[name="apply-advancement"]');
  10529. resolve(checkbox.checked);
  10530. }
  10531. }),
  10532. cancel: {
  10533. icon: '<i class="fas fa-times"></i>',
  10534. label: game.i18n.localize("Cancel"),
  10535. callback: html => reject(null)
  10536. }
  10537. },
  10538. default: "continue",
  10539. close: () => reject(null)
  10540. });
  10541. dialog.render(true);
  10542. });
  10543. }
  10544. }
  10545. /**
  10546. * Internal type used to manage each step within the advancement process.
  10547. *
  10548. * @typedef {object} AdvancementStep
  10549. * @property {string} type Step type from "forward", "reverse", "restore", or "delete".
  10550. * @property {AdvancementFlow} [flow] Flow object for the advancement being applied by this step.
  10551. * @property {Item5e} [item] For "delete" steps only, the item to be removed.
  10552. * @property {object} [class] Contains data on class if step was triggered by class level change.
  10553. * @property {Item5e} [class.item] Class item that caused this advancement step.
  10554. * @property {number} [class.level] Level the class should be during this step.
  10555. * @property {boolean} [automatic=false] Should the manager attempt to apply this step without user interaction?
  10556. */
  10557. /**
  10558. * Application for controlling the advancement workflow and displaying the interface.
  10559. *
  10560. * @param {Actor5e} actor Actor on which this advancement is being performed.
  10561. * @param {object} [options={}] Additional application options.
  10562. */
  10563. class AdvancementManager extends Application {
  10564. constructor(actor, options={}) {
  10565. super(options);
  10566. /**
  10567. * The original actor to which changes will be applied when the process is complete.
  10568. * @type {Actor5e}
  10569. */
  10570. this.actor = actor;
  10571. /**
  10572. * A clone of the original actor to which the changes can be applied during the advancement process.
  10573. * @type {Actor5e}
  10574. */
  10575. this.clone = actor.clone();
  10576. /**
  10577. * Individual steps that will be applied in order.
  10578. * @type {object}
  10579. */
  10580. this.steps = [];
  10581. /**
  10582. * Step being currently displayed.
  10583. * @type {number|null}
  10584. * @private
  10585. */
  10586. this._stepIndex = null;
  10587. /**
  10588. * Is the prompt currently advancing through un-rendered steps?
  10589. * @type {boolean}
  10590. * @private
  10591. */
  10592. this._advancing = false;
  10593. }
  10594. /* -------------------------------------------- */
  10595. /** @inheritdoc */
  10596. static get defaultOptions() {
  10597. return foundry.utils.mergeObject(super.defaultOptions, {
  10598. classes: ["dnd5e", "advancement", "flow"],
  10599. template: "systems/dnd5e/templates/advancement/advancement-manager.hbs",
  10600. width: 460,
  10601. height: "auto"
  10602. });
  10603. }
  10604. /* -------------------------------------------- */
  10605. /** @inheritdoc */
  10606. get title() {
  10607. const visibleSteps = this.steps.filter(s => !s.automatic);
  10608. const visibleIndex = visibleSteps.indexOf(this.step);
  10609. const step = visibleIndex < 0 ? "" : game.i18n.format("DND5E.AdvancementManagerSteps", {
  10610. current: visibleIndex + 1,
  10611. total: visibleSteps.length
  10612. });
  10613. return `${game.i18n.localize("DND5E.AdvancementManagerTitle")} ${step}`;
  10614. }
  10615. /* -------------------------------------------- */
  10616. /** @inheritdoc */
  10617. get id() {
  10618. return `actor-${this.actor.id}-advancement`;
  10619. }
  10620. /* -------------------------------------------- */
  10621. /**
  10622. * Get the step that is currently in progress.
  10623. * @type {object|null}
  10624. */
  10625. get step() {
  10626. return this.steps[this._stepIndex] ?? null;
  10627. }
  10628. /* -------------------------------------------- */
  10629. /**
  10630. * Get the step before the current one.
  10631. * @type {object|null}
  10632. */
  10633. get previousStep() {
  10634. return this.steps[this._stepIndex - 1] ?? null;
  10635. }
  10636. /* -------------------------------------------- */
  10637. /**
  10638. * Get the step after the current one.
  10639. * @type {object|null}
  10640. */
  10641. get nextStep() {
  10642. const nextIndex = this._stepIndex === null ? 0 : this._stepIndex + 1;
  10643. return this.steps[nextIndex] ?? null;
  10644. }
  10645. /* -------------------------------------------- */
  10646. /* Factory Methods */
  10647. /* -------------------------------------------- */
  10648. /**
  10649. * Construct a manager for a newly added advancement from drag-drop.
  10650. * @param {Actor5e} actor Actor from which the advancement should be updated.
  10651. * @param {string} itemId ID of the item to which the advancements are being dropped.
  10652. * @param {Advancement[]} advancements Dropped advancements to add.
  10653. * @param {object} options Rendering options passed to the application.
  10654. * @returns {AdvancementManager} Prepared manager. Steps count can be used to determine if advancements are needed.
  10655. */
  10656. static forNewAdvancement(actor, itemId, advancements, options) {
  10657. const manager = new this(actor, options);
  10658. const clonedItem = manager.clone.items.get(itemId);
  10659. if ( !clonedItem || !advancements.length ) return manager;
  10660. const currentLevel = this.currentLevel(clonedItem, manager.clone);
  10661. const minimumLevel = advancements.reduce((min, a) => Math.min(a.levels[0] ?? Infinity, min), Infinity);
  10662. if ( minimumLevel > currentLevel ) return manager;
  10663. const oldFlows = Array.fromRange(currentLevel + 1).slice(minimumLevel)
  10664. .flatMap(l => this.flowsForLevel(clonedItem, l));
  10665. // Revert advancements through minimum level
  10666. oldFlows.reverse().forEach(flow => manager.steps.push({ type: "reverse", flow, automatic: true }));
  10667. // Add new advancements
  10668. const advancementArray = clonedItem.toObject().system.advancement;
  10669. advancementArray.push(...advancements.map(a => {
  10670. const obj = a.toObject();
  10671. if ( obj.constructor.dataModels?.value ) a.value = (new a.constructor.metadata.dataModels.value()).toObject();
  10672. else obj.value = foundry.utils.deepClone(a.constructor.metadata.defaults?.value ?? {});
  10673. return obj;
  10674. }));
  10675. clonedItem.updateSource({"system.advancement": advancementArray});
  10676. const newFlows = Array.fromRange(currentLevel + 1).slice(minimumLevel)
  10677. .flatMap(l => this.flowsForLevel(clonedItem, l));
  10678. // Restore existing advancements and apply new advancements
  10679. newFlows.forEach(flow => {
  10680. const matchingFlow = oldFlows.find(f => (f.advancement.id === flow.advancement.id) && (f.level === flow.level));
  10681. if ( matchingFlow ) manager.steps.push({ type: "restore", flow: matchingFlow, automatic: true });
  10682. else manager.steps.push({ type: "forward", flow });
  10683. });
  10684. return manager;
  10685. }
  10686. /* -------------------------------------------- */
  10687. /**
  10688. * Construct a manager for a newly added item.
  10689. * @param {Actor5e} actor Actor to which the item is being added.
  10690. * @param {object} itemData Data for the item being added.
  10691. * @param {object} options Rendering options passed to the application.
  10692. * @returns {AdvancementManager} Prepared manager. Steps count can be used to determine if advancements are needed.
  10693. */
  10694. static forNewItem(actor, itemData, options={}) {
  10695. const manager = new this(actor, options);
  10696. // Prepare data for adding to clone
  10697. const dataClone = foundry.utils.deepClone(itemData);
  10698. dataClone._id = foundry.utils.randomID();
  10699. if ( itemData.type === "class" ) {
  10700. dataClone.system.levels = 0;
  10701. if ( !manager.clone.system.details.originalClass ) {
  10702. manager.clone.updateSource({"system.details.originalClass": dataClone._id});
  10703. }
  10704. }
  10705. // Add item to clone & get new instance from clone
  10706. manager.clone.updateSource({items: [dataClone]});
  10707. const clonedItem = manager.clone.items.get(dataClone._id);
  10708. // For class items, prepare level change data
  10709. if ( itemData.type === "class" ) {
  10710. return manager.createLevelChangeSteps(clonedItem, itemData.system?.levels ?? 1);
  10711. }
  10712. // All other items, just create some flows up to current character level (or class level for subclasses)
  10713. let targetLevel = manager.clone.system.details.level;
  10714. if ( clonedItem.type === "subclass" ) targetLevel = clonedItem.class?.system.levels ?? 0;
  10715. Array.fromRange(targetLevel + 1)
  10716. .flatMap(l => this.flowsForLevel(clonedItem, l))
  10717. .forEach(flow => manager.steps.push({ type: "forward", flow }));
  10718. return manager;
  10719. }
  10720. /* -------------------------------------------- */
  10721. /**
  10722. * Construct a manager for modifying choices on an item at a specific level.
  10723. * @param {Actor5e} actor Actor from which the choices should be modified.
  10724. * @param {object} itemId ID of the item whose choices are to be changed.
  10725. * @param {number} level Level at which the choices are being changed.
  10726. * @param {object} options Rendering options passed to the application.
  10727. * @returns {AdvancementManager} Prepared manager. Steps count can be used to determine if advancements are needed.
  10728. */
  10729. static forModifyChoices(actor, itemId, level, options) {
  10730. const manager = new this(actor, options);
  10731. const clonedItem = manager.clone.items.get(itemId);
  10732. if ( !clonedItem ) return manager;
  10733. const flows = Array.fromRange(this.currentLevel(clonedItem, manager.clone) + 1).slice(level)
  10734. .flatMap(l => this.flowsForLevel(clonedItem, l));
  10735. // Revert advancements through changed level
  10736. flows.reverse().forEach(flow => manager.steps.push({ type: "reverse", flow, automatic: true }));
  10737. // Create forward advancements for level being changed
  10738. flows.reverse().filter(f => f.level === level).forEach(flow => manager.steps.push({ type: "forward", flow }));
  10739. // Create restore advancements for other levels
  10740. flows.filter(f => f.level > level).forEach(flow => manager.steps.push({ type: "restore", flow, automatic: true }));
  10741. return manager;
  10742. }
  10743. /* -------------------------------------------- */
  10744. /**
  10745. * Construct a manager for an advancement that needs to be deleted.
  10746. * @param {Actor5e} actor Actor from which the advancement should be unapplied.
  10747. * @param {string} itemId ID of the item from which the advancement should be deleted.
  10748. * @param {string} advancementId ID of the advancement to delete.
  10749. * @param {object} options Rendering options passed to the application.
  10750. * @returns {AdvancementManager} Prepared manager. Steps count can be used to determine if advancements are needed.
  10751. */
  10752. static forDeletedAdvancement(actor, itemId, advancementId, options) {
  10753. const manager = new this(actor, options);
  10754. const clonedItem = manager.clone.items.get(itemId);
  10755. const advancement = clonedItem?.advancement.byId[advancementId];
  10756. if ( !advancement ) return manager;
  10757. const minimumLevel = advancement.levels[0];
  10758. const currentLevel = this.currentLevel(clonedItem, manager.clone);
  10759. // If minimum level is greater than current level, no changes to remove
  10760. if ( (minimumLevel > currentLevel) || !advancement.appliesToClass ) return manager;
  10761. advancement.levels
  10762. .reverse()
  10763. .filter(l => l <= currentLevel)
  10764. .map(l => new advancement.constructor.metadata.apps.flow(clonedItem, advancementId, l))
  10765. .forEach(flow => manager.steps.push({ type: "reverse", flow, automatic: true }));
  10766. if ( manager.steps.length ) manager.steps.push({ type: "delete", advancement, automatic: true });
  10767. return manager;
  10768. }
  10769. /* -------------------------------------------- */
  10770. /**
  10771. * Construct a manager for an item that needs to be deleted.
  10772. * @param {Actor5e} actor Actor from which the item should be deleted.
  10773. * @param {string} itemId ID of the item to be deleted.
  10774. * @param {object} options Rendering options passed to the application.
  10775. * @returns {AdvancementManager} Prepared manager. Steps count can be used to determine if advancements are needed.
  10776. */
  10777. static forDeletedItem(actor, itemId, options) {
  10778. const manager = new this(actor, options);
  10779. const clonedItem = manager.clone.items.get(itemId);
  10780. if ( !clonedItem ) return manager;
  10781. // For class items, prepare level change data
  10782. if ( clonedItem.type === "class" ) {
  10783. return manager.createLevelChangeSteps(clonedItem, clonedItem.system.levels * -1);
  10784. }
  10785. // All other items, just create some flows down from current character level
  10786. Array.fromRange(manager.clone.system.details.level + 1)
  10787. .flatMap(l => this.flowsForLevel(clonedItem, l))
  10788. .reverse()
  10789. .forEach(flow => manager.steps.push({ type: "reverse", flow, automatic: true }));
  10790. // Add a final step to remove the item only if there are advancements to apply
  10791. if ( manager.steps.length ) manager.steps.push({ type: "delete", item: clonedItem, automatic: true });
  10792. return manager;
  10793. }
  10794. /* -------------------------------------------- */
  10795. /**
  10796. * Construct a manager for a change in a class's levels.
  10797. * @param {Actor5e} actor Actor whose level has changed.
  10798. * @param {string} classId ID of the class being changed.
  10799. * @param {number} levelDelta Levels by which to increase or decrease the class.
  10800. * @param {object} options Rendering options passed to the application.
  10801. * @returns {AdvancementManager} Prepared manager. Steps count can be used to determine if advancements are needed.
  10802. */
  10803. static forLevelChange(actor, classId, levelDelta, options={}) {
  10804. const manager = new this(actor, options);
  10805. const clonedItem = manager.clone.items.get(classId);
  10806. if ( !clonedItem ) return manager;
  10807. return manager.createLevelChangeSteps(clonedItem, levelDelta);
  10808. }
  10809. /* -------------------------------------------- */
  10810. /**
  10811. * Create steps based on the provided level change data.
  10812. * @param {string} classItem Class being changed.
  10813. * @param {number} levelDelta Levels by which to increase or decrease the class.
  10814. * @returns {AdvancementManager} Manager with new steps.
  10815. * @private
  10816. */
  10817. createLevelChangeSteps(classItem, levelDelta) {
  10818. const pushSteps = (flows, data) => this.steps.push(...flows.map(flow => ({ flow, ...data })));
  10819. const getItemFlows = characterLevel => this.clone.items.contents.flatMap(i => {
  10820. if ( ["class", "subclass"].includes(i.type) ) return [];
  10821. return this.constructor.flowsForLevel(i, characterLevel);
  10822. });
  10823. // Level increased
  10824. for ( let offset = 1; offset <= levelDelta; offset++ ) {
  10825. const classLevel = classItem.system.levels + offset;
  10826. const characterLevel = this.actor.system.details.level + offset;
  10827. const stepData = { type: "forward", class: {item: classItem, level: classLevel} };
  10828. pushSteps(this.constructor.flowsForLevel(classItem, classLevel), stepData);
  10829. pushSteps(this.constructor.flowsForLevel(classItem.subclass, classLevel), stepData);
  10830. pushSteps(getItemFlows(characterLevel), stepData);
  10831. }
  10832. // Level decreased
  10833. for ( let offset = 0; offset > levelDelta; offset-- ) {
  10834. const classLevel = classItem.system.levels + offset;
  10835. const characterLevel = this.actor.system.details.level + offset;
  10836. const stepData = { type: "reverse", class: {item: classItem, level: classLevel}, automatic: true };
  10837. pushSteps(getItemFlows(characterLevel).reverse(), stepData);
  10838. pushSteps(this.constructor.flowsForLevel(classItem.subclass, classLevel).reverse(), stepData);
  10839. pushSteps(this.constructor.flowsForLevel(classItem, classLevel).reverse(), stepData);
  10840. if ( classLevel === 1 ) this.steps.push({ type: "delete", item: classItem, automatic: true });
  10841. }
  10842. // Ensure the class level ends up at the appropriate point
  10843. this.steps.push({
  10844. type: "forward", automatic: true,
  10845. class: {item: classItem, level: classItem.system.levels += levelDelta}
  10846. });
  10847. return this;
  10848. }
  10849. /* -------------------------------------------- */
  10850. /**
  10851. * Creates advancement flows for all advancements at a specific level.
  10852. * @param {Item5e} item Item that has advancement.
  10853. * @param {number} level Level in question.
  10854. * @returns {AdvancementFlow[]} Created flow applications.
  10855. * @protected
  10856. */
  10857. static flowsForLevel(item, level) {
  10858. return (item?.advancement.byLevel[level] ?? [])
  10859. .filter(a => a.appliesToClass)
  10860. .map(a => new a.constructor.metadata.apps.flow(item, a.id, level));
  10861. }
  10862. /* -------------------------------------------- */
  10863. /**
  10864. * Determine the proper working level either from the provided item or from the cloned actor.
  10865. * @param {Item5e} item Item being advanced. If class or subclass, its level will be used.
  10866. * @param {Actor5e} actor Actor being advanced.
  10867. * @returns {number} Working level.
  10868. */
  10869. static currentLevel(item, actor) {
  10870. return item.system.levels ?? item.class?.system.levels ?? actor.system.details.level;
  10871. }
  10872. /* -------------------------------------------- */
  10873. /* Form Rendering */
  10874. /* -------------------------------------------- */
  10875. /** @inheritdoc */
  10876. getData() {
  10877. if ( !this.step ) return {};
  10878. // Prepare information for subheading
  10879. const item = this.step.flow.item;
  10880. let level = this.step.flow.level;
  10881. if ( (this.step.class) && ["class", "subclass"].includes(item.type) ) level = this.step.class.level;
  10882. const visibleSteps = this.steps.filter(s => !s.automatic);
  10883. const visibleIndex = visibleSteps.indexOf(this.step);
  10884. return {
  10885. actor: this.clone,
  10886. flowId: this.step.flow.id,
  10887. header: item.name,
  10888. subheader: level ? game.i18n.format("DND5E.AdvancementLevelHeader", { level }) : "",
  10889. steps: {
  10890. current: visibleIndex + 1,
  10891. total: visibleSteps.length,
  10892. hasPrevious: visibleIndex > 0,
  10893. hasNext: visibleIndex < visibleSteps.length - 1
  10894. }
  10895. };
  10896. }
  10897. /* -------------------------------------------- */
  10898. /** @inheritdoc */
  10899. render(...args) {
  10900. if ( this.steps.length && (this._stepIndex === null) ) this._stepIndex = 0;
  10901. // Ensure the level on the class item matches the specified level
  10902. if ( this.step?.class ) {
  10903. let level = this.step.class.level;
  10904. if ( this.step.type === "reverse" ) level -= 1;
  10905. this.step.class.item.updateSource({"system.levels": level});
  10906. this.clone.reset();
  10907. }
  10908. /**
  10909. * A hook event that fires when an AdvancementManager is about to be processed.
  10910. * @function dnd5e.preAdvancementManagerRender
  10911. * @memberof hookEvents
  10912. * @param {AdvancementManager} advancementManager The advancement manager about to be rendered
  10913. */
  10914. const allowed = Hooks.call("dnd5e.preAdvancementManagerRender", this);
  10915. // Abort if not allowed
  10916. if ( allowed === false ) return this;
  10917. if ( this.step?.automatic ) {
  10918. if ( this._advancing ) return this;
  10919. this._forward();
  10920. return this;
  10921. }
  10922. return super.render(...args);
  10923. }
  10924. /* -------------------------------------------- */
  10925. /** @inheritdoc */
  10926. async _render(force, options) {
  10927. await super._render(force, options);
  10928. if ( (this._state !== Application.RENDER_STATES.RENDERED) || !this.step ) return;
  10929. // Render the step
  10930. this.step.flow._element = null;
  10931. await this.step.flow._render(force, options);
  10932. this.setPosition();
  10933. }
  10934. /* -------------------------------------------- */
  10935. /** @inheritdoc */
  10936. activateListeners(html) {
  10937. super.activateListeners(html);
  10938. html.find("button[data-action]").click(event => {
  10939. const buttons = html.find("button");
  10940. buttons.attr("disabled", true);
  10941. html.find(".error").removeClass("error");
  10942. try {
  10943. switch ( event.currentTarget.dataset.action ) {
  10944. case "restart":
  10945. if ( !this.previousStep ) return;
  10946. return this._restart(event);
  10947. case "previous":
  10948. if ( !this.previousStep ) return;
  10949. return this._backward(event);
  10950. case "next":
  10951. case "complete":
  10952. return this._forward(event);
  10953. }
  10954. } finally {
  10955. buttons.attr("disabled", false);
  10956. }
  10957. });
  10958. }
  10959. /* -------------------------------------------- */
  10960. /** @inheritdoc */
  10961. async close(options={}) {
  10962. if ( !options.skipConfirmation ) {
  10963. return new Dialog({
  10964. title: `${game.i18n.localize("DND5E.AdvancementManagerCloseTitle")}: ${this.actor.name}`,
  10965. content: game.i18n.localize("DND5E.AdvancementManagerCloseMessage"),
  10966. buttons: {
  10967. close: {
  10968. icon: '<i class="fas fa-times"></i>',
  10969. label: game.i18n.localize("DND5E.AdvancementManagerCloseButtonStop"),
  10970. callback: () => super.close(options)
  10971. },
  10972. continue: {
  10973. icon: '<i class="fas fa-chevron-right"></i>',
  10974. label: game.i18n.localize("DND5E.AdvancementManagerCloseButtonContinue")
  10975. }
  10976. },
  10977. default: "close"
  10978. }).render(true);
  10979. }
  10980. await super.close(options);
  10981. }
  10982. /* -------------------------------------------- */
  10983. /* Process */
  10984. /* -------------------------------------------- */
  10985. /**
  10986. * Advance through the steps until one requiring user interaction is encountered.
  10987. * @param {Event} [event] Triggering click event if one occurred.
  10988. * @returns {Promise}
  10989. * @private
  10990. */
  10991. async _forward(event) {
  10992. this._advancing = true;
  10993. try {
  10994. do {
  10995. const flow = this.step.flow;
  10996. // Apply changes based on step type
  10997. if ( (this.step.type === "delete") && this.step.item ) this.clone.items.delete(this.step.item.id);
  10998. else if ( (this.step.type === "delete") && this.step.advancement ) {
  10999. this.step.advancement.item.deleteAdvancement(this.step.advancement.id, { source: true });
  11000. }
  11001. else if ( this.step.type === "restore" ) await flow.advancement.restore(flow.level, flow.retainedData);
  11002. else if ( this.step.type === "reverse" ) flow.retainedData = await flow.advancement.reverse(flow.level);
  11003. else if ( flow ) await flow._updateObject(event, flow._getSubmitData());
  11004. this._stepIndex++;
  11005. // Ensure the level on the class item matches the specified level
  11006. if ( this.step?.class ) {
  11007. let level = this.step.class.level;
  11008. if ( this.step.type === "reverse" ) level -= 1;
  11009. this.step.class.item.updateSource({"system.levels": level});
  11010. }
  11011. this.clone.reset();
  11012. } while ( this.step?.automatic );
  11013. } catch(error) {
  11014. if ( !(error instanceof Advancement.ERROR) ) throw error;
  11015. ui.notifications.error(error.message);
  11016. this.step.automatic = false;
  11017. if ( this.step.type === "restore" ) this.step.type = "forward";
  11018. } finally {
  11019. this._advancing = false;
  11020. }
  11021. if ( this.step ) this.render(true);
  11022. else this._complete();
  11023. }
  11024. /* -------------------------------------------- */
  11025. /**
  11026. * Reverse through the steps until one requiring user interaction is encountered.
  11027. * @param {Event} [event] Triggering click event if one occurred.
  11028. * @param {object} [options] Additional options to configure behavior.
  11029. * @param {boolean} [options.render=true] Whether to render the Application after the step has been reversed. Used
  11030. * by the restart workflow.
  11031. * @returns {Promise}
  11032. * @private
  11033. */
  11034. async _backward(event, { render=true }={}) {
  11035. this._advancing = true;
  11036. try {
  11037. do {
  11038. this._stepIndex--;
  11039. if ( !this.step ) break;
  11040. const flow = this.step.flow;
  11041. // Reverse step based on step type
  11042. if ( (this.step.type === "delete") && this.step.item ) this.clone.updateSource({items: [this.step.item]});
  11043. else if ( (this.step.type === "delete") && this.step.advancement ) this.advancement.item.createAdvancement(
  11044. this.advancement.typeName, this.advancement._source, { source: true }
  11045. );
  11046. else if ( this.step.type === "reverse" ) await flow.advancement.restore(flow.level, flow.retainedData);
  11047. else if ( flow ) flow.retainedData = await flow.advancement.reverse(flow.level);
  11048. this.clone.reset();
  11049. } while ( this.step?.automatic );
  11050. } catch(error) {
  11051. if ( !(error instanceof Advancement.ERROR) ) throw error;
  11052. ui.notifications.error(error.message);
  11053. this.step.automatic = false;
  11054. } finally {
  11055. this._advancing = false;
  11056. }
  11057. if ( !render ) return;
  11058. if ( this.step ) this.render(true);
  11059. else this.close({ skipConfirmation: true });
  11060. }
  11061. /* -------------------------------------------- */
  11062. /**
  11063. * Reset back to the manager's initial state.
  11064. * @param {MouseEvent} [event] The triggering click event if one occurred.
  11065. * @returns {Promise}
  11066. * @private
  11067. */
  11068. async _restart(event) {
  11069. const restart = await Dialog.confirm({
  11070. title: game.i18n.localize("DND5E.AdvancementManagerRestartConfirmTitle"),
  11071. content: game.i18n.localize("DND5E.AdvancementManagerRestartConfirm")
  11072. });
  11073. if ( !restart ) return;
  11074. // While there is still a renderable step.
  11075. while ( this.steps.slice(0, this._stepIndex).some(s => !s.automatic) ) {
  11076. await this._backward(event, {render: false});
  11077. }
  11078. this.render(true);
  11079. }
  11080. /* -------------------------------------------- */
  11081. /**
  11082. * Apply changes to actual actor after all choices have been made.
  11083. * @param {Event} event Button click that triggered the change.
  11084. * @returns {Promise}
  11085. * @private
  11086. */
  11087. async _complete(event) {
  11088. const updates = this.clone.toObject();
  11089. const items = updates.items;
  11090. delete updates.items;
  11091. // Gather changes to embedded items
  11092. const { toCreate, toUpdate, toDelete } = items.reduce((obj, item) => {
  11093. if ( !this.actor.items.get(item._id) ) {
  11094. obj.toCreate.push(item);
  11095. } else {
  11096. obj.toUpdate.push(item);
  11097. obj.toDelete.findSplice(id => id === item._id);
  11098. }
  11099. return obj;
  11100. }, { toCreate: [], toUpdate: [], toDelete: this.actor.items.map(i => i.id) });
  11101. /**
  11102. * A hook event that fires at the final stage of a character's advancement process, before actor and item updates
  11103. * are applied.
  11104. * @function dnd5e.preAdvancementManagerComplete
  11105. * @memberof hookEvents
  11106. * @param {AdvancementManager} advancementManager The advancement manager.
  11107. * @param {object} actorUpdates Updates to the actor.
  11108. * @param {object[]} toCreate Items that will be created on the actor.
  11109. * @param {object[]} toUpdate Items that will be updated on the actor.
  11110. * @param {string[]} toDelete IDs of items that will be deleted on the actor.
  11111. */
  11112. if ( Hooks.call("dnd5e.preAdvancementManagerComplete", this, updates, toCreate, toUpdate, toDelete) === false ) {
  11113. console.log("AdvancementManager completion was prevented by the 'preAdvancementManagerComplete' hook.");
  11114. return this.close({ skipConfirmation: true });
  11115. }
  11116. // Apply changes from clone to original actor
  11117. await Promise.all([
  11118. this.actor.update(updates, { isAdvancement: true }),
  11119. this.actor.createEmbeddedDocuments("Item", toCreate, { keepId: true, isAdvancement: true }),
  11120. this.actor.updateEmbeddedDocuments("Item", toUpdate, { isAdvancement: true }),
  11121. this.actor.deleteEmbeddedDocuments("Item", toDelete, { isAdvancement: true })
  11122. ]);
  11123. /**
  11124. * A hook event that fires when an AdvancementManager is done modifying an actor.
  11125. * @function dnd5e.advancementManagerComplete
  11126. * @memberof hookEvents
  11127. * @param {AdvancementManager} advancementManager The advancement manager that just completed
  11128. */
  11129. Hooks.callAll("dnd5e.advancementManagerComplete", this);
  11130. // Close prompt
  11131. return this.close({ skipConfirmation: true });
  11132. }
  11133. }
  11134. /**
  11135. * Description for a single part of a property attribution.
  11136. * @typedef {object} AttributionDescription
  11137. * @property {string} label Descriptive label that will be displayed. If the label is in the form
  11138. * of an @ property, the system will try to turn it into a human-readable label.
  11139. * @property {number} mode Application mode for this step as defined in
  11140. * [CONST.ACTIVE_EFFECT_MODES](https://foundryvtt.com/api/module-constants.html#.ACTIVE_EFFECT_MODES).
  11141. * @property {number} value Value of this step.
  11142. */
  11143. /**
  11144. * Interface for viewing what factors went into determining a specific property.
  11145. *
  11146. * @param {Document} object The Document that owns the property being attributed.
  11147. * @param {AttributionDescription[]} attributions An array of all the attribution data.
  11148. * @param {string} property Dot separated path to the property.
  11149. * @param {object} [options={}] Application rendering options.
  11150. */
  11151. class PropertyAttribution extends Application {
  11152. constructor(object, attributions, property, options={}) {
  11153. super(options);
  11154. this.object = object;
  11155. this.attributions = attributions;
  11156. this.property = property;
  11157. }
  11158. /* -------------------------------------------- */
  11159. /** @inheritDoc */
  11160. static get defaultOptions() {
  11161. return foundry.utils.mergeObject(super.defaultOptions, {
  11162. id: "property-attribution",
  11163. classes: ["dnd5e", "property-attribution"],
  11164. template: "systems/dnd5e/templates/apps/property-attribution.hbs",
  11165. width: 320,
  11166. height: "auto"
  11167. });
  11168. }
  11169. /* -------------------------------------------- */
  11170. /**
  11171. * Render this view as a tooltip rather than a whole window.
  11172. * @param {HTMLElement} element The element to which the tooltip should be attached.
  11173. */
  11174. async renderTooltip(element) {
  11175. const data = this.getData(this.options);
  11176. const text = (await this._renderInner(data))[0].outerHTML;
  11177. game.tooltip.activate(element, { text, cssClass: "property-attribution" });
  11178. }
  11179. /* -------------------------------------------- */
  11180. /** @inheritDoc */
  11181. getData() {
  11182. const property = foundry.utils.getProperty(this.object.system, this.property);
  11183. let total;
  11184. if ( Number.isNumeric(property)) total = property;
  11185. else if ( typeof property === "object" && Number.isNumeric(property.value) ) total = property.value;
  11186. const sources = foundry.utils.duplicate(this.attributions);
  11187. return {
  11188. caption: this.options.title,
  11189. sources: sources.map(entry => {
  11190. if ( entry.label.startsWith("@") ) entry.label = this.getPropertyLabel(entry.label.slice(1));
  11191. if ( (entry.mode === CONST.ACTIVE_EFFECT_MODES.ADD) && (entry.value < 0) ) {
  11192. entry.negative = true;
  11193. entry.value = entry.value * -1;
  11194. }
  11195. return entry;
  11196. }),
  11197. total: total
  11198. };
  11199. }
  11200. /* -------------------------------------------- */
  11201. /**
  11202. * Produce a human-readable and localized name for the provided property.
  11203. * @param {string} property Dot separated path to the property.
  11204. * @returns {string} Property name for display.
  11205. */
  11206. getPropertyLabel(property) {
  11207. const parts = property.split(".");
  11208. if ( parts[0] === "abilities" && parts[1] ) {
  11209. return CONFIG.DND5E.abilities[parts[1]]?.label ?? property;
  11210. } else if ( (property === "attributes.ac.dex") && CONFIG.DND5E.abilities.dex ) {
  11211. return CONFIG.DND5E.abilities.dex.label;
  11212. } else if ( (parts[0] === "prof") || (property === "attributes.prof") ) {
  11213. return game.i18n.localize("DND5E.Proficiency");
  11214. }
  11215. return property;
  11216. }
  11217. }
  11218. /**
  11219. * A specialized application used to modify actor traits.
  11220. *
  11221. * @param {Actor5e} actor Actor for whose traits are being edited.
  11222. * @param {string} trait Trait key as defined in CONFIG.traits.
  11223. * @param {object} [options={}]
  11224. * @param {boolean} [options.allowCustom=true] Support user custom trait entries.
  11225. */
  11226. let TraitSelector$1 = class TraitSelector extends BaseConfigSheet {
  11227. constructor(actor, trait, options={}) {
  11228. if ( !CONFIG.DND5E.traits[trait] ) throw new Error(
  11229. `Cannot instantiate TraitSelector with a trait not defined in CONFIG.DND5E.traits: ${trait}.`
  11230. );
  11231. if ( ["saves", "skills"].includes(trait) ) throw new Error(
  11232. `TraitSelector does not support selection of ${trait}. That should be handled through `
  11233. + "that type's more specialized configuration application."
  11234. );
  11235. super(actor, options);
  11236. /**
  11237. * Trait key as defined in CONFIG.traits.
  11238. * @type {string}
  11239. */
  11240. this.trait = trait;
  11241. }
  11242. /* -------------------------------------------- */
  11243. /** @inheritdoc */
  11244. static get defaultOptions() {
  11245. return foundry.utils.mergeObject(super.defaultOptions, {
  11246. id: "trait-selector",
  11247. classes: ["dnd5e", "trait-selector", "subconfig"],
  11248. template: "systems/dnd5e/templates/apps/trait-selector.hbs",
  11249. width: 320,
  11250. height: "auto",
  11251. sheetConfig: false,
  11252. allowCustom: true
  11253. });
  11254. }
  11255. /* -------------------------------------------- */
  11256. /** @inheritdoc */
  11257. get id() {
  11258. return `${this.constructor.name}-${this.trait}-Actor-${this.document.id}`;
  11259. }
  11260. /* -------------------------------------------- */
  11261. /** @inheritdoc */
  11262. get title() {
  11263. return `${this.document.name}: ${traitLabel(this.trait)}`;
  11264. }
  11265. /* -------------------------------------------- */
  11266. /** @inheritdoc */
  11267. async getData() {
  11268. const path = `system.${actorKeyPath(this.trait)}`;
  11269. const data = foundry.utils.getProperty(this.document, path);
  11270. if ( !data ) return super.getData();
  11271. return {
  11272. ...super.getData(),
  11273. choices: await choices(this.trait, data.value),
  11274. custom: data.custom,
  11275. customPath: "custom" in data ? `${path}.custom` : null,
  11276. bypasses: "bypasses" in data ? Object.entries(CONFIG.DND5E.physicalWeaponProperties).reduce((obj, [k, v]) => {
  11277. obj[k] = { label: v, chosen: data.bypasses.has(k) };
  11278. return obj;
  11279. }, {}) : null,
  11280. bypassesPath: "bypasses" in data ? `${path}.bypasses` : null
  11281. };
  11282. }
  11283. /* -------------------------------------------- */
  11284. /** @inheritdoc */
  11285. activateListeners(html) {
  11286. super.activateListeners(html);
  11287. for ( const checkbox of html[0].querySelectorAll("input[type='checkbox']") ) {
  11288. if ( checkbox.checked ) this._onToggleCategory(checkbox);
  11289. }
  11290. }
  11291. /* -------------------------------------------- */
  11292. /** @inheritdoc */
  11293. _getActorOverrides() {
  11294. const overrides = super._getActorOverrides();
  11295. const path = `system.${actorKeyPath(this.trait)}.value`;
  11296. const src = new Set(foundry.utils.getProperty(this.document._source, path));
  11297. const current = foundry.utils.getProperty(this.document, path);
  11298. const delta = current.difference(src);
  11299. for ( const choice of delta ) {
  11300. overrides.push(`choices.${choice}`);
  11301. }
  11302. return overrides;
  11303. }
  11304. /* -------------------------------------------- */
  11305. /** @inheritdoc */
  11306. async _onChangeInput(event) {
  11307. super._onChangeInput(event);
  11308. if ( event.target.name?.startsWith("choices") ) this._onToggleCategory(event.target);
  11309. }
  11310. /* -------------------------------------------- */
  11311. /**
  11312. * Enable/disable all children when a category is checked.
  11313. * @param {HTMLElement} checkbox Checkbox that was changed.
  11314. * @protected
  11315. */
  11316. _onToggleCategory(checkbox) {
  11317. const children = checkbox.closest("li")?.querySelector("ol");
  11318. if ( !children ) return;
  11319. for ( const child of children.querySelectorAll("input[type='checkbox']") ) {
  11320. child.checked = child.disabled = checkbox.checked;
  11321. }
  11322. }
  11323. /* -------------------------------------------- */
  11324. /**
  11325. * Filter a list of choices that begin with the provided key for update.
  11326. * @param {string} prefix They initial form prefix under which the choices are grouped.
  11327. * @param {string} path Path in actor data where the final choices will be saved.
  11328. * @param {object} formData Form data being prepared. *Will be mutated.*
  11329. * @protected
  11330. */
  11331. _prepareChoices(prefix, path, formData) {
  11332. const chosen = [];
  11333. for ( const key of Object.keys(formData).filter(k => k.startsWith(`${prefix}.`)) ) {
  11334. if ( formData[key] ) chosen.push(key.replace(`${prefix}.`, ""));
  11335. delete formData[key];
  11336. }
  11337. formData[path] = chosen;
  11338. }
  11339. /* -------------------------------------------- */
  11340. /** @override */
  11341. async _updateObject(event, formData) {
  11342. const path = `system.${actorKeyPath(this.trait)}`;
  11343. const data = foundry.utils.getProperty(this.document, path);
  11344. this._prepareChoices("choices", `${path}.value`, formData);
  11345. if ( "bypasses" in data ) this._prepareChoices("bypasses", `${path}.bypasses`, formData);
  11346. return this.object.update(formData);
  11347. }
  11348. };
  11349. /**
  11350. * @typedef {FormApplicationOptions} ProficiencyConfigOptions
  11351. * @property {string} key The ID of the skill or tool being configured.
  11352. * @property {string} property The property on the actor being configured, either 'skills', or 'tools'.
  11353. */
  11354. /**
  11355. * An application responsible for configuring proficiencies and bonuses in tools and skills.
  11356. *
  11357. * @param {Actor5e} actor The Actor being configured.
  11358. * @param {ProficiencyConfigOptions} options Additional configuration options.
  11359. */
  11360. class ProficiencyConfig extends BaseConfigSheet {
  11361. /** @inheritdoc */
  11362. static get defaultOptions() {
  11363. return foundry.utils.mergeObject(super.defaultOptions, {
  11364. classes: ["dnd5e"],
  11365. template: "systems/dnd5e/templates/apps/proficiency-config.hbs",
  11366. width: 500,
  11367. height: "auto"
  11368. });
  11369. }
  11370. /* -------------------------------------------- */
  11371. /**
  11372. * Are we configuring a tool?
  11373. * @returns {boolean}
  11374. */
  11375. get isTool() {
  11376. return this.options.property === "tools";
  11377. }
  11378. /* -------------------------------------------- */
  11379. /**
  11380. * Are we configuring a skill?
  11381. * @returns {boolean}
  11382. */
  11383. get isSkill() {
  11384. return this.options.property === "skills";
  11385. }
  11386. /* -------------------------------------------- */
  11387. /** @inheritdoc */
  11388. get title() {
  11389. const label = this.isSkill ? CONFIG.DND5E.skills[this.options.key].label : keyLabel("tool", this.options.key);
  11390. return `${game.i18n.format("DND5E.ProficiencyConfigureTitle", {label})}: ${this.document.name}`;
  11391. }
  11392. /* -------------------------------------------- */
  11393. /** @inheritdoc */
  11394. get id() {
  11395. return `ProficiencyConfig-${this.document.documentName}-${this.document.id}-${this.options.key}`;
  11396. }
  11397. /* -------------------------------------------- */
  11398. /** @inheritdoc */
  11399. getData(options={}) {
  11400. return {
  11401. abilities: CONFIG.DND5E.abilities,
  11402. proficiencyLevels: CONFIG.DND5E.proficiencyLevels,
  11403. entry: this.document.system[this.options.property]?.[this.options.key],
  11404. isTool: this.isTool,
  11405. isSkill: this.isSkill,
  11406. key: this.options.key,
  11407. property: this.options.property
  11408. };
  11409. }
  11410. /* -------------------------------------------- */
  11411. /** @inheritdoc */
  11412. async _updateObject(event, formData) {
  11413. if ( this.isTool ) return super._updateObject(event, formData);
  11414. const passive = formData[`system.skills.${this.options.key}.bonuses.passive`];
  11415. const passiveRoll = new Roll(passive);
  11416. if ( !passiveRoll.isDeterministic ) {
  11417. const message = game.i18n.format("DND5E.FormulaCannotContainDiceError", {
  11418. name: game.i18n.localize("DND5E.SkillBonusPassive")
  11419. });
  11420. ui.notifications.error(message);
  11421. throw new Error(message);
  11422. }
  11423. return super._updateObject(event, formData);
  11424. }
  11425. }
  11426. /**
  11427. * A specialized version of the TraitSelector used for selecting tool and vehicle proficiencies.
  11428. * @extends {TraitSelector}
  11429. */
  11430. class ToolSelector extends TraitSelector$1 {
  11431. /** @inheritdoc */
  11432. async getData() {
  11433. return {
  11434. ...super.getData(),
  11435. choices: await choices(this.trait, Object.keys(this.document.system.tools))
  11436. };
  11437. }
  11438. /* -------------------------------------------- */
  11439. /** @inheritdoc */
  11440. _getActorOverrides() {
  11441. return Object.keys(foundry.utils.flattenObject(this.document.overrides));
  11442. }
  11443. /* -------------------------------------------- */
  11444. /** @inheritdoc */
  11445. async _updateObject(event, formData) {
  11446. return this.document.update(Object.entries(formData).reduce((obj, [k, v]) => {
  11447. const [, key] = k.split(".");
  11448. const tool = this.document.system.tools[key];
  11449. if ( tool && !v ) obj[`system.tools.-=${key}`] = null;
  11450. else if ( !tool && v ) obj[`system.tools.${key}`] = {value: 1};
  11451. return obj;
  11452. }, {}));
  11453. }
  11454. }
  11455. /**
  11456. * Extend the basic ActorSheet class to suppose system-specific logic and functionality.
  11457. * @abstract
  11458. */
  11459. class ActorSheet5e extends ActorSheet {
  11460. /**
  11461. * Track the set of item filters which are applied
  11462. * @type {Object<string, Set>}
  11463. * @protected
  11464. */
  11465. _filters = {
  11466. inventory: new Set(),
  11467. spellbook: new Set(),
  11468. features: new Set(),
  11469. effects: new Set()
  11470. };
  11471. /* -------------------------------------------- */
  11472. /**
  11473. * IDs for items on the sheet that have been expanded.
  11474. * @type {Set<string>}
  11475. * @protected
  11476. */
  11477. _expanded = new Set();
  11478. /* -------------------------------------------- */
  11479. /** @override */
  11480. static get defaultOptions() {
  11481. return foundry.utils.mergeObject(super.defaultOptions, {
  11482. scrollY: [
  11483. ".inventory .inventory-list",
  11484. ".features .inventory-list",
  11485. ".spellbook .inventory-list",
  11486. ".effects .inventory-list",
  11487. ".center-pane"
  11488. ],
  11489. tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}],
  11490. width: 720,
  11491. height: Math.max(680, Math.max(
  11492. 237 + (Object.keys(CONFIG.DND5E.abilities).length * 70),
  11493. 240 + (Object.keys(CONFIG.DND5E.skills).length * 24)
  11494. ))
  11495. });
  11496. }
  11497. /* -------------------------------------------- */
  11498. /**
  11499. * A set of item types that should be prevented from being dropped on this type of actor sheet.
  11500. * @type {Set<string>}
  11501. */
  11502. static unsupportedItemTypes = new Set();
  11503. /* -------------------------------------------- */
  11504. /** @override */
  11505. get template() {
  11506. if ( !game.user.isGM && this.actor.limited ) return "systems/dnd5e/templates/actors/limited-sheet.hbs";
  11507. return `systems/dnd5e/templates/actors/${this.actor.type}-sheet.hbs`;
  11508. }
  11509. /* -------------------------------------------- */
  11510. /* Context Preparation */
  11511. /* -------------------------------------------- */
  11512. /** @override */
  11513. async getData(options) {
  11514. // The Actor's data
  11515. const source = this.actor.toObject();
  11516. // Basic data
  11517. const context = {
  11518. actor: this.actor,
  11519. source: source.system,
  11520. system: this.actor.system,
  11521. items: Array.from(this.actor.items),
  11522. itemContext: {},
  11523. abilities: foundry.utils.deepClone(this.actor.system.abilities),
  11524. skills: foundry.utils.deepClone(this.actor.system.skills ?? {}),
  11525. tools: foundry.utils.deepClone(this.actor.system.tools ?? {}),
  11526. labels: this._getLabels(),
  11527. movement: this._getMovementSpeed(this.actor.system),
  11528. senses: this._getSenses(this.actor.system),
  11529. effects: ActiveEffect5e.prepareActiveEffectCategories(this.actor.effects),
  11530. warnings: foundry.utils.deepClone(this.actor._preparationWarnings),
  11531. filters: this._filters,
  11532. owner: this.actor.isOwner,
  11533. limited: this.actor.limited,
  11534. options: this.options,
  11535. editable: this.isEditable,
  11536. cssClass: this.actor.isOwner ? "editable" : "locked",
  11537. isCharacter: this.actor.type === "character",
  11538. isNPC: this.actor.type === "npc",
  11539. isVehicle: this.actor.type === "vehicle",
  11540. config: CONFIG.DND5E,
  11541. rollableClass: this.isEditable ? "rollable" : "",
  11542. rollData: this.actor.getRollData()
  11543. };
  11544. // Sort Owned Items
  11545. context.items.sort((a, b) => (a.sort || 0) - (b.sort || 0));
  11546. // Temporary HP
  11547. const hp = {...context.system.attributes.hp};
  11548. if ( hp.temp === 0 ) delete hp.temp;
  11549. if ( hp.tempmax === 0 ) delete hp.tempmax;
  11550. context.hp = hp;
  11551. // Ability Scores
  11552. for ( const [a, abl] of Object.entries(context.abilities) ) {
  11553. abl.icon = this._getProficiencyIcon(abl.proficient);
  11554. abl.hover = CONFIG.DND5E.proficiencyLevels[abl.proficient];
  11555. abl.label = CONFIG.DND5E.abilities[a]?.label;
  11556. abl.baseProf = source.system.abilities[a]?.proficient ?? 0;
  11557. }
  11558. // Skills & tools.
  11559. ["skills", "tools"].forEach(prop => {
  11560. for ( const [key, entry] of Object.entries(context[prop]) ) {
  11561. entry.abbreviation = CONFIG.DND5E.abilities[entry.ability]?.abbreviation;
  11562. entry.icon = this._getProficiencyIcon(entry.value);
  11563. entry.hover = CONFIG.DND5E.proficiencyLevels[entry.value];
  11564. entry.label = prop === "skills" ? CONFIG.DND5E.skills[key]?.label : keyLabel("tool", key);
  11565. entry.baseValue = source.system[prop]?.[key]?.value ?? 0;
  11566. }
  11567. });
  11568. // Update traits
  11569. context.traits = this._prepareTraits(context.system);
  11570. // Prepare owned items
  11571. this._prepareItems(context);
  11572. context.expandedData = {};
  11573. for ( const id of this._expanded ) {
  11574. const item = this.actor.items.get(id);
  11575. if ( item ) context.expandedData[id] = await item.getChatData({secrets: this.actor.isOwner});
  11576. }
  11577. // Biography HTML enrichment
  11578. context.biographyHTML = await TextEditor.enrichHTML(context.system.details.biography.value, {
  11579. secrets: this.actor.isOwner,
  11580. rollData: context.rollData,
  11581. async: true,
  11582. relativeTo: this.actor
  11583. });
  11584. return context;
  11585. }
  11586. /* -------------------------------------------- */
  11587. /**
  11588. * Prepare labels object for the context.
  11589. * @returns {object} Object containing various labels.
  11590. * @protected
  11591. */
  11592. _getLabels() {
  11593. const labels = {...this.actor.labels};
  11594. // Currency Labels
  11595. labels.currencies = Object.entries(CONFIG.DND5E.currencies).reduce((obj, [k, c]) => {
  11596. obj[k] = c.label;
  11597. return obj;
  11598. }, {});
  11599. // Proficiency
  11600. labels.proficiency = game.settings.get("dnd5e", "proficiencyModifier") === "dice"
  11601. ? `d${this.actor.system.attributes.prof * 2}`
  11602. : `+${this.actor.system.attributes.prof}`;
  11603. return labels;
  11604. }
  11605. /* -------------------------------------------- */
  11606. /**
  11607. * Prepare the display of movement speed data for the Actor.
  11608. * @param {object} systemData System data for the Actor being prepared.
  11609. * @param {boolean} [largestPrimary=false] Show the largest movement speed as "primary", otherwise show "walk".
  11610. * @returns {{primary: string, special: string}}
  11611. * @protected
  11612. */
  11613. _getMovementSpeed(systemData, largestPrimary=false) {
  11614. const movement = systemData.attributes.movement ?? {};
  11615. // Prepare an array of available movement speeds
  11616. let speeds = [
  11617. [movement.burrow, `${game.i18n.localize("DND5E.MovementBurrow")} ${movement.burrow}`],
  11618. [movement.climb, `${game.i18n.localize("DND5E.MovementClimb")} ${movement.climb}`],
  11619. [movement.fly, `${game.i18n.localize("DND5E.MovementFly")} ${movement.fly}${movement.hover ? ` (${game.i18n.localize("DND5E.MovementHover")})` : ""}`],
  11620. [movement.swim, `${game.i18n.localize("DND5E.MovementSwim")} ${movement.swim}`]
  11621. ];
  11622. if ( largestPrimary ) {
  11623. speeds.push([movement.walk, `${game.i18n.localize("DND5E.MovementWalk")} ${movement.walk}`]);
  11624. }
  11625. // Filter and sort speeds on their values
  11626. speeds = speeds.filter(s => s[0]).sort((a, b) => b[0] - a[0]);
  11627. // Case 1: Largest as primary
  11628. if ( largestPrimary ) {
  11629. let primary = speeds.shift();
  11630. return {
  11631. primary: `${primary ? primary[1] : "0"} ${movement.units}`,
  11632. special: speeds.map(s => s[1]).join(", ")
  11633. };
  11634. }
  11635. // Case 2: Walk as primary
  11636. else {
  11637. return {
  11638. primary: `${movement.walk || 0} ${movement.units}`,
  11639. special: speeds.length ? speeds.map(s => s[1]).join(", ") : ""
  11640. };
  11641. }
  11642. }
  11643. /* -------------------------------------------- */
  11644. /**
  11645. * Prepare senses object for display.
  11646. * @param {object} systemData System data for the Actor being prepared.
  11647. * @returns {object} Senses grouped by key with localized and formatted string.
  11648. * @protected
  11649. */
  11650. _getSenses(systemData) {
  11651. const senses = systemData.attributes.senses ?? {};
  11652. const tags = {};
  11653. for ( let [k, label] of Object.entries(CONFIG.DND5E.senses) ) {
  11654. const v = senses[k] ?? 0;
  11655. if ( v === 0 ) continue;
  11656. tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
  11657. }
  11658. if ( senses.special ) tags.special = senses.special;
  11659. return tags;
  11660. }
  11661. /* -------------------------------------------- */
  11662. /** @inheritdoc */
  11663. async activateEditor(name, options={}, initialContent="") {
  11664. options.relativeLinks = true;
  11665. return super.activateEditor(name, options, initialContent);
  11666. }
  11667. /* --------------------------------------------- */
  11668. /* Property Attribution */
  11669. /* --------------------------------------------- */
  11670. /**
  11671. * Break down all of the Active Effects affecting a given target property.
  11672. * @param {string} target The data property being targeted.
  11673. * @returns {AttributionDescription[]} Any active effects that modify that property.
  11674. * @protected
  11675. */
  11676. _prepareActiveEffectAttributions(target) {
  11677. return this.actor.effects.reduce((arr, e) => {
  11678. let source = e.sourceName;
  11679. if ( e.origin === this.actor.uuid ) source = e.label;
  11680. if ( !source || e.disabled || e.isSuppressed ) return arr;
  11681. const value = e.changes.reduce((n, change) => {
  11682. if ( (change.key !== target) || !Number.isNumeric(change.value) ) return n;
  11683. if ( change.mode !== CONST.ACTIVE_EFFECT_MODES.ADD ) return n;
  11684. return n + Number(change.value);
  11685. }, 0);
  11686. if ( !value ) return arr;
  11687. arr.push({value, label: source, mode: CONST.ACTIVE_EFFECT_MODES.ADD});
  11688. return arr;
  11689. }, []);
  11690. }
  11691. /* -------------------------------------------- */
  11692. /**
  11693. * Produce a list of armor class attribution objects.
  11694. * @param {object} rollData Data provided by Actor5e#getRollData
  11695. * @returns {AttributionDescription[]} List of attribution descriptions.
  11696. * @protected
  11697. */
  11698. _prepareArmorClassAttribution(rollData) {
  11699. const ac = rollData.attributes.ac;
  11700. const cfg = CONFIG.DND5E.armorClasses[ac.calc];
  11701. const attribution = [];
  11702. // Base AC Attribution
  11703. switch ( ac.calc ) {
  11704. // Flat AC
  11705. case "flat":
  11706. return [{
  11707. label: game.i18n.localize("DND5E.ArmorClassFlat"),
  11708. mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE,
  11709. value: ac.flat
  11710. }];
  11711. // Natural armor
  11712. case "natural":
  11713. attribution.push({
  11714. label: game.i18n.localize("DND5E.ArmorClassNatural"),
  11715. mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE,
  11716. value: ac.flat
  11717. });
  11718. break;
  11719. default:
  11720. const formula = ac.calc === "custom" ? ac.formula : cfg.formula;
  11721. let base = ac.base;
  11722. const dataRgx = new RegExp(/@([a-z.0-9_-]+)/gi);
  11723. for ( const [match, term] of formula.matchAll(dataRgx) ) {
  11724. const value = String(foundry.utils.getProperty(rollData, term));
  11725. if ( (term === "attributes.ac.armor") || (value === "0") ) continue;
  11726. if ( Number.isNumeric(value) ) base -= Number(value);
  11727. attribution.push({
  11728. label: match,
  11729. mode: CONST.ACTIVE_EFFECT_MODES.ADD,
  11730. value
  11731. });
  11732. }
  11733. const armorInFormula = formula.includes("@attributes.ac.armor");
  11734. let label = game.i18n.localize("DND5E.PropertyBase");
  11735. if ( armorInFormula ) label = this.actor.armor?.name ?? game.i18n.localize("DND5E.ArmorClassUnarmored");
  11736. attribution.unshift({
  11737. label,
  11738. mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE,
  11739. value: base
  11740. });
  11741. break;
  11742. }
  11743. // Shield
  11744. if ( ac.shield !== 0 ) attribution.push({
  11745. label: this.actor.shield?.name ?? game.i18n.localize("DND5E.EquipmentShield"),
  11746. mode: CONST.ACTIVE_EFFECT_MODES.ADD,
  11747. value: ac.shield
  11748. });
  11749. // Bonus
  11750. if ( ac.bonus !== 0 ) attribution.push(...this._prepareActiveEffectAttributions("system.attributes.ac.bonus"));
  11751. // Cover
  11752. if ( ac.cover !== 0 ) attribution.push({
  11753. label: game.i18n.localize("DND5E.Cover"),
  11754. mode: CONST.ACTIVE_EFFECT_MODES.ADD,
  11755. value: ac.cover
  11756. });
  11757. return attribution;
  11758. }
  11759. /* -------------------------------------------- */
  11760. /**
  11761. * Prepare the data structure for traits data like languages, resistances & vulnerabilities, and proficiencies.
  11762. * @param {object} systemData System data for the Actor being prepared.
  11763. * @returns {object} Prepared trait data.
  11764. * @protected
  11765. */
  11766. _prepareTraits(systemData) {
  11767. const traits = {};
  11768. for ( const [trait$1, traitConfig] of Object.entries(CONFIG.DND5E.traits) ) {
  11769. const key = traitConfig.actorKeyPath ?? `traits.${trait$1}`;
  11770. const data = foundry.utils.deepClone(foundry.utils.getProperty(systemData, key));
  11771. const choices = CONFIG.DND5E[traitConfig.configKey];
  11772. if ( !data ) continue;
  11773. foundry.utils.setProperty(traits, key, data);
  11774. let values = data.value;
  11775. if ( !values ) values = [];
  11776. else if ( values instanceof Set ) values = Array.from(values);
  11777. else if ( !Array.isArray(values) ) values = [values];
  11778. // Split physical damage types from others if bypasses is set
  11779. const physical = [];
  11780. if ( data.bypasses?.size ) {
  11781. values = values.filter(t => {
  11782. if ( !CONFIG.DND5E.physicalDamageTypes[t] ) return true;
  11783. physical.push(t);
  11784. return false;
  11785. });
  11786. }
  11787. data.selected = values.reduce((obj, key) => {
  11788. obj[key] = keyLabel(trait$1, key) ?? key;
  11789. return obj;
  11790. }, {});
  11791. // Display bypassed damage types
  11792. if ( physical.length ) {
  11793. const damageTypesFormatter = new Intl.ListFormat(game.i18n.lang, { style: "long", type: "conjunction" });
  11794. const bypassFormatter = new Intl.ListFormat(game.i18n.lang, { style: "long", type: "disjunction" });
  11795. data.selected.physical = game.i18n.format("DND5E.DamagePhysicalBypasses", {
  11796. damageTypes: damageTypesFormatter.format(physical.map(t => choices[t])),
  11797. bypassTypes: bypassFormatter.format(data.bypasses.map(t => CONFIG.DND5E.physicalWeaponProperties[t]))
  11798. });
  11799. }
  11800. // Add custom entries
  11801. if ( data.custom ) data.custom.split(";").forEach((c, i) => data.selected[`custom${i+1}`] = c.trim());
  11802. data.cssClass = !foundry.utils.isEmpty(data.selected) ? "" : "inactive";
  11803. }
  11804. return traits;
  11805. }
  11806. /* -------------------------------------------- */
  11807. /**
  11808. * Prepare the data structure for items which appear on the actor sheet.
  11809. * Each subclass overrides this method to implement type-specific logic.
  11810. * @protected
  11811. */
  11812. _prepareItems() {}
  11813. /* -------------------------------------------- */
  11814. /**
  11815. * Insert a spell into the spellbook object when rendering the character sheet.
  11816. * @param {object} context Sheet rendering context data being prepared for render.
  11817. * @param {object[]} spells Spells to be included in the spellbook.
  11818. * @returns {object[]} Spellbook sections in the proper order.
  11819. * @protected
  11820. */
  11821. _prepareSpellbook(context, spells) {
  11822. const owner = this.actor.isOwner;
  11823. const levels = context.actor.system.spells;
  11824. const spellbook = {};
  11825. // Define section and label mappings
  11826. const sections = {atwill: -20, innate: -10, pact: 0.5 };
  11827. const useLabels = {"-20": "-", "-10": "-", 0: "&infin;"};
  11828. // Format a spellbook entry for a certain indexed level
  11829. const registerSection = (sl, i, label, {prepMode="prepared", value, max, override}={}) => {
  11830. const aeOverride = foundry.utils.hasProperty(this.actor.overrides, `system.spells.spell${i}.override`);
  11831. spellbook[i] = {
  11832. order: i,
  11833. label: label,
  11834. usesSlots: i > 0,
  11835. canCreate: owner,
  11836. canPrepare: (context.actor.type === "character") && (i >= 1),
  11837. spells: [],
  11838. uses: useLabels[i] || value || 0,
  11839. slots: useLabels[i] || max || 0,
  11840. override: override || 0,
  11841. dataset: {type: "spell", level: prepMode in sections ? 1 : i, "preparation.mode": prepMode},
  11842. prop: sl,
  11843. editable: context.editable && !aeOverride
  11844. };
  11845. };
  11846. // Determine the maximum spell level which has a slot
  11847. const maxLevel = Array.fromRange(10).reduce((max, i) => {
  11848. if ( i === 0 ) return max;
  11849. const level = levels[`spell${i}`];
  11850. if ( (level.max || level.override ) && ( i > max ) ) max = i;
  11851. return max;
  11852. }, 0);
  11853. // Level-based spellcasters have cantrips and leveled slots
  11854. if ( maxLevel > 0 ) {
  11855. registerSection("spell0", 0, CONFIG.DND5E.spellLevels[0]);
  11856. for (let lvl = 1; lvl <= maxLevel; lvl++) {
  11857. const sl = `spell${lvl}`;
  11858. registerSection(sl, lvl, CONFIG.DND5E.spellLevels[lvl], levels[sl]);
  11859. }
  11860. }
  11861. // Pact magic users have cantrips and a pact magic section
  11862. if ( levels.pact && levels.pact.max ) {
  11863. if ( !spellbook["0"] ) registerSection("spell0", 0, CONFIG.DND5E.spellLevels[0]);
  11864. const l = levels.pact;
  11865. const config = CONFIG.DND5E.spellPreparationModes.pact;
  11866. const level = game.i18n.localize(`DND5E.SpellLevel${levels.pact.level}`);
  11867. const label = `${config} — ${level}`;
  11868. registerSection("pact", sections.pact, label, {
  11869. prepMode: "pact",
  11870. value: l.value,
  11871. max: l.max,
  11872. override: l.override
  11873. });
  11874. }
  11875. // Iterate over every spell item, adding spells to the spellbook by section
  11876. spells.forEach(spell => {
  11877. const mode = spell.system.preparation.mode || "prepared";
  11878. let s = spell.system.level || 0;
  11879. const sl = `spell${s}`;
  11880. // Specialized spellcasting modes (if they exist)
  11881. if ( mode in sections ) {
  11882. s = sections[mode];
  11883. if ( !spellbook[s] ) {
  11884. const l = levels[mode] || {};
  11885. const config = CONFIG.DND5E.spellPreparationModes[mode];
  11886. registerSection(mode, s, config, {
  11887. prepMode: mode,
  11888. value: l.value,
  11889. max: l.max,
  11890. override: l.override
  11891. });
  11892. }
  11893. }
  11894. // Sections for higher-level spells which the caster "should not" have, but spell items exist for
  11895. else if ( !spellbook[s] ) {
  11896. registerSection(sl, s, CONFIG.DND5E.spellLevels[s], {levels: levels[sl]});
  11897. }
  11898. // Add the spell to the relevant heading
  11899. spellbook[s].spells.push(spell);
  11900. });
  11901. // Sort the spellbook by section level
  11902. const sorted = Object.values(spellbook);
  11903. sorted.sort((a, b) => a.order - b.order);
  11904. return sorted;
  11905. }
  11906. /* -------------------------------------------- */
  11907. /**
  11908. * Determine whether an Owned Item will be shown based on the current set of filters.
  11909. * @param {object[]} items Copies of item data to be filtered.
  11910. * @param {Set<string>} filters Filters applied to the item list.
  11911. * @returns {object[]} Subset of input items limited by the provided filters.
  11912. * @protected
  11913. */
  11914. _filterItems(items, filters) {
  11915. return items.filter(item => {
  11916. // Action usage
  11917. for ( let f of ["action", "bonus", "reaction"] ) {
  11918. if ( filters.has(f) && (item.system.activation?.type !== f) ) return false;
  11919. }
  11920. // Spell-specific filters
  11921. if ( filters.has("ritual") && (item.system.components.ritual !== true) ) return false;
  11922. if ( filters.has("concentration") && (item.system.components.concentration !== true) ) return false;
  11923. if ( filters.has("prepared") ) {
  11924. if ( (item.system.level === 0) || ["innate", "always"].includes(item.system.preparation.mode) ) return true;
  11925. if ( this.actor.type === "npc" ) return true;
  11926. return item.system.preparation.prepared;
  11927. }
  11928. // Equipment-specific filters
  11929. if ( filters.has("equipped") && (item.system.equipped !== true) ) return false;
  11930. return true;
  11931. });
  11932. }
  11933. /* -------------------------------------------- */
  11934. /**
  11935. * Get the font-awesome icon used to display a certain level of skill proficiency.
  11936. * @param {number} level A proficiency mode defined in `CONFIG.DND5E.proficiencyLevels`.
  11937. * @returns {string} HTML string for the chosen icon.
  11938. * @private
  11939. */
  11940. _getProficiencyIcon(level) {
  11941. const icons = {
  11942. 0: '<i class="far fa-circle"></i>',
  11943. 0.5: '<i class="fas fa-adjust"></i>',
  11944. 1: '<i class="fas fa-check"></i>',
  11945. 2: '<i class="fas fa-check-double"></i>'
  11946. };
  11947. return icons[level] || icons[0];
  11948. }
  11949. /* -------------------------------------------- */
  11950. /* Event Listeners and Handlers */
  11951. /* -------------------------------------------- */
  11952. /** @inheritdoc */
  11953. activateListeners(html) {
  11954. // Activate Item Filters
  11955. const filterLists = html.find(".filter-list");
  11956. filterLists.each(this._initializeFilterItemList.bind(this));
  11957. filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
  11958. // Item summaries
  11959. html.find(".item .item-name.rollable h4").click(event => this._onItemSummary(event));
  11960. // View Item Sheets
  11961. html.find(".item-edit").click(this._onItemEdit.bind(this));
  11962. // Property attributions
  11963. html.find("[data-attribution]").mouseover(this._onPropertyAttribution.bind(this));
  11964. html.find(".attributable").mouseover(this._onPropertyAttribution.bind(this));
  11965. // Preparation Warnings
  11966. html.find(".warnings").click(this._onWarningLink.bind(this));
  11967. // Editable Only Listeners
  11968. if ( this.isEditable ) {
  11969. // Input focus and update
  11970. const inputs = html.find("input");
  11971. inputs.focus(ev => ev.currentTarget.select());
  11972. inputs.addBack().find('[type="text"][data-dtype="Number"]').change(this._onChangeInputDelta.bind(this));
  11973. // Ability Proficiency
  11974. html.find(".ability-proficiency").click(this._onToggleAbilityProficiency.bind(this));
  11975. // Toggle Skill Proficiency
  11976. html.find(".skill-proficiency").on("click contextmenu", event => this._onCycleProficiency(event, "skill"));
  11977. // Toggle Tool Proficiency
  11978. html.find(".tool-proficiency").on("click contextmenu", event => this._onCycleProficiency(event, "tool"));
  11979. // Trait Selector
  11980. html.find(".trait-selector").click(this._onTraitSelector.bind(this));
  11981. // Configure Special Flags
  11982. html.find(".config-button").click(this._onConfigMenu.bind(this));
  11983. // Owned Item management
  11984. html.find(".item-create").click(this._onItemCreate.bind(this));
  11985. html.find(".item-delete").click(this._onItemDelete.bind(this));
  11986. html.find(".item-uses input").click(ev => ev.target.select()).change(this._onUsesChange.bind(this));
  11987. html.find(".slot-max-override").click(this._onSpellSlotOverride.bind(this));
  11988. // Active Effect management
  11989. html.find(".effect-control").click(ev => ActiveEffect5e.onManageActiveEffect(ev, this.actor));
  11990. this._disableOverriddenFields(html);
  11991. }
  11992. // Owner Only Listeners
  11993. if ( this.actor.isOwner ) {
  11994. // Ability Checks
  11995. html.find(".ability-name").click(this._onRollAbilityTest.bind(this));
  11996. // Roll Skill Checks
  11997. html.find(".skill-name").click(this._onRollSkillCheck.bind(this));
  11998. // Roll Tool Checks.
  11999. html.find(".tool-name").on("click", this._onRollToolCheck.bind(this));
  12000. // Item Rolling
  12001. html.find(".rollable .item-image").click(event => this._onItemUse(event));
  12002. html.find(".item .item-recharge").click(event => this._onItemRecharge(event));
  12003. // Item Context Menu
  12004. new ContextMenu(html, ".item-list .item", [], {onOpen: this._onItemContext.bind(this)});
  12005. }
  12006. // Otherwise, remove rollable classes
  12007. else {
  12008. html.find(".rollable").each((i, el) => el.classList.remove("rollable"));
  12009. }
  12010. // Handle default listeners last so system listeners are triggered first
  12011. super.activateListeners(html);
  12012. }
  12013. /* -------------------------------------------- */
  12014. /**
  12015. * Disable any fields that are overridden by active effects and display an informative tooltip.
  12016. * @param {jQuery} html The sheet's rendered HTML.
  12017. * @protected
  12018. */
  12019. _disableOverriddenFields(html) {
  12020. const proficiencyToggles = {
  12021. ability: /system\.abilities\.([^.]+)\.proficient/,
  12022. skill: /system\.skills\.([^.]+)\.value/,
  12023. tool: /system\.tools\.([^.]+)\.value/
  12024. };
  12025. for ( const override of Object.keys(foundry.utils.flattenObject(this.actor.overrides)) ) {
  12026. html.find(`input[name="${override}"],select[name="${override}"]`).each((i, el) => {
  12027. el.disabled = true;
  12028. el.dataset.tooltip = "DND5E.ActiveEffectOverrideWarning";
  12029. });
  12030. for ( const [key, regex] of Object.entries(proficiencyToggles) ) {
  12031. const [, match] = override.match(regex) || [];
  12032. if ( match ) {
  12033. const toggle = html.find(`li[data-${key}="${match}"] .proficiency-toggle`);
  12034. toggle.addClass("disabled");
  12035. toggle.attr("data-tooltip", "DND5E.ActiveEffectOverrideWarning");
  12036. }
  12037. }
  12038. const [, spell] = override.match(/system\.spells\.(spell\d)\.override/) || [];
  12039. if ( spell ) {
  12040. html.find(`.spell-max[data-level="${spell}"]`).attr("data-tooltip", "DND5E.ActiveEffectOverrideWarning");
  12041. }
  12042. }
  12043. }
  12044. /* -------------------------------------------- */
  12045. /**
  12046. * Handle activation of a context menu for an embedded Item or ActiveEffect document.
  12047. * Dynamically populate the array of context menu options.
  12048. * @param {HTMLElement} element The HTML element for which the context menu is activated
  12049. * @protected
  12050. */
  12051. _onItemContext(element) {
  12052. // Active Effects
  12053. if ( element.classList.contains("effect") ) {
  12054. const effect = this.actor.effects.get(element.dataset.effectId);
  12055. if ( !effect ) return;
  12056. ui.context.menuItems = this._getActiveEffectContextOptions(effect);
  12057. Hooks.call("dnd5e.getActiveEffectContextOptions", effect, ui.context.menuItems);
  12058. }
  12059. // Items
  12060. else {
  12061. const item = this.actor.items.get(element.dataset.itemId);
  12062. if ( !item ) return;
  12063. ui.context.menuItems = this._getItemContextOptions(item);
  12064. Hooks.call("dnd5e.getItemContextOptions", item, ui.context.menuItems);
  12065. }
  12066. }
  12067. /* -------------------------------------------- */
  12068. /**
  12069. * Prepare an array of context menu options which are available for owned ActiveEffect documents.
  12070. * @param {ActiveEffect5e} effect The ActiveEffect for which the context menu is activated
  12071. * @returns {ContextMenuEntry[]} An array of context menu options offered for the ActiveEffect
  12072. * @protected
  12073. */
  12074. _getActiveEffectContextOptions(effect) {
  12075. return [
  12076. {
  12077. name: "DND5E.ContextMenuActionEdit",
  12078. icon: "<i class='fas fa-edit fa-fw'></i>",
  12079. callback: () => effect.sheet.render(true)
  12080. },
  12081. {
  12082. name: "DND5E.ContextMenuActionDuplicate",
  12083. icon: "<i class='fas fa-copy fa-fw'></i>",
  12084. callback: () => effect.clone({label: game.i18n.format("DOCUMENT.CopyOf", {name: effect.label})}, {save: true})
  12085. },
  12086. {
  12087. name: "DND5E.ContextMenuActionDelete",
  12088. icon: "<i class='fas fa-trash fa-fw'></i>",
  12089. callback: () => effect.deleteDialog()
  12090. },
  12091. {
  12092. name: effect.disabled ? "DND5E.ContextMenuActionEnable" : "DND5E.ContextMenuActionDisable",
  12093. icon: effect.disabled ? "<i class='fas fa-check fa-fw'></i>" : "<i class='fas fa-times fa-fw'></i>",
  12094. callback: () => effect.update({disabled: !effect.disabled})
  12095. }
  12096. ];
  12097. }
  12098. /* -------------------------------------------- */
  12099. /**
  12100. * Prepare an array of context menu options which are available for owned Item documents.
  12101. * @param {Item5e} item The Item for which the context menu is activated
  12102. * @returns {ContextMenuEntry[]} An array of context menu options offered for the Item
  12103. * @protected
  12104. */
  12105. _getItemContextOptions(item) {
  12106. // Standard Options
  12107. const options = [
  12108. {
  12109. name: "DND5E.ContextMenuActionEdit",
  12110. icon: "<i class='fas fa-edit fa-fw'></i>",
  12111. callback: () => item.sheet.render(true)
  12112. },
  12113. {
  12114. name: "DND5E.ContextMenuActionDuplicate",
  12115. icon: "<i class='fas fa-copy fa-fw'></i>",
  12116. condition: () => !["race", "background", "class", "subclass"].includes(item.type),
  12117. callback: () => item.clone({name: game.i18n.format("DOCUMENT.CopyOf", {name: item.name})}, {save: true})
  12118. },
  12119. {
  12120. name: "DND5E.ContextMenuActionDelete",
  12121. icon: "<i class='fas fa-trash fa-fw'></i>",
  12122. callback: () => item.deleteDialog()
  12123. }
  12124. ];
  12125. // Toggle Attunement State
  12126. if ( ("attunement" in item.system) && (item.system.attunement !== CONFIG.DND5E.attunementTypes.NONE) ) {
  12127. const isAttuned = item.system.attunement === CONFIG.DND5E.attunementTypes.ATTUNED;
  12128. options.push({
  12129. name: isAttuned ? "DND5E.ContextMenuActionUnattune" : "DND5E.ContextMenuActionAttune",
  12130. icon: "<i class='fas fa-sun fa-fw'></i>",
  12131. callback: () => item.update({
  12132. "system.attunement": CONFIG.DND5E.attunementTypes[isAttuned ? "REQUIRED" : "ATTUNED"]
  12133. })
  12134. });
  12135. }
  12136. // Toggle Equipped State
  12137. if ( "equipped" in item.system ) options.push({
  12138. name: item.system.equipped ? "DND5E.ContextMenuActionUnequip" : "DND5E.ContextMenuActionEquip",
  12139. icon: "<i class='fas fa-shield-alt fa-fw'></i>",
  12140. callback: () => item.update({"system.equipped": !item.system.equipped})
  12141. });
  12142. // Toggle Prepared State
  12143. if ( ("preparation" in item.system) && (item.system.preparation?.mode === "prepared") ) options.push({
  12144. name: item.system?.preparation?.prepared ? "DND5E.ContextMenuActionUnprepare" : "DND5E.ContextMenuActionPrepare",
  12145. icon: "<i class='fas fa-sun fa-fw'></i>",
  12146. callback: () => item.update({"system.preparation.prepared": !item.system.preparation?.prepared})
  12147. });
  12148. return options;
  12149. }
  12150. /* -------------------------------------------- */
  12151. /**
  12152. * Initialize Item list filters by activating the set of filters which are currently applied
  12153. * @param {number} i Index of the filter in the list.
  12154. * @param {HTML} ul HTML object for the list item surrounding the filter.
  12155. * @private
  12156. */
  12157. _initializeFilterItemList(i, ul) {
  12158. const set = this._filters[ul.dataset.filter];
  12159. const filters = ul.querySelectorAll(".filter-item");
  12160. for ( let li of filters ) {
  12161. if ( set.has(li.dataset.filter) ) li.classList.add("active");
  12162. }
  12163. }
  12164. /* -------------------------------------------- */
  12165. /* Event Listeners and Handlers */
  12166. /* -------------------------------------------- */
  12167. /**
  12168. * Handle input changes to numeric form fields, allowing them to accept delta-typed inputs.
  12169. * @param {Event} event Triggering event.
  12170. * @protected
  12171. */
  12172. _onChangeInputDelta(event) {
  12173. const input = event.target;
  12174. const value = input.value;
  12175. if ( ["+", "-"].includes(value[0]) ) {
  12176. const delta = parseFloat(value);
  12177. input.value = Number(foundry.utils.getProperty(this.actor, input.name)) + delta;
  12178. } else if ( value[0] === "=" ) input.value = value.slice(1);
  12179. }
  12180. /* -------------------------------------------- */
  12181. /**
  12182. * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options.
  12183. * @param {Event} event The click event which originated the selection.
  12184. * @private
  12185. */
  12186. _onConfigMenu(event) {
  12187. event.preventDefault();
  12188. event.stopPropagation();
  12189. const button = event.currentTarget;
  12190. let app;
  12191. switch ( button.dataset.action ) {
  12192. case "armor":
  12193. app = new ActorArmorConfig(this.actor);
  12194. break;
  12195. case "hit-dice":
  12196. app = new ActorHitDiceConfig(this.actor);
  12197. break;
  12198. case "hit-points":
  12199. app = new ActorHitPointsConfig(this.actor);
  12200. break;
  12201. case "initiative":
  12202. app = new ActorInitiativeConfig(this.actor);
  12203. break;
  12204. case "movement":
  12205. app = new ActorMovementConfig(this.actor);
  12206. break;
  12207. case "flags":
  12208. app = new ActorSheetFlags(this.actor);
  12209. break;
  12210. case "senses":
  12211. app = new ActorSensesConfig(this.actor);
  12212. break;
  12213. case "type":
  12214. app = new ActorTypeConfig(this.actor);
  12215. break;
  12216. case "ability": {
  12217. const ability = event.currentTarget.closest("[data-ability]").dataset.ability;
  12218. app = new ActorAbilityConfig(this.actor, null, ability);
  12219. break;
  12220. }
  12221. case "skill": {
  12222. const skill = event.currentTarget.closest("[data-key]").dataset.key;
  12223. app = new ProficiencyConfig(this.actor, {property: "skills", key: skill});
  12224. break;
  12225. }
  12226. case "tool": {
  12227. const tool = event.currentTarget.closest("[data-key]").dataset.key;
  12228. app = new ProficiencyConfig(this.actor, {property: "tools", key: tool});
  12229. break;
  12230. }
  12231. }
  12232. app?.render(true);
  12233. }
  12234. /* -------------------------------------------- */
  12235. /**
  12236. * Handle cycling proficiency in a skill or tool.
  12237. * @param {Event} event A click or contextmenu event which triggered this action.
  12238. * @returns {Promise|void} Updated data for this actor after changes are applied.
  12239. * @protected
  12240. */
  12241. _onCycleProficiency(event) {
  12242. if ( event.currentTarget.classList.contains("disabled") ) return;
  12243. event.preventDefault();
  12244. const parent = event.currentTarget.closest(".proficiency-row");
  12245. const field = parent.querySelector('[name$=".value"]');
  12246. const {property, key} = parent.dataset;
  12247. const value = this.actor._source.system[property]?.[key]?.value ?? 0;
  12248. // Cycle to the next or previous skill level.
  12249. const levels = [0, 1, .5, 2];
  12250. const idx = levels.indexOf(value);
  12251. const next = idx + (event.type === "contextmenu" ? 3 : 1);
  12252. field.value = levels[next % levels.length];
  12253. // Update the field value and save the form.
  12254. return this._onSubmit(event);
  12255. }
  12256. /* -------------------------------------------- */
  12257. /** @override */
  12258. async _onDropActor(event, data) {
  12259. const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get("dnd5e", "allowPolymorphing"));
  12260. if ( !canPolymorph ) return false;
  12261. // Get the target actor
  12262. const cls = getDocumentClass("Actor");
  12263. const sourceActor = await cls.fromDropData(data);
  12264. if ( !sourceActor ) return;
  12265. // Define a function to record polymorph settings for future use
  12266. const rememberOptions = html => {
  12267. const options = {};
  12268. html.find("input").each((i, el) => {
  12269. options[el.name] = el.checked;
  12270. });
  12271. const settings = foundry.utils.mergeObject(game.settings.get("dnd5e", "polymorphSettings") ?? {}, options);
  12272. game.settings.set("dnd5e", "polymorphSettings", settings);
  12273. return settings;
  12274. };
  12275. // Create and render the Dialog
  12276. return new Dialog({
  12277. title: game.i18n.localize("DND5E.PolymorphPromptTitle"),
  12278. content: {
  12279. options: game.settings.get("dnd5e", "polymorphSettings"),
  12280. settings: CONFIG.DND5E.polymorphSettings,
  12281. effectSettings: CONFIG.DND5E.polymorphEffectSettings,
  12282. isToken: this.actor.isToken
  12283. },
  12284. default: "accept",
  12285. buttons: {
  12286. accept: {
  12287. icon: '<i class="fas fa-check"></i>',
  12288. label: game.i18n.localize("DND5E.PolymorphAcceptSettings"),
  12289. callback: html => this.actor.transformInto(sourceActor, rememberOptions(html))
  12290. },
  12291. wildshape: {
  12292. icon: CONFIG.DND5E.transformationPresets.wildshape.icon,
  12293. label: CONFIG.DND5E.transformationPresets.wildshape.label,
  12294. callback: html => this.actor.transformInto(sourceActor, foundry.utils.mergeObject(
  12295. CONFIG.DND5E.transformationPresets.wildshape.options,
  12296. { transformTokens: rememberOptions(html).transformTokens }
  12297. ))
  12298. },
  12299. polymorph: {
  12300. icon: CONFIG.DND5E.transformationPresets.polymorph.icon,
  12301. label: CONFIG.DND5E.transformationPresets.polymorph.label,
  12302. callback: html => this.actor.transformInto(sourceActor, foundry.utils.mergeObject(
  12303. CONFIG.DND5E.transformationPresets.polymorph.options,
  12304. { transformTokens: rememberOptions(html).transformTokens }
  12305. ))
  12306. },
  12307. self: {
  12308. icon: CONFIG.DND5E.transformationPresets.polymorphSelf.icon,
  12309. label: CONFIG.DND5E.transformationPresets.polymorphSelf.label,
  12310. callback: html => this.actor.transformInto(sourceActor, foundry.utils.mergeObject(
  12311. CONFIG.DND5E.transformationPresets.polymorphSelf.options,
  12312. { transformTokens: rememberOptions(html).transformTokens }
  12313. ))
  12314. },
  12315. cancel: {
  12316. icon: '<i class="fas fa-times"></i>',
  12317. label: game.i18n.localize("Cancel")
  12318. }
  12319. }
  12320. }, {
  12321. classes: ["dialog", "dnd5e", "polymorph"],
  12322. width: 900,
  12323. template: "systems/dnd5e/templates/apps/polymorph-prompt.hbs"
  12324. }).render(true);
  12325. }
  12326. /* -------------------------------------------- */
  12327. /** @override */
  12328. async _onDropItemCreate(itemData) {
  12329. let items = itemData instanceof Array ? itemData : [itemData];
  12330. const itemsWithoutAdvancement = items.filter(i => !i.system.advancement?.length);
  12331. const multipleAdvancements = (items.length - itemsWithoutAdvancement.length) > 1;
  12332. if ( multipleAdvancements && !game.settings.get("dnd5e", "disableAdvancements") ) {
  12333. ui.notifications.warn(game.i18n.format("DND5E.WarnCantAddMultipleAdvancements"));
  12334. items = itemsWithoutAdvancement;
  12335. }
  12336. const toCreate = [];
  12337. for ( const item of items ) {
  12338. const result = await this._onDropSingleItem(item);
  12339. if ( result ) toCreate.push(result);
  12340. }
  12341. // Create the owned items as normal
  12342. return this.actor.createEmbeddedDocuments("Item", toCreate);
  12343. }
  12344. /* -------------------------------------------- */
  12345. /**
  12346. * Handles dropping of a single item onto this character sheet.
  12347. * @param {object} itemData The item data to create.
  12348. * @returns {Promise<object|boolean>} The item data to create after processing, or false if the item should not be
  12349. * created or creation has been otherwise handled.
  12350. * @protected
  12351. */
  12352. async _onDropSingleItem(itemData) {
  12353. // Check to make sure items of this type are allowed on this actor
  12354. if ( this.constructor.unsupportedItemTypes.has(itemData.type) ) {
  12355. ui.notifications.warn(game.i18n.format("DND5E.ActorWarningInvalidItem", {
  12356. itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]),
  12357. actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type])
  12358. }));
  12359. return false;
  12360. }
  12361. // Create a Consumable spell scroll on the Inventory tab
  12362. if ( (itemData.type === "spell")
  12363. && (this._tabs[0].active === "inventory" || this.actor.type === "vehicle") ) {
  12364. const scroll = await Item5e.createScrollFromSpell(itemData);
  12365. return scroll.toObject();
  12366. }
  12367. // Clean up data
  12368. this._onDropResetData(itemData);
  12369. // Stack identical consumables
  12370. const stacked = this._onDropStackConsumables(itemData);
  12371. if ( stacked ) return false;
  12372. // Bypass normal creation flow for any items with advancement
  12373. if ( itemData.system.advancement?.length && !game.settings.get("dnd5e", "disableAdvancements") ) {
  12374. const manager = AdvancementManager.forNewItem(this.actor, itemData);
  12375. if ( manager.steps.length ) {
  12376. manager.render(true);
  12377. return false;
  12378. }
  12379. }
  12380. return itemData;
  12381. }
  12382. /* -------------------------------------------- */
  12383. /**
  12384. * Reset certain pieces of data stored on items when they are dropped onto the actor.
  12385. * @param {object} itemData The item data requested for creation. **Will be mutated.**
  12386. */
  12387. _onDropResetData(itemData) {
  12388. if ( !itemData.system ) return;
  12389. ["equipped", "proficient", "prepared"].forEach(k => delete itemData.system[k]);
  12390. if ( "attunement" in itemData.system ) {
  12391. itemData.system.attunement = Math.min(itemData.system.attunement, CONFIG.DND5E.attunementTypes.REQUIRED);
  12392. }
  12393. }
  12394. /* -------------------------------------------- */
  12395. /**
  12396. * Stack identical consumables when a new one is dropped rather than creating a duplicate item.
  12397. * @param {object} itemData The item data requested for creation.
  12398. * @returns {Promise<Item5e>|null} If a duplicate was found, returns the adjusted item stack.
  12399. */
  12400. _onDropStackConsumables(itemData) {
  12401. const droppedSourceId = itemData.flags.core?.sourceId;
  12402. if ( itemData.type !== "consumable" || !droppedSourceId ) return null;
  12403. const similarItem = this.actor.items.find(i => {
  12404. const sourceId = i.getFlag("core", "sourceId");
  12405. return sourceId && (sourceId === droppedSourceId) && (i.type === "consumable") && (i.name === itemData.name);
  12406. });
  12407. if ( !similarItem ) return null;
  12408. return similarItem.update({
  12409. "system.quantity": similarItem.system.quantity + Math.max(itemData.system.quantity, 1)
  12410. });
  12411. }
  12412. /* -------------------------------------------- */
  12413. /**
  12414. * Handle enabling editing for a spell slot override value.
  12415. * @param {MouseEvent} event The originating click event.
  12416. * @private
  12417. */
  12418. async _onSpellSlotOverride(event) {
  12419. const span = event.currentTarget.parentElement;
  12420. const level = span.dataset.level;
  12421. const override = this.actor.system.spells[level].override || span.dataset.slots;
  12422. const input = document.createElement("INPUT");
  12423. input.type = "text";
  12424. input.name = `system.spells.${level}.override`;
  12425. input.value = override;
  12426. input.placeholder = span.dataset.slots;
  12427. input.dataset.dtype = "Number";
  12428. // Replace the HTML
  12429. const parent = span.parentElement;
  12430. parent.removeChild(span);
  12431. parent.appendChild(input);
  12432. }
  12433. /* -------------------------------------------- */
  12434. /**
  12435. * Change the uses amount of an Owned Item within the Actor.
  12436. * @param {Event} event The triggering click event.
  12437. * @returns {Promise<Item5e>} Updated item.
  12438. * @private
  12439. */
  12440. async _onUsesChange(event) {
  12441. event.preventDefault();
  12442. const itemId = event.currentTarget.closest(".item").dataset.itemId;
  12443. const item = this.actor.items.get(itemId);
  12444. const uses = Math.clamped(0, parseInt(event.target.value), item.system.uses.max);
  12445. event.target.value = uses;
  12446. return item.update({"system.uses.value": uses});
  12447. }
  12448. /* -------------------------------------------- */
  12449. /**
  12450. * Handle using an item from the Actor sheet, obtaining the Item instance, and dispatching to its use method.
  12451. * @param {Event} event The triggering click event.
  12452. * @returns {Promise} Results of the usage.
  12453. * @protected
  12454. */
  12455. _onItemUse(event) {
  12456. event.preventDefault();
  12457. const itemId = event.currentTarget.closest(".item").dataset.itemId;
  12458. const item = this.actor.items.get(itemId);
  12459. return item.use({}, {event});
  12460. }
  12461. /* -------------------------------------------- */
  12462. /**
  12463. * Handle attempting to recharge an item usage by rolling a recharge check.
  12464. * @param {Event} event The originating click event.
  12465. * @returns {Promise<Roll>} The resulting recharge roll.
  12466. * @private
  12467. */
  12468. _onItemRecharge(event) {
  12469. event.preventDefault();
  12470. const itemId = event.currentTarget.closest(".item").dataset.itemId;
  12471. const item = this.actor.items.get(itemId);
  12472. return item.rollRecharge();
  12473. }
  12474. /* -------------------------------------------- */
  12475. /**
  12476. * Handle toggling and items expanded description.
  12477. * @param {Event} event Triggering event.
  12478. * @private
  12479. */
  12480. async _onItemSummary(event) {
  12481. event.preventDefault();
  12482. const li = $(event.currentTarget).parents(".item");
  12483. const item = this.actor.items.get(li.data("item-id"));
  12484. const chatData = await item.getChatData({secrets: this.actor.isOwner});
  12485. // Toggle summary
  12486. if ( li.hasClass("expanded") ) {
  12487. const summary = li.children(".item-summary");
  12488. summary.slideUp(200, () => summary.remove());
  12489. this._expanded.delete(item.id);
  12490. } else {
  12491. const summary = $(await renderTemplate("systems/dnd5e/templates/items/parts/item-summary.hbs", chatData));
  12492. li.append(summary.hide());
  12493. summary.slideDown(200);
  12494. this._expanded.add(item.id);
  12495. }
  12496. li.toggleClass("expanded");
  12497. }
  12498. /* -------------------------------------------- */
  12499. /**
  12500. * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset.
  12501. * @param {Event} event The originating click event.
  12502. * @returns {Promise<Item5e[]>} The newly created item.
  12503. * @private
  12504. */
  12505. _onItemCreate(event) {
  12506. event.preventDefault();
  12507. const header = event.currentTarget;
  12508. const type = header.dataset.type;
  12509. // Check to make sure the newly created class doesn't take player over level cap
  12510. if ( type === "class" && (this.actor.system.details.level + 1 > CONFIG.DND5E.maxLevel) ) {
  12511. const err = game.i18n.format("DND5E.MaxCharacterLevelExceededWarn", {max: CONFIG.DND5E.maxLevel});
  12512. return ui.notifications.error(err);
  12513. }
  12514. const itemData = {
  12515. name: game.i18n.format("DND5E.ItemNew", {type: game.i18n.localize(CONFIG.Item.typeLabels[type])}),
  12516. type: type,
  12517. system: foundry.utils.expandObject({ ...header.dataset })
  12518. };
  12519. delete itemData.system.type;
  12520. return this.actor.createEmbeddedDocuments("Item", [itemData]);
  12521. }
  12522. /* -------------------------------------------- */
  12523. /**
  12524. * Handle editing an existing Owned Item for the Actor.
  12525. * @param {Event} event The originating click event.
  12526. * @returns {ItemSheet5e} The rendered item sheet.
  12527. * @private
  12528. */
  12529. _onItemEdit(event) {
  12530. event.preventDefault();
  12531. const li = event.currentTarget.closest(".item");
  12532. const item = this.actor.items.get(li.dataset.itemId);
  12533. return item.sheet.render(true);
  12534. }
  12535. /* -------------------------------------------- */
  12536. /**
  12537. * Handle deleting an existing Owned Item for the Actor.
  12538. * @param {Event} event The originating click event.
  12539. * @returns {Promise<Item5e|AdvancementManager>|undefined} The deleted item if something was deleted or the
  12540. * advancement manager if advancements need removing.
  12541. * @private
  12542. */
  12543. async _onItemDelete(event) {
  12544. event.preventDefault();
  12545. const li = event.currentTarget.closest(".item");
  12546. const item = this.actor.items.get(li.dataset.itemId);
  12547. if ( !item ) return;
  12548. // If item has advancement, handle it separately
  12549. if ( !game.settings.get("dnd5e", "disableAdvancements") ) {
  12550. const manager = AdvancementManager.forDeletedItem(this.actor, item.id);
  12551. if ( manager.steps.length ) {
  12552. if ( ["class", "subclass"].includes(item.type) ) {
  12553. try {
  12554. const shouldRemoveAdvancements = await AdvancementConfirmationDialog.forDelete(item);
  12555. if ( shouldRemoveAdvancements ) return manager.render(true);
  12556. } catch(err) {
  12557. return;
  12558. }
  12559. } else {
  12560. return manager.render(true);
  12561. }
  12562. }
  12563. }
  12564. return item.deleteDialog();
  12565. }
  12566. /* -------------------------------------------- */
  12567. /**
  12568. * Handle displaying the property attribution tooltip when a property is hovered over.
  12569. * @param {Event} event The originating mouse event.
  12570. * @private
  12571. */
  12572. async _onPropertyAttribution(event) {
  12573. const element = event.target;
  12574. let property = element.dataset.attribution;
  12575. if ( !property ) {
  12576. property = element.dataset.property;
  12577. if ( !property ) return;
  12578. foundry.utils.logCompatibilityWarning(
  12579. "Defining attributable properties on sheets with the `.attributable` class and `data-property` value"
  12580. + " has been deprecated in favor of a single `data-attribution` value.",
  12581. { since: "DnD5e 2.1.3", until: "DnD5e 2.4" }
  12582. );
  12583. }
  12584. const rollData = this.actor.getRollData({ deterministic: true });
  12585. const title = game.i18n.localize(element.dataset.attributionCaption);
  12586. let attributions;
  12587. switch ( property ) {
  12588. case "attributes.ac":
  12589. attributions = this._prepareArmorClassAttribution(rollData); break;
  12590. }
  12591. if ( !attributions ) return;
  12592. new PropertyAttribution(this.actor, attributions, property, {title}).renderTooltip(element);
  12593. }
  12594. /* -------------------------------------------- */
  12595. /**
  12596. * Handle rolling an Ability test or saving throw.
  12597. * @param {Event} event The originating click event.
  12598. * @private
  12599. */
  12600. _onRollAbilityTest(event) {
  12601. event.preventDefault();
  12602. let ability = event.currentTarget.parentElement.dataset.ability;
  12603. this.actor.rollAbility(ability, {event: event});
  12604. }
  12605. /* -------------------------------------------- */
  12606. /**
  12607. * Handle rolling a Skill check.
  12608. * @param {Event} event The originating click event.
  12609. * @returns {Promise<Roll>} The resulting roll.
  12610. * @private
  12611. */
  12612. _onRollSkillCheck(event) {
  12613. event.preventDefault();
  12614. const skill = event.currentTarget.closest("[data-key]").dataset.key;
  12615. return this.actor.rollSkill(skill, {event: event});
  12616. }
  12617. /* -------------------------------------------- */
  12618. _onRollToolCheck(event) {
  12619. event.preventDefault();
  12620. const tool = event.currentTarget.closest("[data-key]").dataset.key;
  12621. return this.actor.rollToolCheck(tool, {event});
  12622. }
  12623. /* -------------------------------------------- */
  12624. /**
  12625. * Handle toggling Ability score proficiency level.
  12626. * @param {Event} event The originating click event.
  12627. * @returns {Promise<Actor5e>|void} Updated actor instance.
  12628. * @private
  12629. */
  12630. _onToggleAbilityProficiency(event) {
  12631. if ( event.currentTarget.classList.contains("disabled") ) return;
  12632. event.preventDefault();
  12633. const field = event.currentTarget.previousElementSibling;
  12634. return this.actor.update({[field.name]: 1 - parseInt(field.value)});
  12635. }
  12636. /* -------------------------------------------- */
  12637. /**
  12638. * Handle toggling of filters to display a different set of owned items.
  12639. * @param {Event} event The click event which triggered the toggle.
  12640. * @returns {ActorSheet5e} This actor sheet with toggled filters.
  12641. * @private
  12642. */
  12643. _onToggleFilter(event) {
  12644. event.preventDefault();
  12645. const li = event.currentTarget;
  12646. const set = this._filters[li.parentElement.dataset.filter];
  12647. const filter = li.dataset.filter;
  12648. if ( set.has(filter) ) set.delete(filter);
  12649. else set.add(filter);
  12650. return this.render();
  12651. }
  12652. /* -------------------------------------------- */
  12653. /**
  12654. * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options.
  12655. * @param {Event} event The click event which originated the selection.
  12656. * @returns {TraitSelector} Newly displayed application.
  12657. * @private
  12658. */
  12659. _onTraitSelector(event) {
  12660. event.preventDefault();
  12661. const trait = event.currentTarget.dataset.trait;
  12662. if ( trait === "tool" ) return new ToolSelector(this.actor, trait).render(true);
  12663. return new TraitSelector$1(this.actor, trait).render(true);
  12664. }
  12665. /* -------------------------------------------- */
  12666. /**
  12667. * Handle links within preparation warnings.
  12668. * @param {Event} event The click event on the warning.
  12669. * @protected
  12670. */
  12671. async _onWarningLink(event) {
  12672. event.preventDefault();
  12673. const a = event.target;
  12674. if ( !a || !a.dataset.target ) return;
  12675. switch ( a.dataset.target ) {
  12676. case "armor":
  12677. (new ActorArmorConfig(this.actor)).render(true);
  12678. return;
  12679. default:
  12680. const item = await fromUuid(a.dataset.target);
  12681. item?.sheet.render(true);
  12682. }
  12683. }
  12684. /* -------------------------------------------- */
  12685. /** @override */
  12686. _getHeaderButtons() {
  12687. let buttons = super._getHeaderButtons();
  12688. if ( this.actor.isPolymorphed ) {
  12689. buttons.unshift({
  12690. label: "DND5E.PolymorphRestoreTransformation",
  12691. class: "restore-transformation",
  12692. icon: "fas fa-backward",
  12693. onclick: () => this.actor.revertOriginalForm()
  12694. });
  12695. }
  12696. return buttons;
  12697. }
  12698. }
  12699. /**
  12700. * An Actor sheet for player character type actors.
  12701. */
  12702. class ActorSheet5eCharacter extends ActorSheet5e {
  12703. /** @inheritDoc */
  12704. static get defaultOptions() {
  12705. return foundry.utils.mergeObject(super.defaultOptions, {
  12706. classes: ["dnd5e", "sheet", "actor", "character"]
  12707. });
  12708. }
  12709. /* -------------------------------------------- */
  12710. /* Context Preparation */
  12711. /* -------------------------------------------- */
  12712. /** @inheritDoc */
  12713. async getData(options={}) {
  12714. const context = await super.getData(options);
  12715. // Resources
  12716. context.resources = ["primary", "secondary", "tertiary"].reduce((arr, r) => {
  12717. const res = context.actor.system.resources[r] || {};
  12718. res.name = r;
  12719. res.placeholder = game.i18n.localize(`DND5E.Resource${r.titleCase()}`);
  12720. if (res && res.value === 0) delete res.value;
  12721. if (res && res.max === 0) delete res.max;
  12722. return arr.concat([res]);
  12723. }, []);
  12724. const classes = this.actor.itemTypes.class;
  12725. return foundry.utils.mergeObject(context, {
  12726. disableExperience: game.settings.get("dnd5e", "disableExperienceTracking"),
  12727. classLabels: classes.map(c => c.name).join(", "),
  12728. multiclassLabels: classes.map(c => [c.subclass?.name ?? "", c.name, c.system.levels].filterJoin(" ")).join(", "),
  12729. weightUnit: game.i18n.localize(`DND5E.Abbreviation${
  12730. game.settings.get("dnd5e", "metricWeightUnits") ? "Kg" : "Lbs"}`),
  12731. encumbrance: context.system.attributes.encumbrance
  12732. });
  12733. }
  12734. /* -------------------------------------------- */
  12735. /** @override */
  12736. _prepareItems(context) {
  12737. // Categorize items as inventory, spellbook, features, and classes
  12738. const inventory = {};
  12739. for ( const type of ["weapon", "equipment", "consumable", "tool", "backpack", "loot"] ) {
  12740. inventory[type] = {label: `${CONFIG.Item.typeLabels[type]}Pl`, items: [], dataset: {type}};
  12741. }
  12742. // Partition items by category
  12743. let {items, spells, feats, backgrounds, classes, subclasses} = context.items.reduce((obj, item) => {
  12744. const {quantity, uses, recharge, target} = item.system;
  12745. // Item details
  12746. const ctx = context.itemContext[item.id] ??= {};
  12747. ctx.isStack = Number.isNumeric(quantity) && (quantity !== 1);
  12748. ctx.attunement = {
  12749. [CONFIG.DND5E.attunementTypes.REQUIRED]: {
  12750. icon: "fa-sun",
  12751. cls: "not-attuned",
  12752. title: "DND5E.AttunementRequired"
  12753. },
  12754. [CONFIG.DND5E.attunementTypes.ATTUNED]: {
  12755. icon: "fa-sun",
  12756. cls: "attuned",
  12757. title: "DND5E.AttunementAttuned"
  12758. }
  12759. }[item.system.attunement];
  12760. // Prepare data needed to display expanded sections
  12761. ctx.isExpanded = this._expanded.has(item.id);
  12762. // Item usage
  12763. ctx.hasUses = uses && (uses.max > 0);
  12764. ctx.isOnCooldown = recharge && !!recharge.value && (recharge.charged === false);
  12765. ctx.isDepleted = ctx.isOnCooldown && (uses.per && (uses.value > 0));
  12766. ctx.hasTarget = !!target && !(["none", ""].includes(target.type));
  12767. // Item toggle state
  12768. this._prepareItemToggleState(item, ctx);
  12769. // Classify items into types
  12770. if ( item.type === "spell" ) obj.spells.push(item);
  12771. else if ( item.type === "feat" ) obj.feats.push(item);
  12772. else if ( item.type === "background" ) obj.backgrounds.push(item);
  12773. else if ( item.type === "class" ) obj.classes.push(item);
  12774. else if ( item.type === "subclass" ) obj.subclasses.push(item);
  12775. else if ( Object.keys(inventory).includes(item.type) ) obj.items.push(item);
  12776. return obj;
  12777. }, { items: [], spells: [], feats: [], backgrounds: [], classes: [], subclasses: [] });
  12778. // Apply active item filters
  12779. items = this._filterItems(items, this._filters.inventory);
  12780. spells = this._filterItems(spells, this._filters.spellbook);
  12781. feats = this._filterItems(feats, this._filters.features);
  12782. // Organize items
  12783. for ( let i of items ) {
  12784. const ctx = context.itemContext[i.id] ??= {};
  12785. ctx.totalWeight = (i.system.quantity * i.system.weight).toNearest(0.1);
  12786. inventory[i.type].items.push(i);
  12787. }
  12788. // Organize Spellbook and count the number of prepared spells (excluding always, at will, etc...)
  12789. const spellbook = this._prepareSpellbook(context, spells);
  12790. const nPrepared = spells.filter(spell => {
  12791. const prep = spell.system.preparation;
  12792. return (spell.system.level > 0) && (prep.mode === "prepared") && prep.prepared;
  12793. }).length;
  12794. // Sort classes and interleave matching subclasses, put unmatched subclasses into features so they don't disappear
  12795. classes.sort((a, b) => b.system.levels - a.system.levels);
  12796. const maxLevelDelta = CONFIG.DND5E.maxLevel - this.actor.system.details.level;
  12797. classes = classes.reduce((arr, cls) => {
  12798. const ctx = context.itemContext[cls.id] ??= {};
  12799. ctx.availableLevels = Array.fromRange(CONFIG.DND5E.maxLevel + 1).slice(1).map(level => {
  12800. const delta = level - cls.system.levels;
  12801. return { level, delta, disabled: delta > maxLevelDelta };
  12802. });
  12803. arr.push(cls);
  12804. const identifier = cls.system.identifier || cls.name.slugify({strict: true});
  12805. const subclass = subclasses.findSplice(s => s.system.classIdentifier === identifier);
  12806. if ( subclass ) arr.push(subclass);
  12807. return arr;
  12808. }, []);
  12809. for ( const subclass of subclasses ) {
  12810. feats.push(subclass);
  12811. const message = game.i18n.format("DND5E.SubclassMismatchWarn", {
  12812. name: subclass.name, class: subclass.system.classIdentifier
  12813. });
  12814. context.warnings.push({ message, type: "warning" });
  12815. }
  12816. // Organize Features
  12817. const features = {
  12818. background: {
  12819. label: CONFIG.Item.typeLabels.background, items: backgrounds,
  12820. hasActions: false, dataset: {type: "background"} },
  12821. classes: {
  12822. label: `${CONFIG.Item.typeLabels.class}Pl`, items: classes,
  12823. hasActions: false, dataset: {type: "class"}, isClass: true },
  12824. active: {
  12825. label: "DND5E.FeatureActive", items: [],
  12826. hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
  12827. passive: {
  12828. label: "DND5E.FeaturePassive", items: [],
  12829. hasActions: false, dataset: {type: "feat"} }
  12830. };
  12831. for ( const feat of feats ) {
  12832. if ( feat.system.activation?.type ) features.active.items.push(feat);
  12833. else features.passive.items.push(feat);
  12834. }
  12835. // Assign and return
  12836. context.inventoryFilters = true;
  12837. context.inventory = Object.values(inventory);
  12838. context.spellbook = spellbook;
  12839. context.preparedSpells = nPrepared;
  12840. context.features = Object.values(features);
  12841. context.labels.background = backgrounds[0]?.name;
  12842. }
  12843. /* -------------------------------------------- */
  12844. /**
  12845. * A helper method to establish the displayed preparation state for an item.
  12846. * @param {Item5e} item Item being prepared for display.
  12847. * @param {object} context Context data for display.
  12848. * @protected
  12849. */
  12850. _prepareItemToggleState(item, context) {
  12851. if ( item.type === "spell" ) {
  12852. const prep = item.system.preparation || {};
  12853. const isAlways = prep.mode === "always";
  12854. const isPrepared = !!prep.prepared;
  12855. context.toggleClass = isPrepared ? "active" : "";
  12856. if ( isAlways ) context.toggleClass = "fixed";
  12857. if ( isAlways ) context.toggleTitle = CONFIG.DND5E.spellPreparationModes.always;
  12858. else if ( isPrepared ) context.toggleTitle = CONFIG.DND5E.spellPreparationModes.prepared;
  12859. else context.toggleTitle = game.i18n.localize("DND5E.SpellUnprepared");
  12860. }
  12861. else {
  12862. const isActive = !!item.system.equipped;
  12863. context.toggleClass = isActive ? "active" : "";
  12864. context.toggleTitle = game.i18n.localize(isActive ? "DND5E.Equipped" : "DND5E.Unequipped");
  12865. context.canToggle = "equipped" in item.system;
  12866. }
  12867. }
  12868. /* -------------------------------------------- */
  12869. /* Event Listeners and Handlers
  12870. /* -------------------------------------------- */
  12871. /** @inheritDoc */
  12872. activateListeners(html) {
  12873. super.activateListeners(html);
  12874. if ( !this.isEditable ) return;
  12875. html.find(".level-selector").change(this._onLevelChange.bind(this));
  12876. html.find(".item-toggle").click(this._onToggleItem.bind(this));
  12877. html.find(".short-rest").click(this._onShortRest.bind(this));
  12878. html.find(".long-rest").click(this._onLongRest.bind(this));
  12879. html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
  12880. }
  12881. /* -------------------------------------------- */
  12882. /**
  12883. * Handle mouse click events for character sheet actions.
  12884. * @param {MouseEvent} event The originating click event.
  12885. * @returns {Promise} Dialog or roll result.
  12886. * @private
  12887. */
  12888. _onSheetAction(event) {
  12889. event.preventDefault();
  12890. const button = event.currentTarget;
  12891. switch ( button.dataset.action ) {
  12892. case "convertCurrency":
  12893. return Dialog.confirm({
  12894. title: `${game.i18n.localize("DND5E.CurrencyConvert")}`,
  12895. content: `<p>${game.i18n.localize("DND5E.CurrencyConvertHint")}</p>`,
  12896. yes: () => this.actor.convertCurrency()
  12897. });
  12898. case "rollDeathSave":
  12899. return this.actor.rollDeathSave({event: event});
  12900. case "rollInitiative":
  12901. return this.actor.rollInitiativeDialog({event});
  12902. }
  12903. }
  12904. /* -------------------------------------------- */
  12905. /**
  12906. * Respond to a new level being selected from the level selector.
  12907. * @param {Event} event The originating change.
  12908. * @returns {Promise<AdvancementManager|Item5e>} Manager if advancements needed, otherwise updated class item.
  12909. * @private
  12910. */
  12911. async _onLevelChange(event) {
  12912. event.preventDefault();
  12913. const delta = Number(event.target.value);
  12914. const classId = event.target.closest(".item")?.dataset.itemId;
  12915. if ( !delta || !classId ) return;
  12916. const classItem = this.actor.items.get(classId);
  12917. if ( !game.settings.get("dnd5e", "disableAdvancements") ) {
  12918. const manager = AdvancementManager.forLevelChange(this.actor, classId, delta);
  12919. if ( manager.steps.length ) {
  12920. if ( delta > 0 ) return manager.render(true);
  12921. try {
  12922. const shouldRemoveAdvancements = await AdvancementConfirmationDialog.forLevelDown(classItem);
  12923. if ( shouldRemoveAdvancements ) return manager.render(true);
  12924. }
  12925. catch(err) {
  12926. return;
  12927. }
  12928. }
  12929. }
  12930. return classItem.update({"system.levels": classItem.system.levels + delta});
  12931. }
  12932. /* -------------------------------------------- */
  12933. /**
  12934. * Handle toggling the state of an Owned Item within the Actor.
  12935. * @param {Event} event The triggering click event.
  12936. * @returns {Promise<Item5e>} Item with the updates applied.
  12937. * @private
  12938. */
  12939. _onToggleItem(event) {
  12940. event.preventDefault();
  12941. const itemId = event.currentTarget.closest(".item").dataset.itemId;
  12942. const item = this.actor.items.get(itemId);
  12943. const attr = item.type === "spell" ? "system.preparation.prepared" : "system.equipped";
  12944. return item.update({[attr]: !foundry.utils.getProperty(item, attr)});
  12945. }
  12946. /* -------------------------------------------- */
  12947. /**
  12948. * Take a short rest, calling the relevant function on the Actor instance.
  12949. * @param {Event} event The triggering click event.
  12950. * @returns {Promise<RestResult>} Result of the rest action.
  12951. * @private
  12952. */
  12953. async _onShortRest(event) {
  12954. event.preventDefault();
  12955. await this._onSubmit(event);
  12956. return this.actor.shortRest();
  12957. }
  12958. /* -------------------------------------------- */
  12959. /**
  12960. * Take a long rest, calling the relevant function on the Actor instance.
  12961. * @param {Event} event The triggering click event.
  12962. * @returns {Promise<RestResult>} Result of the rest action.
  12963. * @private
  12964. */
  12965. async _onLongRest(event) {
  12966. event.preventDefault();
  12967. await this._onSubmit(event);
  12968. return this.actor.longRest();
  12969. }
  12970. /* -------------------------------------------- */
  12971. /** @override */
  12972. async _onDropSingleItem(itemData) {
  12973. // Increment the number of class levels a character instead of creating a new item
  12974. if ( itemData.type === "class" ) {
  12975. const charLevel = this.actor.system.details.level;
  12976. itemData.system.levels = Math.min(itemData.system.levels, CONFIG.DND5E.maxLevel - charLevel);
  12977. if ( itemData.system.levels <= 0 ) {
  12978. const err = game.i18n.format("DND5E.MaxCharacterLevelExceededWarn", { max: CONFIG.DND5E.maxLevel });
  12979. ui.notifications.error(err);
  12980. return false;
  12981. }
  12982. const cls = this.actor.itemTypes.class.find(c => c.identifier === itemData.system.identifier);
  12983. if ( cls ) {
  12984. const priorLevel = cls.system.levels;
  12985. if ( !game.settings.get("dnd5e", "disableAdvancements") ) {
  12986. const manager = AdvancementManager.forLevelChange(this.actor, cls.id, itemData.system.levels);
  12987. if ( manager.steps.length ) {
  12988. manager.render(true);
  12989. return false;
  12990. }
  12991. }
  12992. cls.update({"system.levels": priorLevel + itemData.system.levels});
  12993. return false;
  12994. }
  12995. }
  12996. // If a subclass is dropped, ensure it doesn't match another subclass with the same identifier
  12997. else if ( itemData.type === "subclass" ) {
  12998. const other = this.actor.itemTypes.subclass.find(i => i.identifier === itemData.system.identifier);
  12999. if ( other ) {
  13000. const err = game.i18n.format("DND5E.SubclassDuplicateError", {identifier: other.identifier});
  13001. ui.notifications.error(err);
  13002. return false;
  13003. }
  13004. const cls = this.actor.itemTypes.class.find(i => i.identifier === itemData.system.classIdentifier);
  13005. if ( cls && cls.subclass ) {
  13006. const err = game.i18n.format("DND5E.SubclassAssignmentError", {class: cls.name, subclass: cls.subclass.name});
  13007. ui.notifications.error(err);
  13008. return false;
  13009. }
  13010. }
  13011. return super._onDropSingleItem(itemData);
  13012. }
  13013. }
  13014. /**
  13015. * An Actor sheet for NPC type characters.
  13016. */
  13017. class ActorSheet5eNPC extends ActorSheet5e {
  13018. /** @inheritDoc */
  13019. static get defaultOptions() {
  13020. return foundry.utils.mergeObject(super.defaultOptions, {
  13021. classes: ["dnd5e", "sheet", "actor", "npc"],
  13022. width: 600
  13023. });
  13024. }
  13025. /* -------------------------------------------- */
  13026. /** @override */
  13027. static unsupportedItemTypes = new Set(["background", "class", "subclass"]);
  13028. /* -------------------------------------------- */
  13029. /* Context Preparation */
  13030. /* -------------------------------------------- */
  13031. /** @inheritDoc */
  13032. async getData(options) {
  13033. const context = await super.getData(options);
  13034. // Challenge Rating
  13035. const cr = parseFloat(context.system.details.cr ?? 0);
  13036. const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
  13037. return foundry.utils.mergeObject(context, {
  13038. labels: {
  13039. cr: cr >= 1 ? String(cr) : crLabels[cr] ?? 1,
  13040. type: this.actor.constructor.formatCreatureType(context.system.details.type),
  13041. armorType: this.getArmorLabel()
  13042. }
  13043. });
  13044. }
  13045. /* -------------------------------------------- */
  13046. /** @override */
  13047. _prepareItems(context) {
  13048. // Categorize Items as Features and Spells
  13049. const features = {
  13050. weapons: { label: game.i18n.localize("DND5E.AttackPl"), items: [], hasActions: true,
  13051. dataset: {type: "weapon", "weapon-type": "natural"} },
  13052. actions: { label: game.i18n.localize("DND5E.ActionPl"), items: [], hasActions: true,
  13053. dataset: {type: "feat", "activation.type": "action"} },
  13054. passive: { label: game.i18n.localize("DND5E.Features"), items: [], dataset: {type: "feat"} },
  13055. equipment: { label: game.i18n.localize("DND5E.Inventory"), items: [], dataset: {type: "loot"}}
  13056. };
  13057. // Start by classifying items into groups for rendering
  13058. let [spells, other] = context.items.reduce((arr, item) => {
  13059. const {quantity, uses, recharge, target} = item.system;
  13060. const ctx = context.itemContext[item.id] ??= {};
  13061. ctx.isStack = Number.isNumeric(quantity) && (quantity !== 1);
  13062. ctx.isExpanded = this._expanded.has(item.id);
  13063. ctx.hasUses = uses && (uses.max > 0);
  13064. ctx.isOnCooldown = recharge && !!recharge.value && (recharge.charged === false);
  13065. ctx.isDepleted = item.isOnCooldown && (uses.per && (uses.value > 0));
  13066. ctx.hasTarget = !!target && !(["none", ""].includes(target.type));
  13067. ctx.canToggle = false;
  13068. if ( item.type === "spell" ) arr[0].push(item);
  13069. else arr[1].push(item);
  13070. return arr;
  13071. }, [[], []]);
  13072. // Apply item filters
  13073. spells = this._filterItems(spells, this._filters.spellbook);
  13074. other = this._filterItems(other, this._filters.features);
  13075. // Organize Spellbook
  13076. const spellbook = this._prepareSpellbook(context, spells);
  13077. // Organize Features
  13078. for ( let item of other ) {
  13079. if ( item.type === "weapon" ) features.weapons.items.push(item);
  13080. else if ( item.type === "feat" ) {
  13081. if ( item.system.activation.type ) features.actions.items.push(item);
  13082. else features.passive.items.push(item);
  13083. }
  13084. else features.equipment.items.push(item);
  13085. }
  13086. // Assign and return
  13087. context.inventoryFilters = true;
  13088. context.features = Object.values(features);
  13089. context.spellbook = spellbook;
  13090. }
  13091. /* -------------------------------------------- */
  13092. /**
  13093. * Format NPC armor information into a localized string.
  13094. * @returns {string} Formatted armor label.
  13095. */
  13096. getArmorLabel() {
  13097. const ac = this.actor.system.attributes.ac;
  13098. const label = [];
  13099. if ( ac.calc === "default" ) label.push(this.actor.armor?.name || game.i18n.localize("DND5E.ArmorClassUnarmored"));
  13100. else label.push(game.i18n.localize(CONFIG.DND5E.armorClasses[ac.calc].label));
  13101. if ( this.actor.shield ) label.push(this.actor.shield.name);
  13102. return label.filterJoin(", ");
  13103. }
  13104. /* -------------------------------------------- */
  13105. /* Object Updates */
  13106. /* -------------------------------------------- */
  13107. /** @inheritDoc */
  13108. async _updateObject(event, formData) {
  13109. // Format NPC Challenge Rating
  13110. const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
  13111. let crv = "system.details.cr";
  13112. let cr = formData[crv];
  13113. cr = crs[cr] || parseFloat(cr);
  13114. if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
  13115. // Parent ActorSheet update steps
  13116. return super._updateObject(event, formData);
  13117. }
  13118. }
  13119. /**
  13120. * An Actor sheet for Vehicle type actors.
  13121. */
  13122. class ActorSheet5eVehicle extends ActorSheet5e {
  13123. /** @inheritDoc */
  13124. static get defaultOptions() {
  13125. return foundry.utils.mergeObject(super.defaultOptions, {
  13126. classes: ["dnd5e", "sheet", "actor", "vehicle"]
  13127. });
  13128. }
  13129. /* -------------------------------------------- */
  13130. /** @override */
  13131. static unsupportedItemTypes = new Set(["background", "class", "subclass"]);
  13132. /* -------------------------------------------- */
  13133. /**
  13134. * Creates a new cargo entry for a vehicle Actor.
  13135. * @type {object}
  13136. */
  13137. static get newCargo() {
  13138. return {name: "", quantity: 1};
  13139. }
  13140. /* -------------------------------------------- */
  13141. /* Context Preparation */
  13142. /* -------------------------------------------- */
  13143. /**
  13144. * Compute the total weight of the vehicle's cargo.
  13145. * @param {number} totalWeight The cumulative item weight from inventory items
  13146. * @param {object} actorData The data object for the Actor being rendered
  13147. * @returns {{max: number, value: number, pct: number}}
  13148. * @private
  13149. */
  13150. _computeEncumbrance(totalWeight, actorData) {
  13151. // Compute currency weight
  13152. const totalCoins = Object.values(actorData.system.currency).reduce((acc, denom) => acc + denom, 0);
  13153. const currencyPerWeight = game.settings.get("dnd5e", "metricWeightUnits")
  13154. ? CONFIG.DND5E.encumbrance.currencyPerWeight.metric
  13155. : CONFIG.DND5E.encumbrance.currencyPerWeight.imperial;
  13156. totalWeight += totalCoins / currencyPerWeight;
  13157. // Vehicle weights are an order of magnitude greater.
  13158. totalWeight /= game.settings.get("dnd5e", "metricWeightUnits")
  13159. ? CONFIG.DND5E.encumbrance.vehicleWeightMultiplier.metric
  13160. : CONFIG.DND5E.encumbrance.vehicleWeightMultiplier.imperial;
  13161. // Compute overall encumbrance
  13162. const max = actorData.system.attributes.capacity.cargo;
  13163. const pct = Math.clamped((totalWeight * 100) / max, 0, 100);
  13164. return {value: totalWeight.toNearest(0.1), max, pct};
  13165. }
  13166. /* -------------------------------------------- */
  13167. /** @override */
  13168. _getMovementSpeed(actorData, largestPrimary=true) {
  13169. return super._getMovementSpeed(actorData, largestPrimary);
  13170. }
  13171. /* -------------------------------------------- */
  13172. /**
  13173. * Prepare items that are mounted to a vehicle and require one or more crew to operate.
  13174. * @param {object} item Copy of the item data being prepared for display.
  13175. * @param {object} context Display context for the item.
  13176. * @protected
  13177. */
  13178. _prepareCrewedItem(item, context) {
  13179. // Determine crewed status
  13180. const isCrewed = item.system.crewed;
  13181. context.toggleClass = isCrewed ? "active" : "";
  13182. context.toggleTitle = game.i18n.localize(`DND5E.${isCrewed ? "Crewed" : "Uncrewed"}`);
  13183. // Handle crew actions
  13184. if ( item.type === "feat" && item.system.activation.type === "crew" ) {
  13185. context.cover = game.i18n.localize(`DND5E.${item.system.cover ? "CoverTotal" : "None"}`);
  13186. if ( item.system.cover === .5 ) context.cover = "½";
  13187. else if ( item.system.cover === .75 ) context.cover = "¾";
  13188. else if ( item.system.cover === null ) context.cover = "—";
  13189. }
  13190. // Prepare vehicle weapons
  13191. if ( (item.type === "equipment") || (item.type === "weapon") ) {
  13192. context.threshold = item.system.hp.dt ? item.system.hp.dt : "—";
  13193. }
  13194. }
  13195. /* -------------------------------------------- */
  13196. /** @override */
  13197. _prepareItems(context) {
  13198. const cargoColumns = [{
  13199. label: game.i18n.localize("DND5E.Quantity"),
  13200. css: "item-qty",
  13201. property: "quantity",
  13202. editable: "Number"
  13203. }];
  13204. const equipmentColumns = [{
  13205. label: game.i18n.localize("DND5E.Quantity"),
  13206. css: "item-qty",
  13207. property: "system.quantity",
  13208. editable: "Number"
  13209. }, {
  13210. label: game.i18n.localize("DND5E.AC"),
  13211. css: "item-ac",
  13212. property: "system.armor.value"
  13213. }, {
  13214. label: game.i18n.localize("DND5E.HP"),
  13215. css: "item-hp",
  13216. property: "system.hp.value",
  13217. editable: "Number"
  13218. }, {
  13219. label: game.i18n.localize("DND5E.Threshold"),
  13220. css: "item-threshold",
  13221. property: "threshold"
  13222. }];
  13223. const features = {
  13224. actions: {
  13225. label: game.i18n.localize("DND5E.ActionPl"),
  13226. items: [],
  13227. hasActions: true,
  13228. crewable: true,
  13229. dataset: {type: "feat", "activation.type": "crew"},
  13230. columns: [{
  13231. label: game.i18n.localize("DND5E.Cover"),
  13232. css: "item-cover",
  13233. property: "cover"
  13234. }]
  13235. },
  13236. equipment: {
  13237. label: game.i18n.localize(CONFIG.Item.typeLabels.equipment),
  13238. items: [],
  13239. crewable: true,
  13240. dataset: {type: "equipment", "armor.type": "vehicle"},
  13241. columns: equipmentColumns
  13242. },
  13243. passive: {
  13244. label: game.i18n.localize("DND5E.Features"),
  13245. items: [],
  13246. dataset: {type: "feat"}
  13247. },
  13248. reactions: {
  13249. label: game.i18n.localize("DND5E.ReactionPl"),
  13250. items: [],
  13251. dataset: {type: "feat", "activation.type": "reaction"}
  13252. },
  13253. weapons: {
  13254. label: game.i18n.localize(`${CONFIG.Item.typeLabels.weapon}Pl`),
  13255. items: [],
  13256. crewable: true,
  13257. dataset: {type: "weapon", "weapon-type": "siege"},
  13258. columns: equipmentColumns
  13259. }
  13260. };
  13261. context.items.forEach(item => {
  13262. const {uses, recharge} = item.system;
  13263. const ctx = context.itemContext[item.id] ??= {};
  13264. ctx.canToggle = false;
  13265. ctx.isExpanded = this._expanded.has(item.id);
  13266. ctx.hasUses = uses && (uses.max > 0);
  13267. ctx.isOnCooldown = recharge && !!recharge.value && (recharge.charged === false);
  13268. ctx.isDepleted = item.isOnCooldown && (uses.per && (uses.value > 0));
  13269. });
  13270. const cargo = {
  13271. crew: {
  13272. label: game.i18n.localize("DND5E.VehicleCrew"),
  13273. items: context.actor.system.cargo.crew,
  13274. css: "cargo-row crew",
  13275. editableName: true,
  13276. dataset: {type: "crew"},
  13277. columns: cargoColumns
  13278. },
  13279. passengers: {
  13280. label: game.i18n.localize("DND5E.VehiclePassengers"),
  13281. items: context.actor.system.cargo.passengers,
  13282. css: "cargo-row passengers",
  13283. editableName: true,
  13284. dataset: {type: "passengers"},
  13285. columns: cargoColumns
  13286. },
  13287. cargo: {
  13288. label: game.i18n.localize("DND5E.VehicleCargo"),
  13289. items: [],
  13290. dataset: {type: "loot"},
  13291. columns: [{
  13292. label: game.i18n.localize("DND5E.Quantity"),
  13293. css: "item-qty",
  13294. property: "system.quantity",
  13295. editable: "Number"
  13296. }, {
  13297. label: game.i18n.localize("DND5E.Price"),
  13298. css: "item-price",
  13299. property: "system.price.value",
  13300. editable: "Number"
  13301. }, {
  13302. label: game.i18n.localize("DND5E.Weight"),
  13303. css: "item-weight",
  13304. property: "system.weight",
  13305. editable: "Number"
  13306. }]
  13307. }
  13308. };
  13309. // Classify items owned by the vehicle and compute total cargo weight
  13310. let totalWeight = 0;
  13311. for ( const item of context.items ) {
  13312. const ctx = context.itemContext[item.id] ??= {};
  13313. this._prepareCrewedItem(item, ctx);
  13314. // Handle cargo explicitly
  13315. const isCargo = item.flags.dnd5e?.vehicleCargo === true;
  13316. if ( isCargo ) {
  13317. totalWeight += (item.system.weight || 0) * item.system.quantity;
  13318. cargo.cargo.items.push(item);
  13319. continue;
  13320. }
  13321. // Handle non-cargo item types
  13322. switch ( item.type ) {
  13323. case "weapon":
  13324. features.weapons.items.push(item);
  13325. break;
  13326. case "equipment":
  13327. features.equipment.items.push(item);
  13328. break;
  13329. case "feat":
  13330. const act = item.system.activation;
  13331. if ( !act.type || (act.type === "none") ) features.passive.items.push(item);
  13332. else if (act.type === "reaction") features.reactions.items.push(item);
  13333. else features.actions.items.push(item);
  13334. break;
  13335. default:
  13336. totalWeight += (item.system.weight || 0) * item.system.quantity;
  13337. cargo.cargo.items.push(item);
  13338. }
  13339. }
  13340. // Update the rendering context data
  13341. context.inventoryFilters = false;
  13342. context.features = Object.values(features);
  13343. context.cargo = Object.values(cargo);
  13344. context.encumbrance = this._computeEncumbrance(totalWeight, context);
  13345. }
  13346. /* -------------------------------------------- */
  13347. /* Event Listeners and Handlers */
  13348. /* -------------------------------------------- */
  13349. /** @override */
  13350. activateListeners(html) {
  13351. super.activateListeners(html);
  13352. if ( !this.isEditable ) return;
  13353. html.find(".item-toggle").click(this._onToggleItem.bind(this));
  13354. html.find(".item-hp input")
  13355. .click(evt => evt.target.select())
  13356. .change(this._onHPChange.bind(this));
  13357. html.find(".item:not(.cargo-row) input[data-property]")
  13358. .click(evt => evt.target.select())
  13359. .change(this._onEditInSheet.bind(this));
  13360. html.find(".cargo-row input")
  13361. .click(evt => evt.target.select())
  13362. .change(this._onCargoRowChange.bind(this));
  13363. html.find(".item:not(.cargo-row) .item-qty input")
  13364. .click(evt => evt.target.select())
  13365. .change(this._onQtyChange.bind(this));
  13366. if (this.actor.system.attributes.actions.stations) {
  13367. html.find(".counter.actions, .counter.action-thresholds").hide();
  13368. }
  13369. }
  13370. /* -------------------------------------------- */
  13371. /**
  13372. * Handle saving a cargo row (i.e. crew or passenger) in-sheet.
  13373. * @param {Event} event Triggering event.
  13374. * @returns {Promise<Actor5e>|null} Actor after update if any changes were made.
  13375. * @private
  13376. */
  13377. _onCargoRowChange(event) {
  13378. event.preventDefault();
  13379. const target = event.currentTarget;
  13380. const row = target.closest(".item");
  13381. const idx = Number(row.dataset.itemIndex);
  13382. const property = row.classList.contains("crew") ? "crew" : "passengers";
  13383. // Get the cargo entry
  13384. const cargo = foundry.utils.deepClone(this.actor.system.cargo[property]);
  13385. const entry = cargo[idx];
  13386. if ( !entry ) return null;
  13387. // Update the cargo value
  13388. const key = target.dataset.property ?? "name";
  13389. const type = target.dataset.dtype;
  13390. let value = target.value;
  13391. if (type === "Number") value = Number(value);
  13392. entry[key] = value;
  13393. // Perform the Actor update
  13394. return this.actor.update({[`system.cargo.${property}`]: cargo});
  13395. }
  13396. /* -------------------------------------------- */
  13397. /**
  13398. * Handle editing certain values like quantity, price, and weight in-sheet.
  13399. * @param {Event} event Triggering event.
  13400. * @returns {Promise<Item5e>} Item with updates applied.
  13401. * @private
  13402. */
  13403. _onEditInSheet(event) {
  13404. event.preventDefault();
  13405. const itemID = event.currentTarget.closest(".item").dataset.itemId;
  13406. const item = this.actor.items.get(itemID);
  13407. const property = event.currentTarget.dataset.property;
  13408. const type = event.currentTarget.dataset.dtype;
  13409. let value = event.currentTarget.value;
  13410. switch (type) {
  13411. case "Number": value = parseInt(value); break;
  13412. case "Boolean": value = value === "true"; break;
  13413. }
  13414. return item.update({[`${property}`]: value});
  13415. }
  13416. /* -------------------------------------------- */
  13417. /** @inheritDoc */
  13418. _onItemCreate(event) {
  13419. event.preventDefault();
  13420. // Handle creating a new crew or passenger row.
  13421. const target = event.currentTarget;
  13422. const type = target.dataset.type;
  13423. if (type === "crew" || type === "passengers") {
  13424. const cargo = foundry.utils.deepClone(this.actor.system.cargo[type]);
  13425. cargo.push(this.constructor.newCargo);
  13426. return this.actor.update({[`system.cargo.${type}`]: cargo});
  13427. }
  13428. return super._onItemCreate(event);
  13429. }
  13430. /* -------------------------------------------- */
  13431. /** @inheritDoc */
  13432. _onItemDelete(event) {
  13433. event.preventDefault();
  13434. // Handle deleting a crew or passenger row.
  13435. const row = event.currentTarget.closest(".item");
  13436. if (row.classList.contains("cargo-row")) {
  13437. const idx = Number(row.dataset.itemIndex);
  13438. const type = row.classList.contains("crew") ? "crew" : "passengers";
  13439. const cargo = foundry.utils.deepClone(this.actor.system.cargo[type]).filter((_, i) => i !== idx);
  13440. return this.actor.update({[`system.cargo.${type}`]: cargo});
  13441. }
  13442. return super._onItemDelete(event);
  13443. }
  13444. /* -------------------------------------------- */
  13445. /** @override */
  13446. async _onDropSingleItem(itemData) {
  13447. const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"];
  13448. const isCargo = cargoTypes.includes(itemData.type) && (this._tabs[0].active === "cargo");
  13449. foundry.utils.setProperty(itemData, "flags.dnd5e.vehicleCargo", isCargo);
  13450. return super._onDropSingleItem(itemData);
  13451. }
  13452. /* -------------------------------------------- */
  13453. /**
  13454. * Special handling for editing HP to clamp it within appropriate range.
  13455. * @param {Event} event Triggering event.
  13456. * @returns {Promise<Item5e>} Item after the update is applied.
  13457. * @private
  13458. */
  13459. _onHPChange(event) {
  13460. event.preventDefault();
  13461. const itemID = event.currentTarget.closest(".item").dataset.itemId;
  13462. const item = this.actor.items.get(itemID);
  13463. let hp = Math.clamped(0, parseInt(event.currentTarget.value), item.system.hp.max);
  13464. if ( Number.isNaN(hp) ) hp = 0;
  13465. return item.update({"system.hp.value": hp});
  13466. }
  13467. /* -------------------------------------------- */
  13468. /**
  13469. * Special handling for editing quantity value of equipment and weapons inside the features tab.
  13470. * @param {Event} event Triggering event.
  13471. * @returns {Promise<Item5e>} Item after the update is applied.
  13472. * @private
  13473. */
  13474. _onQtyChange(event) {
  13475. event.preventDefault();
  13476. const itemID = event.currentTarget.closest(".item").dataset.itemId;
  13477. const item = this.actor.items.get(itemID);
  13478. let qty = parseInt(event.currentTarget.value);
  13479. if ( Number.isNaN(qty) ) qty = 0;
  13480. return item.update({"system.quantity": qty});
  13481. }
  13482. /* -------------------------------------------- */
  13483. /**
  13484. * Handle toggling an item's crewed status.
  13485. * @param {Event} event Triggering event.
  13486. * @returns {Promise<Item5e>} Item after the toggling is applied.
  13487. * @private
  13488. */
  13489. _onToggleItem(event) {
  13490. event.preventDefault();
  13491. const itemID = event.currentTarget.closest(".item").dataset.itemId;
  13492. const item = this.actor.items.get(itemID);
  13493. return item.update({"system.crewed": !item.system.crewed});
  13494. }
  13495. }
  13496. /**
  13497. * A character sheet for group-type Actors.
  13498. * The functionality of this sheet is sufficiently different from other Actor types that we extend the base
  13499. * Foundry VTT ActorSheet instead of the ActorSheet5e abstraction used for character, npc, and vehicle types.
  13500. */
  13501. class GroupActorSheet extends ActorSheet {
  13502. /**
  13503. * IDs for items on the sheet that have been expanded.
  13504. * @type {Set<string>}
  13505. * @protected
  13506. */
  13507. _expanded = new Set();
  13508. /* -------------------------------------------- */
  13509. /** @inheritDoc */
  13510. static get defaultOptions() {
  13511. return foundry.utils.mergeObject(super.defaultOptions, {
  13512. classes: ["dnd5e", "sheet", "actor", "group"],
  13513. template: "systems/dnd5e/templates/actors/group-sheet.hbs",
  13514. tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "members"}],
  13515. scrollY: [".inventory .inventory-list"],
  13516. width: 620,
  13517. height: 620
  13518. });
  13519. }
  13520. /* -------------------------------------------- */
  13521. /**
  13522. * A set of item types that should be prevented from being dropped on this type of actor sheet.
  13523. * @type {Set<string>}
  13524. */
  13525. static unsupportedItemTypes = new Set(["background", "class", "subclass", "feat"]);
  13526. /* -------------------------------------------- */
  13527. /* Context Preparation */
  13528. /* -------------------------------------------- */
  13529. /** @inheritDoc */
  13530. async getData(options={}) {
  13531. const context = super.getData(options);
  13532. context.system = context.data.system;
  13533. context.items = Array.from(this.actor.items);
  13534. // Membership
  13535. const {sections, stats} = this.#prepareMembers();
  13536. Object.assign(context, stats);
  13537. context.sections = sections;
  13538. // Movement
  13539. context.movement = this.#prepareMovementSpeed();
  13540. // Inventory
  13541. context.itemContext = {};
  13542. context.inventory = this.#prepareInventory(context);
  13543. context.expandedData = {};
  13544. for ( const id of this._expanded ) {
  13545. const item = this.actor.items.get(id);
  13546. if ( item ) context.expandedData[id] = await item.getChatData({secrets: this.actor.isOwner});
  13547. }
  13548. context.inventoryFilters = false;
  13549. context.rollableClass = this.isEditable ? "rollable" : "";
  13550. // Biography HTML
  13551. context.descriptionFull = await TextEditor.enrichHTML(this.actor.system.description.full, {
  13552. secrets: this.actor.isOwner,
  13553. rollData: context.rollData,
  13554. async: true,
  13555. relativeTo: this.actor
  13556. });
  13557. // Summary tag
  13558. context.summary = this.#getSummary(stats);
  13559. // Text labels
  13560. context.labels = {
  13561. currencies: Object.entries(CONFIG.DND5E.currencies).reduce((obj, [k, c]) => {
  13562. obj[k] = c.label;
  13563. return obj;
  13564. }, {})
  13565. };
  13566. return context;
  13567. }
  13568. /* -------------------------------------------- */
  13569. /**
  13570. * Prepare a localized summary of group membership.
  13571. * @param {{nMembers: number, nVehicles: number}} stats The number of members in the group
  13572. * @returns {string} The formatted summary string
  13573. */
  13574. #getSummary(stats) {
  13575. const formatter = new Intl.ListFormat(game.i18n.lang, {style: "long", type: "conjunction"});
  13576. const members = [];
  13577. if ( stats.nMembers ) members.push(`${stats.nMembers} ${game.i18n.localize("DND5E.GroupMembers")}`);
  13578. if ( stats.nVehicles ) members.push(`${stats.nVehicles} ${game.i18n.localize("DND5E.GroupVehicles")}`);
  13579. return game.i18n.format("DND5E.GroupSummary", {members: formatter.format(members)});
  13580. }
  13581. /* -------------------------------------------- */
  13582. /**
  13583. * Prepare membership data for the sheet.
  13584. * @returns {{sections: object, stats: object}}
  13585. */
  13586. #prepareMembers() {
  13587. const stats = {
  13588. currentHP: 0,
  13589. maxHP: 0,
  13590. nMembers: 0,
  13591. nVehicles: 0
  13592. };
  13593. const sections = {
  13594. character: {label: `${CONFIG.Actor.typeLabels.character}Pl`, members: []},
  13595. npc: {label: `${CONFIG.Actor.typeLabels.npc}Pl`, members: []},
  13596. vehicle: {label: `${CONFIG.Actor.typeLabels.vehicle}Pl`, members: []}
  13597. };
  13598. for ( const member of this.object.system.members ) {
  13599. const m = {
  13600. actor: member,
  13601. id: member.id,
  13602. name: member.name,
  13603. img: member.img,
  13604. hp: {},
  13605. displayHPValues: member.testUserPermission(game.user, "OBSERVER")
  13606. };
  13607. // HP bar
  13608. const hp = member.system.attributes.hp;
  13609. m.hp.current = hp.value + (hp.temp || 0);
  13610. m.hp.max = Math.max(0, hp.max + (hp.tempmax || 0));
  13611. m.hp.pct = Math.clamped((m.hp.current / m.hp.max) * 100, 0, 100).toFixed(2);
  13612. m.hp.color = dnd5e.documents.Actor5e.getHPColor(m.hp.current, m.hp.max).css;
  13613. stats.currentHP += m.hp.current;
  13614. stats.maxHP += m.hp.max;
  13615. if ( member.type === "vehicle" ) stats.nVehicles++;
  13616. else stats.nMembers++;
  13617. sections[member.type].members.push(m);
  13618. }
  13619. for ( const [k, section] of Object.entries(sections) ) {
  13620. if ( !section.members.length ) delete sections[k];
  13621. }
  13622. return {sections, stats};
  13623. }
  13624. /* -------------------------------------------- */
  13625. /**
  13626. * Prepare movement speed data for rendering on the sheet.
  13627. * @returns {{secondary: string, primary: string}}
  13628. */
  13629. #prepareMovementSpeed() {
  13630. const movement = this.object.system.attributes.movement;
  13631. let speeds = [
  13632. [movement.land, `${game.i18n.localize("DND5E.MovementLand")} ${movement.land}`],
  13633. [movement.water, `${game.i18n.localize("DND5E.MovementWater")} ${movement.water}`],
  13634. [movement.air, `${game.i18n.localize("DND5E.MovementAir")} ${movement.air}`]
  13635. ];
  13636. speeds = speeds.filter(s => s[0]).sort((a, b) => b[0] - a[0]);
  13637. const primary = speeds.shift();
  13638. return {
  13639. primary: `${primary ? primary[1] : "0"}`,
  13640. secondary: speeds.map(s => s[1]).join(", ")
  13641. };
  13642. }
  13643. /* -------------------------------------------- */
  13644. /**
  13645. * Prepare inventory items for rendering on the sheet.
  13646. * @param {object} context Prepared rendering context.
  13647. * @returns {Object<string,object>}
  13648. */
  13649. #prepareInventory(context) {
  13650. // Categorize as weapons, equipment, containers, and loot
  13651. const sections = {};
  13652. for ( const type of ["weapon", "equipment", "consumable", "backpack", "loot"] ) {
  13653. sections[type] = {label: `${CONFIG.Item.typeLabels[type]}Pl`, items: [], hasActions: false, dataset: {type}};
  13654. }
  13655. // Classify items
  13656. for ( const item of context.items ) {
  13657. const ctx = context.itemContext[item.id] ??= {};
  13658. const {quantity} = item.system;
  13659. ctx.isStack = Number.isNumeric(quantity) && (quantity > 1);
  13660. ctx.canToggle = false;
  13661. ctx.isExpanded = this._expanded.has(item.id);
  13662. if ( (item.type in sections) && (item.type !== "loot") ) sections[item.type].items.push(item);
  13663. else sections.loot.items.push(item);
  13664. }
  13665. return sections;
  13666. }
  13667. /* -------------------------------------------- */
  13668. /* Rendering Workflow */
  13669. /* -------------------------------------------- */
  13670. /** @inheritDoc */
  13671. async _render(force, options={}) {
  13672. for ( const member of this.object.system.members) {
  13673. member.apps[this.id] = this;
  13674. }
  13675. return super._render(force, options);
  13676. }
  13677. /* -------------------------------------------- */
  13678. /** @inheritDoc */
  13679. async close(options={}) {
  13680. for ( const member of this.object.system.members ) {
  13681. delete member.apps[this.id];
  13682. }
  13683. return super.close(options);
  13684. }
  13685. /* -------------------------------------------- */
  13686. /* Event Listeners and Handlers */
  13687. /* -------------------------------------------- */
  13688. /** @inheritDoc */
  13689. activateListeners(html) {
  13690. super.activateListeners(html);
  13691. html.find(".group-member .name").click(this._onClickMemberName.bind(this));
  13692. if ( this.isEditable ) {
  13693. html.find(".action-button").click(this._onClickActionButton.bind(this));
  13694. html.find(".item-control").click(this._onClickItemControl.bind(this));
  13695. html.find(".item .rollable h4").click(event => this._onClickItemName(event));
  13696. new ContextMenu(html, ".item-list .item", [], {onOpen: this._onItemContext.bind(this)});
  13697. }
  13698. }
  13699. /* -------------------------------------------- */
  13700. /**
  13701. * Handle clicks to action buttons on the group sheet.
  13702. * @param {PointerEvent} event The initiating click event
  13703. * @protected
  13704. */
  13705. _onClickActionButton(event) {
  13706. event.preventDefault();
  13707. const button = event.currentTarget;
  13708. switch ( button.dataset.action ) {
  13709. case "convertCurrency":
  13710. Dialog.confirm({
  13711. title: `${game.i18n.localize("DND5E.CurrencyConvert")}`,
  13712. content: `<p>${game.i18n.localize("DND5E.CurrencyConvertHint")}</p>`,
  13713. yes: () => this.actor.convertCurrency()
  13714. });
  13715. break;
  13716. case "removeMember":
  13717. const removeMemberId = button.closest("li.group-member").dataset.actorId;
  13718. this.object.system.removeMember(removeMemberId);
  13719. break;
  13720. case "movementConfig":
  13721. const movementConfig = new ActorMovementConfig(this.object);
  13722. movementConfig.render(true);
  13723. break;
  13724. }
  13725. }
  13726. /* -------------------------------------------- */
  13727. /**
  13728. * Handle clicks to item control buttons on the group sheet.
  13729. * @param {PointerEvent} event The initiating click event
  13730. * @protected
  13731. */
  13732. _onClickItemControl(event) {
  13733. event.preventDefault();
  13734. const button = event.currentTarget;
  13735. switch ( button.dataset.action ) {
  13736. case "itemCreate":
  13737. this._createItem(button);
  13738. break;
  13739. case "itemDelete":
  13740. const deleteLi = event.currentTarget.closest(".item");
  13741. const deleteItem = this.actor.items.get(deleteLi.dataset.itemId);
  13742. deleteItem.deleteDialog();
  13743. break;
  13744. case "itemEdit":
  13745. const editLi = event.currentTarget.closest(".item");
  13746. const editItem = this.actor.items.get(editLi.dataset.itemId);
  13747. editItem.sheet.render(true);
  13748. break;
  13749. }
  13750. }
  13751. /* -------------------------------------------- */
  13752. /**
  13753. * Handle workflows to create a new Item directly within the Group Actor sheet.
  13754. * @param {HTMLElement} button The clicked create button
  13755. * @returns {Item5e} The created embedded Item
  13756. * @protected
  13757. */
  13758. _createItem(button) {
  13759. const type = button.dataset.type;
  13760. const system = {...button.dataset};
  13761. delete system.type;
  13762. const name = game.i18n.format("DND5E.ItemNew", {type: game.i18n.localize(CONFIG.Item.typeLabels[type])});
  13763. const itemData = {name, type, system};
  13764. return this.actor.createEmbeddedDocuments("Item", [itemData]);
  13765. }
  13766. /* -------------------------------------------- */
  13767. /**
  13768. * Handle activation of a context menu for an embedded Item document.
  13769. * Dynamically populate the array of context menu options.
  13770. * Reuse the item context options provided by the base ActorSheet5e class.
  13771. * @param {HTMLElement} element The HTML element for which the context menu is activated
  13772. * @protected
  13773. */
  13774. _onItemContext(element) {
  13775. const item = this.actor.items.get(element.dataset.itemId);
  13776. if ( !item ) return;
  13777. ui.context.menuItems = ActorSheet5e.prototype._getItemContextOptions.call(this, item);
  13778. Hooks.call("dnd5e.getItemContextOptions", item, ui.context.menuItems);
  13779. }
  13780. /* -------------------------------------------- */
  13781. /**
  13782. * Handle clicks on member names in the members list.
  13783. * @param {PointerEvent} event The initiating click event
  13784. * @protected
  13785. */
  13786. _onClickMemberName(event) {
  13787. event.preventDefault();
  13788. const member = event.currentTarget.closest("li.group-member");
  13789. const actor = game.actors.get(member.dataset.actorId);
  13790. if ( actor ) actor.sheet.render(true, {focus: true});
  13791. }
  13792. /* -------------------------------------------- */
  13793. /**
  13794. * Handle clicks on an item name to expand its description
  13795. * @param {PointerEvent} event The initiating click event
  13796. * @protected
  13797. */
  13798. _onClickItemName(event) {
  13799. game.system.applications.actor.ActorSheet5e.prototype._onItemSummary.call(this, event);
  13800. }
  13801. /* -------------------------------------------- */
  13802. /** @override */
  13803. async _onDropActor(event, data) {
  13804. if ( !this.isEditable ) return;
  13805. const cls = getDocumentClass("Actor");
  13806. const sourceActor = await cls.fromDropData(data);
  13807. if ( !sourceActor ) return;
  13808. return this.object.system.addMember(sourceActor);
  13809. }
  13810. /* -------------------------------------------- */
  13811. /** @override */
  13812. async _onDropItemCreate(itemData) {
  13813. const items = itemData instanceof Array ? itemData : [itemData];
  13814. const toCreate = [];
  13815. for ( const item of items ) {
  13816. const result = await this._onDropSingleItem(item);
  13817. if ( result ) toCreate.push(result);
  13818. }
  13819. // Create the owned items as normal
  13820. return this.actor.createEmbeddedDocuments("Item", toCreate);
  13821. }
  13822. /* -------------------------------------------- */
  13823. /**
  13824. * Handles dropping of a single item onto this group sheet.
  13825. * @param {object} itemData The item data to create.
  13826. * @returns {Promise<object|boolean>} The item data to create after processing, or false if the item should not be
  13827. * created or creation has been otherwise handled.
  13828. * @protected
  13829. */
  13830. async _onDropSingleItem(itemData) {
  13831. // Check to make sure items of this type are allowed on this actor
  13832. if ( this.constructor.unsupportedItemTypes.has(itemData.type) ) {
  13833. ui.notifications.warn(game.i18n.format("DND5E.ActorWarningInvalidItem", {
  13834. itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]),
  13835. actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type])
  13836. }));
  13837. return false;
  13838. }
  13839. // Create a Consumable spell scroll on the Inventory tab
  13840. if ( itemData.type === "spell" ) {
  13841. const scroll = await Item5e.createScrollFromSpell(itemData);
  13842. return scroll.toObject();
  13843. }
  13844. // TODO: Stack identical consumables
  13845. return itemData;
  13846. }
  13847. }
  13848. /**
  13849. * A simple form to set skill configuration for a given skill.
  13850. *
  13851. * @param {Actor} actor The Actor instance being displayed within the sheet.
  13852. * @param {ApplicationOptions} options Additional application configuration options.
  13853. * @param {string} skillId The skill key as defined in CONFIG.DND5E.skills.
  13854. * @deprecated since dnd5e 2.2, targeted for removal in 2.4
  13855. */
  13856. class ActorSkillConfig extends BaseConfigSheet {
  13857. constructor(actor, options, skillId) {
  13858. super(actor, options);
  13859. this._skillId = skillId;
  13860. foundry.utils.logCompatibilityWarning("ActorSkillConfig has been deprecated in favor of the more general "
  13861. + "ProficiencyConfig available at 'dnd5e.applications.actor.ProficiencyConfig'. Support for the old application "
  13862. + "will be removed in a future version.", {since: "DnD5e 2.2", until: "DnD5e 2.4"});
  13863. }
  13864. /* -------------------------------------------- */
  13865. /** @inheritdoc */
  13866. static get defaultOptions() {
  13867. return foundry.utils.mergeObject(super.defaultOptions, {
  13868. classes: ["dnd5e"],
  13869. template: "systems/dnd5e/templates/apps/skill-config.hbs",
  13870. width: 500,
  13871. height: "auto"
  13872. });
  13873. }
  13874. /* -------------------------------------------- */
  13875. /** @inheritdoc */
  13876. get title() {
  13877. const label = CONFIG.DND5E.skills[this._skillId].label;
  13878. return `${game.i18n.format("DND5E.SkillConfigureTitle", {skill: label})}: ${this.document.name}`;
  13879. }
  13880. /* -------------------------------------------- */
  13881. /** @inheritdoc */
  13882. getData(options) {
  13883. const src = this.document.toObject();
  13884. return {
  13885. abilities: CONFIG.DND5E.abilities,
  13886. skill: src.system.skills?.[this._skillId] ?? this.document.system.skills[this._skillId] ?? {},
  13887. skillId: this._skillId,
  13888. proficiencyLevels: CONFIG.DND5E.proficiencyLevels,
  13889. bonusGlobal: src.system.bonuses?.abilities.skill
  13890. };
  13891. }
  13892. /* -------------------------------------------- */
  13893. /** @inheritdoc */
  13894. _updateObject(event, formData) {
  13895. const passive = formData[`system.skills.${this._skillId}.bonuses.passive`];
  13896. const passiveRoll = new Roll(passive);
  13897. if ( !passiveRoll.isDeterministic ) {
  13898. const message = game.i18n.format("DND5E.FormulaCannotContainDiceError", {
  13899. name: game.i18n.localize("DND5E.SkillBonusPassive")
  13900. });
  13901. ui.notifications.error(message);
  13902. throw new Error(message);
  13903. }
  13904. super._updateObject(event, formData);
  13905. }
  13906. }
  13907. var _module$9 = /*#__PURE__*/Object.freeze({
  13908. __proto__: null,
  13909. ActorAbilityConfig: ActorAbilityConfig,
  13910. ActorArmorConfig: ActorArmorConfig,
  13911. ActorHitDiceConfig: ActorHitDiceConfig,
  13912. ActorHitPointsConfig: ActorHitPointsConfig,
  13913. ActorInitiativeConfig: ActorInitiativeConfig,
  13914. ActorMovementConfig: ActorMovementConfig,
  13915. ActorSensesConfig: ActorSensesConfig,
  13916. ActorSheet5e: ActorSheet5e,
  13917. ActorSheet5eCharacter: ActorSheet5eCharacter,
  13918. ActorSheet5eNPC: ActorSheet5eNPC,
  13919. ActorSheet5eVehicle: ActorSheet5eVehicle,
  13920. ActorSheetFlags: ActorSheetFlags,
  13921. ActorSkillConfig: ActorSkillConfig,
  13922. ActorTypeConfig: ActorTypeConfig,
  13923. BaseConfigSheet: BaseConfigSheet,
  13924. GroupActorSheet: GroupActorSheet,
  13925. LongRestDialog: LongRestDialog,
  13926. ProficiencyConfig: ProficiencyConfig,
  13927. ShortRestDialog: ShortRestDialog,
  13928. ToolSelector: ToolSelector,
  13929. TraitSelector: TraitSelector$1
  13930. });
  13931. /**
  13932. * Dialog to select which new advancements should be added to an item.
  13933. */
  13934. class AdvancementMigrationDialog extends Dialog {
  13935. /** @inheritdoc */
  13936. static get defaultOptions() {
  13937. return foundry.utils.mergeObject(super.defaultOptions, {
  13938. classes: ["dnd5e", "advancement-migration", "dialog"],
  13939. jQuery: false,
  13940. width: 500
  13941. });
  13942. }
  13943. /* -------------------------------------------- */
  13944. /**
  13945. * A helper constructor function which displays the migration dialog.
  13946. * @param {Item5e} item Item to which the advancements are being added.
  13947. * @param {Advancement[]} advancements New advancements that should be displayed in the prompt.
  13948. * @returns {Promise<Advancement[]|null>} Resolves with the advancements that should be added, if any.
  13949. */
  13950. static createDialog(item, advancements) {
  13951. const advancementContext = advancements.map(a => ({
  13952. id: a.id, icon: a.icon, title: a.title,
  13953. summary: a.levels.length === 1 ? a.summaryForLevel(a.levels[0]) : ""
  13954. }));
  13955. return new Promise(async (resolve, reject) => {
  13956. const dialog = new this({
  13957. title: `${game.i18n.localize("DND5E.AdvancementMigrationTitle")}: ${item.name}`,
  13958. content: await renderTemplate(
  13959. "systems/dnd5e/templates/advancement/advancement-migration-dialog.hbs",
  13960. { item, advancements: advancementContext }
  13961. ),
  13962. buttons: {
  13963. continue: {
  13964. icon: '<i class="fas fa-check"></i>',
  13965. label: game.i18n.localize("DND5E.AdvancementMigrationConfirm"),
  13966. callback: html => resolve(advancements.filter(a => html.querySelector(`[name="${a.id}"]`)?.checked))
  13967. },
  13968. cancel: {
  13969. icon: '<i class="fas fa-times"></i>',
  13970. label: game.i18n.localize("Cancel"),
  13971. callback: html => reject(null)
  13972. }
  13973. },
  13974. default: "continue",
  13975. close: () => reject(null)
  13976. });
  13977. dialog.render(true);
  13978. });
  13979. }
  13980. }
  13981. /**
  13982. * Presents a list of advancement types to create when clicking the new advancement button.
  13983. * Once a type is selected, this hands the process over to the advancement's individual editing interface.
  13984. *
  13985. * @param {Item5e} item Item to which this advancement will be added.
  13986. * @param {object} [dialogData={}] An object of dialog data which configures how the modal window is rendered.
  13987. * @param {object} [options={}] Dialog rendering options.
  13988. */
  13989. class AdvancementSelection extends Dialog {
  13990. constructor(item, dialogData={}, options={}) {
  13991. super(dialogData, options);
  13992. /**
  13993. * Store a reference to the Item to which this Advancement is being added.
  13994. * @type {Item5e}
  13995. */
  13996. this.item = item;
  13997. }
  13998. /* -------------------------------------------- */
  13999. /** @inheritDoc */
  14000. static get defaultOptions() {
  14001. return foundry.utils.mergeObject(super.defaultOptions, {
  14002. classes: ["dnd5e", "sheet", "advancement"],
  14003. template: "systems/dnd5e/templates/advancement/advancement-selection.hbs",
  14004. title: "DND5E.AdvancementSelectionTitle",
  14005. width: 500,
  14006. height: "auto"
  14007. });
  14008. }
  14009. /* -------------------------------------------- */
  14010. /** @inheritDoc */
  14011. get id() {
  14012. return `item-${this.item.id}-advancement-selection`;
  14013. }
  14014. /* -------------------------------------------- */
  14015. /** @inheritDoc */
  14016. getData() {
  14017. const context = { types: {} };
  14018. for ( const [name, advancement] of Object.entries(CONFIG.DND5E.advancementTypes) ) {
  14019. if ( !(advancement.prototype instanceof Advancement)
  14020. || !advancement.metadata.validItemTypes.has(this.item.type) ) continue;
  14021. context.types[name] = {
  14022. label: advancement.metadata.title,
  14023. icon: advancement.metadata.icon,
  14024. hint: advancement.metadata.hint,
  14025. disabled: !advancement.availableForItem(this.item)
  14026. };
  14027. }
  14028. context.types = dnd5e.utils.sortObjectEntries(context.types, "label");
  14029. return context;
  14030. }
  14031. /* -------------------------------------------- */
  14032. /** @inheritDoc */
  14033. activateListeners(html) {
  14034. super.activateListeners(html);
  14035. html.on("change", "input", this._onChangeInput.bind(this));
  14036. }
  14037. /* -------------------------------------------- */
  14038. /** @inheritDoc */
  14039. _onChangeInput(event) {
  14040. const submit = this.element[0].querySelector("button[data-button='submit']");
  14041. submit.disabled = !this.element[0].querySelector("input[name='type']:checked");
  14042. }
  14043. /* -------------------------------------------- */
  14044. /**
  14045. * A helper constructor function which displays the selection dialog and returns a Promise once its workflow has
  14046. * been resolved.
  14047. * @param {Item5e} item Item to which the advancement should be added.
  14048. * @param {object} [config={}]
  14049. * @param {boolean} [config.rejectClose=false] Trigger a rejection if the window was closed without a choice.
  14050. * @param {object} [config.options={}] Additional rendering options passed to the Dialog.
  14051. * @returns {Promise<AdvancementConfig|null>} Result of `Item5e#createAdvancement`.
  14052. */
  14053. static async createDialog(item, { rejectClose=false, options={} }={}) {
  14054. return new Promise((resolve, reject) => {
  14055. const dialog = new this(item, {
  14056. title: `${game.i18n.localize("DND5E.AdvancementSelectionTitle")}: ${item.name}`,
  14057. buttons: {
  14058. submit: {
  14059. callback: html => {
  14060. const formData = new FormDataExtended(html.querySelector("form"));
  14061. const type = formData.get("type");
  14062. resolve(item.createAdvancement(type));
  14063. }
  14064. }
  14065. },
  14066. close: () => {
  14067. if ( rejectClose ) reject("No advancement type was selected");
  14068. else resolve(null);
  14069. }
  14070. }, foundry.utils.mergeObject(options, { jQuery: false }));
  14071. dialog.render(true);
  14072. });
  14073. }
  14074. }
  14075. var _module$8 = /*#__PURE__*/Object.freeze({
  14076. __proto__: null,
  14077. AdvancementConfig: AdvancementConfig,
  14078. AdvancementConfirmationDialog: AdvancementConfirmationDialog,
  14079. AdvancementFlow: AdvancementFlow,
  14080. AdvancementManager: AdvancementManager,
  14081. AdvancementMigrationDialog: AdvancementMigrationDialog,
  14082. AdvancementSelection: AdvancementSelection,
  14083. HitPointsConfig: HitPointsConfig,
  14084. HitPointsFlow: HitPointsFlow,
  14085. ItemChoiceConfig: ItemChoiceConfig,
  14086. ItemChoiceFlow: ItemChoiceFlow,
  14087. ItemGrantConfig: ItemGrantConfig,
  14088. ItemGrantFlow: ItemGrantFlow,
  14089. ScaleValueConfig: ScaleValueConfig,
  14090. ScaleValueFlow: ScaleValueFlow
  14091. });
  14092. /**
  14093. * An extension of the base CombatTracker class to provide some 5e-specific functionality.
  14094. * @extends {CombatTracker}
  14095. */
  14096. class CombatTracker5e extends CombatTracker {
  14097. /** @inheritdoc */
  14098. async _onCombatantControl(event) {
  14099. const btn = event.currentTarget;
  14100. const combatantId = btn.closest(".combatant").dataset.combatantId;
  14101. const combatant = this.viewed.combatants.get(combatantId);
  14102. if ( (btn.dataset.control === "rollInitiative") && combatant?.actor ) return combatant.actor.rollInitiativeDialog();
  14103. return super._onCombatantControl(event);
  14104. }
  14105. }
  14106. var _module$7 = /*#__PURE__*/Object.freeze({
  14107. __proto__: null,
  14108. CombatTracker5e: CombatTracker5e
  14109. });
  14110. /**
  14111. * A specialized form used to select from a checklist of attributes, traits, or properties.
  14112. * @deprecated since dnd5e 2.1, targeted for removal in 2.3
  14113. */
  14114. class TraitSelector extends DocumentSheet {
  14115. constructor(...args) {
  14116. super(...args);
  14117. if ( !this.options.suppressWarning ) foundry.utils.logCompatibilityWarning(
  14118. `${this.constructor.name} has been deprecated in favor of a more specialized TraitSelector `
  14119. + "available at 'dnd5e.applications.actor.TraitSelector'. Support for the old application will "
  14120. + "be removed in a future version.",
  14121. { since: "DnD5e 2.1", until: "DnD5e 2.3" }
  14122. );
  14123. }
  14124. /* -------------------------------------------- */
  14125. /** @inheritDoc */
  14126. static get defaultOptions() {
  14127. return foundry.utils.mergeObject(super.defaultOptions, {
  14128. id: "trait-selector",
  14129. classes: ["dnd5e", "trait-selector", "subconfig"],
  14130. title: "Actor Trait Selection",
  14131. template: "systems/dnd5e/templates/apps/trait-selector.hbs",
  14132. width: 320,
  14133. height: "auto",
  14134. choices: {},
  14135. allowCustom: true,
  14136. minimum: 0,
  14137. maximum: null,
  14138. labelKey: null,
  14139. valueKey: "value",
  14140. customKey: "custom"
  14141. });
  14142. }
  14143. /* -------------------------------------------- */
  14144. /** @inheritdoc */
  14145. get title() {
  14146. return this.options.title || super.title;
  14147. }
  14148. /* -------------------------------------------- */
  14149. /**
  14150. * Return a reference to the target attribute
  14151. * @type {string}
  14152. */
  14153. get attribute() {
  14154. return this.options.name;
  14155. }
  14156. /* -------------------------------------------- */
  14157. /** @override */
  14158. getData() {
  14159. const attr = foundry.utils.getProperty(this.object, this.attribute);
  14160. const o = this.options;
  14161. const value = (o.valueKey) ? foundry.utils.getProperty(attr, o.valueKey) ?? [] : attr;
  14162. const custom = (o.customKey) ? foundry.utils.getProperty(attr, o.customKey) ?? "" : "";
  14163. // Populate choices
  14164. const choices = Object.entries(o.choices).reduce((obj, e) => {
  14165. let [k, v] = e;
  14166. const label = o.labelKey ? foundry.utils.getProperty(v, o.labelKey) ?? v : v;
  14167. obj[k] = { label, chosen: attr ? value.includes(k) : false };
  14168. return obj;
  14169. }, {});
  14170. // Return data
  14171. return {
  14172. choices: choices,
  14173. custom: custom,
  14174. customPath: o.allowCustom ? "custom" : null
  14175. };
  14176. }
  14177. /* -------------------------------------------- */
  14178. /**
  14179. * Prepare the update data to include choices in the provided object.
  14180. * @param {object} formData Form data to search for choices.
  14181. * @returns {object} Updates to apply to target.
  14182. */
  14183. _prepareUpdateData(formData) {
  14184. const o = this.options;
  14185. formData = foundry.utils.expandObject(formData);
  14186. // Obtain choices
  14187. const chosen = Object.entries(formData.choices).filter(([, v]) => v).map(([k]) => k);
  14188. // Object including custom data
  14189. const updateData = {};
  14190. if ( o.valueKey ) updateData[`${this.attribute}.${o.valueKey}`] = chosen;
  14191. else updateData[this.attribute] = chosen;
  14192. if ( o.allowCustom ) updateData[`${this.attribute}.${o.customKey}`] = formData.custom;
  14193. // Validate the number chosen
  14194. if ( o.minimum && (chosen.length < o.minimum) ) {
  14195. return ui.notifications.error(`You must choose at least ${o.minimum} options`);
  14196. }
  14197. if ( o.maximum && (chosen.length > o.maximum) ) {
  14198. return ui.notifications.error(`You may choose no more than ${o.maximum} options`);
  14199. }
  14200. return updateData;
  14201. }
  14202. /* -------------------------------------------- */
  14203. /** @override */
  14204. async _updateObject(event, formData) {
  14205. const updateData = this._prepareUpdateData(formData);
  14206. if ( updateData ) this.object.update(updateData);
  14207. }
  14208. }
  14209. /**
  14210. * Override and extend the core ItemSheet implementation to handle specific item types.
  14211. */
  14212. class ItemSheet5e extends ItemSheet {
  14213. constructor(...args) {
  14214. super(...args);
  14215. // Expand the default size of the class sheet
  14216. if ( this.object.type === "class" ) {
  14217. this.options.width = this.position.width = 600;
  14218. this.options.height = this.position.height = 680;
  14219. }
  14220. else if ( this.object.type === "subclass" ) {
  14221. this.options.height = this.position.height = 540;
  14222. }
  14223. }
  14224. /* -------------------------------------------- */
  14225. /** @inheritdoc */
  14226. static get defaultOptions() {
  14227. return foundry.utils.mergeObject(super.defaultOptions, {
  14228. width: 560,
  14229. height: 400,
  14230. classes: ["dnd5e", "sheet", "item"],
  14231. resizable: true,
  14232. scrollY: [".tab.details"],
  14233. tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}],
  14234. dragDrop: [
  14235. {dragSelector: "[data-effect-id]", dropSelector: ".effects-list"},
  14236. {dragSelector: ".advancement-item", dropSelector: ".advancement"}
  14237. ]
  14238. });
  14239. }
  14240. /* -------------------------------------------- */
  14241. /**
  14242. * Whether advancements on embedded items should be configurable.
  14243. * @type {boolean}
  14244. */
  14245. advancementConfigurationMode = false;
  14246. /* -------------------------------------------- */
  14247. /** @inheritdoc */
  14248. get template() {
  14249. return `systems/dnd5e/templates/items/${this.item.type}.hbs`;
  14250. }
  14251. /* -------------------------------------------- */
  14252. /* Context Preparation */
  14253. /* -------------------------------------------- */
  14254. /** @override */
  14255. async getData(options) {
  14256. const context = await super.getData(options);
  14257. const item = context.item;
  14258. const source = item.toObject();
  14259. // Game system configuration
  14260. context.config = CONFIG.DND5E;
  14261. // Item rendering data
  14262. foundry.utils.mergeObject(context, {
  14263. source: source.system,
  14264. system: item.system,
  14265. labels: item.labels,
  14266. isEmbedded: item.isEmbedded,
  14267. advancementEditable: (this.advancementConfigurationMode || !item.isEmbedded) && context.editable,
  14268. rollData: this.item.getRollData(),
  14269. // Item Type, Status, and Details
  14270. itemType: game.i18n.localize(CONFIG.Item.typeLabels[this.item.type]),
  14271. itemStatus: this._getItemStatus(),
  14272. itemProperties: this._getItemProperties(),
  14273. baseItems: await this._getItemBaseTypes(),
  14274. isPhysical: item.system.hasOwnProperty("quantity"),
  14275. // Action Details
  14276. isHealing: item.system.actionType === "heal",
  14277. isFlatDC: item.system.save?.scaling === "flat",
  14278. isLine: ["line", "wall"].includes(item.system.target?.type),
  14279. // Vehicles
  14280. isCrewed: item.system.activation?.type === "crew",
  14281. // Armor Class
  14282. hasDexModifier: item.isArmor && (item.system.armor?.type !== "shield"),
  14283. // Advancement
  14284. advancement: this._getItemAdvancement(item),
  14285. // Prepare Active Effects
  14286. effects: ActiveEffect5e.prepareActiveEffectCategories(item.effects)
  14287. });
  14288. context.abilityConsumptionTargets = this._getItemConsumptionTargets();
  14289. // Special handling for specific item types
  14290. switch ( item.type ) {
  14291. case "feat":
  14292. const featureType = CONFIG.DND5E.featureTypes[item.system.type?.value];
  14293. if ( featureType ) {
  14294. context.itemType = featureType.label;
  14295. context.featureSubtypes = featureType.subtypes;
  14296. }
  14297. break;
  14298. case "spell":
  14299. context.spellComponents = {...CONFIG.DND5E.spellComponents, ...CONFIG.DND5E.spellTags};
  14300. break;
  14301. }
  14302. // Enrich HTML description
  14303. context.descriptionHTML = await TextEditor.enrichHTML(item.system.description.value, {
  14304. secrets: item.isOwner,
  14305. async: true,
  14306. relativeTo: this.item,
  14307. rollData: context.rollData
  14308. });
  14309. return context;
  14310. }
  14311. /* -------------------------------------------- */
  14312. /**
  14313. * Get the display object used to show the advancement tab.
  14314. * @param {Item5e} item The item for which the advancement is being prepared.
  14315. * @returns {object} Object with advancement data grouped by levels.
  14316. */
  14317. _getItemAdvancement(item) {
  14318. if ( !item.system.advancement ) return {};
  14319. const advancement = {};
  14320. const configMode = !item.parent || this.advancementConfigurationMode;
  14321. const maxLevel = !configMode
  14322. ? (item.system.levels ?? item.class?.system.levels ?? item.parent.system.details?.level ?? -1) : -1;
  14323. // Improperly configured advancements
  14324. if ( item.advancement.needingConfiguration.length ) {
  14325. advancement.unconfigured = {
  14326. items: item.advancement.needingConfiguration.map(a => ({
  14327. id: a.id,
  14328. order: a.constructor.order,
  14329. title: a.title,
  14330. icon: a.icon,
  14331. classRestriction: a.classRestriction,
  14332. configured: false
  14333. })),
  14334. configured: "partial"
  14335. };
  14336. }
  14337. // All other advancements by level
  14338. for ( let [level, advancements] of Object.entries(item.advancement.byLevel) ) {
  14339. if ( !configMode ) advancements = advancements.filter(a => a.appliesToClass);
  14340. const items = advancements.map(advancement => ({
  14341. id: advancement.id,
  14342. order: advancement.sortingValueForLevel(level),
  14343. title: advancement.titleForLevel(level, { configMode }),
  14344. icon: advancement.icon,
  14345. classRestriction: advancement.classRestriction,
  14346. summary: advancement.summaryForLevel(level, { configMode }),
  14347. configured: advancement.configuredForLevel(level)
  14348. }));
  14349. if ( !items.length ) continue;
  14350. advancement[level] = {
  14351. items: items.sort((a, b) => a.order.localeCompare(b.order)),
  14352. configured: (level > maxLevel) ? false : items.some(a => !a.configured) ? "partial" : "full"
  14353. };
  14354. }
  14355. return advancement;
  14356. }
  14357. /* -------------------------------------------- */
  14358. /**
  14359. * Get the base weapons and tools based on the selected type.
  14360. * @returns {Promise<object>} Object with base items for this type formatted for selectOptions.
  14361. * @protected
  14362. */
  14363. async _getItemBaseTypes() {
  14364. const type = this.item.type === "equipment" ? "armor" : this.item.type;
  14365. const baseIds = CONFIG.DND5E[`${type}Ids`];
  14366. if ( baseIds === undefined ) return {};
  14367. const typeProperty = type === "armor" ? "armor.type" : `${type}Type`;
  14368. const baseType = foundry.utils.getProperty(this.item.system, typeProperty);
  14369. const items = {};
  14370. for ( const [name, id] of Object.entries(baseIds) ) {
  14371. const baseItem = await getBaseItem(id);
  14372. if ( baseType !== foundry.utils.getProperty(baseItem?.system, typeProperty) ) continue;
  14373. items[name] = baseItem.name;
  14374. }
  14375. return Object.fromEntries(Object.entries(items).sort((lhs, rhs) => lhs[1].localeCompare(rhs[1])));
  14376. }
  14377. /* -------------------------------------------- */
  14378. /**
  14379. * Get the valid item consumption targets which exist on the actor
  14380. * @returns {Object<string>} An object of potential consumption targets
  14381. * @private
  14382. */
  14383. _getItemConsumptionTargets() {
  14384. const consume = this.item.system.consume || {};
  14385. if ( !consume.type ) return [];
  14386. const actor = this.item.actor;
  14387. if ( !actor ) return {};
  14388. // Ammunition
  14389. if ( consume.type === "ammo" ) {
  14390. return actor.itemTypes.consumable.reduce((ammo, i) => {
  14391. if ( i.system.consumableType === "ammo" ) ammo[i.id] = `${i.name} (${i.system.quantity})`;
  14392. return ammo;
  14393. }, {});
  14394. }
  14395. // Attributes
  14396. else if ( consume.type === "attribute" ) {
  14397. const attrData = game.dnd5e.isV10 ? actor.system : actor.type;
  14398. const attributes = TokenDocument.implementation.getConsumedAttributes(attrData);
  14399. attributes.bar.forEach(a => a.push("value"));
  14400. return attributes.bar.concat(attributes.value).reduce((obj, a) => {
  14401. let k = a.join(".");
  14402. obj[k] = k;
  14403. return obj;
  14404. }, {});
  14405. }
  14406. // Hit Dice
  14407. else if ( consume.type === "hitDice" ) {
  14408. return {
  14409. smallest: game.i18n.localize("DND5E.ConsumeHitDiceSmallest"),
  14410. ...CONFIG.DND5E.hitDieTypes.reduce((obj, hd) => { obj[hd] = hd; return obj; }, {}),
  14411. largest: game.i18n.localize("DND5E.ConsumeHitDiceLargest")
  14412. };
  14413. }
  14414. // Materials
  14415. else if ( consume.type === "material" ) {
  14416. return actor.items.reduce((obj, i) => {
  14417. if ( ["consumable", "loot"].includes(i.type) && !i.system.activation ) {
  14418. obj[i.id] = `${i.name} (${i.system.quantity})`;
  14419. }
  14420. return obj;
  14421. }, {});
  14422. }
  14423. // Charges
  14424. else if ( consume.type === "charges" ) {
  14425. return actor.items.reduce((obj, i) => {
  14426. // Limited-use items
  14427. const uses = i.system.uses || {};
  14428. if ( uses.per && uses.max ) {
  14429. const label = uses.per === "charges"
  14430. ? ` (${game.i18n.format("DND5E.AbilityUseChargesLabel", {value: uses.value})})`
  14431. : ` (${game.i18n.format("DND5E.AbilityUseConsumableLabel", {max: uses.max, per: uses.per})})`;
  14432. obj[i.id] = i.name + label;
  14433. }
  14434. // Recharging items
  14435. const recharge = i.system.recharge || {};
  14436. if ( recharge.value ) obj[i.id] = `${i.name} (${game.i18n.format("DND5E.Recharge")})`;
  14437. return obj;
  14438. }, {});
  14439. }
  14440. else return {};
  14441. }
  14442. /* -------------------------------------------- */
  14443. /**
  14444. * Get the text item status which is shown beneath the Item type in the top-right corner of the sheet.
  14445. * @returns {string|null} Item status string if applicable to item's type.
  14446. * @protected
  14447. */
  14448. _getItemStatus() {
  14449. switch ( this.item.type ) {
  14450. case "class":
  14451. return game.i18n.format("DND5E.LevelCount", {ordinal: this.item.system.levels.ordinalString()});
  14452. case "equipment":
  14453. case "weapon":
  14454. return game.i18n.localize(this.item.system.equipped ? "DND5E.Equipped" : "DND5E.Unequipped");
  14455. case "feat":
  14456. const typeConfig = CONFIG.DND5E.featureTypes[this.item.system.type.value];
  14457. if ( typeConfig?.subtypes ) return typeConfig.subtypes[this.item.system.type.subtype] ?? null;
  14458. break;
  14459. case "spell":
  14460. return CONFIG.DND5E.spellPreparationModes[this.item.system.preparation];
  14461. case "tool":
  14462. return game.i18n.localize(this.item.system.proficient ? "DND5E.Proficient" : "DND5E.NotProficient");
  14463. }
  14464. return null;
  14465. }
  14466. /* -------------------------------------------- */
  14467. /**
  14468. * Get the Array of item properties which are used in the small sidebar of the description tab.
  14469. * @returns {string[]} List of property labels to be shown.
  14470. * @private
  14471. */
  14472. _getItemProperties() {
  14473. const props = [];
  14474. const labels = this.item.labels;
  14475. switch ( this.item.type ) {
  14476. case "equipment":
  14477. props.push(CONFIG.DND5E.equipmentTypes[this.item.system.armor.type]);
  14478. if ( this.item.isArmor || this.item.isMountable ) props.push(labels.armor);
  14479. break;
  14480. case "feat":
  14481. props.push(labels.featType);
  14482. break;
  14483. case "spell":
  14484. props.push(labels.components.vsm, labels.materials, ...labels.components.tags);
  14485. break;
  14486. case "weapon":
  14487. for ( const [k, v] of Object.entries(this.item.system.properties) ) {
  14488. if ( v === true ) props.push(CONFIG.DND5E.weaponProperties[k]);
  14489. }
  14490. break;
  14491. }
  14492. // Action type
  14493. if ( this.item.system.actionType ) {
  14494. props.push(CONFIG.DND5E.itemActionTypes[this.item.system.actionType]);
  14495. }
  14496. // Action usage
  14497. if ( (this.item.type !== "weapon") && !foundry.utils.isEmpty(this.item.system.activation) ) {
  14498. props.push(labels.activation, labels.range, labels.target, labels.duration);
  14499. }
  14500. return props.filter(p => !!p);
  14501. }
  14502. /* -------------------------------------------- */
  14503. /** @inheritDoc */
  14504. setPosition(position={}) {
  14505. if ( !(this._minimized || position.height) ) {
  14506. position.height = (this._tabs[0].active === "details") ? "auto" : Math.max(this.height, this.options.height);
  14507. }
  14508. return super.setPosition(position);
  14509. }
  14510. /* -------------------------------------------- */
  14511. /** @inheritdoc */
  14512. async activateEditor(name, options={}, initialContent="") {
  14513. options.relativeLinks = true;
  14514. options.plugins = {
  14515. menu: ProseMirror.ProseMirrorMenu.build(ProseMirror.defaultSchema, {
  14516. compact: true,
  14517. destroyOnSave: true,
  14518. onSave: () => this.saveEditor(name, {remove: true})
  14519. })
  14520. };
  14521. return super.activateEditor(name, options, initialContent);
  14522. }
  14523. /* -------------------------------------------- */
  14524. /* Form Submission */
  14525. /* -------------------------------------------- */
  14526. /** @inheritDoc */
  14527. _getSubmitData(updateData={}) {
  14528. const formData = foundry.utils.expandObject(super._getSubmitData(updateData));
  14529. // Handle Damage array
  14530. const damage = formData.system?.damage;
  14531. if ( damage ) damage.parts = Object.values(damage?.parts || {}).map(d => [d[0] || "", d[1] || ""]);
  14532. // Check max uses formula
  14533. const uses = formData.system?.uses;
  14534. if ( uses?.max ) {
  14535. const maxRoll = new Roll(uses.max);
  14536. if ( !maxRoll.isDeterministic ) {
  14537. uses.max = this.item._source.system.uses.max;
  14538. this.form.querySelector("input[name='system.uses.max']").value = uses.max;
  14539. return ui.notifications.error(game.i18n.format("DND5E.FormulaCannotContainDiceError", {
  14540. name: game.i18n.localize("DND5E.LimitedUses")
  14541. }));
  14542. }
  14543. }
  14544. // Check duration value formula
  14545. const duration = formData.system?.duration;
  14546. if ( duration?.value ) {
  14547. const durationRoll = new Roll(duration.value);
  14548. if ( !durationRoll.isDeterministic ) {
  14549. duration.value = this.item._source.system.duration.value;
  14550. this.form.querySelector("input[name='system.duration.value']").value = duration.value;
  14551. return ui.notifications.error(game.i18n.format("DND5E.FormulaCannotContainDiceError", {
  14552. name: game.i18n.localize("DND5E.Duration")
  14553. }));
  14554. }
  14555. }
  14556. // Check class identifier
  14557. if ( formData.system?.identifier && !dnd5e.utils.validators.isValidIdentifier(formData.system.identifier) ) {
  14558. formData.system.identifier = this.item._source.system.identifier;
  14559. this.form.querySelector("input[name='system.identifier']").value = formData.system.identifier;
  14560. return ui.notifications.error(game.i18n.localize("DND5E.IdentifierError"));
  14561. }
  14562. // Return the flattened submission data
  14563. return foundry.utils.flattenObject(formData);
  14564. }
  14565. /* -------------------------------------------- */
  14566. /** @inheritDoc */
  14567. activateListeners(html) {
  14568. super.activateListeners(html);
  14569. if ( this.isEditable ) {
  14570. html.find(".damage-control").click(this._onDamageControl.bind(this));
  14571. html.find(".trait-selector").click(this._onConfigureTraits.bind(this));
  14572. html.find(".effect-control").click(ev => {
  14573. const unsupported = game.dnd5e.isV10 && this.item.isOwned;
  14574. 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.");
  14575. ActiveEffect5e.onManageActiveEffect(ev, this.item);
  14576. });
  14577. html.find(".advancement .item-control").click(event => {
  14578. const t = event.currentTarget;
  14579. if ( t.dataset.action ) this._onAdvancementAction(t, t.dataset.action);
  14580. });
  14581. }
  14582. // Advancement context menu
  14583. const contextOptions = this._getAdvancementContextMenuOptions();
  14584. /**
  14585. * A hook event that fires when the context menu for the advancements list is constructed.
  14586. * @function dnd5e.getItemAdvancementContext
  14587. * @memberof hookEvents
  14588. * @param {jQuery} html The HTML element to which the context options are attached.
  14589. * @param {ContextMenuEntry[]} entryOptions The context menu entries.
  14590. */
  14591. Hooks.call("dnd5e.getItemAdvancementContext", html, contextOptions);
  14592. if ( contextOptions ) new ContextMenu(html, ".advancement-item", contextOptions);
  14593. }
  14594. /* -------------------------------------------- */
  14595. /**
  14596. * Get the set of ContextMenu options which should be applied for advancement entries.
  14597. * @returns {ContextMenuEntry[]} Context menu entries.
  14598. * @protected
  14599. */
  14600. _getAdvancementContextMenuOptions() {
  14601. const condition = li => (this.advancementConfigurationMode || !this.isEmbedded) && this.isEditable;
  14602. return [
  14603. {
  14604. name: "DND5E.AdvancementControlEdit",
  14605. icon: "<i class='fas fa-edit fa-fw'></i>",
  14606. condition,
  14607. callback: li => this._onAdvancementAction(li[0], "edit")
  14608. },
  14609. {
  14610. name: "DND5E.AdvancementControlDuplicate",
  14611. icon: "<i class='fas fa-copy fa-fw'></i>",
  14612. condition: li => {
  14613. const id = li[0].closest(".advancement-item")?.dataset.id;
  14614. const advancement = this.item.advancement.byId[id];
  14615. return condition() && advancement?.constructor.availableForItem(this.item);
  14616. },
  14617. callback: li => this._onAdvancementAction(li[0], "duplicate")
  14618. },
  14619. {
  14620. name: "DND5E.AdvancementControlDelete",
  14621. icon: "<i class='fas fa-trash fa-fw' style='color: rgb(255, 65, 65);'></i>",
  14622. condition,
  14623. callback: li => this._onAdvancementAction(li[0], "delete")
  14624. }
  14625. ];
  14626. }
  14627. /* -------------------------------------------- */
  14628. /**
  14629. * Add or remove a damage part from the damage formula.
  14630. * @param {Event} event The original click event.
  14631. * @returns {Promise<Item5e>|null} Item with updates applied.
  14632. * @private
  14633. */
  14634. async _onDamageControl(event) {
  14635. event.preventDefault();
  14636. const a = event.currentTarget;
  14637. // Add new damage component
  14638. if ( a.classList.contains("add-damage") ) {
  14639. await this._onSubmit(event); // Submit any unsaved changes
  14640. const damage = this.item.system.damage;
  14641. return this.item.update({"system.damage.parts": damage.parts.concat([["", ""]])});
  14642. }
  14643. // Remove a damage component
  14644. if ( a.classList.contains("delete-damage") ) {
  14645. await this._onSubmit(event); // Submit any unsaved changes
  14646. const li = a.closest(".damage-part");
  14647. const damage = foundry.utils.deepClone(this.item.system.damage);
  14648. damage.parts.splice(Number(li.dataset.damagePart), 1);
  14649. return this.item.update({"system.damage.parts": damage.parts});
  14650. }
  14651. }
  14652. /* -------------------------------------------- */
  14653. /** @inheritdoc */
  14654. _onDragStart(event) {
  14655. const li = event.currentTarget;
  14656. if ( event.target.classList.contains("content-link") ) return;
  14657. // Create drag data
  14658. let dragData;
  14659. // Active Effect
  14660. if ( li.dataset.effectId ) {
  14661. const effect = this.item.effects.get(li.dataset.effectId);
  14662. dragData = effect.toDragData();
  14663. } else if ( li.classList.contains("advancement-item") ) {
  14664. dragData = this.item.advancement.byId[li.dataset.id]?.toDragData();
  14665. }
  14666. if ( !dragData ) return;
  14667. // Set data transfer
  14668. event.dataTransfer.setData("text/plain", JSON.stringify(dragData));
  14669. }
  14670. /* -------------------------------------------- */
  14671. /** @inheritdoc */
  14672. _onDrop(event) {
  14673. const data = TextEditor.getDragEventData(event);
  14674. const item = this.item;
  14675. /**
  14676. * A hook event that fires when some useful data is dropped onto an ItemSheet5e.
  14677. * @function dnd5e.dropItemSheetData
  14678. * @memberof hookEvents
  14679. * @param {Item5e} item The Item5e
  14680. * @param {ItemSheet5e} sheet The ItemSheet5e application
  14681. * @param {object} data The data that has been dropped onto the sheet
  14682. * @returns {boolean} Explicitly return `false` to prevent normal drop handling.
  14683. */
  14684. const allowed = Hooks.call("dnd5e.dropItemSheetData", item, this, data);
  14685. if ( allowed === false ) return;
  14686. switch ( data.type ) {
  14687. case "ActiveEffect":
  14688. return this._onDropActiveEffect(event, data);
  14689. case "Advancement":
  14690. case "Item":
  14691. return this._onDropAdvancement(event, data);
  14692. }
  14693. }
  14694. /* -------------------------------------------- */
  14695. /**
  14696. * Handle the dropping of ActiveEffect data onto an Item Sheet
  14697. * @param {DragEvent} event The concluding DragEvent which contains drop data
  14698. * @param {object} data The data transfer extracted from the event
  14699. * @returns {Promise<ActiveEffect|boolean>} The created ActiveEffect object or false if it couldn't be created.
  14700. * @protected
  14701. */
  14702. async _onDropActiveEffect(event, data) {
  14703. const effect = await ActiveEffect.implementation.fromDropData(data);
  14704. if ( !this.item.isOwner || !effect ) return false;
  14705. if ( (this.item.uuid === effect.parent.uuid) || (this.item.uuid === effect.origin) ) return false;
  14706. return ActiveEffect.create({
  14707. ...effect.toObject(),
  14708. origin: this.item.uuid
  14709. }, {parent: this.item});
  14710. }
  14711. /* -------------------------------------------- */
  14712. /**
  14713. * Handle the dropping of an advancement or item with advancements onto the advancements tab.
  14714. * @param {DragEvent} event The concluding DragEvent which contains drop data.
  14715. * @param {object} data The data transfer extracted from the event.
  14716. */
  14717. async _onDropAdvancement(event, data) {
  14718. let advancements;
  14719. let showDialog = false;
  14720. if ( data.type === "Advancement" ) {
  14721. advancements = [await fromUuid(data.uuid)];
  14722. } else if ( data.type === "Item" ) {
  14723. const item = await Item.implementation.fromDropData(data);
  14724. if ( !item ) return false;
  14725. advancements = Object.values(item.advancement.byId);
  14726. showDialog = true;
  14727. } else {
  14728. return false;
  14729. }
  14730. advancements = advancements.filter(a => {
  14731. return !this.item.advancement.byId[a.id]
  14732. && a.constructor.metadata.validItemTypes.has(this.item.type)
  14733. && a.constructor.availableForItem(this.item);
  14734. });
  14735. // Display dialog prompting for which advancements to add
  14736. if ( showDialog ) {
  14737. try {
  14738. advancements = await AdvancementMigrationDialog.createDialog(this.item, advancements);
  14739. } catch(err) {
  14740. return false;
  14741. }
  14742. }
  14743. if ( !advancements.length ) return false;
  14744. if ( this.item.isEmbedded && !game.settings.get("dnd5e", "disableAdvancements") ) {
  14745. const manager = AdvancementManager.forNewAdvancement(this.item.actor, this.item.id, advancements);
  14746. if ( manager.steps.length ) return manager.render(true);
  14747. }
  14748. // If no advancements need to be applied, just add them to the item
  14749. const advancementArray = foundry.utils.deepClone(this.item.system.advancement);
  14750. advancementArray.push(...advancements.map(a => a.toObject()));
  14751. this.item.update({"system.advancement": advancementArray});
  14752. }
  14753. /* -------------------------------------------- */
  14754. /**
  14755. * Handle spawning the TraitSelector application for selection various options.
  14756. * @param {Event} event The click event which originated the selection.
  14757. * @private
  14758. */
  14759. _onConfigureTraits(event) {
  14760. event.preventDefault();
  14761. const a = event.currentTarget;
  14762. const options = {
  14763. name: a.dataset.target,
  14764. title: a.parentElement.innerText,
  14765. choices: [],
  14766. allowCustom: false,
  14767. suppressWarning: true
  14768. };
  14769. switch (a.dataset.options) {
  14770. case "saves":
  14771. options.choices = CONFIG.DND5E.abilities;
  14772. options.valueKey = null;
  14773. options.labelKey = "label";
  14774. break;
  14775. case "skills.choices":
  14776. options.choices = CONFIG.DND5E.skills;
  14777. options.valueKey = null;
  14778. options.labelKey = "label";
  14779. break;
  14780. case "skills":
  14781. const skills = this.item.system.skills;
  14782. const choices = skills.choices?.length ? skills.choices : Object.keys(CONFIG.DND5E.skills);
  14783. options.choices = Object.fromEntries(Object.entries(CONFIG.DND5E.skills).filter(([s]) => choices.includes(s)));
  14784. options.maximum = skills.number;
  14785. options.labelKey = "label";
  14786. break;
  14787. }
  14788. new TraitSelector(this.item, options).render(true);
  14789. }
  14790. /* -------------------------------------------- */
  14791. /**
  14792. * Handle one of the advancement actions from the buttons or context menu.
  14793. * @param {Element} target Button or context menu entry that triggered this action.
  14794. * @param {string} action Action being triggered.
  14795. * @returns {Promise|void}
  14796. */
  14797. _onAdvancementAction(target, action) {
  14798. const id = target.closest(".advancement-item")?.dataset.id;
  14799. const advancement = this.item.advancement.byId[id];
  14800. let manager;
  14801. if ( ["edit", "delete", "duplicate"].includes(action) && !advancement ) return;
  14802. switch (action) {
  14803. case "add": return game.dnd5e.applications.advancement.AdvancementSelection.createDialog(this.item);
  14804. case "edit": return new advancement.constructor.metadata.apps.config(advancement).render(true);
  14805. case "delete":
  14806. if ( this.item.isEmbedded && !game.settings.get("dnd5e", "disableAdvancements") ) {
  14807. manager = AdvancementManager.forDeletedAdvancement(this.item.actor, this.item.id, id);
  14808. if ( manager.steps.length ) return manager.render(true);
  14809. }
  14810. return this.item.deleteAdvancement(id);
  14811. case "duplicate": return this.item.duplicateAdvancement(id);
  14812. case "modify-choices":
  14813. const level = target.closest("li")?.dataset.level;
  14814. manager = AdvancementManager.forModifyChoices(this.item.actor, this.item.id, Number(level));
  14815. if ( manager.steps.length ) manager.render(true);
  14816. return;
  14817. case "toggle-configuration":
  14818. this.advancementConfigurationMode = !this.advancementConfigurationMode;
  14819. return this.render();
  14820. }
  14821. }
  14822. /* -------------------------------------------- */
  14823. /** @inheritdoc */
  14824. async _onSubmit(...args) {
  14825. if ( this._tabs[0].active === "details" ) this.position.height = "auto";
  14826. await super._onSubmit(...args);
  14827. }
  14828. }
  14829. var _module$6 = /*#__PURE__*/Object.freeze({
  14830. __proto__: null,
  14831. AbilityUseDialog: AbilityUseDialog,
  14832. ItemSheet5e: ItemSheet5e
  14833. });
  14834. /**
  14835. * Pop out ProseMirror editor window for journal entries with multiple text areas that need editing.
  14836. *
  14837. * @param {JournalEntryPage} document Journal entry page to be edited.
  14838. * @param {object} options
  14839. * @param {string} options.textKeyPath The path to the specific HTML field being edited.
  14840. */
  14841. class JournalEditor extends DocumentSheet {
  14842. /** @inheritdoc */
  14843. static get defaultOptions() {
  14844. return foundry.utils.mergeObject(super.defaultOptions, {
  14845. classes: ["journal-editor"],
  14846. template: "systems/dnd5e/templates/journal/journal-editor.hbs",
  14847. width: 550,
  14848. height: 640,
  14849. textKeyPath: null,
  14850. resizable: true
  14851. });
  14852. }
  14853. /* -------------------------------------------- */
  14854. /** @inheritdoc */
  14855. get title() {
  14856. if ( this.options.title ) return `${this.document.name}: ${this.options.title}`;
  14857. else return this.document.name;
  14858. }
  14859. /* -------------------------------------------- */
  14860. /** @inheritdoc */
  14861. async getData() {
  14862. const data = super.getData();
  14863. const rawText = foundry.utils.getProperty(this.document, this.options.textKeyPath) ?? "";
  14864. return foundry.utils.mergeObject(data, {
  14865. enriched: await TextEditor.enrichHTML(rawText, {
  14866. relativeTo: this.document, secrets: this.document.isOwner, async: true
  14867. })
  14868. });
  14869. }
  14870. /* -------------------------------------------- */
  14871. /** @inheritdoc */
  14872. _updateObject(event, formData) {
  14873. this.document.update(formData);
  14874. }
  14875. }
  14876. /**
  14877. * Journal entry page that displays an automatically generated summary of a class along with additional description.
  14878. */
  14879. class JournalClassPageSheet extends JournalPageSheet {
  14880. /** @inheritdoc */
  14881. static get defaultOptions() {
  14882. const options = foundry.utils.mergeObject(super.defaultOptions, {
  14883. dragDrop: [{dropSelector: ".drop-target"}],
  14884. submitOnChange: true
  14885. });
  14886. options.classes.push("class-journal");
  14887. return options;
  14888. }
  14889. /* -------------------------------------------- */
  14890. /** @inheritdoc */
  14891. get template() {
  14892. return `systems/dnd5e/templates/journal/page-class-${this.isEditable ? "edit" : "view"}.hbs`;
  14893. }
  14894. /* -------------------------------------------- */
  14895. /** @inheritdoc */
  14896. toc = {};
  14897. /* -------------------------------------------- */
  14898. /** @inheritdoc */
  14899. async getData(options) {
  14900. const context = super.getData(options);
  14901. context.system = context.document.system;
  14902. context.title = Object.fromEntries(
  14903. Array.fromRange(4, 1).map(n => [`level${n}`, context.data.title.level + n - 1])
  14904. );
  14905. const linked = await fromUuid(this.document.system.item);
  14906. context.subclasses = await this._getSubclasses(this.document.system.subclassItems);
  14907. if ( !linked ) return context;
  14908. context.linked = {
  14909. document: linked,
  14910. name: linked.name,
  14911. lowercaseName: linked.name.toLowerCase()
  14912. };
  14913. context.advancement = this._getAdvancement(linked);
  14914. context.enriched = await this._getDescriptions(context.document);
  14915. context.table = await this._getTable(linked);
  14916. context.optionalTable = await this._getOptionalTable(linked);
  14917. context.features = await this._getFeatures(linked);
  14918. context.optionalFeatures = await this._getFeatures(linked, true);
  14919. context.subclasses?.sort((lhs, rhs) => lhs.name.localeCompare(rhs.name));
  14920. return context;
  14921. }
  14922. /* -------------------------------------------- */
  14923. /**
  14924. * Prepare features granted by various advancement types.
  14925. * @param {Item5e} item Class item belonging to this journal.
  14926. * @returns {object} Prepared advancement section.
  14927. */
  14928. _getAdvancement(item) {
  14929. const advancement = {};
  14930. const hp = item.advancement.byType.HitPoints?.[0];
  14931. if ( hp ) {
  14932. advancement.hp = {
  14933. hitDice: `1${hp.hitDie}`,
  14934. max: hp.hitDieValue,
  14935. average: Math.floor(hp.hitDieValue / 2) + 1
  14936. };
  14937. }
  14938. return advancement;
  14939. }
  14940. /* -------------------------------------------- */
  14941. /**
  14942. * Enrich all of the entries within the descriptions object on the sheet's system data.
  14943. * @param {JournalEntryPage} page Journal page being enriched.
  14944. * @returns {Promise<object>} Object with enriched descriptions.
  14945. */
  14946. async _getDescriptions(page) {
  14947. const descriptions = await Promise.all(Object.entries(page.system.description ?? {})
  14948. .map(async ([id, text]) => {
  14949. const enriched = await TextEditor.enrichHTML(text, {
  14950. relativeTo: this.object,
  14951. secrets: this.object.isOwner,
  14952. async: true
  14953. });
  14954. return [id, enriched];
  14955. })
  14956. );
  14957. return Object.fromEntries(descriptions);
  14958. }
  14959. /* -------------------------------------------- */
  14960. /**
  14961. * Prepare table based on non-optional GrantItem advancement & ScaleValue advancement.
  14962. * @param {Item5e} item Class item belonging to this journal.
  14963. * @param {number} [initialLevel=1] Level at which the table begins.
  14964. * @returns {object} Prepared table.
  14965. */
  14966. async _getTable(item, initialLevel=1) {
  14967. const hasFeatures = !!item.advancement.byType.ItemGrant;
  14968. const scaleValues = (item.advancement.byType.ScaleValue ?? []);
  14969. const spellProgression = await this._getSpellProgression(item);
  14970. const headers = [[{content: game.i18n.localize("DND5E.Level")}]];
  14971. if ( item.type === "class" ) headers[0].push({content: game.i18n.localize("DND5E.ProficiencyBonus")});
  14972. if ( hasFeatures ) headers[0].push({content: game.i18n.localize("DND5E.Features")});
  14973. headers[0].push(...scaleValues.map(a => ({content: a.title})));
  14974. if ( spellProgression ) {
  14975. if ( spellProgression.headers.length > 1 ) {
  14976. headers[0].forEach(h => h.rowSpan = 2);
  14977. headers[0].push(...spellProgression.headers[0]);
  14978. headers[1] = spellProgression.headers[1];
  14979. } else {
  14980. headers[0].push(...spellProgression.headers[0]);
  14981. }
  14982. }
  14983. const cols = [{ class: "level", span: 1 }];
  14984. if ( item.type === "class" ) cols.push({class: "prof", span: 1});
  14985. if ( hasFeatures ) cols.push({class: "features", span: 1});
  14986. if ( scaleValues.length ) cols.push({class: "scale", span: scaleValues.length});
  14987. if ( spellProgression ) cols.push(...spellProgression.cols);
  14988. const makeLink = async uuid => (await fromUuid(uuid))?.toAnchor({classes: ["content-link"]}).outerHTML;
  14989. const rows = [];
  14990. for ( const level of Array.fromRange((CONFIG.DND5E.maxLevel - (initialLevel - 1)), initialLevel) ) {
  14991. const features = [];
  14992. for ( const advancement of item.advancement.byLevel[level] ) {
  14993. switch ( advancement.constructor.typeName ) {
  14994. case "ItemGrant":
  14995. if ( advancement.configuration.optional ) continue;
  14996. features.push(...await Promise.all(advancement.configuration.items.map(makeLink)));
  14997. break;
  14998. }
  14999. }
  15000. // Level & proficiency bonus
  15001. const cells = [{class: "level", content: level.ordinalString()}];
  15002. if ( item.type === "class" ) cells.push({class: "prof", content: `+${Proficiency.calculateMod(level)}`});
  15003. if ( hasFeatures ) cells.push({class: "features", content: features.join(", ")});
  15004. scaleValues.forEach(s => cells.push({class: "scale", content: s.valueForLevel(level)?.display}));
  15005. const spellCells = spellProgression?.rows[rows.length];
  15006. if ( spellCells ) cells.push(...spellCells);
  15007. // Skip empty rows on subclasses
  15008. if ( (item.type === "subclass") && !features.length && !scaleValues.length && !spellCells ) continue;
  15009. rows.push(cells);
  15010. }
  15011. return { headers, cols, rows };
  15012. }
  15013. /* -------------------------------------------- */
  15014. /**
  15015. * Build out the spell progression data.
  15016. * @param {Item5e} item Class item belonging to this journal.
  15017. * @returns {object} Prepared spell progression table.
  15018. */
  15019. async _getSpellProgression(item) {
  15020. const spellcasting = foundry.utils.deepClone(item.spellcasting);
  15021. if ( !spellcasting || (spellcasting.progression === "none") ) return null;
  15022. const table = { rows: [] };
  15023. if ( spellcasting.type === "leveled" ) {
  15024. const spells = {};
  15025. const maxSpellLevel = CONFIG.DND5E.SPELL_SLOT_TABLE[CONFIG.DND5E.SPELL_SLOT_TABLE.length - 1].length;
  15026. Array.fromRange(maxSpellLevel, 1).forEach(l => spells[`spell${l}`] = {});
  15027. let largestSlot;
  15028. for ( const level of Array.fromRange(CONFIG.DND5E.maxLevel, 1).reverse() ) {
  15029. const progression = { slot: 0 };
  15030. spellcasting.levels = level;
  15031. Actor5e.computeClassProgression(progression, item, { spellcasting });
  15032. Actor5e.prepareSpellcastingSlots(spells, "leveled", progression);
  15033. if ( !largestSlot ) largestSlot = Object.entries(spells).reduce((slot, [key, data]) => {
  15034. if ( !data.max ) return slot;
  15035. const level = parseInt(key.slice(5));
  15036. if ( !Number.isNaN(level) && (level > slot) ) return level;
  15037. return slot;
  15038. }, -1);
  15039. table.rows.push(Array.fromRange(largestSlot, 1).map(spellLevel => {
  15040. return {class: "spell-slots", content: spells[`spell${spellLevel}`]?.max || "&mdash;"};
  15041. }));
  15042. }
  15043. // Prepare headers & columns
  15044. table.headers = [
  15045. [{content: game.i18n.localize("JOURNALENTRYPAGE.DND5E.Class.SpellSlotsPerSpellLevel"), colSpan: largestSlot}],
  15046. Array.fromRange(largestSlot, 1).map(spellLevel => ({content: spellLevel.ordinalString()}))
  15047. ];
  15048. table.cols = [{class: "spellcasting", span: largestSlot}];
  15049. table.rows.reverse();
  15050. }
  15051. else if ( spellcasting.type === "pact" ) {
  15052. const spells = { pact: {} };
  15053. table.headers = [[
  15054. { content: game.i18n.localize("JOURNALENTRYPAGE.DND5E.Class.SpellSlots") },
  15055. { content: game.i18n.localize("JOURNALENTRYPAGE.DND5E.Class.SpellSlotLevel") }
  15056. ]];
  15057. table.cols = [{class: "spellcasting", span: 2}];
  15058. // Loop through each level, gathering "Spell Slots" & "Slot Level" for each one
  15059. for ( const level of Array.fromRange(CONFIG.DND5E.maxLevel, 1) ) {
  15060. const progression = { pact: 0 };
  15061. spellcasting.levels = level;
  15062. Actor5e.computeClassProgression(progression, item, { spellcasting });
  15063. Actor5e.prepareSpellcastingSlots(spells, "pact", progression);
  15064. table.rows.push([
  15065. { class: "spell-slots", content: `${spells.pact.max}` },
  15066. { class: "slot-level", content: spells.pact.level.ordinalString() }
  15067. ]);
  15068. }
  15069. }
  15070. else {
  15071. /**
  15072. * A hook event that fires to generate the table for custom spellcasting types.
  15073. * The actual hook names include the spellcasting type (e.g. `dnd5e.buildPsionicSpellcastingTable`).
  15074. * @param {object} table Table definition being built. *Will be mutated.*
  15075. * @param {Item5e} item Class for which the spellcasting table is being built.
  15076. * @param {SpellcastingDescription} spellcasting Spellcasting descriptive object.
  15077. * @function dnd5e.buildSpellcastingTable
  15078. * @memberof hookEvents
  15079. */
  15080. Hooks.callAll(
  15081. `dnd5e.build${spellcasting.type.capitalize()}SpellcastingTable`, table, item, spellcasting
  15082. );
  15083. }
  15084. return table;
  15085. }
  15086. /* -------------------------------------------- */
  15087. /**
  15088. * Prepare options table based on optional GrantItem advancement.
  15089. * @param {Item5e} item Class item belonging to this journal.
  15090. * @returns {object|null} Prepared optional features table.
  15091. */
  15092. async _getOptionalTable(item) {
  15093. const headers = [[
  15094. { content: game.i18n.localize("DND5E.Level") },
  15095. { content: game.i18n.localize("DND5E.Features") }
  15096. ]];
  15097. const cols = [
  15098. { class: "level", span: 1 },
  15099. { class: "features", span: 1 }
  15100. ];
  15101. const makeLink = async uuid => (await fromUuid(uuid))?.toAnchor({classes: ["content-link"]}).outerHTML;
  15102. const rows = [];
  15103. for ( const level of Array.fromRange(CONFIG.DND5E.maxLevel, 1) ) {
  15104. const features = [];
  15105. for ( const advancement of item.advancement.byLevel[level] ) {
  15106. switch ( advancement.constructor.typeName ) {
  15107. case "ItemGrant":
  15108. if ( !advancement.configuration.optional ) continue;
  15109. features.push(...await Promise.all(advancement.configuration.items.map(makeLink)));
  15110. break;
  15111. }
  15112. }
  15113. if ( !features.length ) continue;
  15114. // Level & proficiency bonus
  15115. const cells = [
  15116. { class: "level", content: level.ordinalString() },
  15117. { class: "features", content: features.join(", ") }
  15118. ];
  15119. rows.push(cells);
  15120. }
  15121. if ( !rows.length ) return null;
  15122. return { headers, cols, rows };
  15123. }
  15124. /* -------------------------------------------- */
  15125. /**
  15126. * Fetch data for each class feature listed.
  15127. * @param {Item5e} item Class or subclass item belonging to this journal.
  15128. * @param {boolean} [optional=false] Should optional features be fetched rather than required features?
  15129. * @returns {object[]} Prepared features.
  15130. */
  15131. async _getFeatures(item, optional=false) {
  15132. const prepareFeature = async uuid => {
  15133. const document = await fromUuid(uuid);
  15134. return {
  15135. document,
  15136. name: document.name,
  15137. description: await TextEditor.enrichHTML(document.system.description.value, {
  15138. relativeTo: item, secrets: false, async: true
  15139. })
  15140. };
  15141. };
  15142. let features = [];
  15143. for ( const advancement of item.advancement.byType.ItemGrant ?? [] ) {
  15144. if ( !!advancement.configuration.optional !== optional ) continue;
  15145. features.push(...advancement.configuration.items.map(prepareFeature));
  15146. }
  15147. features = await Promise.all(features);
  15148. return features;
  15149. }
  15150. /* -------------------------------------------- */
  15151. /**
  15152. * Fetch each subclass and their features.
  15153. * @param {string[]} uuids UUIDs for the subclasses to fetch.
  15154. * @returns {object[]|null} Prepared subclasses.
  15155. */
  15156. async _getSubclasses(uuids) {
  15157. const prepareSubclass = async uuid => {
  15158. const document = await fromUuid(uuid);
  15159. return this._getSubclass(document);
  15160. };
  15161. const subclasses = await Promise.all(uuids.map(prepareSubclass));
  15162. return subclasses.length ? subclasses : null;
  15163. }
  15164. /* -------------------------------------------- */
  15165. /**
  15166. * Prepare data for the provided subclass.
  15167. * @param {Item5e} item Subclass item being prepared.
  15168. * @returns {object} Presentation data for this subclass.
  15169. */
  15170. async _getSubclass(item) {
  15171. const initialLevel = Object.entries(item.advancement.byLevel).find(([lvl, d]) => d.length)?.[0] ?? 1;
  15172. return {
  15173. document: item,
  15174. name: item.name,
  15175. description: await TextEditor.enrichHTML(item.system.description.value, {
  15176. relativeTo: item, secrets: false, async: true
  15177. }),
  15178. features: await this._getFeatures(item),
  15179. table: await this._getTable(item, parseInt(initialLevel))
  15180. };
  15181. }
  15182. /* -------------------------------------------- */
  15183. /* Rendering */
  15184. /* -------------------------------------------- */
  15185. /** @inheritdoc */
  15186. async _renderInner(...args) {
  15187. const html = await super._renderInner(...args);
  15188. this.toc = JournalEntryPage.buildTOC(html.get());
  15189. return html;
  15190. }
  15191. /* -------------------------------------------- */
  15192. /* Event Handlers */
  15193. /* -------------------------------------------- */
  15194. /** @inheritdoc */
  15195. activateListeners(html) {
  15196. super.activateListeners(html);
  15197. html[0].querySelectorAll(".item-delete").forEach(e => {
  15198. e.addEventListener("click", this._onDeleteItem.bind(this));
  15199. });
  15200. html[0].querySelectorAll(".launch-text-editor").forEach(e => {
  15201. e.addEventListener("click", this._onLaunchTextEditor.bind(this));
  15202. });
  15203. }
  15204. /* -------------------------------------------- */
  15205. /**
  15206. * Handle deleting a dropped item.
  15207. * @param {Event} event The triggering click event.
  15208. * @returns {JournalClassSummary5ePageSheet}
  15209. */
  15210. async _onDeleteItem(event) {
  15211. event.preventDefault();
  15212. const container = event.currentTarget.closest("[data-item-uuid]");
  15213. const uuidToDelete = container?.dataset.itemUuid;
  15214. if ( !uuidToDelete ) return;
  15215. switch (container.dataset.itemType) {
  15216. case "class":
  15217. await this.document.update({"system.item": ""});
  15218. return this.render();
  15219. case "subclass":
  15220. const itemSet = this.document.system.subclassItems;
  15221. itemSet.delete(uuidToDelete);
  15222. await this.document.update({"system.subclassItems": Array.from(itemSet)});
  15223. return this.render();
  15224. }
  15225. }
  15226. /* -------------------------------------------- */
  15227. /**
  15228. * Handle launching the individual text editing window.
  15229. * @param {Event} event The triggering click event.
  15230. */
  15231. _onLaunchTextEditor(event) {
  15232. event.preventDefault();
  15233. const textKeyPath = event.currentTarget.dataset.target;
  15234. const label = event.target.closest(".form-group").querySelector("label");
  15235. const editor = new JournalEditor(this.document, { textKeyPath, title: label?.innerText });
  15236. editor.render(true);
  15237. }
  15238. /* -------------------------------------------- */
  15239. /** @inheritdoc */
  15240. async _onDrop(event) {
  15241. const data = TextEditor.getDragEventData(event);
  15242. if ( data?.type !== "Item" ) return false;
  15243. const item = await Item.implementation.fromDropData(data);
  15244. switch ( item.type ) {
  15245. case "class":
  15246. await this.document.update({"system.item": item.uuid});
  15247. return this.render();
  15248. case "subclass":
  15249. const itemSet = this.document.system.subclassItems;
  15250. itemSet.add(item.uuid);
  15251. await this.document.update({"system.subclassItems": Array.from(itemSet)});
  15252. return this.render();
  15253. default:
  15254. return false;
  15255. }
  15256. }
  15257. }
  15258. class SRDCompendium extends Compendium {
  15259. /** @inheritdoc */
  15260. static get defaultOptions() {
  15261. return foundry.utils.mergeObject(super.defaultOptions, {
  15262. classes: ["srd-compendium"],
  15263. template: "systems/dnd5e/templates/journal/srd-compendium.hbs",
  15264. width: 800,
  15265. height: 950,
  15266. resizable: true
  15267. });
  15268. }
  15269. /* -------------------------------------------- */
  15270. /**
  15271. * The IDs of some special pages that we use when configuring the display of the compendium.
  15272. * @type {Object<string>}
  15273. * @protected
  15274. */
  15275. static _SPECIAL_PAGES = {
  15276. disclaimer: "xxt7YT2t76JxNTel",
  15277. magicItemList: "sfJtvPjEs50Ruzi4",
  15278. spellList: "plCB5ei1JbVtBseb"
  15279. };
  15280. /* -------------------------------------------- */
  15281. /** @inheritdoc */
  15282. async getData(options) {
  15283. const data = await super.getData(options);
  15284. const documents = await this.collection.getDocuments();
  15285. const getOrder = o => ({chapter: 0, appendix: 100}[o.flags?.dnd5e?.type] ?? 200) + (o.flags?.dnd5e?.position ?? 0);
  15286. data.disclaimer = this.collection.get(this.constructor._SPECIAL_PAGES.disclaimer).pages.contents[0].text.content;
  15287. data.chapters = documents.reduce((arr, entry) => {
  15288. const type = entry.getFlag("dnd5e", "type");
  15289. if ( !type ) return arr;
  15290. const e = entry.toObject();
  15291. e.showPages = (e.pages.length > 1) && (type === "chapter");
  15292. arr.push(e);
  15293. return arr;
  15294. }, []).sort((a, b) => getOrder(a) - getOrder(b));
  15295. // Add spells A-Z to the end of Chapter 10.
  15296. const spellList = this.collection.get(this.constructor._SPECIAL_PAGES.spellList);
  15297. data.chapters[9].pages.push({_id: spellList.id, name: spellList.name, entry: true});
  15298. // Add magic items A-Z to the end of Chapter 11.
  15299. const magicItemList = this.collection.get(this.constructor._SPECIAL_PAGES.magicItemList);
  15300. data.chapters[10].pages.push({_id: magicItemList.id, name: magicItemList.name, entry: true});
  15301. return data;
  15302. }
  15303. /* -------------------------------------------- */
  15304. /** @inheritdoc */
  15305. activateListeners(html) {
  15306. super.activateListeners(html);
  15307. html.find("a").on("click", this._onClickLink.bind(this));
  15308. }
  15309. /* -------------------------------------------- */
  15310. /**
  15311. * Handle clicking a link to a journal entry or page.
  15312. * @param {MouseEvent} event The triggering click event.
  15313. * @protected
  15314. */
  15315. async _onClickLink(event) {
  15316. const target = event.currentTarget;
  15317. const entryId = target.closest("[data-entry-id]")?.dataset.entryId;
  15318. const pageId = target.closest("[data-page-id]")?.dataset.pageId;
  15319. if ( !entryId ) return;
  15320. const options = {};
  15321. if ( pageId ) options.pageId = pageId;
  15322. const entry = await this.collection.getDocument(entryId);
  15323. entry?.sheet.render(true, options);
  15324. }
  15325. }
  15326. var _module$5 = /*#__PURE__*/Object.freeze({
  15327. __proto__: null,
  15328. JournalClassPageSheet: JournalClassPageSheet,
  15329. JournalEditor: JournalEditor,
  15330. SRDCompendium: SRDCompendium
  15331. });
  15332. /**
  15333. * @deprecated since dnd5e 2.1, targeted for removal in 2.3
  15334. */
  15335. class DamageTraitSelector extends TraitSelector {
  15336. /** @inheritdoc */
  15337. static get defaultOptions() {
  15338. return foundry.utils.mergeObject(super.defaultOptions, {
  15339. template: "systems/dnd5e/templates/apps/damage-trait-selector.hbs"
  15340. });
  15341. }
  15342. /* -------------------------------------------- */
  15343. /** @override */
  15344. getData() {
  15345. const data = super.getData();
  15346. const attr = foundry.utils.getProperty(this.object, this.attribute);
  15347. data.bypasses = Object.entries(this.options.bypasses).reduce((obj, [k, v]) => {
  15348. obj[k] = { label: v, chosen: attr ? attr.bypasses.includes(k) : false };
  15349. return obj;
  15350. }, {});
  15351. return data;
  15352. }
  15353. /* -------------------------------------------- */
  15354. /** @override */
  15355. async _updateObject(event, formData) {
  15356. const data = foundry.utils.expandObject(formData);
  15357. const updateData = this._prepareUpdateData(data.choices);
  15358. if ( !updateData ) return;
  15359. updateData[`${this.attribute}.bypasses`] = Object.entries(data.bypasses).filter(([, v]) => v).map(([k]) => k);
  15360. this.object.update(updateData);
  15361. }
  15362. }
  15363. /**
  15364. * An application for selecting proficiencies with categories that can contain children.
  15365. * @deprecated since dnd5e 2.1, targeted for removal in 2.3
  15366. */
  15367. class ProficiencySelector extends TraitSelector {
  15368. /** @inheritdoc */
  15369. static get defaultOptions() {
  15370. return foundry.utils.mergeObject(super.defaultOptions, {
  15371. title: "Actor Proficiency Selection",
  15372. type: ""
  15373. });
  15374. }
  15375. /* -------------------------------------------- */
  15376. /** @inheritdoc */
  15377. async getData() {
  15378. const attr = foundry.utils.getProperty(this.object, this.attribute);
  15379. const chosen = (this.options.valueKey) ? foundry.utils.getProperty(attr, this.options.valueKey) ?? [] : attr;
  15380. const data = super.getData();
  15381. data.choices = await choices(this.options.type, chosen);
  15382. return data;
  15383. }
  15384. /* -------------------------------------------- */
  15385. /**
  15386. * A static helper method to get a list of choices for a proficiency type.
  15387. *
  15388. * @param {string} type Proficiency type to select, either `armor`, `tool`, or `weapon`.
  15389. * @param {string[]} [chosen] Optional list of items to be marked as chosen.
  15390. * @returns {Object<string, SelectChoices>} Object mapping proficiency ids to choice objects.
  15391. * @deprecated since dnd5e 2.1, targeted for removal in 2.3
  15392. */
  15393. static async getChoices(type, chosen=[]) {
  15394. foundry.utils.logCompatibilityWarning(
  15395. "ProficiencySelector#getChoices has been deprecated in favor of Trait#choices.",
  15396. { since: "DnD5e 2.1", until: "DnD5e 2.3" }
  15397. );
  15398. return choices(type, chosen);
  15399. }
  15400. /* -------------------------------------------- */
  15401. /**
  15402. * Fetch an item for the provided ID. If the provided ID contains a compendium pack name
  15403. * it will be fetched from that pack, otherwise it will be fetched from the compendium defined
  15404. * in `DND5E.sourcePacks.ITEMS`.
  15405. *
  15406. * @param {string} identifier Simple ID or compendium name and ID separated by a dot.
  15407. * @param {object} [options]
  15408. * @param {boolean} [options.indexOnly] If set to true, only the index data will be fetched (will never return
  15409. * Promise).
  15410. * @param {boolean} [options.fullItem] If set to true, the full item will be returned as long as `indexOnly` is
  15411. * false.
  15412. * @returns {Promise<Item5e>|object} Promise for a `Document` if `indexOnly` is false & `fullItem` is true,
  15413. * otherwise else a simple object containing the minimal index data.
  15414. * @deprecated since dnd5e 2.1, targeted for removal in 2.3
  15415. */
  15416. static getBaseItem(identifier, options) {
  15417. foundry.utils.logCompatibilityWarning(
  15418. "ProficiencySelector#getBaseItem has been deprecated in favor of Trait#getBaseItem.",
  15419. { since: "DnD5e 2.1", until: "DnD5e 2.3" }
  15420. );
  15421. return getBaseItem(identifier, options);
  15422. }
  15423. /* -------------------------------------------- */
  15424. /** @inheritdoc */
  15425. activateListeners(html) {
  15426. super.activateListeners(html);
  15427. for ( const checkbox of html[0].querySelectorAll("input[type='checkbox']") ) {
  15428. if ( checkbox.checked ) this._onToggleCategory(checkbox);
  15429. }
  15430. }
  15431. /* -------------------------------------------- */
  15432. /** @inheritdoc */
  15433. async _onChangeInput(event) {
  15434. super._onChangeInput(event);
  15435. if ( event.target.tagName === "INPUT" ) this._onToggleCategory(event.target);
  15436. }
  15437. /* -------------------------------------------- */
  15438. /**
  15439. * Enable/disable all children when a category is checked.
  15440. *
  15441. * @param {HTMLElement} checkbox Checkbox that was changed.
  15442. * @private
  15443. */
  15444. _onToggleCategory(checkbox) {
  15445. const children = checkbox.closest("li")?.querySelector("ol");
  15446. if ( !children ) return;
  15447. for ( const child of children.querySelectorAll("input[type='checkbox']") ) {
  15448. child.checked = child.disabled = checkbox.checked;
  15449. }
  15450. }
  15451. }
  15452. var applications = /*#__PURE__*/Object.freeze({
  15453. __proto__: null,
  15454. DamageTraitSelector: DamageTraitSelector,
  15455. ProficiencySelector: ProficiencySelector,
  15456. PropertyAttribution: PropertyAttribution,
  15457. TraitSelector: TraitSelector,
  15458. actor: _module$9,
  15459. advancement: _module$8,
  15460. combat: _module$7,
  15461. item: _module$6,
  15462. journal: _module$5
  15463. });
  15464. /**
  15465. * A helper class for building MeasuredTemplates for 5e spells and abilities
  15466. */
  15467. class AbilityTemplate extends MeasuredTemplate {
  15468. /**
  15469. * Track the timestamp when the last mouse move event was captured.
  15470. * @type {number}
  15471. */
  15472. #moveTime = 0;
  15473. /* -------------------------------------------- */
  15474. /**
  15475. * The initially active CanvasLayer to re-activate after the workflow is complete.
  15476. * @type {CanvasLayer}
  15477. */
  15478. #initialLayer;
  15479. /* -------------------------------------------- */
  15480. /**
  15481. * Track the bound event handlers so they can be properly canceled later.
  15482. * @type {object}
  15483. */
  15484. #events;
  15485. /* -------------------------------------------- */
  15486. /**
  15487. * A factory method to create an AbilityTemplate instance using provided data from an Item5e instance
  15488. * @param {Item5e} item The Item object for which to construct the template
  15489. * @returns {AbilityTemplate|null} The template object, or null if the item does not produce a template
  15490. */
  15491. static fromItem(item) {
  15492. const target = item.system.target ?? {};
  15493. const templateShape = dnd5e.config.areaTargetTypes[target.type]?.template;
  15494. if ( !templateShape ) return null;
  15495. // Prepare template data
  15496. const templateData = {
  15497. t: templateShape,
  15498. user: game.user.id,
  15499. distance: target.value,
  15500. direction: 0,
  15501. x: 0,
  15502. y: 0,
  15503. fillColor: game.user.color,
  15504. flags: { dnd5e: { origin: item.uuid } }
  15505. };
  15506. // Additional type-specific data
  15507. switch ( templateShape ) {
  15508. case "cone":
  15509. templateData.angle = CONFIG.MeasuredTemplate.defaults.angle;
  15510. break;
  15511. case "rect": // 5e rectangular AoEs are always cubes
  15512. templateData.distance = Math.hypot(target.value, target.value);
  15513. templateData.width = target.value;
  15514. templateData.direction = 45;
  15515. break;
  15516. case "ray": // 5e rays are most commonly 1 square (5 ft) in width
  15517. templateData.width = target.width ?? canvas.dimensions.distance;
  15518. break;
  15519. }
  15520. // Return the template constructed from the item data
  15521. const cls = CONFIG.MeasuredTemplate.documentClass;
  15522. const template = new cls(templateData, {parent: canvas.scene});
  15523. const object = new this(template);
  15524. object.item = item;
  15525. object.actorSheet = item.actor?.sheet || null;
  15526. return object;
  15527. }
  15528. /* -------------------------------------------- */
  15529. /**
  15530. * Creates a preview of the spell template.
  15531. * @returns {Promise} A promise that resolves with the final measured template if created.
  15532. */
  15533. drawPreview() {
  15534. const initialLayer = canvas.activeLayer;
  15535. // Draw the template and switch to the template layer
  15536. this.draw();
  15537. this.layer.activate();
  15538. this.layer.preview.addChild(this);
  15539. // Hide the sheet that originated the preview
  15540. this.actorSheet?.minimize();
  15541. // Activate interactivity
  15542. return this.activatePreviewListeners(initialLayer);
  15543. }
  15544. /* -------------------------------------------- */
  15545. /**
  15546. * Activate listeners for the template preview
  15547. * @param {CanvasLayer} initialLayer The initially active CanvasLayer to re-activate after the workflow is complete
  15548. * @returns {Promise} A promise that resolves with the final measured template if created.
  15549. */
  15550. activatePreviewListeners(initialLayer) {
  15551. return new Promise((resolve, reject) => {
  15552. this.#initialLayer = initialLayer;
  15553. this.#events = {
  15554. cancel: this._onCancelPlacement.bind(this),
  15555. confirm: this._onConfirmPlacement.bind(this),
  15556. move: this._onMovePlacement.bind(this),
  15557. resolve,
  15558. reject,
  15559. rotate: this._onRotatePlacement.bind(this)
  15560. };
  15561. // Activate listeners
  15562. canvas.stage.on("mousemove", this.#events.move);
  15563. canvas.stage.on("mousedown", this.#events.confirm);
  15564. canvas.app.view.oncontextmenu = this.#events.cancel;
  15565. canvas.app.view.onwheel = this.#events.rotate;
  15566. });
  15567. }
  15568. /* -------------------------------------------- */
  15569. /**
  15570. * Shared code for when template placement ends by being confirmed or canceled.
  15571. * @param {Event} event Triggering event that ended the placement.
  15572. */
  15573. async _finishPlacement(event) {
  15574. this.layer._onDragLeftCancel(event);
  15575. canvas.stage.off("mousemove", this.#events.move);
  15576. canvas.stage.off("mousedown", this.#events.confirm);
  15577. canvas.app.view.oncontextmenu = null;
  15578. canvas.app.view.onwheel = null;
  15579. this.#initialLayer.activate();
  15580. await this.actorSheet?.maximize();
  15581. }
  15582. /* -------------------------------------------- */
  15583. /**
  15584. * Move the template preview when the mouse moves.
  15585. * @param {Event} event Triggering mouse event.
  15586. */
  15587. _onMovePlacement(event) {
  15588. event.stopPropagation();
  15589. const now = Date.now(); // Apply a 20ms throttle
  15590. if ( now - this.#moveTime <= 20 ) return;
  15591. const center = event.data.getLocalPosition(this.layer);
  15592. const interval = canvas.grid.type === CONST.GRID_TYPES.GRIDLESS ? 0 : 2;
  15593. const snapped = canvas.grid.getSnappedPosition(center.x, center.y, interval);
  15594. this.document.updateSource({x: snapped.x, y: snapped.y});
  15595. this.refresh();
  15596. this.#moveTime = now;
  15597. }
  15598. /* -------------------------------------------- */
  15599. /**
  15600. * Rotate the template preview by 3˚ increments when the mouse wheel is rotated.
  15601. * @param {Event} event Triggering mouse event.
  15602. */
  15603. _onRotatePlacement(event) {
  15604. if ( event.ctrlKey ) event.preventDefault(); // Avoid zooming the browser window
  15605. event.stopPropagation();
  15606. const delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15;
  15607. const snap = event.shiftKey ? delta : 5;
  15608. const update = {direction: this.document.direction + (snap * Math.sign(event.deltaY))};
  15609. this.document.updateSource(update);
  15610. this.refresh();
  15611. }
  15612. /* -------------------------------------------- */
  15613. /**
  15614. * Confirm placement when the left mouse button is clicked.
  15615. * @param {Event} event Triggering mouse event.
  15616. */
  15617. async _onConfirmPlacement(event) {
  15618. await this._finishPlacement(event);
  15619. const interval = canvas.grid.type === CONST.GRID_TYPES.GRIDLESS ? 0 : 2;
  15620. const destination = canvas.grid.getSnappedPosition(this.document.x, this.document.y, interval);
  15621. this.document.updateSource(destination);
  15622. this.#events.resolve(canvas.scene.createEmbeddedDocuments("MeasuredTemplate", [this.document.toObject()]));
  15623. }
  15624. /* -------------------------------------------- */
  15625. /**
  15626. * Cancel placement when the right mouse button is clicked.
  15627. * @param {Event} event Triggering mouse event.
  15628. */
  15629. async _onCancelPlacement(event) {
  15630. await this._finishPlacement(event);
  15631. this.#events.reject();
  15632. }
  15633. }
  15634. /**
  15635. * Extend the base Token class to implement additional system-specific logic.
  15636. */
  15637. class Token5e extends Token {
  15638. /** @inheritdoc */
  15639. _drawBar(number, bar, data) {
  15640. if ( data.attribute === "attributes.hp" ) return this._drawHPBar(number, bar, data);
  15641. return super._drawBar(number, bar, data);
  15642. }
  15643. /* -------------------------------------------- */
  15644. /**
  15645. * Specialized drawing function for HP bars.
  15646. * @param {number} number The Bar number
  15647. * @param {PIXI.Graphics} bar The Bar container
  15648. * @param {object} data Resource data for this bar
  15649. * @private
  15650. */
  15651. _drawHPBar(number, bar, data) {
  15652. // Extract health data
  15653. let {value, max, temp, tempmax} = this.document.actor.system.attributes.hp;
  15654. temp = Number(temp || 0);
  15655. tempmax = Number(tempmax || 0);
  15656. // Differentiate between effective maximum and displayed maximum
  15657. const effectiveMax = Math.max(0, max + tempmax);
  15658. let displayMax = max + (tempmax > 0 ? tempmax : 0);
  15659. // Allocate percentages of the total
  15660. const tempPct = Math.clamped(temp, 0, displayMax) / displayMax;
  15661. const colorPct = Math.clamped(value, 0, effectiveMax) / displayMax;
  15662. const hpColor = dnd5e.documents.Actor5e.getHPColor(value, effectiveMax);
  15663. // Determine colors to use
  15664. const blk = 0x000000;
  15665. const c = CONFIG.DND5E.tokenHPColors;
  15666. // Determine the container size (logic borrowed from core)
  15667. const w = this.w;
  15668. let h = Math.max((canvas.dimensions.size / 12), 8);
  15669. if ( this.document.height >= 2 ) h *= 1.6;
  15670. const bs = Math.clamped(h / 8, 1, 2);
  15671. const bs1 = bs+1;
  15672. // Overall bar container
  15673. bar.clear();
  15674. bar.beginFill(blk, 0.5).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, w, h, 3);
  15675. // Temporary maximum HP
  15676. if (tempmax > 0) {
  15677. const pct = max / effectiveMax;
  15678. bar.beginFill(c.tempmax, 1.0).lineStyle(1, blk, 1.0).drawRoundedRect(pct*w, 0, (1-pct)*w, h, 2);
  15679. }
  15680. // Maximum HP penalty
  15681. else if (tempmax < 0) {
  15682. const pct = (max + tempmax) / max;
  15683. bar.beginFill(c.negmax, 1.0).lineStyle(1, blk, 1.0).drawRoundedRect(pct*w, 0, (1-pct)*w, h, 2);
  15684. }
  15685. // Health bar
  15686. bar.beginFill(hpColor, 1.0).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, colorPct*w, h, 2);
  15687. // Temporary hit points
  15688. if ( temp > 0 ) {
  15689. bar.beginFill(c.temp, 1.0).lineStyle(0).drawRoundedRect(bs1, bs1, (tempPct*w)-(2*bs1), h-(2*bs1), 1);
  15690. }
  15691. // Set position
  15692. let posY = (number === 0) ? (this.h - h) : 0;
  15693. bar.position.set(0, posY);
  15694. }
  15695. }
  15696. /** @inheritDoc */
  15697. function measureDistances(segments, options={}) {
  15698. if ( !options.gridSpaces ) return BaseGrid.prototype.measureDistances.call(this, segments, options);
  15699. // Track the total number of diagonals
  15700. let nDiagonal = 0;
  15701. const rule = this.parent.diagonalRule;
  15702. const d = canvas.dimensions;
  15703. // Iterate over measured segments
  15704. return segments.map(s => {
  15705. let r = s.ray;
  15706. // Determine the total distance traveled
  15707. let nx = Math.abs(Math.ceil(r.dx / d.size));
  15708. let ny = Math.abs(Math.ceil(r.dy / d.size));
  15709. // Determine the number of straight and diagonal moves
  15710. let nd = Math.min(nx, ny);
  15711. let ns = Math.abs(ny - nx);
  15712. nDiagonal += nd;
  15713. // Alternative DMG Movement
  15714. if (rule === "5105") {
  15715. let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2);
  15716. let spaces = (nd10 * 2) + (nd - nd10) + ns;
  15717. return spaces * canvas.dimensions.distance;
  15718. }
  15719. // Euclidean Measurement
  15720. else if (rule === "EUCL") {
  15721. return Math.round(Math.hypot(nx, ny) * canvas.scene.grid.distance);
  15722. }
  15723. // Standard PHB Movement
  15724. else return (ns + nd) * canvas.scene.grid.distance;
  15725. });
  15726. }
  15727. var canvas$1 = /*#__PURE__*/Object.freeze({
  15728. __proto__: null,
  15729. AbilityTemplate: AbilityTemplate,
  15730. Token5e: Token5e,
  15731. measureDistances: measureDistances
  15732. });
  15733. /**
  15734. * Data Model variant with some extra methods to support template mix-ins.
  15735. *
  15736. * **Note**: This uses some advanced Javascript techniques that are not necessary for most data models.
  15737. * Please refer to the [advancement data models]{@link BaseAdvancement} for an example of a more typical usage.
  15738. *
  15739. * In template.json, each Actor or Item type can incorporate several templates which are chunks of data that are
  15740. * common across all the types that use them. One way to represent them in the schema for a given Document type is to
  15741. * duplicate schema definitions for the templates and write them directly into the Data Model for the Document type.
  15742. * This works fine for small templates or systems that do not need many Document types but for more complex systems
  15743. * this boilerplate can become prohibitive.
  15744. *
  15745. * Here we have opted to instead create a separate Data Model for each template available. These define their own
  15746. * schemas which are then mixed-in to the final schema for the Document type's Data Model. A Document type Data Model
  15747. * can define its own schema unique to it, and then add templates in direct correspondence to those in template.json
  15748. * via SystemDataModel.mixin.
  15749. */
  15750. class SystemDataModel extends foundry.abstract.DataModel {
  15751. /** @inheritdoc */
  15752. static _enableV10Validation = true;
  15753. /**
  15754. * System type that this system data model represents (e.g. "character", "npc", "vehicle").
  15755. * @type {string}
  15756. */
  15757. static _systemType;
  15758. /* -------------------------------------------- */
  15759. /**
  15760. * Base templates used for construction.
  15761. * @type {*[]}
  15762. * @private
  15763. */
  15764. static _schemaTemplates = [];
  15765. /* -------------------------------------------- */
  15766. /**
  15767. * A list of properties that should not be mixed-in to the final type.
  15768. * @type {Set<string>}
  15769. * @private
  15770. */
  15771. static _immiscible = new Set(["length", "mixed", "name", "prototype", "migrateData", "defineSchema"]);
  15772. /* -------------------------------------------- */
  15773. /** @inheritdoc */
  15774. static defineSchema() {
  15775. const schema = {};
  15776. for ( const template of this._schemaTemplates ) {
  15777. if ( !template.defineSchema ) {
  15778. throw new Error(`Invalid dnd5e template mixin ${template} defined on class ${this.constructor}`);
  15779. }
  15780. this.mergeSchema(schema, template.defineSchema());
  15781. }
  15782. return schema;
  15783. }
  15784. /* -------------------------------------------- */
  15785. /**
  15786. * Merge two schema definitions together as well as possible.
  15787. * @param {DataSchema} a First schema that forms the basis for the merge. *Will be mutated.*
  15788. * @param {DataSchema} b Second schema that will be merged in, overwriting any non-mergeable properties.
  15789. * @returns {DataSchema} Fully merged schema.
  15790. */
  15791. static mergeSchema(a, b) {
  15792. Object.assign(a, b);
  15793. return a;
  15794. }
  15795. /* -------------------------------------------- */
  15796. /** @inheritdoc */
  15797. static migrateData(source) {
  15798. for ( const template of this._schemaTemplates ) {
  15799. template.migrateData?.(source);
  15800. }
  15801. return super.migrateData(source);
  15802. }
  15803. /* -------------------------------------------- */
  15804. /** @inheritdoc */
  15805. validate(options={}) {
  15806. if ( this.constructor._enableV10Validation === false ) return true;
  15807. return super.validate(options);
  15808. }
  15809. /* -------------------------------------------- */
  15810. /**
  15811. * Mix multiple templates with the base type.
  15812. * @param {...*} templates Template classes to mix.
  15813. * @returns {typeof SystemDataModel} Final prepared type.
  15814. */
  15815. static mixin(...templates) {
  15816. const Base = class extends this {};
  15817. Object.defineProperty(Base, "_schemaTemplates", {
  15818. value: Object.seal([...this._schemaTemplates, ...templates]),
  15819. writable: false,
  15820. configurable: false
  15821. });
  15822. for ( const template of templates ) {
  15823. // Take all static methods and fields from template and mix in to base class
  15824. for ( const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(template)) ) {
  15825. if ( this._immiscible.has(key) ) continue;
  15826. Object.defineProperty(Base, key, descriptor);
  15827. }
  15828. // Take all instance methods and fields from template and mix in to base class
  15829. for ( const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(template.prototype)) ) {
  15830. if ( ["constructor"].includes(key) ) continue;
  15831. Object.defineProperty(Base.prototype, key, descriptor);
  15832. }
  15833. }
  15834. return Base;
  15835. }
  15836. }
  15837. /**
  15838. * Shared contents of the attributes schema between various actor types.
  15839. */
  15840. class AttributesFields {
  15841. /**
  15842. * Fields shared between characters, NPCs, and vehicles.
  15843. *
  15844. * @type {object}
  15845. * @property {object} init
  15846. * @property {number} init.value Calculated initiative modifier.
  15847. * @property {number} init.bonus Fixed bonus provided to initiative rolls.
  15848. * @property {object} movement
  15849. * @property {number} movement.burrow Actor burrowing speed.
  15850. * @property {number} movement.climb Actor climbing speed.
  15851. * @property {number} movement.fly Actor flying speed.
  15852. * @property {number} movement.swim Actor swimming speed.
  15853. * @property {number} movement.walk Actor walking speed.
  15854. * @property {string} movement.units Movement used to measure the various speeds.
  15855. * @property {boolean} movement.hover Is this flying creature able to hover in place.
  15856. */
  15857. static get common() {
  15858. return {
  15859. init: new foundry.data.fields.SchemaField({
  15860. ability: new foundry.data.fields.StringField({label: "DND5E.AbilityModifier"}),
  15861. bonus: new FormulaField({label: "DND5E.InitiativeBonus"})
  15862. }, { label: "DND5E.Initiative" }),
  15863. movement: new foundry.data.fields.SchemaField({
  15864. burrow: new foundry.data.fields.NumberField({
  15865. nullable: false, min: 0, step: 0.1, initial: 0, label: "DND5E.MovementBurrow"
  15866. }),
  15867. climb: new foundry.data.fields.NumberField({
  15868. nullable: false, min: 0, step: 0.1, initial: 0, label: "DND5E.MovementClimb"
  15869. }),
  15870. fly: new foundry.data.fields.NumberField({
  15871. nullable: false, min: 0, step: 0.1, initial: 0, label: "DND5E.MovementFly"
  15872. }),
  15873. swim: new foundry.data.fields.NumberField({
  15874. nullable: false, min: 0, step: 0.1, initial: 0, label: "DND5E.MovementSwim"
  15875. }),
  15876. walk: new foundry.data.fields.NumberField({
  15877. nullable: false, min: 0, step: 0.1, initial: 30, label: "DND5E.MovementWalk"
  15878. }),
  15879. units: new foundry.data.fields.StringField({initial: "ft", label: "DND5E.MovementUnits"}),
  15880. hover: new foundry.data.fields.BooleanField({label: "DND5E.MovementHover"})
  15881. }, {label: "DND5E.Movement"})
  15882. };
  15883. }
  15884. /* -------------------------------------------- */
  15885. /**
  15886. * Fields shared between characters and NPCs.
  15887. *
  15888. * @type {object}
  15889. * @property {object} attunement
  15890. * @property {number} attunement.max Maximum number of attuned items.
  15891. * @property {object} senses
  15892. * @property {number} senses.darkvision Creature's darkvision range.
  15893. * @property {number} senses.blindsight Creature's blindsight range.
  15894. * @property {number} senses.tremorsense Creature's tremorsense range.
  15895. * @property {number} senses.truesight Creature's truesight range.
  15896. * @property {string} senses.units Distance units used to measure senses.
  15897. * @property {string} senses.special Description of any special senses or restrictions.
  15898. * @property {string} spellcasting Primary spellcasting ability.
  15899. */
  15900. static get creature() {
  15901. return {
  15902. attunement: new foundry.data.fields.SchemaField({
  15903. max: new foundry.data.fields.NumberField({
  15904. required: true, nullable: false, integer: true, min: 0, initial: 3, label: "DND5E.AttunementMax"
  15905. })
  15906. }, {label: "DND5E.Attunement"}),
  15907. senses: new foundry.data.fields.SchemaField({
  15908. darkvision: new foundry.data.fields.NumberField({
  15909. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.SenseDarkvision"
  15910. }),
  15911. blindsight: new foundry.data.fields.NumberField({
  15912. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.SenseBlindsight"
  15913. }),
  15914. tremorsense: new foundry.data.fields.NumberField({
  15915. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.SenseTremorsense"
  15916. }),
  15917. truesight: new foundry.data.fields.NumberField({
  15918. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.SenseTruesight"
  15919. }),
  15920. units: new foundry.data.fields.StringField({required: true, initial: "ft", label: "DND5E.SenseUnits"}),
  15921. special: new foundry.data.fields.StringField({required: true, label: "DND5E.SenseSpecial"})
  15922. }, {label: "DND5E.Senses"}),
  15923. spellcasting: new foundry.data.fields.StringField({
  15924. required: true, blank: true, initial: "int", label: "DND5E.SpellAbility"
  15925. })
  15926. };
  15927. }
  15928. /* -------------------------------------------- */
  15929. /**
  15930. * Migrate the old init.value and incorporate it into init.bonus.
  15931. * @param {object} source The source attributes object.
  15932. * @internal
  15933. */
  15934. static _migrateInitiative(source) {
  15935. const init = source?.init;
  15936. if ( !init?.value || (typeof init?.bonus === "string") ) return;
  15937. if ( init.bonus ) init.bonus += init.value < 0 ? ` - ${init.value * -1}` : ` + ${init.value}`;
  15938. else init.bonus = `${init.value}`;
  15939. }
  15940. }
  15941. /**
  15942. * A template for currently held currencies.
  15943. *
  15944. * @property {object} currency Object containing currencies as numbers.
  15945. * @mixin
  15946. */
  15947. class CurrencyTemplate extends foundry.abstract.DataModel {
  15948. /** @inheritdoc */
  15949. static defineSchema() {
  15950. return {
  15951. currency: new MappingField(new foundry.data.fields.NumberField({
  15952. required: true, nullable: false, integer: true, min: 0, initial: 0
  15953. }), {initialKeys: CONFIG.DND5E.currencies, initialKeysOnly: true, label: "DND5E.Currency"})
  15954. };
  15955. }
  15956. }
  15957. /**
  15958. * @typedef {object} AbilityData
  15959. * @property {number} value Ability score.
  15960. * @property {number} proficient Proficiency value for saves.
  15961. * @property {object} bonuses Bonuses that modify ability checks and saves.
  15962. * @property {string} bonuses.check Numeric or dice bonus to ability checks.
  15963. * @property {string} bonuses.save Numeric or dice bonus to ability saving throws.
  15964. */
  15965. /**
  15966. * A template for all actors that share the common template.
  15967. *
  15968. * @property {Object<string, AbilityData>} abilities Actor's abilities.
  15969. * @mixin
  15970. */
  15971. class CommonTemplate extends SystemDataModel.mixin(CurrencyTemplate) {
  15972. /** @inheritdoc */
  15973. static defineSchema() {
  15974. return this.mergeSchema(super.defineSchema(), {
  15975. abilities: new MappingField(new foundry.data.fields.SchemaField({
  15976. value: new foundry.data.fields.NumberField({
  15977. required: true, nullable: false, integer: true, min: 0, initial: 10, label: "DND5E.AbilityScore"
  15978. }),
  15979. proficient: new foundry.data.fields.NumberField({required: true, initial: 0, label: "DND5E.ProficiencyLevel"}),
  15980. bonuses: new foundry.data.fields.SchemaField({
  15981. check: new FormulaField({required: true, label: "DND5E.AbilityCheckBonus"}),
  15982. save: new FormulaField({required: true, label: "DND5E.SaveBonus"})
  15983. }, {label: "DND5E.AbilityBonuses"})
  15984. }), {
  15985. initialKeys: CONFIG.DND5E.abilities, initialValue: this._initialAbilityValue.bind(this),
  15986. initialKeysOnly: true, label: "DND5E.Abilities"
  15987. })
  15988. });
  15989. }
  15990. /* -------------------------------------------- */
  15991. /**
  15992. * Populate the proper initial value for abilities.
  15993. * @param {string} key Key for which the initial data will be created.
  15994. * @param {object} initial The initial skill object created by SkillData.
  15995. * @param {object} existing Any existing mapping data.
  15996. * @returns {object} Initial ability object.
  15997. * @private
  15998. */
  15999. static _initialAbilityValue(key, initial, existing) {
  16000. const config = CONFIG.DND5E.abilities[key];
  16001. if ( config ) {
  16002. let defaultValue = config.defaults?.[this._systemType] ?? initial.value;
  16003. if ( typeof defaultValue === "string" ) defaultValue = existing?.[defaultValue]?.value ?? initial.value;
  16004. initial.value = defaultValue;
  16005. }
  16006. return initial;
  16007. }
  16008. /* -------------------------------------------- */
  16009. /* Migrations */
  16010. /* -------------------------------------------- */
  16011. /** @inheritdoc */
  16012. static migrateData(source) {
  16013. super.migrateData(source);
  16014. CommonTemplate.#migrateACData(source);
  16015. CommonTemplate.#migrateMovementData(source);
  16016. }
  16017. /* -------------------------------------------- */
  16018. /**
  16019. * Migrate the actor ac.value to new ac.flat override field.
  16020. * @param {object} source The candidate source data from which the model will be constructed.
  16021. */
  16022. static #migrateACData(source) {
  16023. if ( !source.attributes?.ac ) return;
  16024. const ac = source.attributes.ac;
  16025. // If the actor has a numeric ac.value, then their AC has not been migrated to the auto-calculation schema yet.
  16026. if ( Number.isNumeric(ac.value) ) {
  16027. ac.flat = parseInt(ac.value);
  16028. ac.calc = this._systemType === "npc" ? "natural" : "flat";
  16029. return;
  16030. }
  16031. // Migrate ac.base in custom formulas to ac.armor
  16032. if ( (typeof ac.formula === "string") && ac.formula.includes("@attributes.ac.base") ) {
  16033. ac.formula = ac.formula.replaceAll("@attributes.ac.base", "@attributes.ac.armor");
  16034. }
  16035. }
  16036. /* -------------------------------------------- */
  16037. /**
  16038. * Migrate the actor speed string to movement object.
  16039. * @param {object} source The candidate source data from which the model will be constructed.
  16040. */
  16041. static #migrateMovementData(source) {
  16042. const original = source.attributes?.speed?.value ?? source.attributes?.speed;
  16043. if ( (typeof original !== "string") || (source.attributes.movement?.walk !== undefined) ) return;
  16044. source.attributes.movement ??= {};
  16045. const s = original.split(" ");
  16046. if ( s.length > 0 ) source.attributes.movement.walk = Number.isNumeric(s[0]) ? parseInt(s[0]) : 0;
  16047. }
  16048. }
  16049. /**
  16050. * @typedef {object} SkillData
  16051. * @property {number} value Proficiency level creature has in this skill.
  16052. * @property {string} ability Default ability used for this skill.
  16053. * @property {object} bonuses Bonuses for this skill.
  16054. * @property {string} bonuses.check Numeric or dice bonus to skill's check.
  16055. * @property {string} bonuses.passive Numeric bonus to skill's passive check.
  16056. */
  16057. /**
  16058. * A template for all actors that are creatures
  16059. *
  16060. * @property {object} bonuses
  16061. * @property {AttackBonusesData} bonuses.mwak Bonuses to melee weapon attacks.
  16062. * @property {AttackBonusesData} bonuses.rwak Bonuses to ranged weapon attacks.
  16063. * @property {AttackBonusesData} bonuses.msak Bonuses to melee spell attacks.
  16064. * @property {AttackBonusesData} bonuses.rsak Bonuses to ranged spell attacks.
  16065. * @property {object} bonuses.abilities Bonuses to ability scores.
  16066. * @property {string} bonuses.abilities.check Numeric or dice bonus to ability checks.
  16067. * @property {string} bonuses.abilities.save Numeric or dice bonus to ability saves.
  16068. * @property {string} bonuses.abilities.skill Numeric or dice bonus to skill checks.
  16069. * @property {object} bonuses.spell Bonuses to spells.
  16070. * @property {string} bonuses.spell.dc Numeric bonus to spellcasting DC.
  16071. * @property {Object<string, SkillData>} skills Actor's skills.
  16072. * @property {Object<string, SpellSlotData>} spells Actor's spell slots.
  16073. */
  16074. class CreatureTemplate extends CommonTemplate {
  16075. static defineSchema() {
  16076. return this.mergeSchema(super.defineSchema(), {
  16077. bonuses: new foundry.data.fields.SchemaField({
  16078. mwak: makeAttackBonuses({label: "DND5E.BonusMWAttack"}),
  16079. rwak: makeAttackBonuses({label: "DND5E.BonusRWAttack"}),
  16080. msak: makeAttackBonuses({label: "DND5E.BonusMSAttack"}),
  16081. rsak: makeAttackBonuses({label: "DND5E.BonusRSAttack"}),
  16082. abilities: new foundry.data.fields.SchemaField({
  16083. check: new FormulaField({required: true, label: "DND5E.BonusAbilityCheck"}),
  16084. save: new FormulaField({required: true, label: "DND5E.BonusAbilitySave"}),
  16085. skill: new FormulaField({required: true, label: "DND5E.BonusAbilitySkill"})
  16086. }, {label: "DND5E.BonusAbility"}),
  16087. spell: new foundry.data.fields.SchemaField({
  16088. dc: new FormulaField({required: true, deterministic: true, label: "DND5E.BonusSpellDC"})
  16089. }, {label: "DND5E.BonusSpell"})
  16090. }, {label: "DND5E.Bonuses"}),
  16091. skills: new MappingField(new foundry.data.fields.SchemaField({
  16092. value: new foundry.data.fields.NumberField({
  16093. required: true, min: 0, max: 2, step: 0.5, initial: 0, label: "DND5E.ProficiencyLevel"
  16094. }),
  16095. ability: new foundry.data.fields.StringField({required: true, initial: "dex", label: "DND5E.Ability"}),
  16096. bonuses: new foundry.data.fields.SchemaField({
  16097. check: new FormulaField({required: true, label: "DND5E.SkillBonusCheck"}),
  16098. passive: new FormulaField({required: true, label: "DND5E.SkillBonusPassive"})
  16099. }, {label: "DND5E.SkillBonuses"})
  16100. }), {
  16101. initialKeys: CONFIG.DND5E.skills, initialValue: this._initialSkillValue,
  16102. initialKeysOnly: true, label: "DND5E.Skills"
  16103. }),
  16104. tools: new MappingField(new foundry.data.fields.SchemaField({
  16105. value: new foundry.data.fields.NumberField({
  16106. required: true, min: 0, max: 2, step: 0.5, initial: 1, label: "DND5E.ProficiencyLevel"
  16107. }),
  16108. ability: new foundry.data.fields.StringField({required: true, initial: "int", label: "DND5E.Ability"}),
  16109. bonuses: new foundry.data.fields.SchemaField({
  16110. check: new FormulaField({required: true, label: "DND5E.CheckBonus"})
  16111. }, {label: "DND5E.ToolBonuses"})
  16112. })),
  16113. spells: new MappingField(new foundry.data.fields.SchemaField({
  16114. value: new foundry.data.fields.NumberField({
  16115. nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.SpellProgAvailable"
  16116. }),
  16117. override: new foundry.data.fields.NumberField({
  16118. integer: true, min: 0, label: "DND5E.SpellProgOverride"
  16119. })
  16120. }), {initialKeys: this._spellLevels, label: "DND5E.SpellLevels"})
  16121. });
  16122. }
  16123. /* -------------------------------------------- */
  16124. /**
  16125. * Populate the proper initial abilities for the skills.
  16126. * @param {string} key Key for which the initial data will be created.
  16127. * @param {object} initial The initial skill object created by SkillData.
  16128. * @returns {object} Initial skills object with the ability defined.
  16129. * @private
  16130. */
  16131. static _initialSkillValue(key, initial) {
  16132. if ( CONFIG.DND5E.skills[key]?.ability ) initial.ability = CONFIG.DND5E.skills[key].ability;
  16133. return initial;
  16134. }
  16135. /* -------------------------------------------- */
  16136. /**
  16137. * Helper for building the default list of spell levels.
  16138. * @type {string[]}
  16139. * @private
  16140. */
  16141. static get _spellLevels() {
  16142. const levels = Object.keys(CONFIG.DND5E.spellLevels).filter(a => a !== "0").map(l => `spell${l}`);
  16143. return [...levels, "pact"];
  16144. }
  16145. /* -------------------------------------------- */
  16146. /* Migrations */
  16147. /* -------------------------------------------- */
  16148. /** @inheritdoc */
  16149. static migrateData(source) {
  16150. super.migrateData(source);
  16151. CreatureTemplate.#migrateSensesData(source);
  16152. CreatureTemplate.#migrateToolData(source);
  16153. }
  16154. /* -------------------------------------------- */
  16155. /**
  16156. * Migrate the actor traits.senses string to attributes.senses object.
  16157. * @param {object} source The candidate source data from which the model will be constructed.
  16158. */
  16159. static #migrateSensesData(source) {
  16160. const original = source.traits?.senses;
  16161. if ( (original === undefined) || (typeof original !== "string") ) return;
  16162. source.attributes ??= {};
  16163. source.attributes.senses ??= {};
  16164. // Try to match old senses with the format like "Darkvision 60 ft, Blindsight 30 ft"
  16165. const pattern = /([A-z]+)\s?([0-9]+)\s?([A-z]+)?/;
  16166. let wasMatched = false;
  16167. // Match each comma-separated term
  16168. for ( let s of original.split(",") ) {
  16169. s = s.trim();
  16170. const match = s.match(pattern);
  16171. if ( !match ) continue;
  16172. const type = match[1].toLowerCase();
  16173. if ( (type in CONFIG.DND5E.senses) && !(type in source.attributes.senses) ) {
  16174. source.attributes.senses[type] = Number(match[2]).toNearest(0.5);
  16175. wasMatched = true;
  16176. }
  16177. }
  16178. // If nothing was matched, but there was an old string - put the whole thing in "special"
  16179. if ( !wasMatched && original ) source.attributes.senses.special = original;
  16180. }
  16181. /* -------------------------------------------- */
  16182. /**
  16183. * Migrate traits.toolProf to the tools field.
  16184. * @param {object} source The candidate source data from which the model will be constructed.
  16185. */
  16186. static #migrateToolData(source) {
  16187. const original = source.traits?.toolProf;
  16188. if ( !original || foundry.utils.isEmpty(original.value) ) return;
  16189. source.tools ??= {};
  16190. for ( const prof of original.value ) {
  16191. const validProf = (prof in CONFIG.DND5E.toolProficiencies) || (prof in CONST.DND5E.toolIds);
  16192. if ( !validProf || (prof in source.tools) ) continue;
  16193. source.tools[prof] = {
  16194. value: 1,
  16195. ability: "int",
  16196. bonuses: {check: ""}
  16197. };
  16198. }
  16199. }
  16200. }
  16201. /* -------------------------------------------- */
  16202. /**
  16203. * Data on configuration of a specific spell slot.
  16204. *
  16205. * @typedef {object} SpellSlotData
  16206. * @property {number} value Currently available spell slots.
  16207. * @property {number} override Number to replace auto-calculated max slots.
  16208. */
  16209. /* -------------------------------------------- */
  16210. /**
  16211. * Data structure for actor's attack bonuses.
  16212. *
  16213. * @typedef {object} AttackBonusesData
  16214. * @property {string} attack Numeric or dice bonus to attack rolls.
  16215. * @property {string} damage Numeric or dice bonus to damage rolls.
  16216. */
  16217. /**
  16218. * Produce the schema field for a simple trait.
  16219. * @param {object} schemaOptions Options passed to the outer schema.
  16220. * @returns {AttackBonusesData}
  16221. */
  16222. function makeAttackBonuses(schemaOptions={}) {
  16223. return new foundry.data.fields.SchemaField({
  16224. attack: new FormulaField({required: true, label: "DND5E.BonusAttack"}),
  16225. damage: new FormulaField({required: true, label: "DND5E.BonusDamage"})
  16226. }, schemaOptions);
  16227. }
  16228. /**
  16229. * Shared contents of the details schema between various actor types.
  16230. */
  16231. class DetailsField {
  16232. /**
  16233. * Fields shared between characters, NPCs, and vehicles.
  16234. *
  16235. * @type {object}
  16236. * @property {object} biography Actor's biography data.
  16237. * @property {string} biography.value Full HTML biography information.
  16238. * @property {string} biography.public Biography that will be displayed to players with observer privileges.
  16239. */
  16240. static get common() {
  16241. return {
  16242. biography: new foundry.data.fields.SchemaField({
  16243. value: new foundry.data.fields.HTMLField({label: "DND5E.Biography"}),
  16244. public: new foundry.data.fields.HTMLField({label: "DND5E.BiographyPublic"})
  16245. }, {label: "DND5E.Biography"})
  16246. };
  16247. }
  16248. /* -------------------------------------------- */
  16249. /**
  16250. * Fields shared between characters and NPCs.
  16251. *
  16252. * @type {object}
  16253. * @property {string} alignment Creature's alignment.
  16254. * @property {string} race Creature's race.
  16255. */
  16256. static get creature() {
  16257. return {
  16258. alignment: new foundry.data.fields.StringField({required: true, label: "DND5E.Alignment"}),
  16259. race: new foundry.data.fields.StringField({required: true, label: "DND5E.Race"})
  16260. };
  16261. }
  16262. }
  16263. /**
  16264. * Shared contents of the traits schema between various actor types.
  16265. */
  16266. class TraitsField {
  16267. /**
  16268. * Data structure for a standard actor trait.
  16269. *
  16270. * @typedef {object} SimpleTraitData
  16271. * @property {Set<string>} value Keys for currently selected traits.
  16272. * @property {string} custom Semicolon-separated list of custom traits.
  16273. */
  16274. /**
  16275. * Data structure for a damage actor trait.
  16276. *
  16277. * @typedef {object} DamageTraitData
  16278. * @property {Set<string>} value Keys for currently selected traits.
  16279. * @property {Set<string>} bypasses Keys for physical weapon properties that cause resistances to be bypassed.
  16280. * @property {string} custom Semicolon-separated list of custom traits.
  16281. */
  16282. /* -------------------------------------------- */
  16283. /**
  16284. * Fields shared between characters, NPCs, and vehicles.
  16285. *
  16286. * @type {object}
  16287. * @property {string} size Actor's size.
  16288. * @property {DamageTraitData} di Damage immunities.
  16289. * @property {DamageTraitData} dr Damage resistances.
  16290. * @property {DamageTraitData} dv Damage vulnerabilities.
  16291. * @property {SimpleTraitData} ci Condition immunities.
  16292. */
  16293. static get common() {
  16294. return {
  16295. size: new foundry.data.fields.StringField({required: true, initial: "med", label: "DND5E.Size"}),
  16296. di: this.makeDamageTrait({label: "DND5E.DamImm"}),
  16297. dr: this.makeDamageTrait({label: "DND5E.DamRes"}),
  16298. dv: this.makeDamageTrait({label: "DND5E.DamVuln"}),
  16299. ci: this.makeSimpleTrait({label: "DND5E.ConImm"})
  16300. };
  16301. }
  16302. /* -------------------------------------------- */
  16303. /**
  16304. * Fields shared between characters and NPCs.
  16305. *
  16306. * @type {object}
  16307. * @property {SimpleTraitData} languages Languages known by this creature.
  16308. */
  16309. static get creature() {
  16310. return {
  16311. languages: this.makeSimpleTrait({label: "DND5E.Languages"})
  16312. };
  16313. }
  16314. /* -------------------------------------------- */
  16315. /**
  16316. * Produce the schema field for a simple trait.
  16317. * @param {object} [schemaOptions={}] Options passed to the outer schema.
  16318. * @param {object} [options={}]
  16319. * @param {string[]} [options.initial={}] The initial value for the value set.
  16320. * @param {object} [options.extraFields={}] Additional fields added to schema.
  16321. * @returns {SchemaField}
  16322. */
  16323. static makeSimpleTrait(schemaOptions={}, {initial=[], extraFields={}}={}) {
  16324. return new foundry.data.fields.SchemaField({
  16325. ...extraFields,
  16326. value: new foundry.data.fields.SetField(
  16327. new foundry.data.fields.StringField(), {label: "DND5E.TraitsChosen", initial}
  16328. ),
  16329. custom: new foundry.data.fields.StringField({required: true, label: "DND5E.Special"})
  16330. }, schemaOptions);
  16331. }
  16332. /* -------------------------------------------- */
  16333. /**
  16334. * Produce the schema field for a damage trait.
  16335. * @param {object} [schemaOptions={}] Options passed to the outer schema.
  16336. * @param {object} [options={}]
  16337. * @param {string[]} [options.initial={}] The initial value for the value set.
  16338. * @param {object} [options.extraFields={}] Additional fields added to schema.
  16339. * @returns {SchemaField}
  16340. */
  16341. static makeDamageTrait(schemaOptions={}, {initial=[], initialBypasses=[], extraFields={}}={}) {
  16342. return this.makeSimpleTrait(schemaOptions, {initial, extraFields: {
  16343. ...extraFields,
  16344. bypasses: new foundry.data.fields.SetField(new foundry.data.fields.StringField(), {
  16345. label: "DND5E.DamagePhysicalBypass", hint: "DND5E.DamagePhysicalBypassHint", initial: initialBypasses
  16346. })
  16347. }});
  16348. }
  16349. }
  16350. /**
  16351. * System data definition for Characters.
  16352. *
  16353. * @property {object} attributes
  16354. * @property {object} attributes.ac
  16355. * @property {number} attributes.ac.flat Flat value used for flat or natural armor calculation.
  16356. * @property {string} attributes.ac.calc Name of one of the built-in formulas to use.
  16357. * @property {string} attributes.ac.formula Custom formula to use.
  16358. * @property {object} attributes.hp
  16359. * @property {number} attributes.hp.value Current hit points.
  16360. * @property {number} attributes.hp.max Override for maximum HP.
  16361. * @property {number} attributes.hp.temp Temporary HP applied on top of value.
  16362. * @property {number} attributes.hp.tempmax Temporary change to the maximum HP.
  16363. * @property {object} attributes.hp.bonuses
  16364. * @property {string} attributes.hp.bonuses.level Bonus formula applied for each class level.
  16365. * @property {string} attributes.hp.bonuses.overall Bonus formula applied to total HP.
  16366. * @property {object} attributes.death
  16367. * @property {number} attributes.death.success Number of successful death saves.
  16368. * @property {number} attributes.death.failure Number of failed death saves.
  16369. * @property {number} attributes.exhaustion Number of levels of exhaustion.
  16370. * @property {number} attributes.inspiration Does this character have inspiration?
  16371. * @property {object} details
  16372. * @property {string} details.background Name of character's background.
  16373. * @property {string} details.originalClass ID of first class taken by character.
  16374. * @property {XPData} details.xp Experience points gained.
  16375. * @property {number} details.xp.value Total experience points earned.
  16376. * @property {string} details.appearance Description of character's appearance.
  16377. * @property {string} details.trait Character's personality traits.
  16378. * @property {string} details.ideal Character's ideals.
  16379. * @property {string} details.bond Character's bonds.
  16380. * @property {string} details.flaw Character's flaws.
  16381. * @property {object} traits
  16382. * @property {SimpleTraitData} traits.weaponProf Character's weapon proficiencies.
  16383. * @property {SimpleTraitData} traits.armorProf Character's armor proficiencies.
  16384. * @property {object} resources
  16385. * @property {CharacterResourceData} resources.primary Resource number one.
  16386. * @property {CharacterResourceData} resources.secondary Resource number two.
  16387. * @property {CharacterResourceData} resources.tertiary Resource number three.
  16388. */
  16389. class CharacterData extends CreatureTemplate {
  16390. /** @inheritdoc */
  16391. static _systemType = "character";
  16392. /* -------------------------------------------- */
  16393. /** @inheritdoc */
  16394. static defineSchema() {
  16395. return this.mergeSchema(super.defineSchema(), {
  16396. attributes: new foundry.data.fields.SchemaField({
  16397. ...AttributesFields.common,
  16398. ...AttributesFields.creature,
  16399. ac: new foundry.data.fields.SchemaField({
  16400. flat: new foundry.data.fields.NumberField({integer: true, min: 0, label: "DND5E.ArmorClassFlat"}),
  16401. calc: new foundry.data.fields.StringField({initial: "default", label: "DND5E.ArmorClassCalculation"}),
  16402. formula: new FormulaField({deterministic: true, label: "DND5E.ArmorClassFormula"})
  16403. }, {label: "DND5E.ArmorClass"}),
  16404. hp: new foundry.data.fields.SchemaField({
  16405. value: new foundry.data.fields.NumberField({
  16406. nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.HitPointsCurrent"
  16407. }),
  16408. max: new foundry.data.fields.NumberField({
  16409. nullable: true, integer: true, min: 0, initial: null, label: "DND5E.HitPointsOverride"
  16410. }),
  16411. temp: new foundry.data.fields.NumberField({integer: true, initial: 0, min: 0, label: "DND5E.HitPointsTemp"}),
  16412. tempmax: new foundry.data.fields.NumberField({integer: true, initial: 0, label: "DND5E.HitPointsTempMax"}),
  16413. bonuses: new foundry.data.fields.SchemaField({
  16414. level: new FormulaField({deterministic: true, label: "DND5E.HitPointsBonusLevel"}),
  16415. overall: new FormulaField({deterministic: true, label: "DND5E.HitPointsBonusOverall"})
  16416. })
  16417. }, {label: "DND5E.HitPoints"}),
  16418. death: new foundry.data.fields.SchemaField({
  16419. success: new foundry.data.fields.NumberField({
  16420. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.DeathSaveSuccesses"
  16421. }),
  16422. failure: new foundry.data.fields.NumberField({
  16423. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.DeathSaveFailures"
  16424. })
  16425. }, {label: "DND5E.DeathSave"}),
  16426. exhaustion: new foundry.data.fields.NumberField({
  16427. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.Exhaustion"
  16428. }),
  16429. inspiration: new foundry.data.fields.BooleanField({required: true, label: "DND5E.Inspiration"})
  16430. }, {label: "DND5E.Attributes"}),
  16431. details: new foundry.data.fields.SchemaField({
  16432. ...DetailsField.common,
  16433. ...DetailsField.creature,
  16434. background: new foundry.data.fields.StringField({required: true, label: "DND5E.Background"}),
  16435. originalClass: new foundry.data.fields.StringField({required: true, label: "DND5E.ClassOriginal"}),
  16436. xp: new foundry.data.fields.SchemaField({
  16437. value: new foundry.data.fields.NumberField({
  16438. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.ExperiencePointsCurrent"
  16439. })
  16440. }, {label: "DND5E.ExperiencePoints"}),
  16441. appearance: new foundry.data.fields.StringField({required: true, label: "DND5E.Appearance"}),
  16442. trait: new foundry.data.fields.StringField({required: true, label: "DND5E.PersonalityTraits"}),
  16443. ideal: new foundry.data.fields.StringField({required: true, label: "DND5E.Ideals"}),
  16444. bond: new foundry.data.fields.StringField({required: true, label: "DND5E.Bonds"}),
  16445. flaw: new foundry.data.fields.StringField({required: true, label: "DND5E.Flaws"})
  16446. }, {label: "DND5E.Details"}),
  16447. traits: new foundry.data.fields.SchemaField({
  16448. ...TraitsField.common,
  16449. ...TraitsField.creature,
  16450. weaponProf: TraitsField.makeSimpleTrait({label: "DND5E.TraitWeaponProf"}),
  16451. armorProf: TraitsField.makeSimpleTrait({label: "DND5E.TraitArmorProf"})
  16452. }, {label: "DND5E.Traits"}),
  16453. resources: new foundry.data.fields.SchemaField({
  16454. primary: makeResourceField({label: "DND5E.ResourcePrimary"}),
  16455. secondary: makeResourceField({label: "DND5E.ResourceSecondary"}),
  16456. tertiary: makeResourceField({label: "DND5E.ResourceTertiary"})
  16457. }, {label: "DND5E.Resources"})
  16458. });
  16459. }
  16460. /* -------------------------------------------- */
  16461. /** @inheritdoc */
  16462. static migrateData(source) {
  16463. super.migrateData(source);
  16464. AttributesFields._migrateInitiative(source.attributes);
  16465. }
  16466. }
  16467. /* -------------------------------------------- */
  16468. /**
  16469. * Data structure for character's resources.
  16470. *
  16471. * @typedef {object} ResourceData
  16472. * @property {number} value Available uses of this resource.
  16473. * @property {number} max Maximum allowed uses of this resource.
  16474. * @property {boolean} sr Does this resource recover on a short rest?
  16475. * @property {boolean} lr Does this resource recover on a long rest?
  16476. * @property {string} label Displayed name.
  16477. */
  16478. /**
  16479. * Produce the schema field for a simple trait.
  16480. * @param {object} schemaOptions Options passed to the outer schema.
  16481. * @returns {ResourceData}
  16482. */
  16483. function makeResourceField(schemaOptions={}) {
  16484. return new foundry.data.fields.SchemaField({
  16485. value: new foundry.data.fields.NumberField({
  16486. required: true, integer: true, initial: 0, labels: "DND5E.ResourceValue"
  16487. }),
  16488. max: new foundry.data.fields.NumberField({
  16489. required: true, integer: true, initial: 0, labels: "DND5E.ResourceMax"
  16490. }),
  16491. sr: new foundry.data.fields.BooleanField({required: true, labels: "DND5E.ShortRestRecovery"}),
  16492. lr: new foundry.data.fields.BooleanField({required: true, labels: "DND5E.LongRestRecovery"}),
  16493. label: new foundry.data.fields.StringField({required: true, labels: "DND5E.ResourceLabel"})
  16494. }, schemaOptions);
  16495. }
  16496. /**
  16497. * A data model and API layer which handles the schema and functionality of "group" type Actors in the dnd5e system.
  16498. * @mixes CurrencyTemplate
  16499. *
  16500. * @property {object} description
  16501. * @property {string} description.full Description of this group.
  16502. * @property {string} description.summary Summary description (currently unused).
  16503. * @property {Set<string>} members IDs of actors belonging to this group in the world collection.
  16504. * @property {object} attributes
  16505. * @property {object} attributes.movement
  16506. * @property {number} attributes.movement.land Base movement speed over land.
  16507. * @property {number} attributes.movement.water Base movement speed over water.
  16508. * @property {number} attributes.movement.air Base movement speed through the air.
  16509. *
  16510. * @example Create a new Group
  16511. * const g = new dnd5e.documents.Actor5e({
  16512. * type: "group",
  16513. * name: "Test Group",
  16514. * system: {
  16515. * members: ["3f3hoYFWUgDqBP4U"]
  16516. * }
  16517. * });
  16518. */
  16519. class GroupActor extends SystemDataModel.mixin(CurrencyTemplate) {
  16520. /** @inheritdoc */
  16521. static defineSchema() {
  16522. return this.mergeSchema(super.defineSchema(), {
  16523. description: new foundry.data.fields.SchemaField({
  16524. full: new foundry.data.fields.HTMLField({label: "DND5E.Description"}),
  16525. summary: new foundry.data.fields.HTMLField({label: "DND5E.DescriptionSummary"})
  16526. }),
  16527. members: new foundry.data.fields.SetField(
  16528. new foundry.data.fields.ForeignDocumentField(foundry.documents.BaseActor, {idOnly: true}),
  16529. {label: "DND5E.GroupMembers"}
  16530. ),
  16531. attributes: new foundry.data.fields.SchemaField({
  16532. movement: new foundry.data.fields.SchemaField({
  16533. land: new foundry.data.fields.NumberField({
  16534. nullable: false, min: 0, step: 0.1, initial: 0, label: "DND5E.MovementLand"
  16535. }),
  16536. water: new foundry.data.fields.NumberField({
  16537. nullable: false, min: 0, step: 0.1, initial: 0, label: "DND5E.MovementWater"
  16538. }),
  16539. air: new foundry.data.fields.NumberField({
  16540. nullable: false, min: 0, step: 0.1, initial: 0, label: "DND5E.MovementAir"
  16541. })
  16542. })
  16543. }, {label: "DND5E.Attributes"})
  16544. });
  16545. }
  16546. /* -------------------------------------------- */
  16547. /* Data Preparation */
  16548. /* -------------------------------------------- */
  16549. /**
  16550. * Prepare base data for group actors.
  16551. * @internal
  16552. */
  16553. _prepareBaseData() {
  16554. this.members.clear();
  16555. for ( const id of this._source.members ) {
  16556. const a = game.actors.get(id);
  16557. if ( a ) {
  16558. if ( a.type === "group" ) {
  16559. console.warn(`Group "${this._id}" may not contain another Group "${a.id}" as a member.`);
  16560. }
  16561. else this.members.add(a);
  16562. }
  16563. else console.warn(`Actor "${id}" in group "${this._id}" does not exist within the World.`);
  16564. }
  16565. }
  16566. /**
  16567. * Prepare derived data for group actors.
  16568. * @internal
  16569. */
  16570. _prepareDerivedData() {
  16571. // No preparation needed at this time
  16572. }
  16573. /* -------------------------------------------- */
  16574. /* Methods */
  16575. /* -------------------------------------------- */
  16576. /**
  16577. * Add a new member to the group.
  16578. * @param {Actor5e} actor A non-group Actor to add to the group
  16579. * @returns {Promise<Actor5e>} The updated group Actor
  16580. */
  16581. async addMember(actor) {
  16582. if ( actor.type === "group" ) throw new Error("You may not add a group within a group.");
  16583. if ( actor.pack ) throw new Error("You may only add Actors to the group which exist within the World.");
  16584. const memberIds = this._source.members;
  16585. if ( memberIds.includes(actor.id) ) return;
  16586. return this.parent.update({
  16587. system: {
  16588. members: memberIds.concat([actor.id])
  16589. }
  16590. });
  16591. }
  16592. /* -------------------------------------------- */
  16593. /**
  16594. * Remove a member from the group.
  16595. * @param {Actor5e|string} actor An Actor or ID to remove from this group
  16596. * @returns {Promise<Actor5e>} The updated group Actor
  16597. */
  16598. async removeMember(actor) {
  16599. const memberIds = foundry.utils.deepClone(this._source.members);
  16600. // Handle user input
  16601. let actorId;
  16602. if ( typeof actor === "string" ) actorId = actor;
  16603. else if ( actor instanceof Actor ) actorId = actor.id;
  16604. else throw new Error("You must provide an Actor document or an actor ID to remove a group member");
  16605. if ( !memberIds.includes(actorId) ) throw new Error(`Actor id "${actorId}" is not a group member`);
  16606. // Remove the actor and update the parent document
  16607. memberIds.findSplice(id => id === actorId);
  16608. return this.parent.update({
  16609. system: {
  16610. members: memberIds
  16611. }
  16612. });
  16613. }
  16614. }
  16615. /**
  16616. * System data definition for NPCs.
  16617. *
  16618. * @property {object} attributes
  16619. * @property {object} attributes.ac
  16620. * @property {number} attributes.ac.flat Flat value used for flat or natural armor calculation.
  16621. * @property {string} attributes.ac.calc Name of one of the built-in formulas to use.
  16622. * @property {string} attributes.ac.formula Custom formula to use.
  16623. * @property {object} attributes.hp
  16624. * @property {number} attributes.hp.value Current hit points.
  16625. * @property {number} attributes.hp.max Maximum allowed HP value.
  16626. * @property {number} attributes.hp.temp Temporary HP applied on top of value.
  16627. * @property {number} attributes.hp.tempmax Temporary change to the maximum HP.
  16628. * @property {string} attributes.hp.formula Formula used to determine hit points.
  16629. * @property {object} details
  16630. * @property {TypeData} details.type Creature type of this NPC.
  16631. * @property {string} details.type.value NPC's type as defined in the system configuration.
  16632. * @property {string} details.type.subtype NPC's subtype usually displayed in parenthesis after main type.
  16633. * @property {string} details.type.swarm Size of the individual creatures in a swarm, if a swarm.
  16634. * @property {string} details.type.custom Custom type beyond what is available in the configuration.
  16635. * @property {string} details.environment Common environments in which this NPC is found.
  16636. * @property {number} details.cr NPC's challenge rating.
  16637. * @property {number} details.spellLevel Spellcasting level of this NPC.
  16638. * @property {string} details.source What book or adventure is this NPC from?
  16639. * @property {object} resources
  16640. * @property {object} resources.legact NPC's legendary actions.
  16641. * @property {number} resources.legact.value Currently available legendary actions.
  16642. * @property {number} resources.legact.max Maximum number of legendary actions.
  16643. * @property {object} resources.legres NPC's legendary resistances.
  16644. * @property {number} resources.legres.value Currently available legendary resistances.
  16645. * @property {number} resources.legres.max Maximum number of legendary resistances.
  16646. * @property {object} resources.lair NPC's lair actions.
  16647. * @property {boolean} resources.lair.value Does this NPC use lair actions.
  16648. * @property {number} resources.lair.initiative Initiative count when lair actions are triggered.
  16649. */
  16650. class NPCData extends CreatureTemplate {
  16651. /** @inheritdoc */
  16652. static _systemType = "npc";
  16653. /* -------------------------------------------- */
  16654. /** @inheritdoc */
  16655. static defineSchema() {
  16656. return this.mergeSchema(super.defineSchema(), {
  16657. attributes: new foundry.data.fields.SchemaField({
  16658. ...AttributesFields.common,
  16659. ...AttributesFields.creature,
  16660. ac: new foundry.data.fields.SchemaField({
  16661. flat: new foundry.data.fields.NumberField({integer: true, min: 0, label: "DND5E.ArmorClassFlat"}),
  16662. calc: new foundry.data.fields.StringField({initial: "default", label: "DND5E.ArmorClassCalculation"}),
  16663. formula: new FormulaField({deterministic: true, label: "DND5E.ArmorClassFormula"})
  16664. }, {label: "DND5E.ArmorClass"}),
  16665. hp: new foundry.data.fields.SchemaField({
  16666. value: new foundry.data.fields.NumberField({
  16667. nullable: false, integer: true, min: 0, initial: 10, label: "DND5E.HitPointsCurrent"
  16668. }),
  16669. max: new foundry.data.fields.NumberField({
  16670. nullable: false, integer: true, min: 0, initial: 10, label: "DND5E.HitPointsMax"
  16671. }),
  16672. temp: new foundry.data.fields.NumberField({integer: true, initial: 0, min: 0, label: "DND5E.HitPointsTemp"}),
  16673. tempmax: new foundry.data.fields.NumberField({integer: true, initial: 0, label: "DND5E.HitPointsTempMax"}),
  16674. formula: new FormulaField({required: true, label: "DND5E.HPFormula"})
  16675. }, {label: "DND5E.HitPoints"})
  16676. }, {label: "DND5E.Attributes"}),
  16677. details: new foundry.data.fields.SchemaField({
  16678. ...DetailsField.common,
  16679. ...DetailsField.creature,
  16680. type: new foundry.data.fields.SchemaField({
  16681. value: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.CreatureType"}),
  16682. subtype: new foundry.data.fields.StringField({required: true, label: "DND5E.CreatureTypeSelectorSubtype"}),
  16683. swarm: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.CreatureSwarmSize"}),
  16684. custom: new foundry.data.fields.StringField({required: true, label: "DND5E.CreatureTypeSelectorCustom"})
  16685. }, {label: "DND5E.CreatureType"}),
  16686. environment: new foundry.data.fields.StringField({required: true, label: "DND5E.Environment"}),
  16687. cr: new foundry.data.fields.NumberField({
  16688. required: true, nullable: false, min: 0, initial: 1, label: "DND5E.ChallengeRating"
  16689. }),
  16690. spellLevel: new foundry.data.fields.NumberField({
  16691. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.SpellcasterLevel"
  16692. }),
  16693. source: new foundry.data.fields.StringField({required: true, label: "DND5E.Source"})
  16694. }, {label: "DND5E.Details"}),
  16695. resources: new foundry.data.fields.SchemaField({
  16696. legact: new foundry.data.fields.SchemaField({
  16697. value: new foundry.data.fields.NumberField({
  16698. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.LegActRemaining"
  16699. }),
  16700. max: new foundry.data.fields.NumberField({
  16701. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.LegActMax"
  16702. })
  16703. }, {label: "DND5E.LegAct"}),
  16704. legres: new foundry.data.fields.SchemaField({
  16705. value: new foundry.data.fields.NumberField({
  16706. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.LegResRemaining"
  16707. }),
  16708. max: new foundry.data.fields.NumberField({
  16709. required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.LegResMax"
  16710. })
  16711. }, {label: "DND5E.LegRes"}),
  16712. lair: new foundry.data.fields.SchemaField({
  16713. value: new foundry.data.fields.BooleanField({required: true, label: "DND5E.LairAct"}),
  16714. initiative: new foundry.data.fields.NumberField({
  16715. required: true, integer: true, label: "DND5E.LairActionInitiative"
  16716. })
  16717. }, {label: "DND5E.LairActionLabel"})
  16718. }, {label: "DND5E.Resources"}),
  16719. traits: new foundry.data.fields.SchemaField({
  16720. ...TraitsField.common,
  16721. ...TraitsField.creature
  16722. }, {label: "DND5E.Traits"})
  16723. });
  16724. }
  16725. /* -------------------------------------------- */
  16726. /** @inheritdoc */
  16727. static migrateData(source) {
  16728. super.migrateData(source);
  16729. NPCData.#migrateTypeData(source);
  16730. AttributesFields._migrateInitiative(source.attributes);
  16731. }
  16732. /* -------------------------------------------- */
  16733. /**
  16734. * Migrate the actor type string to type object.
  16735. * @param {object} source The candidate source data from which the model will be constructed.
  16736. */
  16737. static #migrateTypeData(source) {
  16738. const original = source.type;
  16739. if ( typeof original !== "string" ) return;
  16740. source.type = {
  16741. value: "",
  16742. subtype: "",
  16743. swarm: "",
  16744. custom: ""
  16745. };
  16746. // Match the existing string
  16747. const pattern = /^(?:swarm of (?<size>[\w-]+) )?(?<type>[^(]+?)(?:\((?<subtype>[^)]+)\))?$/i;
  16748. const match = original.trim().match(pattern);
  16749. if ( match ) {
  16750. // Match a known creature type
  16751. const typeLc = match.groups.type.trim().toLowerCase();
  16752. const typeMatch = Object.entries(CONFIG.DND5E.creatureTypes).find(([k, v]) => {
  16753. return (typeLc === k)
  16754. || (typeLc === game.i18n.localize(v).toLowerCase())
  16755. || (typeLc === game.i18n.localize(`${v}Pl`).toLowerCase());
  16756. });
  16757. if ( typeMatch ) source.type.value = typeMatch[0];
  16758. else {
  16759. source.type.value = "custom";
  16760. source.type.custom = match.groups.type.trim().titleCase();
  16761. }
  16762. source.type.subtype = match.groups.subtype?.trim().titleCase() ?? "";
  16763. // Match a swarm
  16764. if ( match.groups.size ) {
  16765. const sizeLc = match.groups.size ? match.groups.size.trim().toLowerCase() : "tiny";
  16766. const sizeMatch = Object.entries(CONFIG.DND5E.actorSizes).find(([k, v]) => {
  16767. return (sizeLc === k) || (sizeLc === game.i18n.localize(v).toLowerCase());
  16768. });
  16769. source.type.swarm = sizeMatch ? sizeMatch[0] : "tiny";
  16770. }
  16771. else source.type.swarm = "";
  16772. }
  16773. // No match found
  16774. else {
  16775. source.type.value = "custom";
  16776. source.type.custom = original;
  16777. }
  16778. }
  16779. }
  16780. /**
  16781. * System data definition for Vehicles.
  16782. *
  16783. * @property {string} vehicleType Type of vehicle as defined in `DND5E.vehicleTypes`.
  16784. * @property {object} attributes
  16785. * @property {object} attributes.ac
  16786. * @property {number} attributes.ac.flat Flat value used for flat or natural armor calculation.
  16787. * @property {string} attributes.ac.calc Name of one of the built-in formulas to use.
  16788. * @property {string} attributes.ac.formula Custom formula to use.
  16789. * @property {string} attributes.ac.motionless Changes to vehicle AC when not moving.
  16790. * @property {object} attributes.hp
  16791. * @property {number} attributes.hp.value Current hit points.
  16792. * @property {number} attributes.hp.max Maximum allowed HP value.
  16793. * @property {number} attributes.hp.temp Temporary HP applied on top of value.
  16794. * @property {number} attributes.hp.tempmax Temporary change to the maximum HP.
  16795. * @property {number} attributes.hp.dt Damage threshold.
  16796. * @property {number} attributes.hp.mt Mishap threshold.
  16797. * @property {object} attributes.actions Information on how the vehicle performs actions.
  16798. * @property {boolean} attributes.actions.stations Does this vehicle rely on action stations that required
  16799. * individual crewing rather than general crew thresholds?
  16800. * @property {number} attributes.actions.value Maximum number of actions available with full crewing.
  16801. * @property {object} attributes.actions.thresholds Crew thresholds needed to perform various actions.
  16802. * @property {number} attributes.actions.thresholds.2 Minimum crew needed to take full action complement.
  16803. * @property {number} attributes.actions.thresholds.1 Minimum crew needed to take reduced action complement.
  16804. * @property {number} attributes.actions.thresholds.0 Minimum crew needed to perform any actions.
  16805. * @property {object} attributes.capacity Information on the vehicle's carrying capacity.
  16806. * @property {string} attributes.capacity.creature Description of the number of creatures the vehicle can carry.
  16807. * @property {number} attributes.capacity.cargo Cargo carrying capacity measured in tons.
  16808. * @property {object} traits
  16809. * @property {string} traits.dimensions Width and length of the vehicle.
  16810. * @property {object} cargo Details on this vehicle's crew and cargo capacities.
  16811. * @property {PassengerData[]} cargo.crew Creatures responsible for operating the vehicle.
  16812. * @property {PassengerData[]} cargo.passengers Creatures just takin' a ride.
  16813. */
  16814. class VehicleData extends CommonTemplate {
  16815. /** @inheritdoc */
  16816. static _systemType = "vehicle";
  16817. /* -------------------------------------------- */
  16818. /** @inheritdoc */
  16819. static defineSchema() {
  16820. return this.mergeSchema(super.defineSchema(), {
  16821. vehicleType: new foundry.data.fields.StringField({required: true, initial: "water", label: "DND5E.VehicleType"}),
  16822. attributes: new foundry.data.fields.SchemaField({
  16823. ...AttributesFields.common,
  16824. ac: new foundry.data.fields.SchemaField({
  16825. flat: new foundry.data.fields.NumberField({integer: true, min: 0, label: "DND5E.ArmorClassFlat"}),
  16826. calc: new foundry.data.fields.StringField({initial: "default", label: "DND5E.ArmorClassCalculation"}),
  16827. formula: new FormulaField({deterministic: true, label: "DND5E.ArmorClassFormula"}),
  16828. motionless: new foundry.data.fields.StringField({required: true, label: "DND5E.ArmorClassMotionless"})
  16829. }, {label: "DND5E.ArmorClass"}),
  16830. hp: new foundry.data.fields.SchemaField({
  16831. value: new foundry.data.fields.NumberField({
  16832. nullable: true, integer: true, min: 0, initial: null, label: "DND5E.HitPointsCurrent"
  16833. }),
  16834. max: new foundry.data.fields.NumberField({
  16835. nullable: true, integer: true, min: 0, initial: null, label: "DND5E.HitPointsMax"
  16836. }),
  16837. temp: new foundry.data.fields.NumberField({integer: true, initial: 0, min: 0, label: "DND5E.HitPointsTemp"}),
  16838. tempmax: new foundry.data.fields.NumberField({integer: true, initial: 0, label: "DND5E.HitPointsTempMax"}),
  16839. dt: new foundry.data.fields.NumberField({
  16840. required: true, integer: true, min: 0, label: "DND5E.DamageThreshold"
  16841. }),
  16842. mt: new foundry.data.fields.NumberField({
  16843. required: true, integer: true, min: 0, label: "DND5E.VehicleMishapThreshold"
  16844. })
  16845. }, {label: "DND5E.HitPoints"}),
  16846. actions: new foundry.data.fields.SchemaField({
  16847. stations: new foundry.data.fields.BooleanField({required: true, label: "DND5E.VehicleActionStations"}),
  16848. value: new foundry.data.fields.NumberField({
  16849. required: true, nullable: false, integer: true, initial: 0, min: 0, label: "DND5E.VehicleActionMax"
  16850. }),
  16851. thresholds: new foundry.data.fields.SchemaField({
  16852. 2: new foundry.data.fields.NumberField({
  16853. required: true, integer: true, min: 0, label: "DND5E.VehicleActionThresholdsFull"
  16854. }),
  16855. 1: new foundry.data.fields.NumberField({
  16856. required: true, integer: true, min: 0, label: "DND5E.VehicleActionThresholdsMid"
  16857. }),
  16858. 0: new foundry.data.fields.NumberField({
  16859. required: true, integer: true, min: 0, label: "DND5E.VehicleActionThresholdsMin"
  16860. })
  16861. }, {label: "DND5E.VehicleActionThresholds"})
  16862. }, {label: "DND5E.VehicleActions"}),
  16863. capacity: new foundry.data.fields.SchemaField({
  16864. creature: new foundry.data.fields.StringField({required: true, label: "DND5E.VehicleCreatureCapacity"}),
  16865. cargo: new foundry.data.fields.NumberField({
  16866. required: true, nullable: false, integer: true, initial: 0, min: 0, label: "DND5E.VehicleCargoCapacity"
  16867. })
  16868. }, {label: "DND5E.VehicleCargoCrew"})
  16869. }, {label: "DND5E.Attributes"}),
  16870. details: new foundry.data.fields.SchemaField({
  16871. ...DetailsField.common,
  16872. source: new foundry.data.fields.StringField({required: true, label: "DND5E.Source"})
  16873. }, {label: "DND5E.Details"}),
  16874. traits: new foundry.data.fields.SchemaField({
  16875. ...TraitsField.common,
  16876. size: new foundry.data.fields.StringField({required: true, initial: "lg", label: "DND5E.Size"}),
  16877. di: TraitsField.makeDamageTrait({label: "DND5E.DamImm"}, {initial: ["poison", "psychic"]}),
  16878. ci: TraitsField.makeSimpleTrait({label: "DND5E.ConImm"}, {initial: [
  16879. "blinded", "charmed", "deafened", "frightened", "paralyzed",
  16880. "petrified", "poisoned", "stunned", "unconscious"
  16881. ]}),
  16882. dimensions: new foundry.data.fields.StringField({required: true, label: "DND5E.Dimensions"})
  16883. }, {label: "DND5E.Traits"}),
  16884. cargo: new foundry.data.fields.SchemaField({
  16885. crew: new foundry.data.fields.ArrayField(makePassengerData(), {label: "DND5E.VehicleCrew"}),
  16886. passengers: new foundry.data.fields.ArrayField(makePassengerData(), {label: "DND5E.VehiclePassengers"})
  16887. }, {label: "DND5E.VehicleCrewPassengers"})
  16888. });
  16889. }
  16890. /* -------------------------------------------- */
  16891. /** @inheritdoc */
  16892. static migrateData(source) {
  16893. super.migrateData(source);
  16894. AttributesFields._migrateInitiative(source.attributes);
  16895. }
  16896. }
  16897. /* -------------------------------------------- */
  16898. /**
  16899. * Data structure for an entry in a vehicle's crew or passenger lists.
  16900. *
  16901. * @typedef {object} PassengerData
  16902. * @property {string} name Name of individual or type of creature.
  16903. * @property {number} quantity How many of this creature are onboard?
  16904. */
  16905. /**
  16906. * Produce the schema field for a simple trait.
  16907. * @param {object} schemaOptions Options passed to the outer schema.
  16908. * @returns {PassengerData}
  16909. */
  16910. function makePassengerData(schemaOptions={}) {
  16911. return new foundry.data.fields.SchemaField({
  16912. name: new foundry.data.fields.StringField({required: true, label: "DND5E.VehiclePassengerName"}),
  16913. quantity: new foundry.data.fields.NumberField({
  16914. required: true, nullable: false, integer: true, initial: 0, min: 0, label: "DND5E.VehiclePassengerQuantity"
  16915. })
  16916. }, schemaOptions);
  16917. }
  16918. const config$2 = {
  16919. character: CharacterData,
  16920. group: GroupActor,
  16921. npc: NPCData,
  16922. vehicle: VehicleData
  16923. };
  16924. var _module$4 = /*#__PURE__*/Object.freeze({
  16925. __proto__: null,
  16926. AttributesFields: AttributesFields,
  16927. CharacterData: CharacterData,
  16928. CommonTemplate: CommonTemplate,
  16929. CreatureTemplate: CreatureTemplate,
  16930. DetailsFields: DetailsField,
  16931. GroupData: GroupActor,
  16932. NPCData: NPCData,
  16933. TraitsFields: TraitsField,
  16934. VehicleData: VehicleData,
  16935. config: config$2
  16936. });
  16937. var _module$3 = /*#__PURE__*/Object.freeze({
  16938. __proto__: null,
  16939. BaseAdvancement: BaseAdvancement,
  16940. ItemChoiceConfigurationData: ItemChoiceConfigurationData,
  16941. ItemGrantConfigurationData: ItemGrantConfigurationData,
  16942. SpellConfigurationData: SpellConfigurationData,
  16943. scaleValue: scaleValue
  16944. });
  16945. /**
  16946. * Data model template with item description & source.
  16947. *
  16948. * @property {object} description Various item descriptions.
  16949. * @property {string} description.value Full item description.
  16950. * @property {string} description.chat Description displayed in chat card.
  16951. * @property {string} description.unidentified Description displayed if item is unidentified.
  16952. * @property {string} source Adventure or sourcebook where this item originated.
  16953. * @mixin
  16954. */
  16955. class ItemDescriptionTemplate extends foundry.abstract.DataModel {
  16956. /** @inheritdoc */
  16957. static defineSchema() {
  16958. return {
  16959. description: new foundry.data.fields.SchemaField({
  16960. value: new foundry.data.fields.HTMLField({required: true, nullable: true, label: "DND5E.Description"}),
  16961. chat: new foundry.data.fields.HTMLField({required: true, nullable: true, label: "DND5E.DescriptionChat"}),
  16962. unidentified: new foundry.data.fields.HTMLField({
  16963. required: true, nullable: true, label: "DND5E.DescriptionUnidentified"
  16964. })
  16965. }),
  16966. source: new foundry.data.fields.StringField({required: true, label: "DND5E.Source"})
  16967. };
  16968. }
  16969. /* -------------------------------------------- */
  16970. /* Migrations */
  16971. /* -------------------------------------------- */
  16972. /** @inheritdoc */
  16973. static migrateData(source) {
  16974. ItemDescriptionTemplate.#migrateSource(source);
  16975. }
  16976. /* -------------------------------------------- */
  16977. /**
  16978. * Convert null source to the blank string.
  16979. * @param {object} source The candidate source data from which the model will be constructed.
  16980. */
  16981. static #migrateSource(source) {
  16982. if ( source.source === null ) source.source = "";
  16983. }
  16984. }
  16985. /**
  16986. * Data definition for Background items.
  16987. * @mixes ItemDescriptionTemplate
  16988. *
  16989. * @property {object[]} advancement Advancement objects for this background.
  16990. */
  16991. class BackgroundData extends SystemDataModel.mixin(ItemDescriptionTemplate) {
  16992. /** @inheritdoc */
  16993. static defineSchema() {
  16994. return this.mergeSchema(super.defineSchema(), {
  16995. advancement: new foundry.data.fields.ArrayField(new AdvancementField(), {label: "DND5E.AdvancementTitle"})
  16996. });
  16997. }
  16998. }
  16999. /**
  17000. * Data definition for Class items.
  17001. * @mixes ItemDescriptionTemplate
  17002. *
  17003. * @property {string} identifier Identifier slug for this class.
  17004. * @property {number} levels Current number of levels in this class.
  17005. * @property {string} hitDice Denomination of hit dice available as defined in `DND5E.hitDieTypes`.
  17006. * @property {number} hitDiceUsed Number of hit dice consumed.
  17007. * @property {object[]} advancement Advancement objects for this class.
  17008. * @property {string[]} saves Savings throws in which this class grants proficiency.
  17009. * @property {object} skills Available class skills and selected skills.
  17010. * @property {number} skills.number Number of skills selectable by the player.
  17011. * @property {string[]} skills.choices List of skill keys that are valid to be chosen.
  17012. * @property {string[]} skills.value List of skill keys the player has chosen.
  17013. * @property {object} spellcasting Details on class's spellcasting ability.
  17014. * @property {string} spellcasting.progression Spell progression granted by class as from `DND5E.spellProgression`.
  17015. * @property {string} spellcasting.ability Ability score to use for spellcasting.
  17016. */
  17017. class ClassData extends SystemDataModel.mixin(ItemDescriptionTemplate) {
  17018. /** @inheritdoc */
  17019. static defineSchema() {
  17020. return this.mergeSchema(super.defineSchema(), {
  17021. identifier: new IdentifierField({required: true, label: "DND5E.Identifier"}),
  17022. levels: new foundry.data.fields.NumberField({
  17023. required: true, nullable: false, integer: true, min: 0, initial: 1, label: "DND5E.ClassLevels"
  17024. }),
  17025. hitDice: new foundry.data.fields.StringField({
  17026. required: true, initial: "d6", blank: false, label: "DND5E.HitDice",
  17027. validate: v => /d\d+/.test(v), validationError: "must be a dice value in the format d#"
  17028. }),
  17029. hitDiceUsed: new foundry.data.fields.NumberField({
  17030. required: true, nullable: false, integer: true, initial: 0, min: 0, label: "DND5E.HitDiceUsed"
  17031. }),
  17032. advancement: new foundry.data.fields.ArrayField(new AdvancementField(), {label: "DND5E.AdvancementTitle"}),
  17033. saves: new foundry.data.fields.ArrayField(new foundry.data.fields.StringField(), {label: "DND5E.ClassSaves"}),
  17034. skills: new foundry.data.fields.SchemaField({
  17035. number: new foundry.data.fields.NumberField({
  17036. required: true, nullable: false, integer: true, min: 0, initial: 2, label: "DND5E.ClassSkillsNumber"
  17037. }),
  17038. choices: new foundry.data.fields.ArrayField(
  17039. new foundry.data.fields.StringField(), {label: "DND5E.ClassSkillsEligible"}
  17040. ),
  17041. value: new foundry.data.fields.ArrayField(
  17042. new foundry.data.fields.StringField(), {label: "DND5E.ClassSkillsChosen"}
  17043. )
  17044. }),
  17045. spellcasting: new foundry.data.fields.SchemaField({
  17046. progression: new foundry.data.fields.StringField({
  17047. required: true, initial: "none", blank: false, label: "DND5E.SpellProgression"
  17048. }),
  17049. ability: new foundry.data.fields.StringField({required: true, label: "DND5E.SpellAbility"})
  17050. }, {label: "DND5E.Spellcasting"})
  17051. });
  17052. }
  17053. /* -------------------------------------------- */
  17054. /* Migrations */
  17055. /* -------------------------------------------- */
  17056. /** @inheritdoc */
  17057. static migrateData(source) {
  17058. super.migrateData(source);
  17059. ClassData.#migrateLevels(source);
  17060. ClassData.#migrateSpellcastingData(source);
  17061. }
  17062. /* -------------------------------------------- */
  17063. /**
  17064. * Migrate the class levels.
  17065. * @param {object} source The candidate source data from which the model will be constructed.
  17066. */
  17067. static #migrateLevels(source) {
  17068. if ( typeof source.levels !== "string" ) return;
  17069. if ( source.levels === "" ) source.levels = 1;
  17070. else if ( Number.isNumeric(source.levels) ) source.levels = Number(source.levels);
  17071. }
  17072. /* -------------------------------------------- */
  17073. /**
  17074. * Migrate the class's spellcasting string to object.
  17075. * @param {object} source The candidate source data from which the model will be constructed.
  17076. */
  17077. static #migrateSpellcastingData(source) {
  17078. if ( source.spellcasting?.progression === "" ) source.spellcasting.progression = "none";
  17079. if ( typeof source.spellcasting !== "string" ) return;
  17080. source.spellcasting = {
  17081. progression: source.spellcasting,
  17082. ability: ""
  17083. };
  17084. }
  17085. }
  17086. /**
  17087. * Data model template for item actions.
  17088. *
  17089. * @property {string} ability Ability score to use when determining modifier.
  17090. * @property {string} actionType Action type as defined in `DND5E.itemActionTypes`.
  17091. * @property {string} attackBonus Numeric or dice bonus to attack rolls.
  17092. * @property {string} chatFlavor Extra text displayed in chat.
  17093. * @property {object} critical Information on how critical hits are handled.
  17094. * @property {number} critical.threshold Minimum number on the dice to roll a critical hit.
  17095. * @property {string} critical.damage Extra damage on critical hit.
  17096. * @property {object} damage Item damage formulas.
  17097. * @property {string[][]} damage.parts Array of damage formula and types.
  17098. * @property {string} damage.versatile Special versatile damage formula.
  17099. * @property {string} formula Other roll formula.
  17100. * @property {object} save Item saving throw data.
  17101. * @property {string} save.ability Ability required for the save.
  17102. * @property {number} save.dc Custom saving throw value.
  17103. * @property {string} save.scaling Method for automatically determining saving throw DC.
  17104. * @mixin
  17105. */
  17106. class ActionTemplate extends foundry.abstract.DataModel {
  17107. /** @inheritdoc */
  17108. static defineSchema() {
  17109. return {
  17110. ability: new foundry.data.fields.StringField({
  17111. required: true, nullable: true, initial: null, label: "DND5E.AbilityModifier"
  17112. }),
  17113. actionType: new foundry.data.fields.StringField({
  17114. required: true, nullable: true, initial: null, label: "DND5E.ItemActionType"
  17115. }),
  17116. attackBonus: new FormulaField({required: true, label: "DND5E.ItemAttackBonus"}),
  17117. chatFlavor: new foundry.data.fields.StringField({required: true, label: "DND5E.ChatFlavor"}),
  17118. critical: new foundry.data.fields.SchemaField({
  17119. threshold: new foundry.data.fields.NumberField({
  17120. required: true, integer: true, initial: null, positive: true, label: "DND5E.ItemCritThreshold"
  17121. }),
  17122. damage: new FormulaField({required: true, label: "DND5E.ItemCritExtraDamage"})
  17123. }),
  17124. damage: new foundry.data.fields.SchemaField({
  17125. parts: new foundry.data.fields.ArrayField(new foundry.data.fields.ArrayField(
  17126. new foundry.data.fields.StringField({nullable: true})
  17127. ), {required: true}),
  17128. versatile: new FormulaField({required: true, label: "DND5E.VersatileDamage"})
  17129. }, {label: "DND5E.Damage"}),
  17130. formula: new FormulaField({required: true, label: "DND5E.OtherFormula"}),
  17131. save: new foundry.data.fields.SchemaField({
  17132. ability: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.Ability"}),
  17133. dc: new foundry.data.fields.NumberField({
  17134. required: true, min: 0, integer: true, label: "DND5E.AbbreviationDC"
  17135. }),
  17136. scaling: new foundry.data.fields.StringField({
  17137. required: true, blank: false, initial: "spell", label: "DND5E.ScalingFormula"
  17138. })
  17139. }, {label: "DND5E.SavingThrow"})
  17140. };
  17141. }
  17142. /* -------------------------------------------- */
  17143. /* Migrations */
  17144. /* -------------------------------------------- */
  17145. /** @inheritdoc */
  17146. static migrateData(source) {
  17147. ActionTemplate.#migrateAbility(source);
  17148. ActionTemplate.#migrateAttackBonus(source);
  17149. ActionTemplate.#migrateCritical(source);
  17150. ActionTemplate.#migrateSave(source);
  17151. ActionTemplate.#migrateDamage(source);
  17152. }
  17153. /* -------------------------------------------- */
  17154. /**
  17155. * Migrate the ability field.
  17156. * @param {object} source The candidate source data from which the model will be constructed.
  17157. */
  17158. static #migrateAbility(source) {
  17159. if ( Array.isArray(source.ability) ) source.ability = source.ability[0];
  17160. }
  17161. /* -------------------------------------------- */
  17162. /**
  17163. * Ensure a 0 or null in attack bonus is converted to an empty string rather than "0".
  17164. * @param {object} source The candidate source data from which the model will be constructed.
  17165. */
  17166. static #migrateAttackBonus(source) {
  17167. if ( [0, "0", null].includes(source.attackBonus) ) source.attackBonus = "";
  17168. else if ( typeof source.attackBonus === "number" ) source.attackBonus = source.attackBonus.toString();
  17169. }
  17170. /* -------------------------------------------- */
  17171. /**
  17172. * Ensure the critical field is an object.
  17173. * @param {object} source The candidate source data from which the model will be constructed.
  17174. */
  17175. static #migrateCritical(source) {
  17176. if ( !("critical" in source) ) return;
  17177. if ( (typeof source.critical !== "object") || (source.critical === null) ) source.critical = {
  17178. threshold: null,
  17179. damage: ""
  17180. };
  17181. if ( source.critical.damage === null ) source.critical.damage = "";
  17182. }
  17183. /* -------------------------------------------- */
  17184. /**
  17185. * Migrate the save field.
  17186. * @param {object} source The candidate source data from which the model will be constructed.
  17187. */
  17188. static #migrateSave(source) {
  17189. if ( !("save" in source) ) return;
  17190. source.save ??= {};
  17191. if ( source.save.scaling === "" ) source.save.scaling = "spell";
  17192. if ( source.save.ability === null ) source.save.ability = "";
  17193. if ( typeof source.save.dc === "string" ) {
  17194. if ( source.save.dc === "" ) source.save.dc = null;
  17195. else if ( Number.isNumeric(source.save.dc) ) source.save.dc = Number(source.save.dc);
  17196. }
  17197. }
  17198. /* -------------------------------------------- */
  17199. /**
  17200. * Migrate damage parts.
  17201. * @param {object} source The candidate source data from which the model will be constructed.
  17202. */
  17203. static #migrateDamage(source) {
  17204. if ( !("damage" in source) ) return;
  17205. source.damage ??= {};
  17206. source.damage.parts ??= [];
  17207. }
  17208. /* -------------------------------------------- */
  17209. /* Getters */
  17210. /* -------------------------------------------- */
  17211. /**
  17212. * Which ability score modifier is used by this item?
  17213. * @type {string|null}
  17214. */
  17215. get abilityMod() {
  17216. return this.ability || this._typeAbilityMod || {
  17217. mwak: "str",
  17218. rwak: "dex",
  17219. msak: this.parent?.actor?.system.attributes.spellcasting || "int",
  17220. rsak: this.parent?.actor?.system.attributes.spellcasting || "int"
  17221. }[this.actionType] || null;
  17222. }
  17223. /* -------------------------------------------- */
  17224. /**
  17225. * Default ability key defined for this type.
  17226. * @type {string|null}
  17227. * @internal
  17228. */
  17229. get _typeAbilityMod() {
  17230. return null;
  17231. }
  17232. /* -------------------------------------------- */
  17233. /**
  17234. * What is the critical hit threshold for this item? Uses the smallest value from among the following sources:
  17235. * - `critical.threshold` defined on the item
  17236. * - `critical.threshold` defined on ammunition, if consumption mode is set to ammo
  17237. * - Type-specific critical threshold
  17238. * @type {number|null}
  17239. */
  17240. get criticalThreshold() {
  17241. if ( !this.hasAttack ) return null;
  17242. let ammoThreshold = Infinity;
  17243. if ( this.consume?.type === "ammo" ) {
  17244. ammoThreshold = this.parent?.actor?.items.get(this.consume.target).system.critical.threshold ?? Infinity;
  17245. }
  17246. const threshold = Math.min(this.critical.threshold ?? Infinity, this._typeCriticalThreshold, ammoThreshold);
  17247. return threshold < Infinity ? threshold : 20;
  17248. }
  17249. /* -------------------------------------------- */
  17250. /**
  17251. * Default critical threshold for this type.
  17252. * @type {number}
  17253. * @internal
  17254. */
  17255. get _typeCriticalThreshold() {
  17256. return Infinity;
  17257. }
  17258. /* -------------------------------------------- */
  17259. /**
  17260. * Does the Item implement an ability check as part of its usage?
  17261. * @type {boolean}
  17262. */
  17263. get hasAbilityCheck() {
  17264. return (this.actionType === "abil") && !!this.ability;
  17265. }
  17266. /* -------------------------------------------- */
  17267. /**
  17268. * Does the Item implement an attack roll as part of its usage?
  17269. * @type {boolean}
  17270. */
  17271. get hasAttack() {
  17272. return ["mwak", "rwak", "msak", "rsak"].includes(this.actionType);
  17273. }
  17274. /* -------------------------------------------- */
  17275. /**
  17276. * Does the Item implement a damage roll as part of its usage?
  17277. * @type {boolean}
  17278. */
  17279. get hasDamage() {
  17280. return this.actionType && (this.damage.parts.length > 0);
  17281. }
  17282. /* -------------------------------------------- */
  17283. /**
  17284. * Does the Item implement a saving throw as part of its usage?
  17285. * @type {boolean}
  17286. */
  17287. get hasSave() {
  17288. return this.actionType && !!(this.save.ability && this.save.scaling);
  17289. }
  17290. /* -------------------------------------------- */
  17291. /**
  17292. * Does the Item provide an amount of healing instead of conventional damage?
  17293. * @type {boolean}
  17294. */
  17295. get isHealing() {
  17296. return (this.actionType === "heal") && this.hasDamage;
  17297. }
  17298. /* -------------------------------------------- */
  17299. /**
  17300. * Does the Item implement a versatile damage roll as part of its usage?
  17301. * @type {boolean}
  17302. */
  17303. get isVersatile() {
  17304. return this.actionType && !!(this.hasDamage && this.damage.versatile);
  17305. }
  17306. }
  17307. /**
  17308. * Data model template for items that can be used as some sort of action.
  17309. *
  17310. * @property {object} activation Effect's activation conditions.
  17311. * @property {string} activation.type Activation type as defined in `DND5E.abilityActivationTypes`.
  17312. * @property {number} activation.cost How much of the activation type is needed to use this item's effect.
  17313. * @property {string} activation.condition Special conditions required to activate the item.
  17314. * @property {object} duration Effect's duration.
  17315. * @property {number} duration.value How long the effect lasts.
  17316. * @property {string} duration.units Time duration period as defined in `DND5E.timePeriods`.
  17317. * @property {number} cover Amount of cover does this item affords to its crew on a vehicle.
  17318. * @property {object} target Effect's valid targets.
  17319. * @property {number} target.value Length or radius of target depending on targeting mode selected.
  17320. * @property {number} target.width Width of line when line type is selected.
  17321. * @property {string} target.units Units used for value and width as defined in `DND5E.distanceUnits`.
  17322. * @property {string} target.type Targeting mode as defined in `DND5E.targetTypes`.
  17323. * @property {object} range Effect's range.
  17324. * @property {number} range.value Regular targeting distance for item's effect.
  17325. * @property {number} range.long Maximum targeting distance for features that have a separate long range.
  17326. * @property {string} range.units Units used for value and long as defined in `DND5E.distanceUnits`.
  17327. * @property {object} uses Effect's limited uses.
  17328. * @property {number} uses.value Current available uses.
  17329. * @property {string} uses.max Maximum possible uses or a formula to derive that number.
  17330. * @property {string} uses.per Recharge time for limited uses as defined in `DND5E.limitedUsePeriods`.
  17331. * @property {object} consume Effect's resource consumption.
  17332. * @property {string} consume.type Type of resource to consume as defined in `DND5E.abilityConsumptionTypes`.
  17333. * @property {string} consume.target Item ID or resource key path of resource to consume.
  17334. * @property {number} consume.amount Quantity of the resource to consume per use.
  17335. * @mixin
  17336. */
  17337. class ActivatedEffectTemplate extends foundry.abstract.DataModel {
  17338. /** @inheritdoc */
  17339. static defineSchema() {
  17340. return {
  17341. activation: new foundry.data.fields.SchemaField({
  17342. type: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.ItemActivationType"}),
  17343. cost: new foundry.data.fields.NumberField({required: true, label: "DND5E.ItemActivationCost"}),
  17344. condition: new foundry.data.fields.StringField({required: true, label: "DND5E.ItemActivationCondition"})
  17345. }, {label: "DND5E.ItemActivation"}),
  17346. duration: new foundry.data.fields.SchemaField({
  17347. value: new FormulaField({required: true, deterministic: true, label: "DND5E.Duration"}),
  17348. units: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.DurationType"})
  17349. }, {label: "DND5E.Duration"}),
  17350. cover: new foundry.data.fields.NumberField({
  17351. required: true, nullable: true, min: 0, max: 1, label: "DND5E.Cover"
  17352. }),
  17353. crewed: new foundry.data.fields.BooleanField({label: "DND5E.Crewed"}),
  17354. target: new foundry.data.fields.SchemaField({
  17355. value: new foundry.data.fields.NumberField({required: true, min: 0, label: "DND5E.TargetValue"}),
  17356. width: new foundry.data.fields.NumberField({required: true, min: 0, label: "DND5E.TargetWidth"}),
  17357. units: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.TargetUnits"}),
  17358. type: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.TargetType"})
  17359. }, {label: "DND5E.Target"}),
  17360. range: new foundry.data.fields.SchemaField({
  17361. value: new foundry.data.fields.NumberField({required: true, min: 0, label: "DND5E.RangeNormal"}),
  17362. long: new foundry.data.fields.NumberField({required: true, min: 0, label: "DND5E.RangeLong"}),
  17363. units: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.RangeUnits"})
  17364. }, {label: "DND5E.Range"}),
  17365. uses: new this.ItemUsesField({}, {label: "DND5E.LimitedUses"}),
  17366. consume: new foundry.data.fields.SchemaField({
  17367. type: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.ConsumeType"}),
  17368. target: new foundry.data.fields.StringField({
  17369. required: true, nullable: true, initial: null, label: "DND5E.ConsumeTarget"
  17370. }),
  17371. amount: new foundry.data.fields.NumberField({required: true, integer: true, label: "DND5E.ConsumeAmount"})
  17372. }, {label: "DND5E.ConsumeTitle"})
  17373. };
  17374. }
  17375. /* -------------------------------------------- */
  17376. /**
  17377. * Extension of SchemaField used to track item uses.
  17378. * @internal
  17379. */
  17380. static ItemUsesField = class ItemUsesField extends foundry.data.fields.SchemaField {
  17381. constructor(extraSchema, options) {
  17382. super(SystemDataModel.mergeSchema({
  17383. value: new foundry.data.fields.NumberField({
  17384. required: true, min: 0, integer: true, label: "DND5E.LimitedUsesAvailable"
  17385. }),
  17386. max: new FormulaField({required: true, deterministic: true, label: "DND5E.LimitedUsesMax"}),
  17387. per: new foundry.data.fields.StringField({
  17388. required: true, nullable: true, blank: false, initial: null, label: "DND5E.LimitedUsesPer"
  17389. }),
  17390. recovery: new FormulaField({required: true, label: "DND5E.RecoveryFormula"})
  17391. }, extraSchema), options);
  17392. }
  17393. };
  17394. /* -------------------------------------------- */
  17395. /* Migrations */
  17396. /* -------------------------------------------- */
  17397. /** @inheritdoc */
  17398. static migrateData(source) {
  17399. ActivatedEffectTemplate.#migrateFormulaFields(source);
  17400. ActivatedEffectTemplate.#migrateRanges(source);
  17401. ActivatedEffectTemplate.#migrateTargets(source);
  17402. ActivatedEffectTemplate.#migrateUses(source);
  17403. ActivatedEffectTemplate.#migrateConsume(source);
  17404. }
  17405. /* -------------------------------------------- */
  17406. /**
  17407. * Ensure a 0 or null in max uses & durations are converted to an empty string rather than "0". Convert numbers into
  17408. * strings.
  17409. * @param {object} source The candidate source data from which the model will be constructed.
  17410. */
  17411. static #migrateFormulaFields(source) {
  17412. if ( [0, "0", null].includes(source.uses?.max) ) source.uses.max = "";
  17413. else if ( typeof source.uses?.max === "number" ) source.uses.max = source.uses.max.toString();
  17414. if ( [0, "0", null].includes(source.duration?.value) ) source.duration.value = "";
  17415. else if ( typeof source.duration?.value === "number" ) source.duration.value = source.duration.value.toString();
  17416. }
  17417. /* -------------------------------------------- */
  17418. /**
  17419. * Fix issue with some imported range data that uses the format "100/400" in the range field,
  17420. * rather than splitting it between "range.value" & "range.long".
  17421. * @param {object} source The candidate source data from which the model will be constructed.
  17422. */
  17423. static #migrateRanges(source) {
  17424. if ( !("range" in source) ) return;
  17425. source.range ??= {};
  17426. if ( source.range.units === null ) source.range.units = "";
  17427. if ( typeof source.range.long === "string" ) {
  17428. if ( source.range.long === "" ) source.range.long = null;
  17429. else if ( Number.isNumeric(source.range.long) ) source.range.long = Number(source.range.long);
  17430. }
  17431. if ( typeof source.range.value !== "string" ) return;
  17432. if ( source.range.value === "" ) {
  17433. source.range.value = null;
  17434. return;
  17435. }
  17436. const [value, long] = source.range.value.split("/");
  17437. if ( Number.isNumeric(value) ) source.range.value = Number(value);
  17438. if ( Number.isNumeric(long) ) source.range.long = Number(long);
  17439. }
  17440. /* -------------------------------------------- */
  17441. /**
  17442. * Ensure blank strings in targets are converted to null.
  17443. * @param {object} source The candidate source data from which the model will be constructed.
  17444. */
  17445. static #migrateTargets(source) {
  17446. if ( !("target" in source) ) return;
  17447. source.target ??= {};
  17448. if ( source.target.value === "" ) source.target.value = null;
  17449. if ( source.target.units === null ) source.target.units = "";
  17450. if ( source.target.type === null ) source.target.type = "";
  17451. }
  17452. /* -------------------------------------------- */
  17453. /**
  17454. * Ensure a blank string in uses.value is converted to null.
  17455. * @param {object} source The candidate source data from which the model will be constructed.
  17456. */
  17457. static #migrateUses(source) {
  17458. if ( !("uses" in source) ) return;
  17459. source.uses ??= {};
  17460. const value = source.uses.value;
  17461. if ( typeof value === "string" ) {
  17462. if ( value === "" ) source.uses.value = null;
  17463. else if ( Number.isNumeric(value) ) source.uses.value = Number(source.uses.value);
  17464. }
  17465. if ( source.uses.recovery === undefined ) source.uses.recovery = "";
  17466. }
  17467. /* -------------------------------------------- */
  17468. /**
  17469. * Migrate the consume field.
  17470. * @param {object} source The candidate source data from which the model will be constructed.
  17471. */
  17472. static #migrateConsume(source) {
  17473. if ( !("consume" in source) ) return;
  17474. source.consume ??= {};
  17475. if ( source.consume.type === null ) source.consume.type = "";
  17476. const amount = source.consume.amount;
  17477. if ( typeof amount === "string" ) {
  17478. if ( amount === "" ) source.consume.amount = null;
  17479. else if ( Number.isNumeric(amount) ) source.consume.amount = Number(amount);
  17480. }
  17481. }
  17482. /* -------------------------------------------- */
  17483. /* Getters */
  17484. /* -------------------------------------------- */
  17485. /**
  17486. * Chat properties for activated effects.
  17487. * @type {string[]}
  17488. */
  17489. get activatedEffectChatProperties() {
  17490. return [
  17491. this.parent.labels.activation + (this.activation.condition ? ` (${this.activation.condition})` : ""),
  17492. this.parent.labels.target,
  17493. this.parent.labels.range,
  17494. this.parent.labels.duration
  17495. ];
  17496. }
  17497. /* -------------------------------------------- */
  17498. /**
  17499. * Does the Item have an area of effect target?
  17500. * @type {boolean}
  17501. */
  17502. get hasAreaTarget() {
  17503. return this.target.type in CONFIG.DND5E.areaTargetTypes;
  17504. }
  17505. /* -------------------------------------------- */
  17506. /**
  17507. * Does the Item target one or more distinct targets?
  17508. * @type {boolean}
  17509. */
  17510. get hasIndividualTarget() {
  17511. return this.target.type in CONFIG.DND5E.individualTargetTypes;
  17512. }
  17513. /* -------------------------------------------- */
  17514. /**
  17515. * Is this Item limited in its ability to be used by charges or by recharge?
  17516. * @type {boolean}
  17517. */
  17518. get hasLimitedUses() {
  17519. return !!this.uses.per && (this.uses.max > 0);
  17520. }
  17521. /* -------------------------------------------- */
  17522. /**
  17523. * Does the Item duration accept an associated numeric value or formula?
  17524. * @type {boolean}
  17525. */
  17526. get hasScalarDuration() {
  17527. return this.duration.units in CONFIG.DND5E.scalarTimePeriods;
  17528. }
  17529. /* -------------------------------------------- */
  17530. /**
  17531. * Does the Item range accept an associated numeric value?
  17532. * @type {boolean}
  17533. */
  17534. get hasScalarRange() {
  17535. return this.range.units in CONFIG.DND5E.movementUnits;
  17536. }
  17537. /* -------------------------------------------- */
  17538. /**
  17539. * Does the Item target accept an associated numeric value?
  17540. * @type {boolean}
  17541. */
  17542. get hasScalarTarget() {
  17543. return ![null, "", "self"].includes(this.target.type);
  17544. }
  17545. /* -------------------------------------------- */
  17546. /**
  17547. * Does the Item have a target?
  17548. * @type {boolean}
  17549. */
  17550. get hasTarget() {
  17551. return !["", null].includes(this.target.type);
  17552. }
  17553. }
  17554. /**
  17555. * Data model template with information on items that can be attuned and equipped.
  17556. *
  17557. * @property {number} attunement Attunement information as defined in `DND5E.attunementTypes`.
  17558. * @property {boolean} equipped Is this item equipped on its owning actor.
  17559. * @mixin
  17560. */
  17561. class EquippableItemTemplate extends foundry.abstract.DataModel {
  17562. /** @inheritdoc */
  17563. static defineSchema() {
  17564. return {
  17565. attunement: new foundry.data.fields.NumberField({
  17566. required: true, integer: true, initial: CONFIG.DND5E.attunementTypes.NONE, label: "DND5E.Attunement"
  17567. }),
  17568. equipped: new foundry.data.fields.BooleanField({required: true, label: "DND5E.Equipped"})
  17569. };
  17570. }
  17571. /* -------------------------------------------- */
  17572. /* Migrations */
  17573. /* -------------------------------------------- */
  17574. /** @inheritdoc */
  17575. static migrateData(source) {
  17576. EquippableItemTemplate.#migrateAttunement(source);
  17577. EquippableItemTemplate.#migrateEquipped(source);
  17578. }
  17579. /* -------------------------------------------- */
  17580. /**
  17581. * Migrate the item's attuned boolean to attunement string.
  17582. * @param {object} source The candidate source data from which the model will be constructed.
  17583. */
  17584. static #migrateAttunement(source) {
  17585. if ( (source.attuned === undefined) || (source.attunement !== undefined) ) return;
  17586. source.attunement = source.attuned ? CONFIG.DND5E.attunementTypes.ATTUNED : CONFIG.DND5E.attunementTypes.NONE;
  17587. }
  17588. /* -------------------------------------------- */
  17589. /**
  17590. * Migrate the equipped field.
  17591. * @param {object} source The candidate source data from which the model will be constructed.
  17592. */
  17593. static #migrateEquipped(source) {
  17594. if ( !("equipped" in source) ) return;
  17595. if ( (source.equipped === null) || (source.equipped === undefined) ) source.equipped = false;
  17596. }
  17597. /* -------------------------------------------- */
  17598. /* Getters */
  17599. /* -------------------------------------------- */
  17600. /**
  17601. * Chat properties for equippable items.
  17602. * @type {string[]}
  17603. */
  17604. get equippableItemChatProperties() {
  17605. const req = CONFIG.DND5E.attunementTypes.REQUIRED;
  17606. return [
  17607. this.attunement === req ? CONFIG.DND5E.attunements[req] : null,
  17608. game.i18n.localize(this.equipped ? "DND5E.Equipped" : "DND5E.Unequipped"),
  17609. ("proficient" in this) ? CONFIG.DND5E.proficiencyLevels[Number(this.proficient)] : null
  17610. ];
  17611. }
  17612. }
  17613. /**
  17614. * Data model template with information on physical items.
  17615. *
  17616. * @property {number} quantity Number of items in a stack.
  17617. * @property {number} weight Item's weight in pounds or kilograms (depending on system setting).
  17618. * @property {object} price
  17619. * @property {number} price.value Item's cost in the specified denomination.
  17620. * @property {string} price.denomination Currency denomination used to determine price.
  17621. * @property {string} rarity Item rarity as defined in `DND5E.itemRarity`.
  17622. * @property {boolean} identified Has this item been identified?
  17623. * @mixin
  17624. */
  17625. class PhysicalItemTemplate extends foundry.abstract.DataModel {
  17626. /** @inheritdoc */
  17627. static defineSchema() {
  17628. return {
  17629. quantity: new foundry.data.fields.NumberField({
  17630. required: true, nullable: false, integer: true, initial: 1, min: 0, label: "DND5E.Quantity"
  17631. }),
  17632. weight: new foundry.data.fields.NumberField({
  17633. required: true, nullable: false, initial: 0, min: 0, label: "DND5E.Weight"
  17634. }),
  17635. price: new foundry.data.fields.SchemaField({
  17636. value: new foundry.data.fields.NumberField({
  17637. required: true, nullable: false, initial: 0, min: 0, label: "DND5E.Price"
  17638. }),
  17639. denomination: new foundry.data.fields.StringField({
  17640. required: true, blank: false, initial: "gp", label: "DND5E.Currency"
  17641. })
  17642. }, {label: "DND5E.Price"}),
  17643. rarity: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.Rarity"}),
  17644. identified: new foundry.data.fields.BooleanField({required: true, initial: true, label: "DND5E.Identified"})
  17645. };
  17646. }
  17647. /* -------------------------------------------- */
  17648. /* Migrations */
  17649. /* -------------------------------------------- */
  17650. /** @inheritdoc */
  17651. static migrateData(source) {
  17652. PhysicalItemTemplate.#migratePrice(source);
  17653. PhysicalItemTemplate.#migrateRarity(source);
  17654. PhysicalItemTemplate.#migrateWeight(source);
  17655. }
  17656. /* -------------------------------------------- */
  17657. /**
  17658. * Migrate the item's price from a single field to an object with currency.
  17659. * @param {object} source The candidate source data from which the model will be constructed.
  17660. */
  17661. static #migratePrice(source) {
  17662. if ( !("price" in source) || foundry.utils.getType(source.price) === "Object" ) return;
  17663. source.price = {
  17664. value: Number.isNumeric(source.price) ? Number(source.price) : 0,
  17665. denomination: "gp"
  17666. };
  17667. }
  17668. /* -------------------------------------------- */
  17669. /**
  17670. * Migrate the item's rarity from freeform string to enum value.
  17671. * @param {object} source The candidate source data from which the model will be constructed.
  17672. */
  17673. static #migrateRarity(source) {
  17674. if ( !("rarity" in source) || CONFIG.DND5E.itemRarity[source.rarity] ) return;
  17675. source.rarity = Object.keys(CONFIG.DND5E.itemRarity).find(key =>
  17676. CONFIG.DND5E.itemRarity[key].toLowerCase() === source.rarity.toLowerCase()
  17677. ) ?? "";
  17678. }
  17679. /* -------------------------------------------- */
  17680. /**
  17681. * Convert null weights to 0.
  17682. * @param {object} source The candidate source data from which the model will be constructed.
  17683. */
  17684. static #migrateWeight(source) {
  17685. if ( !("weight" in source) ) return;
  17686. if ( (source.weight === null) || (source.weight === undefined) ) source.weight = 0;
  17687. }
  17688. }
  17689. /**
  17690. * Data definition for Consumable items.
  17691. * @mixes ItemDescriptionTemplate
  17692. * @mixes PhysicalItemTemplate
  17693. * @mixes EquippableItemTemplate
  17694. * @mixes ActivatedEffectTemplate
  17695. * @mixes ActionTemplate
  17696. *
  17697. * @property {string} consumableType Type of consumable as defined in `DND5E.consumableTypes`.
  17698. * @property {object} uses
  17699. * @property {boolean} uses.autoDestroy Should this item be destroyed when it runs out of uses.
  17700. */
  17701. class ConsumableData extends SystemDataModel.mixin(
  17702. ItemDescriptionTemplate, PhysicalItemTemplate, EquippableItemTemplate, ActivatedEffectTemplate, ActionTemplate
  17703. ) {
  17704. /** @inheritdoc */
  17705. static defineSchema() {
  17706. return this.mergeSchema(super.defineSchema(), {
  17707. consumableType: new foundry.data.fields.StringField({
  17708. required: true, initial: "potion", label: "DND5E.ItemConsumableType"
  17709. }),
  17710. uses: new ActivatedEffectTemplate.ItemUsesField({
  17711. autoDestroy: new foundry.data.fields.BooleanField({required: true, label: "DND5E.ItemDestroyEmpty"})
  17712. }, {label: "DND5E.LimitedUses"})
  17713. });
  17714. }
  17715. /* -------------------------------------------- */
  17716. /* Getters */
  17717. /* -------------------------------------------- */
  17718. /**
  17719. * Properties displayed in chat.
  17720. * @type {string[]}
  17721. */
  17722. get chatProperties() {
  17723. return [
  17724. CONFIG.DND5E.consumableTypes[this.consumableType],
  17725. this.hasLimitedUses ? `${this.uses.value}/${this.uses.max} ${game.i18n.localize("DND5E.Charges")}` : null
  17726. ];
  17727. }
  17728. /* -------------------------------------------- */
  17729. /** @inheritdoc */
  17730. get _typeAbilityMod() {
  17731. if ( this.consumableType !== "scroll" ) return null;
  17732. return this.parent?.actor?.system.attributes.spellcasting || "int";
  17733. }
  17734. }
  17735. /**
  17736. * Data definition for Backpack items.
  17737. * @mixes ItemDescriptionTemplate
  17738. * @mixes PhysicalItemTemplate
  17739. * @mixes EquippableItemTemplate
  17740. * @mixes CurrencyTemplate
  17741. *
  17742. * @property {object} capacity Information on container's carrying capacity.
  17743. * @property {string} capacity.type Method for tracking max capacity as defined in `DND5E.itemCapacityTypes`.
  17744. * @property {number} capacity.value Total amount of the type this container can carry.
  17745. * @property {boolean} capacity.weightless Does the weight of the items in the container carry over to the actor?
  17746. */
  17747. class ContainerData extends SystemDataModel.mixin(
  17748. ItemDescriptionTemplate, PhysicalItemTemplate, EquippableItemTemplate, CurrencyTemplate
  17749. ) {
  17750. /** @inheritdoc */
  17751. static defineSchema() {
  17752. return this.mergeSchema(super.defineSchema(), {
  17753. capacity: new foundry.data.fields.SchemaField({
  17754. type: new foundry.data.fields.StringField({
  17755. required: true, initial: "weight", blank: false, label: "DND5E.ItemContainerCapacityType"
  17756. }),
  17757. value: new foundry.data.fields.NumberField({
  17758. required: true, min: 0, label: "DND5E.ItemContainerCapacityMax"
  17759. }),
  17760. weightless: new foundry.data.fields.BooleanField({required: true, label: "DND5E.ItemContainerWeightless"})
  17761. }, {label: "DND5E.ItemContainerCapacity"})
  17762. });
  17763. }
  17764. }
  17765. /**
  17766. * Data model template for equipment that can be mounted on a vehicle.
  17767. *
  17768. * @property {object} armor Equipment's armor class.
  17769. * @property {number} armor.value Armor class value for equipment.
  17770. * @property {object} hp Equipment's hit points.
  17771. * @property {number} hp.value Current hit point value.
  17772. * @property {number} hp.max Max hit points.
  17773. * @property {number} hp.dt Damage threshold.
  17774. * @property {string} hp.conditions Conditions that are triggered when this equipment takes damage.
  17775. * @mixin
  17776. */
  17777. class MountableTemplate extends foundry.abstract.DataModel {
  17778. /** @inheritdoc */
  17779. static defineSchema() {
  17780. return {
  17781. armor: new foundry.data.fields.SchemaField({
  17782. value: new foundry.data.fields.NumberField({
  17783. required: true, integer: true, min: 0, label: "DND5E.ArmorClass"
  17784. })
  17785. }, {label: "DND5E.ArmorClass"}),
  17786. hp: new foundry.data.fields.SchemaField({
  17787. value: new foundry.data.fields.NumberField({
  17788. required: true, integer: true, min: 0, label: "DND5E.HitPointsCurrent"
  17789. }),
  17790. max: new foundry.data.fields.NumberField({
  17791. required: true, integer: true, min: 0, label: "DND5E.HitPointsMax"
  17792. }),
  17793. dt: new foundry.data.fields.NumberField({
  17794. required: true, integer: true, min: 0, label: "DND5E.DamageThreshold"
  17795. }),
  17796. conditions: new foundry.data.fields.StringField({required: true, label: "DND5E.HealthConditions"})
  17797. }, {label: "DND5E.HitPoints"})
  17798. };
  17799. }
  17800. }
  17801. /**
  17802. * Data definition for Equipment items.
  17803. * @mixes ItemDescriptionTemplate
  17804. * @mixes PhysicalItemTemplate
  17805. * @mixes EquippableItemTemplate
  17806. * @mixes ActivatedEffectTemplate
  17807. * @mixes ActionTemplate
  17808. * @mixes MountableTemplate
  17809. *
  17810. * @property {object} armor Armor details and equipment type information.
  17811. * @property {string} armor.type Equipment type as defined in `DND5E.equipmentTypes`.
  17812. * @property {number} armor.value Base armor class or shield bonus.
  17813. * @property {number} armor.dex Maximum dex bonus added to armor class.
  17814. * @property {string} baseItem Base armor as defined in `DND5E.armorIds` for determining proficiency.
  17815. * @property {object} speed Speed granted by a piece of vehicle equipment.
  17816. * @property {number} speed.value Speed granted by this piece of equipment measured in feet or meters
  17817. * depending on system setting.
  17818. * @property {string} speed.conditions Conditions that may affect item's speed.
  17819. * @property {number} strength Minimum strength required to use a piece of armor.
  17820. * @property {boolean} stealth Does this equipment grant disadvantage on stealth checks when used?
  17821. * @property {boolean} proficient Does the owner have proficiency in this piece of equipment?
  17822. */
  17823. class EquipmentData extends SystemDataModel.mixin(
  17824. ItemDescriptionTemplate, PhysicalItemTemplate, EquippableItemTemplate,
  17825. ActivatedEffectTemplate, ActionTemplate, MountableTemplate
  17826. ) {
  17827. /** @inheritdoc */
  17828. static defineSchema() {
  17829. return this.mergeSchema(super.defineSchema(), {
  17830. armor: new foundry.data.fields.SchemaField({
  17831. type: new foundry.data.fields.StringField({
  17832. required: true, initial: "light", label: "DND5E.ItemEquipmentType"
  17833. }),
  17834. value: new foundry.data.fields.NumberField({required: true, integer: true, min: 0, label: "DND5E.ArmorClass"}),
  17835. dex: new foundry.data.fields.NumberField({required: true, integer: true, label: "DND5E.ItemEquipmentDexMod"})
  17836. }, {label: ""}),
  17837. baseItem: new foundry.data.fields.StringField({required: true, label: "DND5E.ItemEquipmentBase"}),
  17838. speed: new foundry.data.fields.SchemaField({
  17839. value: new foundry.data.fields.NumberField({required: true, min: 0, label: "DND5E.Speed"}),
  17840. conditions: new foundry.data.fields.StringField({required: true, label: "DND5E.SpeedConditions"})
  17841. }, {label: "DND5E.Speed"}),
  17842. strength: new foundry.data.fields.NumberField({
  17843. required: true, integer: true, min: 0, label: "DND5E.ItemRequiredStr"
  17844. }),
  17845. stealth: new foundry.data.fields.BooleanField({required: true, label: "DND5E.ItemEquipmentStealthDisav"}),
  17846. proficient: new foundry.data.fields.BooleanField({required: true, initial: true, label: "DND5E.Proficient"})
  17847. });
  17848. }
  17849. /* -------------------------------------------- */
  17850. /* Migrations */
  17851. /* -------------------------------------------- */
  17852. /** @inheritdoc */
  17853. static migrateData(source) {
  17854. super.migrateData(source);
  17855. EquipmentData.#migrateArmor(source);
  17856. EquipmentData.#migrateStrength(source);
  17857. }
  17858. /* -------------------------------------------- */
  17859. /**
  17860. * Apply migrations to the armor field.
  17861. * @param {object} source The candidate source data from which the model will be constructed.
  17862. */
  17863. static #migrateArmor(source) {
  17864. if ( !("armor" in source) ) return;
  17865. source.armor ??= {};
  17866. if ( source.armor.type === "bonus" ) source.armor.type = "trinket";
  17867. if ( (typeof source.armor.dex === "string") ) {
  17868. const dex = source.armor.dex;
  17869. if ( dex === "" ) source.armor.dex = null;
  17870. else if ( Number.isNumeric(dex) ) source.armor.dex = Number(dex);
  17871. }
  17872. }
  17873. /* -------------------------------------------- */
  17874. /**
  17875. * Ensure blank strength values are migrated to null, and string values are converted to numbers.
  17876. * @param {object} source The candidate source data from which the model will be constructed.
  17877. */
  17878. static #migrateStrength(source) {
  17879. if ( typeof source.strength !== "string" ) return;
  17880. if ( source.strength === "" ) source.strength = null;
  17881. if ( Number.isNumeric(source.strength) ) source.strength = Number(source.strength);
  17882. }
  17883. /* -------------------------------------------- */
  17884. /* Getters */
  17885. /* -------------------------------------------- */
  17886. /**
  17887. * Properties displayed in chat.
  17888. * @type {string[]}
  17889. */
  17890. get chatProperties() {
  17891. return [
  17892. CONFIG.DND5E.equipmentTypes[this.armor.type],
  17893. this.parent.labels?.armor ?? null,
  17894. this.stealth ? game.i18n.localize("DND5E.StealthDisadvantage") : null
  17895. ];
  17896. }
  17897. /* -------------------------------------------- */
  17898. /**
  17899. * Is this Item any of the armor subtypes?
  17900. * @type {boolean}
  17901. */
  17902. get isArmor() {
  17903. return this.armor.type in CONFIG.DND5E.armorTypes;
  17904. }
  17905. /* -------------------------------------------- */
  17906. /**
  17907. * Is this item a separate large object like a siege engine or vehicle component that is
  17908. * usually mounted on fixtures rather than equipped, and has its own AC and HP?
  17909. * @type {boolean}
  17910. */
  17911. get isMountable() {
  17912. return this.armor.type === "vehicle";
  17913. }
  17914. }
  17915. /**
  17916. * Data definition for Feature items.
  17917. * @mixes ItemDescriptionTemplate
  17918. * @mixes ActivatedEffectTemplate
  17919. * @mixes ActionTemplate
  17920. *
  17921. * @property {object} type
  17922. * @property {string} type.value Category to which this feature belongs.
  17923. * @property {string} type.subtype Feature subtype according to its category.
  17924. * @property {string} requirements Actor details required to use this feature.
  17925. * @property {object} recharge Details on how a feature can roll for recharges.
  17926. * @property {number} recharge.value Minimum number needed to roll on a d6 to recharge this feature.
  17927. * @property {boolean} recharge.charged Does this feature have a charge remaining?
  17928. */
  17929. class FeatData extends SystemDataModel.mixin(
  17930. ItemDescriptionTemplate, ActivatedEffectTemplate, ActionTemplate
  17931. ) {
  17932. /** @inheritdoc */
  17933. static defineSchema() {
  17934. return this.mergeSchema(super.defineSchema(), {
  17935. type: new foundry.data.fields.SchemaField({
  17936. value: new foundry.data.fields.StringField({required: true, label: "DND5E.Type"}),
  17937. subtype: new foundry.data.fields.StringField({required: true, label: "DND5E.Subtype"})
  17938. }, {label: "DND5E.ItemFeatureType"}),
  17939. requirements: new foundry.data.fields.StringField({required: true, nullable: true, label: "DND5E.Requirements"}),
  17940. recharge: new foundry.data.fields.SchemaField({
  17941. value: new foundry.data.fields.NumberField({
  17942. required: true, integer: true, min: 1, label: "DND5E.FeatureRechargeOn"
  17943. }),
  17944. charged: new foundry.data.fields.BooleanField({required: true, label: "DND5E.Charged"})
  17945. }, {label: "DND5E.FeatureActionRecharge"})
  17946. });
  17947. }
  17948. /* -------------------------------------------- */
  17949. /* Migrations */
  17950. /* -------------------------------------------- */
  17951. /** @inheritdoc */
  17952. static migrateData(source) {
  17953. super.migrateData(source);
  17954. FeatData.#migrateType(source);
  17955. FeatData.#migrateRecharge(source);
  17956. }
  17957. /* -------------------------------------------- */
  17958. /**
  17959. * Ensure feats have a type object.
  17960. * @param {object} source The candidate source data from which the model will be constructed.
  17961. */
  17962. static #migrateType(source) {
  17963. if ( !("type" in source) ) return;
  17964. if ( !source.type ) source.type = {value: "", subtype: ""};
  17965. }
  17966. /* -------------------------------------------- */
  17967. /**
  17968. * Migrate 0 values to null.
  17969. * @param {object} source The candidate source data from which the model will be constructed.
  17970. */
  17971. static #migrateRecharge(source) {
  17972. if ( !("recharge" in source) ) return;
  17973. const value = source.recharge.value;
  17974. if ( (value === 0) || (value === "") ) source.recharge.value = null;
  17975. else if ( (typeof value === "string") && Number.isNumeric(value) ) source.recharge.value = Number(value);
  17976. if ( source.recharge.charged === null ) source.recharge.charged = false;
  17977. }
  17978. /* -------------------------------------------- */
  17979. /* Getters */
  17980. /* -------------------------------------------- */
  17981. /**
  17982. * Properties displayed in chat.
  17983. * @type {string[]}
  17984. */
  17985. get chatProperties() {
  17986. return [this.requirements];
  17987. }
  17988. /* -------------------------------------------- */
  17989. /** @inheritdoc */
  17990. get hasLimitedUses() {
  17991. return !!this.recharge.value || super.hasLimitedUses;
  17992. }
  17993. }
  17994. /**
  17995. * Data definition for Loot items.
  17996. * @mixes ItemDescriptionTemplate
  17997. * @mixes PhysicalItemTemplate
  17998. */
  17999. class LootData extends SystemDataModel.mixin(ItemDescriptionTemplate, PhysicalItemTemplate) {
  18000. /* -------------------------------------------- */
  18001. /* Getters */
  18002. /* -------------------------------------------- */
  18003. /**
  18004. * Properties displayed in chat.
  18005. * @type {string[]}
  18006. */
  18007. get chatProperties() {
  18008. return [
  18009. game.i18n.localize(CONFIG.Item.typeLabels.loot),
  18010. this.weight ? `${this.weight} ${game.i18n.localize("DND5E.AbbreviationLbs")}` : null
  18011. ];
  18012. }
  18013. }
  18014. /**
  18015. * Data definition for Spell items.
  18016. * @mixes ItemDescriptionTemplate
  18017. * @mixes ActivatedEffectTemplate
  18018. * @mixes ActionTemplate
  18019. *
  18020. * @property {number} level Base level of the spell.
  18021. * @property {string} school Magical school to which this spell belongs.
  18022. * @property {object} components General components and tags for this spell.
  18023. * @property {boolean} components.vocal Does this spell require vocal components?
  18024. * @property {boolean} components.somatic Does this spell require somatic components?
  18025. * @property {boolean} components.material Does this spell require material components?
  18026. * @property {boolean} components.ritual Can this spell be cast as a ritual?
  18027. * @property {boolean} components.concentration Does this spell require concentration?
  18028. * @property {object} materials Details on material components required for this spell.
  18029. * @property {string} materials.value Description of the material components required for casting.
  18030. * @property {boolean} materials.consumed Are these material components consumed during casting?
  18031. * @property {number} materials.cost GP cost for the required components.
  18032. * @property {number} materials.supply Quantity of this component available.
  18033. * @property {object} preparation Details on how this spell is prepared.
  18034. * @property {string} preparation.mode Spell preparation mode as defined in `DND5E.spellPreparationModes`.
  18035. * @property {boolean} preparation.prepared Is the spell currently prepared?
  18036. * @property {object} scaling Details on how casting at higher levels affects this spell.
  18037. * @property {string} scaling.mode Spell scaling mode as defined in `DND5E.spellScalingModes`.
  18038. * @property {string} scaling.formula Dice formula used for scaling.
  18039. */
  18040. class SpellData extends SystemDataModel.mixin(
  18041. ItemDescriptionTemplate, ActivatedEffectTemplate, ActionTemplate
  18042. ) {
  18043. /** @inheritdoc */
  18044. static defineSchema() {
  18045. return this.mergeSchema(super.defineSchema(), {
  18046. level: new foundry.data.fields.NumberField({
  18047. required: true, integer: true, initial: 1, min: 0, label: "DND5E.SpellLevel"
  18048. }),
  18049. school: new foundry.data.fields.StringField({required: true, label: "DND5E.SpellSchool"}),
  18050. components: new MappingField(new foundry.data.fields.BooleanField(), {
  18051. required: true, label: "DND5E.SpellComponents",
  18052. initialKeys: [...Object.keys(CONFIG.DND5E.spellComponents), ...Object.keys(CONFIG.DND5E.spellTags)]
  18053. }),
  18054. materials: new foundry.data.fields.SchemaField({
  18055. value: new foundry.data.fields.StringField({required: true, label: "DND5E.SpellMaterialsDescription"}),
  18056. consumed: new foundry.data.fields.BooleanField({required: true, label: "DND5E.SpellMaterialsConsumed"}),
  18057. cost: new foundry.data.fields.NumberField({
  18058. required: true, initial: 0, min: 0, label: "DND5E.SpellMaterialsCost"
  18059. }),
  18060. supply: new foundry.data.fields.NumberField({
  18061. required: true, initial: 0, min: 0, label: "DND5E.SpellMaterialsSupply"
  18062. })
  18063. }, {label: "DND5E.SpellMaterials"}),
  18064. preparation: new foundry.data.fields.SchemaField({
  18065. mode: new foundry.data.fields.StringField({
  18066. required: true, initial: "prepared", label: "DND5E.SpellPreparationMode"
  18067. }),
  18068. prepared: new foundry.data.fields.BooleanField({required: true, label: "DND5E.SpellPrepared"})
  18069. }, {label: "DND5E.SpellPreparation"}),
  18070. scaling: new foundry.data.fields.SchemaField({
  18071. mode: new foundry.data.fields.StringField({required: true, initial: "none", label: "DND5E.ScalingMode"}),
  18072. formula: new FormulaField({required: true, nullable: true, initial: null, label: "DND5E.ScalingFormula"})
  18073. }, {label: "DND5E.LevelScaling"})
  18074. });
  18075. }
  18076. /* -------------------------------------------- */
  18077. /* Migrations */
  18078. /* -------------------------------------------- */
  18079. /** @inheritdoc */
  18080. static migrateData(source) {
  18081. super.migrateData(source);
  18082. SpellData.#migrateComponentData(source);
  18083. SpellData.#migrateScaling(source);
  18084. }
  18085. /* -------------------------------------------- */
  18086. /**
  18087. * Migrate the spell's component object to remove any old, non-boolean values.
  18088. * @param {object} source The candidate source data from which the model will be constructed.
  18089. */
  18090. static #migrateComponentData(source) {
  18091. if ( !source.components ) return;
  18092. for ( const [key, value] of Object.entries(source.components) ) {
  18093. if ( typeof value !== "boolean" ) delete source.components[key];
  18094. }
  18095. }
  18096. /* -------------------------------------------- */
  18097. /**
  18098. * Migrate spell scaling.
  18099. * @param {object} source The candidate source data from which the model will be constructed.
  18100. */
  18101. static #migrateScaling(source) {
  18102. if ( !("scaling" in source) ) return;
  18103. if ( (source.scaling.mode === "") || (source.scaling.mode === null) ) source.scaling.mode = "none";
  18104. }
  18105. /* -------------------------------------------- */
  18106. /* Getters */
  18107. /* -------------------------------------------- */
  18108. /**
  18109. * Properties displayed in chat.
  18110. * @type {string[]}
  18111. */
  18112. get chatProperties() {
  18113. return [
  18114. this.parent.labels.level,
  18115. this.parent.labels.components.vsm + (this.parent.labels.materials ? ` (${this.parent.labels.materials})` : ""),
  18116. ...this.parent.labels.components.tags
  18117. ];
  18118. }
  18119. /* -------------------------------------------- */
  18120. /** @inheritdoc */
  18121. get _typeAbilityMod() {
  18122. return this.parent?.actor?.system.attributes.spellcasting || "int";
  18123. }
  18124. /* -------------------------------------------- */
  18125. /** @inheritdoc */
  18126. get _typeCriticalThreshold() {
  18127. return this.parent?.actor?.flags.dnd5e?.spellCriticalThreshold ?? Infinity;
  18128. }
  18129. }
  18130. /**
  18131. * Data definition for Subclass items.
  18132. * @mixes ItemDescriptionTemplate
  18133. *
  18134. * @property {string} identifier Identifier slug for this subclass.
  18135. * @property {string} classIdentifier Identifier slug for the class with which this subclass should be associated.
  18136. * @property {object[]} advancement Advancement objects for this subclass.
  18137. * @property {object} spellcasting Details on subclass's spellcasting ability.
  18138. * @property {string} spellcasting.progression Spell progression granted by class as from `DND5E.spellProgression`.
  18139. * @property {string} spellcasting.ability Ability score to use for spellcasting.
  18140. */
  18141. class SubclassData extends SystemDataModel.mixin(ItemDescriptionTemplate) {
  18142. /** @inheritdoc */
  18143. static defineSchema() {
  18144. return this.mergeSchema(super.defineSchema(), {
  18145. identifier: new IdentifierField({required: true, label: "DND5E.Identifier"}),
  18146. classIdentifier: new IdentifierField({
  18147. required: true, label: "DND5E.ClassIdentifier", hint: "DND5E.ClassIdentifierHint"
  18148. }),
  18149. advancement: new foundry.data.fields.ArrayField(new AdvancementField(), {label: "DND5E.AdvancementTitle"}),
  18150. spellcasting: new foundry.data.fields.SchemaField({
  18151. progression: new foundry.data.fields.StringField({
  18152. required: true, initial: "none", blank: false, label: "DND5E.SpellProgression"
  18153. }),
  18154. ability: new foundry.data.fields.StringField({required: true, label: "DND5E.SpellAbility"})
  18155. }, {label: "DND5E.Spellcasting"})
  18156. });
  18157. }
  18158. }
  18159. /**
  18160. * Data definition for Tool items.
  18161. * @mixes ItemDescriptionTemplate
  18162. * @mixes PhysicalItemTemplate
  18163. * @mixes EquippableItemTemplate
  18164. *
  18165. * @property {string} toolType Tool category as defined in `DND5E.toolTypes`.
  18166. * @property {string} baseItem Base tool as defined in `DND5E.toolIds` for determining proficiency.
  18167. * @property {string} ability Default ability when this tool is being used.
  18168. * @property {string} chatFlavor Additional text added to chat when this tool is used.
  18169. * @property {number} proficient Level of proficiency in this tool as defined in `DND5E.proficiencyLevels`.
  18170. * @property {string} bonus Bonus formula added to tool rolls.
  18171. */
  18172. class ToolData extends SystemDataModel.mixin(
  18173. ItemDescriptionTemplate, PhysicalItemTemplate, EquippableItemTemplate
  18174. ) {
  18175. /** @inheritdoc */
  18176. static defineSchema() {
  18177. return this.mergeSchema(super.defineSchema(), {
  18178. toolType: new foundry.data.fields.StringField({required: true, label: "DND5E.ItemToolType"}),
  18179. baseItem: new foundry.data.fields.StringField({required: true, label: "DND5E.ItemToolBase"}),
  18180. ability: new foundry.data.fields.StringField({
  18181. required: true, blank: true, label: "DND5E.DefaultAbilityCheck"
  18182. }),
  18183. chatFlavor: new foundry.data.fields.StringField({required: true, label: "DND5E.ChatFlavor"}),
  18184. proficient: new foundry.data.fields.NumberField({
  18185. required: true, nullable: false, initial: 0, min: 0, label: "DND5E.ItemToolProficiency"
  18186. }),
  18187. bonus: new FormulaField({required: true, label: "DND5E.ItemToolBonus"})
  18188. });
  18189. }
  18190. /* -------------------------------------------- */
  18191. /* Migrations */
  18192. /* -------------------------------------------- */
  18193. /** @inheritdoc */
  18194. static migrateData(source) {
  18195. super.migrateData(source);
  18196. ToolData.#migrateAbility(source);
  18197. }
  18198. /* -------------------------------------------- */
  18199. /**
  18200. * Migrate the ability field.
  18201. * @param {object} source The candidate source data from which the model will be constructed.
  18202. */
  18203. static #migrateAbility(source) {
  18204. if ( Array.isArray(source.ability) ) source.ability = source.ability[0];
  18205. }
  18206. /* -------------------------------------------- */
  18207. /* Getters */
  18208. /* -------------------------------------------- */
  18209. /**
  18210. * Properties displayed in chat.
  18211. * @type {string[]}
  18212. */
  18213. get chatProperties() {
  18214. return [CONFIG.DND5E.abilities[this.ability]?.label];
  18215. }
  18216. /* -------------------------------------------- */
  18217. /**
  18218. * Which ability score modifier is used by this item?
  18219. * @type {string|null}
  18220. */
  18221. get abilityMod() {
  18222. return this.ability || "int";
  18223. }
  18224. }
  18225. /**
  18226. * Data definition for Weapon items.
  18227. * @mixes ItemDescriptionTemplate
  18228. * @mixes PhysicalItemTemplate
  18229. * @mixes EquippableItemTemplate
  18230. * @mixes ActivatedEffectTemplate
  18231. * @mixes ActionTemplate
  18232. * @mixes MountableTemplate
  18233. *
  18234. * @property {string} weaponType Weapon category as defined in `DND5E.weaponTypes`.
  18235. * @property {string} baseItem Base weapon as defined in `DND5E.weaponIds` for determining proficiency.
  18236. * @property {object} properties Mapping of various weapon property booleans.
  18237. * @property {boolean} proficient Does the weapon's owner have proficiency?
  18238. */
  18239. class WeaponData extends SystemDataModel.mixin(
  18240. ItemDescriptionTemplate, PhysicalItemTemplate, EquippableItemTemplate,
  18241. ActivatedEffectTemplate, ActionTemplate, MountableTemplate
  18242. ) {
  18243. /** @inheritdoc */
  18244. static defineSchema() {
  18245. return this.mergeSchema(super.defineSchema(), {
  18246. weaponType: new foundry.data.fields.StringField({
  18247. required: true, initial: "simpleM", label: "DND5E.ItemWeaponType"
  18248. }),
  18249. baseItem: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.ItemWeaponBase"}),
  18250. properties: new MappingField(new foundry.data.fields.BooleanField(), {
  18251. required: true, initialKeys: CONFIG.DND5E.weaponProperties, label: "DND5E.ItemWeaponProperties"
  18252. }),
  18253. proficient: new foundry.data.fields.BooleanField({required: true, initial: true, label: "DND5E.Proficient"})
  18254. });
  18255. }
  18256. /* -------------------------------------------- */
  18257. /* Migrations */
  18258. /* -------------------------------------------- */
  18259. /** @inheritdoc */
  18260. static migrateData(source) {
  18261. super.migrateData(source);
  18262. WeaponData.#migratePropertiesData(source);
  18263. WeaponData.#migrateProficient(source);
  18264. WeaponData.#migrateWeaponType(source);
  18265. }
  18266. /* -------------------------------------------- */
  18267. /**
  18268. * Migrate the weapons's properties object to remove any old, non-boolean values.
  18269. * @param {object} source The candidate source data from which the model will be constructed.
  18270. */
  18271. static #migratePropertiesData(source) {
  18272. if ( !source.properties ) return;
  18273. for ( const [key, value] of Object.entries(source.properties) ) {
  18274. if ( typeof value !== "boolean" ) delete source.properties[key];
  18275. }
  18276. }
  18277. /* -------------------------------------------- */
  18278. /**
  18279. * Migrate the proficient field to remove non-boolean values.
  18280. * @param {object} source The candidate source data from which the model will be constructed.
  18281. */
  18282. static #migrateProficient(source) {
  18283. if ( typeof source.proficient === "number" ) source.proficient = Boolean(source.proficient);
  18284. }
  18285. /* -------------------------------------------- */
  18286. /**
  18287. * Migrate the weapon type.
  18288. * @param {object} source The candidate source data from which the model will be constructed.
  18289. */
  18290. static #migrateWeaponType(source) {
  18291. if ( source.weaponType === null ) source.weaponType = "simpleM";
  18292. }
  18293. /* -------------------------------------------- */
  18294. /* Getters */
  18295. /* -------------------------------------------- */
  18296. /**
  18297. * Properties displayed in chat.
  18298. * @type {string[]}
  18299. */
  18300. get chatProperties() {
  18301. return [CONFIG.DND5E.weaponTypes[this.weaponType]];
  18302. }
  18303. /* -------------------------------------------- */
  18304. /** @inheritdoc */
  18305. get _typeAbilityMod() {
  18306. if ( ["simpleR", "martialR"].includes(this.weaponType) ) return "dex";
  18307. const abilities = this.parent?.actor?.system.abilities;
  18308. if ( this.properties.fin && abilities ) {
  18309. return (abilities.dex?.mod ?? 0) >= (abilities.str?.mod ?? 0) ? "dex" : "str";
  18310. }
  18311. return null;
  18312. }
  18313. /* -------------------------------------------- */
  18314. /** @inheritdoc */
  18315. get _typeCriticalThreshold() {
  18316. return this.parent?.actor?.flags.dnd5e?.weaponCriticalThreshold ?? Infinity;
  18317. }
  18318. /* -------------------------------------------- */
  18319. /**
  18320. * Is this item a separate large object like a siege engine or vehicle component that is
  18321. * usually mounted on fixtures rather than equipped, and has its own AC and HP?
  18322. * @type {boolean}
  18323. */
  18324. get isMountable() {
  18325. return this.weaponType === "siege";
  18326. }
  18327. }
  18328. const config$1 = {
  18329. background: BackgroundData,
  18330. backpack: ContainerData,
  18331. class: ClassData,
  18332. consumable: ConsumableData,
  18333. equipment: EquipmentData,
  18334. feat: FeatData,
  18335. loot: LootData,
  18336. spell: SpellData,
  18337. subclass: SubclassData,
  18338. tool: ToolData,
  18339. weapon: WeaponData
  18340. };
  18341. var _module$2 = /*#__PURE__*/Object.freeze({
  18342. __proto__: null,
  18343. ActionTemplate: ActionTemplate,
  18344. ActivatedEffectTemplate: ActivatedEffectTemplate,
  18345. BackgroundData: BackgroundData,
  18346. ClassData: ClassData,
  18347. ConsumableData: ConsumableData,
  18348. ContainerData: ContainerData,
  18349. EquipmentData: EquipmentData,
  18350. EquippableItemTemplate: EquippableItemTemplate,
  18351. FeatData: FeatData,
  18352. ItemDescriptionTemplate: ItemDescriptionTemplate,
  18353. LootData: LootData,
  18354. MountableTemplate: MountableTemplate,
  18355. PhysicalItemTemplate: PhysicalItemTemplate,
  18356. SpellData: SpellData,
  18357. SubclassData: SubclassData,
  18358. ToolData: ToolData,
  18359. WeaponData: WeaponData,
  18360. config: config$1
  18361. });
  18362. /**
  18363. * Data definition for Class Summary journal entry pages.
  18364. *
  18365. * @property {string} item UUID of the class item included.
  18366. * @property {object} description
  18367. * @property {string} description.value Introductory description for the class.
  18368. * @property {string} description.additionalHitPoints Additional text displayed beneath the hit points section.
  18369. * @property {string} description.additionalTraits Additional text displayed beneath the traits section.
  18370. * @property {string} description.additionalEquipment Additional text displayed beneath the equipment section.
  18371. * @property {string} description.subclass Introduction to the subclass section.
  18372. * @property {string} subclassHeader Subclass header to replace the default.
  18373. * @property {Set<string>} subclassItems UUIDs of all subclasses to display.
  18374. */
  18375. class ClassJournalPageData extends foundry.abstract.DataModel {
  18376. static defineSchema() {
  18377. return {
  18378. item: new foundry.data.fields.StringField({required: true, label: "JOURNALENTRYPAGE.DND5E.Class.Item"}),
  18379. description: new foundry.data.fields.SchemaField({
  18380. value: new foundry.data.fields.HTMLField({
  18381. label: "JOURNALENTRYPAGE.DND5E.Class.Description",
  18382. hint: "JOURNALENTRYPAGE.DND5E.Class.DescriptionHint"
  18383. }),
  18384. additionalHitPoints: new foundry.data.fields.HTMLField({
  18385. label: "JOURNALENTRYPAGE.DND5E.Class.AdditionalHitPoints",
  18386. hint: "JOURNALENTRYPAGE.DND5E.Class.AdditionalHitPointsHint"
  18387. }),
  18388. additionalTraits: new foundry.data.fields.HTMLField({
  18389. label: "JOURNALENTRYPAGE.DND5E.Class.AdditionalTraits",
  18390. hint: "JOURNALENTRYPAGE.DND5E.Class.AdditionalTraitsHint"
  18391. }),
  18392. additionalEquipment: new foundry.data.fields.HTMLField({
  18393. label: "JOURNALENTRYPAGE.DND5E.Class.AdditionalEquipment",
  18394. hint: "JOURNALENTRYPAGE.DND5E.Class.AdditionalEquipmentHint"
  18395. }),
  18396. subclass: new foundry.data.fields.HTMLField({
  18397. label: "JOURNALENTRYPAGE.DND5E.Class.SubclassDescription",
  18398. hint: "JOURNALENTRYPAGE.DND5E.Class.SubclassDescriptionHint"
  18399. })
  18400. }),
  18401. subclassHeader: new foundry.data.fields.StringField({
  18402. label: "JOURNALENTRYPAGE.DND5E.Class.SubclassHeader"
  18403. }),
  18404. subclassItems: new foundry.data.fields.SetField(new foundry.data.fields.StringField(), {
  18405. label: "JOURNALENTRYPAGE.DND5E.Class.SubclassItems"
  18406. })
  18407. };
  18408. }
  18409. }
  18410. const config = {
  18411. class: ClassJournalPageData
  18412. };
  18413. var _module$1 = /*#__PURE__*/Object.freeze({
  18414. __proto__: null,
  18415. ClassJournalPageData: ClassJournalPageData,
  18416. config: config
  18417. });
  18418. var _module = /*#__PURE__*/Object.freeze({
  18419. __proto__: null,
  18420. CurrencyTemplate: CurrencyTemplate
  18421. });
  18422. var dataModels = /*#__PURE__*/Object.freeze({
  18423. __proto__: null,
  18424. SystemDataModel: SystemDataModel,
  18425. actor: _module$4,
  18426. advancement: _module$3,
  18427. fields: fields,
  18428. item: _module$2,
  18429. journal: _module$1,
  18430. shared: _module
  18431. });
  18432. /**
  18433. * A type of Roll specific to a d20-based check, save, or attack roll in the 5e system.
  18434. * @param {string} formula The string formula to parse
  18435. * @param {object} data The data object against which to parse attributes within the formula
  18436. * @param {object} [options={}] Extra optional arguments which describe or modify the D20Roll
  18437. * @param {number} [options.advantageMode] What advantage modifier to apply to the roll (none, advantage,
  18438. * disadvantage)
  18439. * @param {number} [options.critical] The value of d20 result which represents a critical success
  18440. * @param {number} [options.fumble] The value of d20 result which represents a critical failure
  18441. * @param {(number)} [options.targetValue] Assign a target value against which the result of this roll should be
  18442. * compared
  18443. * @param {boolean} [options.elvenAccuracy=false] Allow Elven Accuracy to modify this roll?
  18444. * @param {boolean} [options.halflingLucky=false] Allow Halfling Luck to modify this roll?
  18445. * @param {boolean} [options.reliableTalent=false] Allow Reliable Talent to modify this roll?
  18446. */
  18447. class D20Roll extends Roll {
  18448. constructor(formula, data, options) {
  18449. super(formula, data, options);
  18450. if ( !this.options.configured ) this.configureModifiers();
  18451. }
  18452. /* -------------------------------------------- */
  18453. /**
  18454. * Create a D20Roll from a standard Roll instance.
  18455. * @param {Roll} roll
  18456. * @returns {D20Roll}
  18457. */
  18458. static fromRoll(roll) {
  18459. const newRoll = new this(roll.formula, roll.data, roll.options);
  18460. Object.assign(newRoll, roll);
  18461. return newRoll;
  18462. }
  18463. /* -------------------------------------------- */
  18464. /**
  18465. * Determine whether a d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied.
  18466. * @param {object} [options]
  18467. * @param {Event} [options.event] The Event that triggered the roll.
  18468. * @param {boolean} [options.advantage] Is something granting this roll advantage?
  18469. * @param {boolean} [options.disadvantage] Is something granting this roll disadvantage?
  18470. * @param {boolean} [options.fastForward] Should the roll dialog be skipped?
  18471. * @returns {{advantageMode: D20Roll.ADV_MODE, isFF: boolean}} Whether the roll is fast-forwarded, and its advantage
  18472. * mode.
  18473. */
  18474. static determineAdvantageMode({event, advantage=false, disadvantage=false, fastForward}={}) {
  18475. const isFF = fastForward ?? (event?.shiftKey || event?.altKey || event?.ctrlKey || event?.metaKey);
  18476. let advantageMode = this.ADV_MODE.NORMAL;
  18477. if ( advantage || event?.altKey ) advantageMode = this.ADV_MODE.ADVANTAGE;
  18478. else if ( disadvantage || event?.ctrlKey || event?.metaKey ) advantageMode = this.ADV_MODE.DISADVANTAGE;
  18479. return {isFF: !!isFF, advantageMode};
  18480. }
  18481. /* -------------------------------------------- */
  18482. /**
  18483. * Advantage mode of a 5e d20 roll
  18484. * @enum {number}
  18485. */
  18486. static ADV_MODE = {
  18487. NORMAL: 0,
  18488. ADVANTAGE: 1,
  18489. DISADVANTAGE: -1
  18490. }
  18491. /* -------------------------------------------- */
  18492. /**
  18493. * The HTML template path used to configure evaluation of this Roll
  18494. * @type {string}
  18495. */
  18496. static EVALUATION_TEMPLATE = "systems/dnd5e/templates/chat/roll-dialog.hbs";
  18497. /* -------------------------------------------- */
  18498. /**
  18499. * Does this roll start with a d20?
  18500. * @type {boolean}
  18501. */
  18502. get validD20Roll() {
  18503. return (this.terms[0] instanceof Die) && (this.terms[0].faces === 20);
  18504. }
  18505. /* -------------------------------------------- */
  18506. /**
  18507. * A convenience reference for whether this D20Roll has advantage
  18508. * @type {boolean}
  18509. */
  18510. get hasAdvantage() {
  18511. return this.options.advantageMode === D20Roll.ADV_MODE.ADVANTAGE;
  18512. }
  18513. /* -------------------------------------------- */
  18514. /**
  18515. * A convenience reference for whether this D20Roll has disadvantage
  18516. * @type {boolean}
  18517. */
  18518. get hasDisadvantage() {
  18519. return this.options.advantageMode === D20Roll.ADV_MODE.DISADVANTAGE;
  18520. }
  18521. /* -------------------------------------------- */
  18522. /**
  18523. * Is this roll a critical success? Returns undefined if roll isn't evaluated.
  18524. * @type {boolean|void}
  18525. */
  18526. get isCritical() {
  18527. if ( !this.validD20Roll || !this._evaluated ) return undefined;
  18528. if ( !Number.isNumeric(this.options.critical) ) return false;
  18529. return this.dice[0].total >= this.options.critical;
  18530. }
  18531. /* -------------------------------------------- */
  18532. /**
  18533. * Is this roll a critical failure? Returns undefined if roll isn't evaluated.
  18534. * @type {boolean|void}
  18535. */
  18536. get isFumble() {
  18537. if ( !this.validD20Roll || !this._evaluated ) return undefined;
  18538. if ( !Number.isNumeric(this.options.fumble) ) return false;
  18539. return this.dice[0].total <= this.options.fumble;
  18540. }
  18541. /* -------------------------------------------- */
  18542. /* D20 Roll Methods */
  18543. /* -------------------------------------------- */
  18544. /**
  18545. * Apply optional modifiers which customize the behavior of the d20term
  18546. * @private
  18547. */
  18548. configureModifiers() {
  18549. if ( !this.validD20Roll ) return;
  18550. const d20 = this.terms[0];
  18551. d20.modifiers = [];
  18552. // Halfling Lucky
  18553. if ( this.options.halflingLucky ) d20.modifiers.push("r1=1");
  18554. // Reliable Talent
  18555. if ( this.options.reliableTalent ) d20.modifiers.push("min10");
  18556. // Handle Advantage or Disadvantage
  18557. if ( this.hasAdvantage ) {
  18558. d20.number = this.options.elvenAccuracy ? 3 : 2;
  18559. d20.modifiers.push("kh");
  18560. d20.options.advantage = true;
  18561. }
  18562. else if ( this.hasDisadvantage ) {
  18563. d20.number = 2;
  18564. d20.modifiers.push("kl");
  18565. d20.options.disadvantage = true;
  18566. }
  18567. else d20.number = 1;
  18568. // Assign critical and fumble thresholds
  18569. if ( this.options.critical ) d20.options.critical = this.options.critical;
  18570. if ( this.options.fumble ) d20.options.fumble = this.options.fumble;
  18571. if ( this.options.targetValue ) d20.options.target = this.options.targetValue;
  18572. // Re-compile the underlying formula
  18573. this._formula = this.constructor.getFormula(this.terms);
  18574. // Mark configuration as complete
  18575. this.options.configured = true;
  18576. }
  18577. /* -------------------------------------------- */
  18578. /** @inheritdoc */
  18579. async toMessage(messageData={}, options={}) {
  18580. // Evaluate the roll now so we have the results available to determine whether reliable talent came into play
  18581. if ( !this._evaluated ) await this.evaluate({async: true});
  18582. // Add appropriate advantage mode message flavor and dnd5e roll flags
  18583. messageData.flavor = messageData.flavor || this.options.flavor;
  18584. if ( this.hasAdvantage ) messageData.flavor += ` (${game.i18n.localize("DND5E.Advantage")})`;
  18585. else if ( this.hasDisadvantage ) messageData.flavor += ` (${game.i18n.localize("DND5E.Disadvantage")})`;
  18586. // Add reliable talent to the d20-term flavor text if it applied
  18587. if ( this.validD20Roll && this.options.reliableTalent ) {
  18588. const d20 = this.dice[0];
  18589. const isRT = d20.results.every(r => !r.active || (r.result < 10));
  18590. const label = `(${game.i18n.localize("DND5E.FlagsReliableTalent")})`;
  18591. if ( isRT ) d20.options.flavor = d20.options.flavor ? `${d20.options.flavor} (${label})` : label;
  18592. }
  18593. // Record the preferred rollMode
  18594. options.rollMode = options.rollMode ?? this.options.rollMode;
  18595. return super.toMessage(messageData, options);
  18596. }
  18597. /* -------------------------------------------- */
  18598. /* Configuration Dialog */
  18599. /* -------------------------------------------- */
  18600. /**
  18601. * Create a Dialog prompt used to configure evaluation of an existing D20Roll instance.
  18602. * @param {object} data Dialog configuration data
  18603. * @param {string} [data.title] The title of the shown dialog window
  18604. * @param {number} [data.defaultRollMode] The roll mode that the roll mode select element should default to
  18605. * @param {number} [data.defaultAction] The button marked as default
  18606. * @param {boolean} [data.chooseModifier] Choose which ability modifier should be applied to the roll?
  18607. * @param {string} [data.defaultAbility] For tool rolls, the default ability modifier applied to the roll
  18608. * @param {string} [data.template] A custom path to an HTML template to use instead of the default
  18609. * @param {object} options Additional Dialog customization options
  18610. * @returns {Promise<D20Roll|null>} A resulting D20Roll object constructed with the dialog, or null if the
  18611. * dialog was closed
  18612. */
  18613. async configureDialog({title, defaultRollMode, defaultAction=D20Roll.ADV_MODE.NORMAL, chooseModifier=false,
  18614. defaultAbility, template}={}, options={}) {
  18615. // Render the Dialog inner HTML
  18616. const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
  18617. formula: `${this.formula} + @bonus`,
  18618. defaultRollMode,
  18619. rollModes: CONFIG.Dice.rollModes,
  18620. chooseModifier,
  18621. defaultAbility,
  18622. abilities: CONFIG.DND5E.abilities
  18623. });
  18624. let defaultButton = "normal";
  18625. switch ( defaultAction ) {
  18626. case D20Roll.ADV_MODE.ADVANTAGE: defaultButton = "advantage"; break;
  18627. case D20Roll.ADV_MODE.DISADVANTAGE: defaultButton = "disadvantage"; break;
  18628. }
  18629. // Create the Dialog window and await submission of the form
  18630. return new Promise(resolve => {
  18631. new Dialog({
  18632. title,
  18633. content,
  18634. buttons: {
  18635. advantage: {
  18636. label: game.i18n.localize("DND5E.Advantage"),
  18637. callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.ADVANTAGE))
  18638. },
  18639. normal: {
  18640. label: game.i18n.localize("DND5E.Normal"),
  18641. callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.NORMAL))
  18642. },
  18643. disadvantage: {
  18644. label: game.i18n.localize("DND5E.Disadvantage"),
  18645. callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.DISADVANTAGE))
  18646. }
  18647. },
  18648. default: defaultButton,
  18649. close: () => resolve(null)
  18650. }, options).render(true);
  18651. });
  18652. }
  18653. /* -------------------------------------------- */
  18654. /**
  18655. * Handle submission of the Roll evaluation configuration Dialog
  18656. * @param {jQuery} html The submitted dialog content
  18657. * @param {number} advantageMode The chosen advantage mode
  18658. * @returns {D20Roll} This damage roll.
  18659. * @private
  18660. */
  18661. _onDialogSubmit(html, advantageMode) {
  18662. const form = html[0].querySelector("form");
  18663. // Append a situational bonus term
  18664. if ( form.bonus.value ) {
  18665. const bonus = new Roll(form.bonus.value, this.data);
  18666. if ( !(bonus.terms[0] instanceof OperatorTerm) ) this.terms.push(new OperatorTerm({operator: "+"}));
  18667. this.terms = this.terms.concat(bonus.terms);
  18668. }
  18669. // Customize the modifier
  18670. if ( form.ability?.value ) {
  18671. const abl = this.data.abilities[form.ability.value];
  18672. this.terms = this.terms.flatMap(t => {
  18673. if ( t.term === "@mod" ) return new NumericTerm({number: abl.mod});
  18674. if ( t.term === "@abilityCheckBonus" ) {
  18675. const bonus = abl.bonuses?.check;
  18676. if ( bonus ) return new Roll(bonus, this.data).terms;
  18677. return new NumericTerm({number: 0});
  18678. }
  18679. return t;
  18680. });
  18681. this.options.flavor += ` (${CONFIG.DND5E.abilities[form.ability.value]?.label ?? ""})`;
  18682. }
  18683. // Apply advantage or disadvantage
  18684. this.options.advantageMode = advantageMode;
  18685. this.options.rollMode = form.rollMode.value;
  18686. this.configureModifiers();
  18687. return this;
  18688. }
  18689. }
  18690. /**
  18691. * A type of Roll specific to a damage (or healing) roll in the 5e system.
  18692. * @param {string} formula The string formula to parse
  18693. * @param {object} data The data object against which to parse attributes within the formula
  18694. * @param {object} [options={}] Extra optional arguments which describe or modify the DamageRoll
  18695. * @param {number} [options.criticalBonusDice=0] A number of bonus damage dice that are added for critical hits
  18696. * @param {number} [options.criticalMultiplier=2] A critical hit multiplier which is applied to critical hits
  18697. * @param {boolean} [options.multiplyNumeric=false] Multiply numeric terms by the critical multiplier
  18698. * @param {boolean} [options.powerfulCritical=false] Apply the "powerful criticals" house rule to critical hits
  18699. * @param {string} [options.criticalBonusDamage] An extra damage term that is applied only on a critical hit
  18700. */
  18701. class DamageRoll extends Roll {
  18702. constructor(formula, data, options) {
  18703. super(formula, data, options);
  18704. if ( !this.options.preprocessed ) this.preprocessFormula();
  18705. // For backwards compatibility, skip rolls which do not have the "critical" option defined
  18706. if ( (this.options.critical !== undefined) && !this.options.configured ) this.configureDamage();
  18707. }
  18708. /* -------------------------------------------- */
  18709. /**
  18710. * Create a DamageRoll from a standard Roll instance.
  18711. * @param {Roll} roll
  18712. * @returns {DamageRoll}
  18713. */
  18714. static fromRoll(roll) {
  18715. const newRoll = new this(roll.formula, roll.data, roll.options);
  18716. Object.assign(newRoll, roll);
  18717. return newRoll;
  18718. }
  18719. /* -------------------------------------------- */
  18720. /**
  18721. * The HTML template path used to configure evaluation of this Roll
  18722. * @type {string}
  18723. */
  18724. static EVALUATION_TEMPLATE = "systems/dnd5e/templates/chat/roll-dialog.hbs";
  18725. /* -------------------------------------------- */
  18726. /**
  18727. * A convenience reference for whether this DamageRoll is a critical hit
  18728. * @type {boolean}
  18729. */
  18730. get isCritical() {
  18731. return this.options.critical;
  18732. }
  18733. /* -------------------------------------------- */
  18734. /* Damage Roll Methods */
  18735. /* -------------------------------------------- */
  18736. /**
  18737. * Perform any term-merging required to ensure that criticals can be calculated successfully.
  18738. * @protected
  18739. */
  18740. preprocessFormula() {
  18741. for ( let [i, term] of this.terms.entries() ) {
  18742. const nextTerm = this.terms[i + 1];
  18743. const prevTerm = this.terms[i - 1];
  18744. // Convert shorthand dX terms to 1dX preemptively to allow them to be appropriately doubled for criticals
  18745. if ( (term instanceof StringTerm) && /^d\d+/.test(term.term) && !(prevTerm instanceof ParentheticalTerm) ) {
  18746. const formula = `1${term.term}`;
  18747. const newTerm = new Roll(formula).terms[0];
  18748. this.terms.splice(i, 1, newTerm);
  18749. term = newTerm;
  18750. }
  18751. // Merge parenthetical terms that follow string terms to build a dice term (to allow criticals)
  18752. else if ( (term instanceof ParentheticalTerm) && (prevTerm instanceof StringTerm)
  18753. && prevTerm.term.match(/^[0-9]*d$/)) {
  18754. if ( term.isDeterministic ) {
  18755. let newFormula = `${prevTerm.term}${term.evaluate().total}`;
  18756. let deleteCount = 2;
  18757. // Merge in any roll modifiers
  18758. if ( nextTerm instanceof StringTerm ) {
  18759. newFormula += nextTerm.term;
  18760. deleteCount += 1;
  18761. }
  18762. const newTerm = (new Roll(newFormula)).terms[0];
  18763. this.terms.splice(i - 1, deleteCount, newTerm);
  18764. term = newTerm;
  18765. }
  18766. }
  18767. // Merge any parenthetical terms followed by string terms
  18768. else if ( (term instanceof ParentheticalTerm || term instanceof MathTerm) && (nextTerm instanceof StringTerm)
  18769. && nextTerm.term.match(/^d[0-9]*$/)) {
  18770. if ( term.isDeterministic ) {
  18771. const newFormula = `${term.evaluate().total}${nextTerm.term}`;
  18772. const newTerm = (new Roll(newFormula)).terms[0];
  18773. this.terms.splice(i, 2, newTerm);
  18774. term = newTerm;
  18775. }
  18776. }
  18777. }
  18778. // Re-compile the underlying formula
  18779. this._formula = this.constructor.getFormula(this.terms);
  18780. // Mark configuration as complete
  18781. this.options.preprocessed = true;
  18782. }
  18783. /* -------------------------------------------- */
  18784. /**
  18785. * Apply optional modifiers which customize the behavior of the d20term.
  18786. * @protected
  18787. */
  18788. configureDamage() {
  18789. let flatBonus = 0;
  18790. for ( let [i, term] of this.terms.entries() ) {
  18791. // Multiply dice terms
  18792. if ( term instanceof DiceTerm ) {
  18793. term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
  18794. term.number = term.options.baseNumber;
  18795. if ( this.isCritical ) {
  18796. let cm = this.options.criticalMultiplier ?? 2;
  18797. // Powerful critical - maximize damage and reduce the multiplier by 1
  18798. if ( this.options.powerfulCritical ) {
  18799. flatBonus += (term.number * term.faces);
  18800. cm = Math.max(1, cm-1);
  18801. }
  18802. // Alter the damage term
  18803. let cb = (this.options.criticalBonusDice && (i === 0)) ? this.options.criticalBonusDice : 0;
  18804. term.alter(cm, cb);
  18805. term.options.critical = true;
  18806. }
  18807. }
  18808. // Multiply numeric terms
  18809. else if ( this.options.multiplyNumeric && (term instanceof NumericTerm) ) {
  18810. term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
  18811. term.number = term.options.baseNumber;
  18812. if ( this.isCritical ) {
  18813. term.number *= (this.options.criticalMultiplier ?? 2);
  18814. term.options.critical = true;
  18815. }
  18816. }
  18817. }
  18818. // Add powerful critical bonus
  18819. if ( this.options.powerfulCritical && (flatBonus > 0) ) {
  18820. this.terms.push(new OperatorTerm({operator: "+"}));
  18821. this.terms.push(new NumericTerm({number: flatBonus}, {flavor: game.i18n.localize("DND5E.PowerfulCritical")}));
  18822. }
  18823. // Add extra critical damage term
  18824. if ( this.isCritical && this.options.criticalBonusDamage ) {
  18825. const extra = new Roll(this.options.criticalBonusDamage, this.data);
  18826. if ( !(extra.terms[0] instanceof OperatorTerm) ) this.terms.push(new OperatorTerm({operator: "+"}));
  18827. this.terms.push(...extra.terms);
  18828. }
  18829. // Re-compile the underlying formula
  18830. this._formula = this.constructor.getFormula(this.terms);
  18831. // Mark configuration as complete
  18832. this.options.configured = true;
  18833. }
  18834. /* -------------------------------------------- */
  18835. /** @inheritdoc */
  18836. toMessage(messageData={}, options={}) {
  18837. messageData.flavor = messageData.flavor || this.options.flavor;
  18838. if ( this.isCritical ) {
  18839. const label = game.i18n.localize("DND5E.CriticalHit");
  18840. messageData.flavor = messageData.flavor ? `${messageData.flavor} (${label})` : label;
  18841. }
  18842. options.rollMode = options.rollMode ?? this.options.rollMode;
  18843. return super.toMessage(messageData, options);
  18844. }
  18845. /* -------------------------------------------- */
  18846. /* Configuration Dialog */
  18847. /* -------------------------------------------- */
  18848. /**
  18849. * Create a Dialog prompt used to configure evaluation of an existing D20Roll instance.
  18850. * @param {object} data Dialog configuration data
  18851. * @param {string} [data.title] The title of the shown dialog window
  18852. * @param {number} [data.defaultRollMode] The roll mode that the roll mode select element should default to
  18853. * @param {string} [data.defaultCritical] Should critical be selected as default
  18854. * @param {string} [data.template] A custom path to an HTML template to use instead of the default
  18855. * @param {boolean} [data.allowCritical=true] Allow critical hit to be chosen as a possible damage mode
  18856. * @param {object} options Additional Dialog customization options
  18857. * @returns {Promise<D20Roll|null>} A resulting D20Roll object constructed with the dialog, or null if the
  18858. * dialog was closed
  18859. */
  18860. async configureDialog({title, defaultRollMode, defaultCritical=false, template, allowCritical=true}={}, options={}) {
  18861. // Render the Dialog inner HTML
  18862. const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
  18863. formula: `${this.formula} + @bonus`,
  18864. defaultRollMode,
  18865. rollModes: CONFIG.Dice.rollModes
  18866. });
  18867. // Create the Dialog window and await submission of the form
  18868. return new Promise(resolve => {
  18869. new Dialog({
  18870. title,
  18871. content,
  18872. buttons: {
  18873. critical: {
  18874. condition: allowCritical,
  18875. label: game.i18n.localize("DND5E.CriticalHit"),
  18876. callback: html => resolve(this._onDialogSubmit(html, true))
  18877. },
  18878. normal: {
  18879. label: game.i18n.localize(allowCritical ? "DND5E.Normal" : "DND5E.Roll"),
  18880. callback: html => resolve(this._onDialogSubmit(html, false))
  18881. }
  18882. },
  18883. default: defaultCritical ? "critical" : "normal",
  18884. close: () => resolve(null)
  18885. }, options).render(true);
  18886. });
  18887. }
  18888. /* -------------------------------------------- */
  18889. /**
  18890. * Handle submission of the Roll evaluation configuration Dialog
  18891. * @param {jQuery} html The submitted dialog content
  18892. * @param {boolean} isCritical Is the damage a critical hit?
  18893. * @returns {DamageRoll} This damage roll.
  18894. * @private
  18895. */
  18896. _onDialogSubmit(html, isCritical) {
  18897. const form = html[0].querySelector("form");
  18898. // Append a situational bonus term
  18899. if ( form.bonus.value ) {
  18900. const bonus = new DamageRoll(form.bonus.value, this.data);
  18901. if ( !(bonus.terms[0] instanceof OperatorTerm) ) this.terms.push(new OperatorTerm({operator: "+"}));
  18902. this.terms = this.terms.concat(bonus.terms);
  18903. }
  18904. // Apply advantage or disadvantage
  18905. this.options.critical = isCritical;
  18906. this.options.rollMode = form.rollMode.value;
  18907. this.configureDamage();
  18908. return this;
  18909. }
  18910. /* -------------------------------------------- */
  18911. /** @inheritdoc */
  18912. static fromData(data) {
  18913. const roll = super.fromData(data);
  18914. roll._formula = this.getFormula(roll.terms);
  18915. return roll;
  18916. }
  18917. }
  18918. var dice = /*#__PURE__*/Object.freeze({
  18919. __proto__: null,
  18920. D20Roll: D20Roll,
  18921. DamageRoll: DamageRoll,
  18922. d20Roll: d20Roll,
  18923. damageRoll: damageRoll,
  18924. simplifyRollFormula: simplifyRollFormula
  18925. });
  18926. /**
  18927. * Extend the base TokenDocument class to implement system-specific HP bar logic.
  18928. */
  18929. class TokenDocument5e extends TokenDocument {
  18930. /** @inheritdoc */
  18931. getBarAttribute(...args) {
  18932. const data = super.getBarAttribute(...args);
  18933. if ( data && (data.attribute === "attributes.hp") ) {
  18934. const hp = this.actor.system.attributes.hp || {};
  18935. data.value += (hp.temp || 0);
  18936. data.max = Math.max(0, data.max + (hp.tempmax || 0));
  18937. }
  18938. return data;
  18939. }
  18940. /* -------------------------------------------- */
  18941. /** @inheritdoc */
  18942. static getTrackedAttributes(data, _path=[]) {
  18943. if ( !game.dnd5e.isV10 ) return super.getTrackedAttributes(data, _path);
  18944. if ( data instanceof foundry.abstract.DataModel ) return this._getTrackedAttributesFromSchema(data.schema, _path);
  18945. const attributes = super.getTrackedAttributes(data, _path);
  18946. if ( _path.length ) return attributes;
  18947. const allowed = CONFIG.DND5E.trackableAttributes;
  18948. attributes.value = attributes.value.filter(attrs => this._isAllowedAttribute(allowed, attrs));
  18949. return attributes;
  18950. }
  18951. /* -------------------------------------------- */
  18952. /** @inheritdoc */
  18953. static _getTrackedAttributesFromSchema(schema, _path=[]) {
  18954. const isSchema = field => field instanceof foundry.data.fields.SchemaField;
  18955. const isModel = field => field instanceof foundry.data.fields.EmbeddedDataField;
  18956. const attributes = {bar: [], value: []};
  18957. for ( const [name, field] of Object.entries(schema.fields) ) {
  18958. const p = _path.concat([name]);
  18959. if ( field instanceof foundry.data.fields.NumberField ) attributes.value.push(p);
  18960. if ( isSchema(field) || isModel(field) ) {
  18961. const schema = isModel(field) ? field.model.schema : field;
  18962. const isBar = schema.has("value") && schema.has("max");
  18963. if ( isBar ) attributes.bar.push(p);
  18964. else {
  18965. const inner = this._getTrackedAttributesFromSchema(schema, p);
  18966. attributes.bar.push(...inner.bar);
  18967. attributes.value.push(...inner.value);
  18968. }
  18969. }
  18970. if ( !(field instanceof MappingField) ) continue;
  18971. if ( !field.initialKeys || foundry.utils.isEmpty(field.initialKeys) ) continue;
  18972. if ( !isSchema(field.model) && !isModel(field.model) ) continue;
  18973. const keys = Array.isArray(field.initialKeys) ? field.initialKeys : Object.keys(field.initialKeys);
  18974. for ( const key of keys ) {
  18975. const inner = this._getTrackedAttributesFromSchema(field.model, p.concat([key]));
  18976. attributes.bar.push(...inner.bar);
  18977. attributes.value.push(...inner.value);
  18978. }
  18979. }
  18980. return attributes;
  18981. }
  18982. /* -------------------------------------------- */
  18983. /**
  18984. * Get an Array of attribute choices which are suitable for being consumed by an item usage.
  18985. * @param {object} data The actor data.
  18986. * @returns {{bar: string[], value: string[]}}
  18987. */
  18988. static getConsumedAttributes(data) {
  18989. const attributes = super.getTrackedAttributes(data);
  18990. attributes.value.push(...Object.keys(CONFIG.DND5E.currencies).map(denom => ["currency", denom]));
  18991. const allowed = CONFIG.DND5E.consumableResources;
  18992. attributes.value = attributes.value.filter(attrs => this._isAllowedAttribute(allowed, attrs));
  18993. return attributes;
  18994. }
  18995. /* -------------------------------------------- */
  18996. /**
  18997. * Traverse the configured allowed attributes to see if the provided one matches.
  18998. * @param {object} allowed The allowed attributes structure.
  18999. * @param {string[]} attrs The attributes list to test.
  19000. * @returns {boolean} Whether the given attribute is allowed.
  19001. * @private
  19002. */
  19003. static _isAllowedAttribute(allowed, attrs) {
  19004. let allow = allowed;
  19005. for ( const attr of attrs ) {
  19006. if ( allow === undefined ) return false;
  19007. if ( allow === true ) return true;
  19008. if ( allow["*"] !== undefined ) allow = allow["*"];
  19009. else allow = allow[attr];
  19010. }
  19011. return allow !== undefined;
  19012. }
  19013. }
  19014. /**
  19015. * Highlight critical success or failure on d20 rolls.
  19016. * @param {ChatMessage} message Message being prepared.
  19017. * @param {HTMLElement} html Rendered contents of the message.
  19018. * @param {object} data Configuration data passed to the message.
  19019. */
  19020. function highlightCriticalSuccessFailure(message, html, data) {
  19021. if ( !message.isRoll || !message.isContentVisible || !message.rolls.length ) return;
  19022. // Highlight rolls where the first part is a d20 roll
  19023. let d20Roll = message.rolls.find(r => {
  19024. const d0 = r.dice[0];
  19025. return (d0?.faces === 20) && (d0?.values.length === 1);
  19026. });
  19027. if ( !d20Roll ) return;
  19028. d20Roll = dnd5e.dice.D20Roll.fromRoll(d20Roll);
  19029. const d = d20Roll.dice[0];
  19030. const isModifiedRoll = ("success" in d.results[0]) || d.options.marginSuccess || d.options.marginFailure;
  19031. if ( isModifiedRoll ) return;
  19032. // Highlight successes and failures
  19033. if ( d20Roll.isCritical ) html.find(".dice-total").addClass("critical");
  19034. else if ( d20Roll.isFumble ) html.find(".dice-total").addClass("fumble");
  19035. else if ( d.options.target ) {
  19036. if ( d20Roll.total >= d.options.target ) html.find(".dice-total").addClass("success");
  19037. else html.find(".dice-total").addClass("failure");
  19038. }
  19039. }
  19040. /* -------------------------------------------- */
  19041. /**
  19042. * Optionally hide the display of chat card action buttons which cannot be performed by the user
  19043. * @param {ChatMessage} message Message being prepared.
  19044. * @param {HTMLElement} html Rendered contents of the message.
  19045. * @param {object} data Configuration data passed to the message.
  19046. */
  19047. function displayChatActionButtons(message, html, data) {
  19048. const chatCard = html.find(".dnd5e.chat-card");
  19049. if ( chatCard.length > 0 ) {
  19050. const flavor = html.find(".flavor-text");
  19051. if ( flavor.text() === html.find(".item-name").text() ) flavor.remove();
  19052. // If the user is the message author or the actor owner, proceed
  19053. let actor = game.actors.get(data.message.speaker.actor);
  19054. if ( actor && actor.isOwner ) return;
  19055. else if ( game.user.isGM || (data.author.id === game.user.id)) return;
  19056. // Otherwise conceal action buttons except for saving throw
  19057. const buttons = chatCard.find("button[data-action]");
  19058. buttons.each((i, btn) => {
  19059. if ( btn.dataset.action === "save" ) return;
  19060. btn.style.display = "none";
  19061. });
  19062. }
  19063. }
  19064. /* -------------------------------------------- */
  19065. /**
  19066. * This function is used to hook into the Chat Log context menu to add additional options to each message
  19067. * These options make it easy to conveniently apply damage to controlled tokens based on the value of a Roll
  19068. *
  19069. * @param {HTMLElement} html The Chat Message being rendered
  19070. * @param {object[]} options The Array of Context Menu options
  19071. *
  19072. * @returns {object[]} The extended options Array including new context choices
  19073. */
  19074. function addChatMessageContextOptions(html, options) {
  19075. let canApply = li => {
  19076. const message = game.messages.get(li.data("messageId"));
  19077. return message?.isRoll && message?.isContentVisible && canvas.tokens?.controlled.length;
  19078. };
  19079. options.push(
  19080. {
  19081. name: game.i18n.localize("DND5E.ChatContextDamage"),
  19082. icon: '<i class="fas fa-user-minus"></i>',
  19083. condition: canApply,
  19084. callback: li => applyChatCardDamage(li, 1)
  19085. },
  19086. {
  19087. name: game.i18n.localize("DND5E.ChatContextHealing"),
  19088. icon: '<i class="fas fa-user-plus"></i>',
  19089. condition: canApply,
  19090. callback: li => applyChatCardDamage(li, -1)
  19091. },
  19092. {
  19093. name: game.i18n.localize("DND5E.ChatContextTempHP"),
  19094. icon: '<i class="fas fa-user-clock"></i>',
  19095. condition: canApply,
  19096. callback: li => applyChatCardTemp(li)
  19097. },
  19098. {
  19099. name: game.i18n.localize("DND5E.ChatContextDoubleDamage"),
  19100. icon: '<i class="fas fa-user-injured"></i>',
  19101. condition: canApply,
  19102. callback: li => applyChatCardDamage(li, 2)
  19103. },
  19104. {
  19105. name: game.i18n.localize("DND5E.ChatContextHalfDamage"),
  19106. icon: '<i class="fas fa-user-shield"></i>',
  19107. condition: canApply,
  19108. callback: li => applyChatCardDamage(li, 0.5)
  19109. }
  19110. );
  19111. return options;
  19112. }
  19113. /* -------------------------------------------- */
  19114. /**
  19115. * Apply rolled dice damage to the token or tokens which are currently controlled.
  19116. * This allows for damage to be scaled by a multiplier to account for healing, critical hits, or resistance
  19117. *
  19118. * @param {HTMLElement} li The chat entry which contains the roll data
  19119. * @param {number} multiplier A damage multiplier to apply to the rolled damage.
  19120. * @returns {Promise}
  19121. */
  19122. function applyChatCardDamage(li, multiplier) {
  19123. const message = game.messages.get(li.data("messageId"));
  19124. const roll = message.rolls[0];
  19125. return Promise.all(canvas.tokens.controlled.map(t => {
  19126. const a = t.actor;
  19127. return a.applyDamage(roll.total, multiplier);
  19128. }));
  19129. }
  19130. /* -------------------------------------------- */
  19131. /**
  19132. * Apply rolled dice as temporary hit points to the controlled token(s).
  19133. * @param {HTMLElement} li The chat entry which contains the roll data
  19134. * @returns {Promise}
  19135. */
  19136. function applyChatCardTemp(li) {
  19137. const message = game.messages.get(li.data("messageId"));
  19138. const roll = message.rolls[0];
  19139. return Promise.all(canvas.tokens.controlled.map(t => {
  19140. const a = t.actor;
  19141. return a.applyTempHP(roll.total);
  19142. }));
  19143. }
  19144. /* -------------------------------------------- */
  19145. /**
  19146. * Handle rendering of a chat message to the log
  19147. * @param {ChatLog} app The ChatLog instance
  19148. * @param {jQuery} html Rendered chat message HTML
  19149. * @param {object} data Data passed to the render context
  19150. */
  19151. function onRenderChatMessage(app, html, data) {
  19152. displayChatActionButtons(app, html, data);
  19153. highlightCriticalSuccessFailure(app, html);
  19154. if (game.settings.get("dnd5e", "autoCollapseItemCards")) html.find(".card-content").hide();
  19155. }
  19156. var chatMessage = /*#__PURE__*/Object.freeze({
  19157. __proto__: null,
  19158. addChatMessageContextOptions: addChatMessageContextOptions,
  19159. displayChatActionButtons: displayChatActionButtons,
  19160. highlightCriticalSuccessFailure: highlightCriticalSuccessFailure,
  19161. onRenderChatMessage: onRenderChatMessage
  19162. });
  19163. /**
  19164. * Override the core method for obtaining a Roll instance used for the Combatant.
  19165. * @see {Actor5e#getInitiativeRoll}
  19166. * @param {string} [formula] A formula to use if no Actor is defined
  19167. * @returns {D20Roll} The D20Roll instance which is used to determine initiative for the Combatant
  19168. */
  19169. function getInitiativeRoll(formula="1d20") {
  19170. if ( !this.actor ) return new CONFIG.Dice.D20Roll(formula ?? "1d20", {});
  19171. return this.actor.getInitiativeRoll();
  19172. }
  19173. var combat = /*#__PURE__*/Object.freeze({
  19174. __proto__: null,
  19175. getInitiativeRoll: getInitiativeRoll
  19176. });
  19177. /**
  19178. * Attempt to create a macro from the dropped data. Will use an existing macro if one exists.
  19179. * @param {object} dropData The dropped data
  19180. * @param {number} slot The hotbar slot to use
  19181. */
  19182. async function create5eMacro(dropData, slot) {
  19183. const macroData = { type: "script", scope: "actor" };
  19184. switch ( dropData.type ) {
  19185. case "Item":
  19186. const itemData = await Item.implementation.fromDropData(dropData);
  19187. if ( !itemData ) return ui.notifications.warn(game.i18n.localize("MACRO.5eUnownedWarn"));
  19188. foundry.utils.mergeObject(macroData, {
  19189. name: itemData.name,
  19190. img: itemData.img,
  19191. command: `dnd5e.documents.macro.rollItem("${itemData.name}")`,
  19192. flags: {"dnd5e.itemMacro": true}
  19193. });
  19194. break;
  19195. case "ActiveEffect":
  19196. const effectData = await ActiveEffect.implementation.fromDropData(dropData);
  19197. if ( !effectData ) return ui.notifications.warn(game.i18n.localize("MACRO.5eUnownedWarn"));
  19198. foundry.utils.mergeObject(macroData, {
  19199. name: effectData.label,
  19200. img: effectData.icon,
  19201. command: `dnd5e.documents.macro.toggleEffect("${effectData.label}")`,
  19202. flags: {"dnd5e.effectMacro": true}
  19203. });
  19204. break;
  19205. default:
  19206. return;
  19207. }
  19208. // Assign the macro to the hotbar
  19209. const macro = game.macros.find(m => (m.name === macroData.name) && (m.command === macroData.command)
  19210. && m.author.isSelf) || await Macro.create(macroData);
  19211. game.user.assignHotbarMacro(macro, slot);
  19212. }
  19213. /* -------------------------------------------- */
  19214. /**
  19215. * Find a document of the specified name and type on an assigned or selected actor.
  19216. * @param {string} name Document name to locate.
  19217. * @param {string} documentType Type of embedded document (e.g. "Item" or "ActiveEffect").
  19218. * @returns {Document} Document if found, otherwise nothing.
  19219. */
  19220. function getMacroTarget(name, documentType) {
  19221. let actor;
  19222. const speaker = ChatMessage.getSpeaker();
  19223. if ( speaker.token ) actor = game.actors.tokens[speaker.token];
  19224. actor ??= game.actors.get(speaker.actor);
  19225. if ( !actor ) return ui.notifications.warn(game.i18n.localize("MACRO.5eNoActorSelected"));
  19226. const collection = (documentType === "Item") ? actor.items : actor.effects;
  19227. const nameKeyPath = (documentType === "Item") ? "name" : "label";
  19228. // Find item in collection
  19229. const documents = collection.filter(i => foundry.utils.getProperty(i, nameKeyPath) === name);
  19230. const type = game.i18n.localize(`DOCUMENT.${documentType}`);
  19231. if ( documents.length === 0 ) {
  19232. return ui.notifications.warn(game.i18n.format("MACRO.5eMissingTargetWarn", { actor: actor.name, type, name }));
  19233. }
  19234. if ( documents.length > 1 ) {
  19235. ui.notifications.warn(game.i18n.format("MACRO.5eMultipleTargetsWarn", { actor: actor.name, type, name }));
  19236. }
  19237. return documents[0];
  19238. }
  19239. /* -------------------------------------------- */
  19240. /**
  19241. * Trigger an item to roll when a macro is clicked.
  19242. * @param {string} itemName Name of the item on the selected actor to trigger.
  19243. * @returns {Promise<ChatMessage|object>} Roll result.
  19244. */
  19245. function rollItem(itemName) {
  19246. return getMacroTarget(itemName, "Item")?.use();
  19247. }
  19248. /* -------------------------------------------- */
  19249. /**
  19250. * Toggle an effect on and off when a macro is clicked.
  19251. * @param {string} effectName Name of the effect to be toggled.
  19252. * @returns {Promise<ActiveEffect>} The effect after it has been toggled.
  19253. */
  19254. function toggleEffect(effectName) {
  19255. const effect = getMacroTarget(effectName, "ActiveEffect");
  19256. return effect?.update({disabled: !effect.disabled});
  19257. }
  19258. var macro = /*#__PURE__*/Object.freeze({
  19259. __proto__: null,
  19260. create5eMacro: create5eMacro,
  19261. rollItem: rollItem,
  19262. toggleEffect: toggleEffect
  19263. });
  19264. // Document Classes
  19265. var documents = /*#__PURE__*/Object.freeze({
  19266. __proto__: null,
  19267. ActiveEffect5e: ActiveEffect5e,
  19268. Actor5e: Actor5e,
  19269. Item5e: Item5e,
  19270. Proficiency: Proficiency,
  19271. TokenDocument5e: TokenDocument5e,
  19272. Trait: trait,
  19273. advancement: _module$a,
  19274. chat: chatMessage,
  19275. combat: combat,
  19276. macro: macro
  19277. });
  19278. /**
  19279. * Perform a system migration for the entire World, applying migrations for Actors, Items, and Compendium packs
  19280. * @returns {Promise} A Promise which resolves once the migration is completed
  19281. */
  19282. const migrateWorld = async function() {
  19283. const version = game.system.version;
  19284. ui.notifications.info(game.i18n.format("MIGRATION.5eBegin", {version}), {permanent: true});
  19285. const migrationData = await getMigrationData();
  19286. // Migrate World Actors
  19287. const actors = game.actors.map(a => [a, true])
  19288. .concat(Array.from(game.actors.invalidDocumentIds).map(id => [game.actors.getInvalid(id), false]));
  19289. for ( const [actor, valid] of actors ) {
  19290. try {
  19291. const source = valid ? actor.toObject() : game.data.actors.find(a => a._id === actor.id);
  19292. const updateData = migrateActorData(source, migrationData);
  19293. if ( !foundry.utils.isEmpty(updateData) ) {
  19294. console.log(`Migrating Actor document ${actor.name}`);
  19295. await actor.update(updateData, {enforceTypes: false, diff: valid});
  19296. }
  19297. } catch(err) {
  19298. err.message = `Failed dnd5e system migration for Actor ${actor.name}: ${err.message}`;
  19299. console.error(err);
  19300. }
  19301. }
  19302. // Migrate World Items
  19303. const items = game.items.map(i => [i, true])
  19304. .concat(Array.from(game.items.invalidDocumentIds).map(id => [game.items.getInvalid(id), false]));
  19305. for ( const [item, valid] of items ) {
  19306. try {
  19307. const source = valid ? item.toObject() : game.data.items.find(i => i._id === item.id);
  19308. const updateData = migrateItemData(source, migrationData);
  19309. if ( !foundry.utils.isEmpty(updateData) ) {
  19310. console.log(`Migrating Item document ${item.name}`);
  19311. await item.update(updateData, {enforceTypes: false, diff: valid});
  19312. }
  19313. } catch(err) {
  19314. err.message = `Failed dnd5e system migration for Item ${item.name}: ${err.message}`;
  19315. console.error(err);
  19316. }
  19317. }
  19318. // Migrate World Macros
  19319. for ( const m of game.macros ) {
  19320. try {
  19321. const updateData = migrateMacroData(m.toObject(), migrationData);
  19322. if ( !foundry.utils.isEmpty(updateData) ) {
  19323. console.log(`Migrating Macro document ${m.name}`);
  19324. await m.update(updateData, {enforceTypes: false});
  19325. }
  19326. } catch(err) {
  19327. err.message = `Failed dnd5e system migration for Macro ${m.name}: ${err.message}`;
  19328. console.error(err);
  19329. }
  19330. }
  19331. // Migrate Actor Override Tokens
  19332. for ( let s of game.scenes ) {
  19333. try {
  19334. const updateData = migrateSceneData(s, migrationData);
  19335. if ( !foundry.utils.isEmpty(updateData) ) {
  19336. console.log(`Migrating Scene document ${s.name}`);
  19337. await s.update(updateData, {enforceTypes: false});
  19338. // If we do not do this, then synthetic token actors remain in cache
  19339. // with the un-updated actorData.
  19340. s.tokens.forEach(t => t._actor = null);
  19341. }
  19342. } catch(err) {
  19343. err.message = `Failed dnd5e system migration for Scene ${s.name}: ${err.message}`;
  19344. console.error(err);
  19345. }
  19346. }
  19347. // Migrate World Compendium Packs
  19348. for ( let p of game.packs ) {
  19349. if ( p.metadata.packageType !== "world" ) continue;
  19350. if ( !["Actor", "Item", "Scene"].includes(p.documentName) ) continue;
  19351. await migrateCompendium(p);
  19352. }
  19353. // Set the migration as complete
  19354. game.settings.set("dnd5e", "systemMigrationVersion", game.system.version);
  19355. ui.notifications.info(game.i18n.format("MIGRATION.5eComplete", {version}), {permanent: true});
  19356. };
  19357. /* -------------------------------------------- */
  19358. /**
  19359. * Apply migration rules to all Documents within a single Compendium pack
  19360. * @param {CompendiumCollection} pack Pack to be migrated.
  19361. * @returns {Promise}
  19362. */
  19363. const migrateCompendium = async function(pack) {
  19364. const documentName = pack.documentName;
  19365. if ( !["Actor", "Item", "Scene"].includes(documentName) ) return;
  19366. const migrationData = await getMigrationData();
  19367. // Unlock the pack for editing
  19368. const wasLocked = pack.locked;
  19369. await pack.configure({locked: false});
  19370. // Begin by requesting server-side data model migration and get the migrated content
  19371. await pack.migrate();
  19372. const documents = await pack.getDocuments();
  19373. // Iterate over compendium entries - applying fine-tuned migration functions
  19374. for ( let doc of documents ) {
  19375. let updateData = {};
  19376. try {
  19377. switch (documentName) {
  19378. case "Actor":
  19379. updateData = migrateActorData(doc.toObject(), migrationData);
  19380. break;
  19381. case "Item":
  19382. updateData = migrateItemData(doc.toObject(), migrationData);
  19383. break;
  19384. case "Scene":
  19385. updateData = migrateSceneData(doc.toObject(), migrationData);
  19386. break;
  19387. }
  19388. // Save the entry, if data was changed
  19389. if ( foundry.utils.isEmpty(updateData) ) continue;
  19390. await doc.update(updateData);
  19391. console.log(`Migrated ${documentName} document ${doc.name} in Compendium ${pack.collection}`);
  19392. }
  19393. // Handle migration failures
  19394. catch(err) {
  19395. err.message = `Failed dnd5e system migration for document ${doc.name} in pack ${pack.collection}: ${err.message}`;
  19396. console.error(err);
  19397. }
  19398. }
  19399. // Apply the original locked status for the pack
  19400. await pack.configure({locked: wasLocked});
  19401. console.log(`Migrated all ${documentName} documents from Compendium ${pack.collection}`);
  19402. };
  19403. /* -------------------------------------------- */
  19404. /**
  19405. * Update all compendium packs using the new system data model.
  19406. */
  19407. async function refreshAllCompendiums() {
  19408. for ( const pack of game.packs ) {
  19409. await refreshCompendium(pack);
  19410. }
  19411. }
  19412. /* -------------------------------------------- */
  19413. /**
  19414. * Update all Documents in a compendium using the new system data model.
  19415. * @param {CompendiumCollection} pack Pack to refresh.
  19416. */
  19417. async function refreshCompendium(pack) {
  19418. if ( !pack?.documentName ) return;
  19419. dnd5e.moduleArt.suppressArt = true;
  19420. const DocumentClass = CONFIG[pack.documentName].documentClass;
  19421. const wasLocked = pack.locked;
  19422. await pack.configure({locked: false});
  19423. await pack.migrate();
  19424. ui.notifications.info(`Beginning to refresh Compendium ${pack.collection}`);
  19425. const documents = await pack.getDocuments();
  19426. for ( const doc of documents ) {
  19427. const data = doc.toObject();
  19428. await doc.delete();
  19429. await DocumentClass.create(data, {keepId: true, keepEmbeddedIds: true, pack: pack.collection});
  19430. }
  19431. await pack.configure({locked: wasLocked});
  19432. dnd5e.moduleArt.suppressArt = false;
  19433. ui.notifications.info(`Refreshed all documents from Compendium ${pack.collection}`);
  19434. }
  19435. /* -------------------------------------------- */
  19436. /**
  19437. * Apply 'smart' AC migration to a given Actor compendium. This will perform the normal AC migration but additionally
  19438. * check to see if the actor has armor already equipped, and opt to use that instead.
  19439. * @param {CompendiumCollection|string} pack Pack or name of pack to migrate.
  19440. * @returns {Promise}
  19441. */
  19442. const migrateArmorClass = async function(pack) {
  19443. if ( typeof pack === "string" ) pack = game.packs.get(pack);
  19444. if ( pack.documentName !== "Actor" ) return;
  19445. const wasLocked = pack.locked;
  19446. await pack.configure({locked: false});
  19447. const actors = await pack.getDocuments();
  19448. const updates = [];
  19449. const armor = new Set(Object.keys(CONFIG.DND5E.armorTypes));
  19450. for ( const actor of actors ) {
  19451. try {
  19452. console.log(`Migrating ${actor.name}...`);
  19453. const src = actor.toObject();
  19454. const update = {_id: actor.id};
  19455. // Perform the normal migration.
  19456. _migrateActorAC(src, update);
  19457. // TODO: See if AC migration within DataModel is enough to handle this
  19458. updates.push(update);
  19459. // CASE 1: Armor is equipped
  19460. const hasArmorEquipped = actor.itemTypes.equipment.some(e => {
  19461. return armor.has(e.system.armor?.type) && e.system.equipped;
  19462. });
  19463. if ( hasArmorEquipped ) update["system.attributes.ac.calc"] = "default";
  19464. // CASE 2: NPC Natural Armor
  19465. else if ( src.type === "npc" ) update["system.attributes.ac.calc"] = "natural";
  19466. } catch(e) {
  19467. console.warn(`Failed to migrate armor class for Actor ${actor.name}`, e);
  19468. }
  19469. }
  19470. await Actor.implementation.updateDocuments(updates, {pack: pack.collection});
  19471. await pack.getDocuments(); // Force a re-prepare of all actors.
  19472. await pack.configure({locked: wasLocked});
  19473. console.log(`Migrated the AC of all Actors from Compendium ${pack.collection}`);
  19474. };
  19475. /* -------------------------------------------- */
  19476. /* Document Type Migration Helpers */
  19477. /* -------------------------------------------- */
  19478. /**
  19479. * Migrate a single Actor document to incorporate latest data model changes
  19480. * Return an Object of updateData to be applied
  19481. * @param {object} actor The actor data object to update
  19482. * @param {object} [migrationData] Additional data to perform the migration
  19483. * @returns {object} The updateData to apply
  19484. */
  19485. const migrateActorData = function(actor, migrationData) {
  19486. const updateData = {};
  19487. _migrateTokenImage(actor, updateData);
  19488. _migrateActorAC(actor, updateData);
  19489. // Migrate embedded effects
  19490. if ( actor.effects ) {
  19491. const effects = migrateEffects(actor, migrationData);
  19492. if ( effects.length > 0 ) updateData.effects = effects;
  19493. }
  19494. // Migrate Owned Items
  19495. if ( !actor.items ) return updateData;
  19496. const items = actor.items.reduce((arr, i) => {
  19497. // Migrate the Owned Item
  19498. const itemData = i instanceof CONFIG.Item.documentClass ? i.toObject() : i;
  19499. let itemUpdate = migrateItemData(itemData, migrationData);
  19500. // Prepared, Equipped, and Proficient for NPC actors
  19501. if ( actor.type === "npc" ) {
  19502. if (foundry.utils.getProperty(itemData.system, "preparation.prepared") === false) itemUpdate["system.preparation.prepared"] = true;
  19503. if (foundry.utils.getProperty(itemData.system, "equipped") === false) itemUpdate["system.equipped"] = true;
  19504. if (foundry.utils.getProperty(itemData.system, "proficient") === false) itemUpdate["system.proficient"] = true;
  19505. }
  19506. // Update the Owned Item
  19507. if ( !foundry.utils.isEmpty(itemUpdate) ) {
  19508. itemUpdate._id = itemData._id;
  19509. arr.push(foundry.utils.expandObject(itemUpdate));
  19510. }
  19511. // Update tool expertise.
  19512. if ( actor.system.tools ) {
  19513. const hasToolProf = itemData.system.baseItem in actor.system.tools;
  19514. if ( (itemData.type === "tool") && (itemData.system.proficient > 1) && hasToolProf ) {
  19515. updateData[`system.tools.${itemData.system.baseItem}.value`] = itemData.system.proficient;
  19516. }
  19517. }
  19518. return arr;
  19519. }, []);
  19520. if ( items.length > 0 ) updateData.items = items;
  19521. return updateData;
  19522. };
  19523. /* -------------------------------------------- */
  19524. /**
  19525. * Migrate a single Item document to incorporate latest data model changes
  19526. *
  19527. * @param {object} item Item data to migrate
  19528. * @param {object} [migrationData] Additional data to perform the migration
  19529. * @returns {object} The updateData to apply
  19530. */
  19531. function migrateItemData(item, migrationData) {
  19532. const updateData = {};
  19533. _migrateDocumentIcon(item, updateData, migrationData);
  19534. // Migrate embedded effects
  19535. if ( item.effects ) {
  19536. const effects = migrateEffects(item, migrationData);
  19537. if ( effects.length > 0 ) updateData.effects = effects;
  19538. }
  19539. return updateData;
  19540. }
  19541. /* -------------------------------------------- */
  19542. /**
  19543. * Migrate any active effects attached to the provided parent.
  19544. * @param {object} parent Data of the parent being migrated.
  19545. * @param {object} [migrationData] Additional data to perform the migration.
  19546. * @returns {object[]} Updates to apply on the embedded effects.
  19547. */
  19548. const migrateEffects = function(parent, migrationData) {
  19549. if ( !parent.effects ) return {};
  19550. return parent.effects.reduce((arr, e) => {
  19551. const effectData = e instanceof CONFIG.ActiveEffect.documentClass ? e.toObject() : e;
  19552. let effectUpdate = migrateEffectData(effectData, migrationData);
  19553. if ( !foundry.utils.isEmpty(effectUpdate) ) {
  19554. effectUpdate._id = effectData._id;
  19555. arr.push(foundry.utils.expandObject(effectUpdate));
  19556. }
  19557. return arr;
  19558. }, []);
  19559. };
  19560. /* -------------------------------------------- */
  19561. /**
  19562. * Migrate the provided active effect data.
  19563. * @param {object} effect Effect data to migrate.
  19564. * @param {object} [migrationData] Additional data to perform the migration.
  19565. * @returns {object} The updateData to apply.
  19566. */
  19567. const migrateEffectData = function(effect, migrationData) {
  19568. const updateData = {};
  19569. _migrateDocumentIcon(effect, updateData, {...migrationData, field: "icon"});
  19570. _migrateEffectArmorClass(effect, updateData);
  19571. return updateData;
  19572. };
  19573. /* -------------------------------------------- */
  19574. /**
  19575. * Migrate a single Macro document to incorporate latest data model changes.
  19576. * @param {object} macro Macro data to migrate
  19577. * @param {object} [migrationData] Additional data to perform the migration
  19578. * @returns {object} The updateData to apply
  19579. */
  19580. const migrateMacroData = function(macro, migrationData) {
  19581. const updateData = {};
  19582. _migrateDocumentIcon(macro, updateData, migrationData);
  19583. _migrateMacroCommands(macro, updateData);
  19584. return updateData;
  19585. };
  19586. /* -------------------------------------------- */
  19587. /**
  19588. * Migrate a single Scene document to incorporate changes to the data model of it's actor data overrides
  19589. * Return an Object of updateData to be applied
  19590. * @param {object} scene The Scene data to Update
  19591. * @param {object} [migrationData] Additional data to perform the migration
  19592. * @returns {object} The updateData to apply
  19593. */
  19594. const migrateSceneData = function(scene, migrationData) {
  19595. const tokens = scene.tokens.map(token => {
  19596. const t = token instanceof foundry.abstract.DataModel ? token.toObject() : token;
  19597. const update = {};
  19598. _migrateTokenImage(t, update);
  19599. if ( Object.keys(update).length ) foundry.utils.mergeObject(t, update);
  19600. if ( !game.actors.has(t.actorId) ) t.actorId = null;
  19601. if ( !t.actorId || t.actorLink ) t.actorData = {};
  19602. else if ( !t.actorLink ) {
  19603. const actorData = token.delta?.toObject() ?? foundry.utils.deepClone(t.actorData);
  19604. actorData.type = token.actor?.type;
  19605. const update = migrateActorData(actorData, migrationData);
  19606. if ( game.dnd5e.isV10 ) {
  19607. ["items", "effects"].forEach(embeddedName => {
  19608. if ( !update[embeddedName]?.length ) return;
  19609. const updates = new Map(update[embeddedName].map(u => [u._id, u]));
  19610. t.actorData[embeddedName].forEach(original => {
  19611. const update = updates.get(original._id);
  19612. if ( update ) foundry.utils.mergeObject(original, update);
  19613. });
  19614. delete update[embeddedName];
  19615. });
  19616. foundry.utils.mergeObject(t.actorData, update);
  19617. }
  19618. else t.delta = update;
  19619. }
  19620. return t;
  19621. });
  19622. return {tokens};
  19623. };
  19624. /* -------------------------------------------- */
  19625. /**
  19626. * Fetch bundled data for large-scale migrations.
  19627. * @returns {Promise<object>} Object mapping original system icons to their core replacements.
  19628. */
  19629. const getMigrationData = async function() {
  19630. const data = {};
  19631. try {
  19632. const icons = await fetch("systems/dnd5e/json/icon-migration.json");
  19633. const spellIcons = await fetch("systems/dnd5e/json/spell-icon-migration.json");
  19634. data.iconMap = {...await icons.json(), ...await spellIcons.json()};
  19635. } catch(err) {
  19636. console.warn(`Failed to retrieve icon migration data: ${err.message}`);
  19637. }
  19638. return data;
  19639. };
  19640. /* -------------------------------------------- */
  19641. /* Low level migration utilities
  19642. /* -------------------------------------------- */
  19643. /**
  19644. * Migrate the actor attributes.ac.value to the new ac.flat override field.
  19645. * @param {object} actorData Actor data being migrated.
  19646. * @param {object} updateData Existing updates being applied to actor. *Will be mutated.*
  19647. * @returns {object} Modified version of update data.
  19648. * @private
  19649. */
  19650. function _migrateActorAC(actorData, updateData) {
  19651. const ac = actorData.system?.attributes?.ac;
  19652. // If the actor has a numeric ac.value, then their AC has not been migrated to the auto-calculation schema yet.
  19653. if ( Number.isNumeric(ac?.value) ) {
  19654. updateData["system.attributes.ac.flat"] = parseInt(ac.value);
  19655. updateData["system.attributes.ac.calc"] = actorData.type === "npc" ? "natural" : "flat";
  19656. updateData["system.attributes.ac.-=value"] = null;
  19657. return updateData;
  19658. }
  19659. // Migrate ac.base in custom formulas to ac.armor
  19660. if ( (typeof ac?.formula === "string") && ac?.formula.includes("@attributes.ac.base") ) {
  19661. updateData["system.attributes.ac.formula"] = ac.formula.replaceAll("@attributes.ac.base", "@attributes.ac.armor");
  19662. }
  19663. // Protect against string values created by character sheets or importers that don't enforce data types
  19664. if ( (typeof ac?.flat === "string") && Number.isNumeric(ac.flat) ) {
  19665. updateData["system.attributes.ac.flat"] = parseInt(ac.flat);
  19666. }
  19667. // Remove invalid AC formula strings.
  19668. if ( ac?.formula ) {
  19669. try {
  19670. const roll = new Roll(ac.formula);
  19671. Roll.safeEval(roll.formula);
  19672. } catch( e ) {
  19673. updateData["system.attributes.ac.formula"] = "";
  19674. }
  19675. }
  19676. return updateData;
  19677. }
  19678. /* -------------------------------------------- */
  19679. /**
  19680. * Migrate any system token images from PNG to WEBP.
  19681. * @param {object} actorData Actor or token data to migrate.
  19682. * @param {object} updateData Existing update to expand upon.
  19683. * @returns {object} The updateData to apply
  19684. * @private
  19685. */
  19686. function _migrateTokenImage(actorData, updateData) {
  19687. const oldSystemPNG = /^systems\/dnd5e\/tokens\/([a-z]+)\/([A-z]+).png$/;
  19688. for ( const path of ["texture.src", "prototypeToken.texture.src"] ) {
  19689. const v = foundry.utils.getProperty(actorData, path);
  19690. if ( oldSystemPNG.test(v) ) {
  19691. const [type, fileName] = v.match(oldSystemPNG).slice(1);
  19692. updateData[path] = `systems/dnd5e/tokens/${type}/${fileName}.webp`;
  19693. }
  19694. }
  19695. return updateData;
  19696. }
  19697. /* -------------------------------------------- */
  19698. /**
  19699. * Convert system icons to use bundled core webp icons.
  19700. * @param {object} document Document data to migrate
  19701. * @param {object} updateData Existing update to expand upon
  19702. * @param {object} [migrationData={}] Additional data to perform the migration
  19703. * @param {Object<string, string>} [migrationData.iconMap] A mapping of system icons to core foundry icons
  19704. * @param {string} [migrationData.field] The document field to migrate
  19705. * @returns {object} The updateData to apply
  19706. * @private
  19707. */
  19708. function _migrateDocumentIcon(document, updateData, {iconMap, field="img"}={}) {
  19709. let path = document?.[field];
  19710. if ( path && iconMap ) {
  19711. if ( path.startsWith("/") || path.startsWith("\\") ) path = path.substring(1);
  19712. const rename = iconMap[path];
  19713. if ( rename ) updateData[field] = rename;
  19714. }
  19715. return updateData;
  19716. }
  19717. /* -------------------------------------------- */
  19718. /**
  19719. * Change active effects that target AC.
  19720. * @param {object} effect Effect data to migrate.
  19721. * @param {object} updateData Existing update to expand upon.
  19722. * @returns {object} The updateData to apply.
  19723. */
  19724. function _migrateEffectArmorClass(effect, updateData) {
  19725. let containsUpdates = false;
  19726. const changes = (effect.changes || []).map(c => {
  19727. if ( c.key !== "system.attributes.ac.base" ) return c;
  19728. c.key = "system.attributes.ac.armor";
  19729. containsUpdates = true;
  19730. return c;
  19731. });
  19732. if ( containsUpdates ) updateData.changes = changes;
  19733. return updateData;
  19734. }
  19735. /* -------------------------------------------- */
  19736. /**
  19737. * Migrate macros from the old 'dnd5e.rollItemMacro' and 'dnd5e.macros' commands to the new location.
  19738. * @param {object} macro Macro data to migrate.
  19739. * @param {object} updateData Existing update to expand upon.
  19740. * @returns {object} The updateData to apply.
  19741. */
  19742. function _migrateMacroCommands(macro, updateData) {
  19743. if ( macro.command.includes("game.dnd5e.rollItemMacro") ) {
  19744. updateData.command = macro.command.replaceAll("game.dnd5e.rollItemMacro", "dnd5e.documents.macro.rollItem");
  19745. } else if ( macro.command.includes("game.dnd5e.macros.") ) {
  19746. updateData.command = macro.command.replaceAll("game.dnd5e.macros.", "dnd5e.documents.macro.");
  19747. }
  19748. return updateData;
  19749. }
  19750. /* -------------------------------------------- */
  19751. /**
  19752. * A general tool to purge flags from all documents in a Compendium pack.
  19753. * @param {CompendiumCollection} pack The compendium pack to clean.
  19754. * @private
  19755. */
  19756. async function purgeFlags(pack) {
  19757. const cleanFlags = flags => {
  19758. const flags5e = flags.dnd5e || null;
  19759. return flags5e ? {dnd5e: flags5e} : {};
  19760. };
  19761. await pack.configure({locked: false});
  19762. const content = await pack.getDocuments();
  19763. for ( let doc of content ) {
  19764. const update = {flags: cleanFlags(doc.flags)};
  19765. if ( pack.documentName === "Actor" ) {
  19766. update.items = doc.items.map(i => {
  19767. i.flags = cleanFlags(i.flags);
  19768. return i;
  19769. });
  19770. }
  19771. await doc.update(update, {recursive: false});
  19772. console.log(`Purged flags from ${doc.name}`);
  19773. }
  19774. await pack.configure({locked: true});
  19775. }
  19776. var migrations = /*#__PURE__*/Object.freeze({
  19777. __proto__: null,
  19778. getMigrationData: getMigrationData,
  19779. migrateActorData: migrateActorData,
  19780. migrateArmorClass: migrateArmorClass,
  19781. migrateCompendium: migrateCompendium,
  19782. migrateEffectData: migrateEffectData,
  19783. migrateEffects: migrateEffects,
  19784. migrateItemData: migrateItemData,
  19785. migrateMacroData: migrateMacroData,
  19786. migrateSceneData: migrateSceneData,
  19787. migrateWorld: migrateWorld,
  19788. purgeFlags: purgeFlags,
  19789. refreshAllCompendiums: refreshAllCompendiums,
  19790. refreshCompendium: refreshCompendium
  19791. });
  19792. /**
  19793. * The DnD5e game system for Foundry Virtual Tabletop
  19794. * A system for playing the fifth edition of the world's most popular role-playing game.
  19795. * Author: Atropos
  19796. * Software License: MIT
  19797. * Content License: https://www.dndbeyond.com/attachments/39j2li89/SRD5.1-CCBY4.0License.pdf
  19798. * Repository: https://github.com/foundryvtt/dnd5e
  19799. * Issue Tracker: https://github.com/foundryvtt/dnd5e/issues
  19800. */
  19801. /* -------------------------------------------- */
  19802. /* Define Module Structure */
  19803. /* -------------------------------------------- */
  19804. globalThis.dnd5e = {
  19805. applications,
  19806. canvas: canvas$1,
  19807. config: DND5E,
  19808. dataModels,
  19809. dice,
  19810. documents,
  19811. migrations,
  19812. utils
  19813. };
  19814. /* -------------------------------------------- */
  19815. /* Foundry VTT Initialization */
  19816. /* -------------------------------------------- */
  19817. Hooks.once("init", function() {
  19818. globalThis.dnd5e = game.dnd5e = Object.assign(game.system, globalThis.dnd5e);
  19819. console.log(`DnD5e | Initializing the DnD5e Game System - Version ${dnd5e.version}\n${DND5E.ASCII}`);
  19820. // Record Configuration Values
  19821. CONFIG.DND5E = DND5E;
  19822. CONFIG.ActiveEffect.documentClass = ActiveEffect5e;
  19823. CONFIG.Actor.documentClass = Actor5e;
  19824. CONFIG.Item.documentClass = Item5e;
  19825. CONFIG.Token.documentClass = TokenDocument5e;
  19826. CONFIG.Token.objectClass = Token5e;
  19827. CONFIG.time.roundTime = 6;
  19828. CONFIG.Dice.DamageRoll = DamageRoll;
  19829. CONFIG.Dice.D20Roll = D20Roll;
  19830. CONFIG.MeasuredTemplate.defaults.angle = 53.13; // 5e cone RAW should be 53.13 degrees
  19831. CONFIG.ui.combat = CombatTracker5e;
  19832. CONFIG.compatibility.excludePatterns.push(/\bActiveEffect5e#label\b/); // backwards compatibility with v10
  19833. game.dnd5e.isV10 = game.release.generation < 11;
  19834. // Configure trackable attributes.
  19835. _configureTrackableAttributes();
  19836. // Register System Settings
  19837. registerSystemSettings();
  19838. // Validation strictness.
  19839. if ( game.dnd5e.isV10 ) _determineValidationStrictness();
  19840. // Configure module art.
  19841. game.dnd5e.moduleArt = new ModuleArt();
  19842. // Remove honor & sanity from configuration if they aren't enabled
  19843. if ( !game.settings.get("dnd5e", "honorScore") ) delete DND5E.abilities.hon;
  19844. if ( !game.settings.get("dnd5e", "sanityScore") ) delete DND5E.abilities.san;
  19845. // Patch Core Functions
  19846. Combatant.prototype.getInitiativeRoll = getInitiativeRoll;
  19847. // Register Roll Extensions
  19848. CONFIG.Dice.rolls.push(D20Roll);
  19849. CONFIG.Dice.rolls.push(DamageRoll);
  19850. // Hook up system data types
  19851. const modelType = game.dnd5e.isV10 ? "systemDataModels" : "dataModels";
  19852. CONFIG.Actor[modelType] = config$2;
  19853. CONFIG.Item[modelType] = config$1;
  19854. CONFIG.JournalEntryPage[modelType] = config;
  19855. // Register sheet application classes
  19856. Actors.unregisterSheet("core", ActorSheet);
  19857. Actors.registerSheet("dnd5e", ActorSheet5eCharacter, {
  19858. types: ["character"],
  19859. makeDefault: true,
  19860. label: "DND5E.SheetClassCharacter"
  19861. });
  19862. Actors.registerSheet("dnd5e", ActorSheet5eNPC, {
  19863. types: ["npc"],
  19864. makeDefault: true,
  19865. label: "DND5E.SheetClassNPC"
  19866. });
  19867. Actors.registerSheet("dnd5e", ActorSheet5eVehicle, {
  19868. types: ["vehicle"],
  19869. makeDefault: true,
  19870. label: "DND5E.SheetClassVehicle"
  19871. });
  19872. Actors.registerSheet("dnd5e", GroupActorSheet, {
  19873. types: ["group"],
  19874. makeDefault: true,
  19875. label: "DND5E.SheetClassGroup"
  19876. });
  19877. Items.unregisterSheet("core", ItemSheet);
  19878. Items.registerSheet("dnd5e", ItemSheet5e, {
  19879. makeDefault: true,
  19880. label: "DND5E.SheetClassItem"
  19881. });
  19882. DocumentSheetConfig.registerSheet(JournalEntryPage, "dnd5e", JournalClassPageSheet, {
  19883. label: "DND5E.SheetClassClassSummary",
  19884. types: ["class"]
  19885. });
  19886. // Preload Handlebars helpers & partials
  19887. registerHandlebarsHelpers();
  19888. preloadHandlebarsTemplates();
  19889. });
  19890. /**
  19891. * Determine if this is a 'legacy' world with permissive validation, or one where strict validation is enabled.
  19892. * @internal
  19893. */
  19894. function _determineValidationStrictness() {
  19895. SystemDataModel._enableV10Validation = game.settings.get("dnd5e", "strictValidation");
  19896. }
  19897. /**
  19898. * Update the world's validation strictness setting based on whether validation errors were encountered.
  19899. * @internal
  19900. */
  19901. async function _configureValidationStrictness() {
  19902. if ( !game.user.isGM ) return;
  19903. const invalidDocuments = game.actors.invalidDocumentIds.size + game.items.invalidDocumentIds.size
  19904. + game.scenes.invalidDocumentIds.size;
  19905. const strictValidation = game.settings.get("dnd5e", "strictValidation");
  19906. if ( invalidDocuments && strictValidation ) {
  19907. await game.settings.set("dnd5e", "strictValidation", false);
  19908. game.socket.emit("reload");
  19909. foundry.utils.debouncedReload();
  19910. }
  19911. }
  19912. /**
  19913. * Configure explicit lists of attributes that are trackable on the token HUD and in the combat tracker.
  19914. * @internal
  19915. */
  19916. function _configureTrackableAttributes() {
  19917. const common = {
  19918. bar: [],
  19919. value: [
  19920. ...Object.keys(DND5E.abilities).map(ability => `abilities.${ability}.value`),
  19921. ...Object.keys(DND5E.movementTypes).map(movement => `attributes.movement.${movement}`),
  19922. "attributes.ac.value", "attributes.init.total"
  19923. ]
  19924. };
  19925. const creature = {
  19926. bar: [...common.bar, "attributes.hp"],
  19927. value: [
  19928. ...common.value,
  19929. ...Object.keys(DND5E.skills).map(skill => `skills.${skill}.passive`),
  19930. ...Object.keys(DND5E.senses).map(sense => `attributes.senses.${sense}`),
  19931. "attributes.spelldc"
  19932. ]
  19933. };
  19934. CONFIG.Actor.trackableAttributes = {
  19935. character: {
  19936. bar: [...creature.bar, "resources.primary", "resources.secondary", "resources.tertiary", "details.xp"],
  19937. value: [...creature.value]
  19938. },
  19939. npc: {
  19940. bar: [...creature.bar, "resources.legact", "resources.legres"],
  19941. value: [...creature.value, "details.cr", "details.spellLevel", "details.xp.value"]
  19942. },
  19943. vehicle: {
  19944. bar: [...common.bar, "attributes.hp"],
  19945. value: [...common.value]
  19946. },
  19947. group: {
  19948. bar: [],
  19949. value: []
  19950. }
  19951. };
  19952. }
  19953. /* -------------------------------------------- */
  19954. /* Foundry VTT Setup */
  19955. /* -------------------------------------------- */
  19956. /**
  19957. * Prepare attribute lists.
  19958. */
  19959. Hooks.once("setup", function() {
  19960. CONFIG.DND5E.trackableAttributes = expandAttributeList(CONFIG.DND5E.trackableAttributes);
  19961. CONFIG.DND5E.consumableResources = expandAttributeList(CONFIG.DND5E.consumableResources);
  19962. game.dnd5e.moduleArt.registerModuleArt();
  19963. // Apply custom compendium styles to the SRD rules compendium.
  19964. if ( !game.dnd5e.isV10 ) {
  19965. const rules = game.packs.get("dnd5e.rules");
  19966. rules.applicationClass = SRDCompendium;
  19967. }
  19968. });
  19969. /* --------------------------------------------- */
  19970. /**
  19971. * Expand a list of attribute paths into an object that can be traversed.
  19972. * @param {string[]} attributes The initial attributes configuration.
  19973. * @returns {object} The expanded object structure.
  19974. */
  19975. function expandAttributeList(attributes) {
  19976. return attributes.reduce((obj, attr) => {
  19977. foundry.utils.setProperty(obj, attr, true);
  19978. return obj;
  19979. }, {});
  19980. }
  19981. /* --------------------------------------------- */
  19982. /**
  19983. * Perform one-time pre-localization and sorting of some configuration objects
  19984. */
  19985. Hooks.once("i18nInit", () => performPreLocalization(CONFIG.DND5E));
  19986. /* -------------------------------------------- */
  19987. /* Foundry VTT Ready */
  19988. /* -------------------------------------------- */
  19989. /**
  19990. * Once the entire VTT framework is initialized, check to see if we should perform a data migration
  19991. */
  19992. Hooks.once("ready", function() {
  19993. if ( game.dnd5e.isV10 ) {
  19994. // Configure validation strictness.
  19995. _configureValidationStrictness();
  19996. // Apply custom compendium styles to the SRD rules compendium.
  19997. const rules = game.packs.get("dnd5e.rules");
  19998. rules.apps = [new SRDCompendium(rules)];
  19999. }
  20000. // Wait to register hotbar drop hook on ready so that modules could register earlier if they want to
  20001. Hooks.on("hotbarDrop", (bar, data, slot) => {
  20002. if ( ["Item", "ActiveEffect"].includes(data.type) ) {
  20003. create5eMacro(data, slot);
  20004. return false;
  20005. }
  20006. });
  20007. // Determine whether a system migration is required and feasible
  20008. if ( !game.user.isGM ) return;
  20009. const cv = game.settings.get("dnd5e", "systemMigrationVersion") || game.world.flags.dnd5e?.version;
  20010. const totalDocuments = game.actors.size + game.scenes.size + game.items.size;
  20011. if ( !cv && totalDocuments === 0 ) return game.settings.set("dnd5e", "systemMigrationVersion", game.system.version);
  20012. if ( cv && !isNewerVersion(game.system.flags.needsMigrationVersion, cv) ) return;
  20013. // Perform the migration
  20014. if ( cv && isNewerVersion(game.system.flags.compatibleMigrationVersion, cv) ) {
  20015. ui.notifications.error(game.i18n.localize("MIGRATION.5eVersionTooOldWarning"), {permanent: true});
  20016. }
  20017. migrateWorld();
  20018. });
  20019. /* -------------------------------------------- */
  20020. /* Canvas Initialization */
  20021. /* -------------------------------------------- */
  20022. Hooks.on("canvasInit", gameCanvas => {
  20023. gameCanvas.grid.diagonalRule = game.settings.get("dnd5e", "diagonalMovement");
  20024. SquareGrid.prototype.measureDistances = measureDistances;
  20025. });
  20026. /* -------------------------------------------- */
  20027. /* Other Hooks */
  20028. /* -------------------------------------------- */
  20029. Hooks.on("renderChatMessage", onRenderChatMessage);
  20030. Hooks.on("getChatLogEntryContext", addChatMessageContextOptions);
  20031. Hooks.on("renderChatLog", (app, html, data) => Item5e.chatListeners(html));
  20032. Hooks.on("renderChatPopout", (app, html, data) => Item5e.chatListeners(html));
  20033. Hooks.on("getActorDirectoryEntryContext", Actor5e.addDirectoryContextOptions);
  20034. export { DND5E, applications, canvas$1 as canvas, dataModels, dice, documents, migrations, utils };
  20035. //# sourceMappingURL=dnd5e-compiled.mjs.map