From 80be7f9944f16ab9cb11529eddedd095be2c5800 Mon Sep 17 00:00:00 2001 From: Jeff Hitchcock Date: Thu, 12 Dec 2024 12:21:28 -0800 Subject: [PATCH 1/2] [#1982] Add support for ranged communication like telepathy Adds `communication` data to the languages data model that tracks distances and units for features like telepathy. Also includes a large refactor of how trait data models are constructed to match the custom field pattern the system has adopted elsewhere. Closes #1982 --- dnd5e.mjs | 2 +- lang/en.json | 71 +++++++------ module/applications/actor/_module.mjs | 1 + module/applications/actor/base-sheet.mjs | 15 ++- .../actor/config/languages-config.mjs | 59 +++++++++++ module/applications/actor/sheet-v2-mixin.mjs | 8 ++ module/config.mjs | 71 +++++++------ module/data/actor/character.mjs | 21 ++-- .../data/actor/fields/damage-trait-field.mjs | 22 +++++ .../data/actor/fields/simple-trait-field.mjs | 24 +++++ module/data/actor/templates/traits.mjs | 99 ++++++++++--------- module/data/actor/vehicle.mjs | 6 +- templates/actors/config/languages-config.hbs | 26 +++++ 13 files changed, 304 insertions(+), 121 deletions(-) create mode 100644 module/applications/actor/config/languages-config.mjs create mode 100644 module/data/actor/fields/damage-trait-field.mjs create mode 100644 module/data/actor/fields/simple-trait-field.mjs create mode 100644 templates/actors/config/languages-config.hbs diff --git a/dnd5e.mjs b/dnd5e.mjs index 17d1c31fc3..a706bc7f18 100644 --- a/dnd5e.mjs +++ b/dnd5e.mjs @@ -445,7 +445,7 @@ Hooks.once("i18nInit", () => { DND5E: { "Feature.Species": game.i18n.localize("DND5E.Feature.SpeciesLegacy"), FlagsAlertHint: game.i18n.localize("DND5E.FlagsAlertHintLegacy"), - LanguagesExotic: game.i18n.localize("DND5E.LanguagesExoticLegacy"), + "Language.Category.Rare": game.i18n.localize("DND5E.Language.Category.Exotic"), LongRestHint: game.i18n.localize("DND5E.LongRestHintLegacy"), LongRestHintGroup: game.i18n.localize("DND5E.LongRestHintGroupLegacy"), "TARGET.Type.Emanation": foundry.utils.mergeObject( diff --git a/lang/en.json b/lang/en.json index 1512f7d36d..a8411562a7 100644 --- a/lang/en.json +++ b/lang/en.json @@ -2328,36 +2328,49 @@ "DND5E.LairAct": "Uses Lair Action", "DND5E.LairActionLabel": "Lair Action", "DND5E.LairActionInitiative": "Lair Action Initiative Count", + +"DND5E.Language": { + "Label": "Language", + "Category": { + "Exotic": "Exotic Languages", + "Rare": "Rare Languages", + "Standard": "Standard Languages" + }, + "Communication": { + "Label": "Ranged Communication", + "Telepathy": "Telepathy" + }, + "Dialect": { + "Aarakocra": "Aarakocra", + "Abyssal": "Abyssal", + "Aquan": "Aquan", + "Auran": "Auran", + "Celestial": "Celestial", + "Common": "Common", + "CommonSign": "Common Sign Language", + "DeepSpeech": "Deep Speech", + "Draconic": "Draconic", + "Druidic": "Druidic", + "Dwarvish": "Dwarvish", + "Elvish": "Elvish", + "Giant": "Giant", + "Gith": "Gith", + "Gnoll": "Gnoll", + "Gnomish": "Gnomish", + "Goblin": "Goblin", + "Halfling": "Halfling", + "Ignan": "Ignan", + "Infernal": "Infernal", + "Orc": "Orc", + "Primordial": "Primordial", + "Sylvan": "Sylvan", + "Terran": "Terran", + "ThievesCant": "Thieves' Cant", + "Undercommon": "Undercommon" + } +}, "DND5E.Languages": "Languages", -"DND5E.LanguagesAarakocra": "Aarakocra", -"DND5E.LanguagesAbyssal": "Abyssal", -"DND5E.LanguagesAquan": "Aquan", -"DND5E.LanguagesAuran": "Auran", -"DND5E.LanguagesCelestial": "Celestial", -"DND5E.LanguagesCommon": "Common", -"DND5E.LanguagesCommonSign": "Common Sign Language", -"DND5E.LanguagesDeepSpeech": "Deep Speech", -"DND5E.LanguagesDraconic": "Draconic", -"DND5E.LanguagesDruidic": "Druidic", -"DND5E.LanguagesDwarvish": "Dwarvish", -"DND5E.LanguagesElvish": "Elvish", -"DND5E.LanguagesExotic": "Rare Languages", -"DND5E.LanguagesExoticLegacy": "Exotic Languages", -"DND5E.LanguagesGiant": "Giant", -"DND5E.LanguagesGith": "Gith", -"DND5E.LanguagesGnoll": "Gnoll", -"DND5E.LanguagesGnomish": "Gnomish", -"DND5E.LanguagesGoblin": "Goblin", -"DND5E.LanguagesHalfling": "Halfling", -"DND5E.LanguagesIgnan": "Ignan", -"DND5E.LanguagesInfernal": "Infernal", -"DND5E.LanguagesOrc": "Orc", -"DND5E.LanguagesPrimordial": "Primordial", -"DND5E.LanguagesStandard": "Standard Languages", -"DND5E.LanguagesSylvan": "Sylvan", -"DND5E.LanguagesTerran": "Terran", -"DND5E.LanguagesThievesCant": "Thieves' Cant", -"DND5E.LanguagesUndercommon": "Undercommon", + "DND5E.LegAct": "Legendary Actions", "DND5E.LegActN.one": "1st Legendary Action", "DND5E.LegActN.two": "2nd Legendary Action", diff --git a/module/applications/actor/_module.mjs b/module/applications/actor/_module.mjs index d15be80345..719895eb86 100644 --- a/module/applications/actor/_module.mjs +++ b/module/applications/actor/_module.mjs @@ -23,6 +23,7 @@ export {default as DamagesConfig} from "./config/damages-config.mjs"; export {default as HitDiceConfig} from "./config/hit-dice-config.mjs"; export {default as HitPointsConfig} from "./config/hit-points-config.mjs"; export {default as InitiativeConfig} from "./config/initiative-config.mjs"; +export {default as LanguagesConfig} from "./config/languages-config.mjs"; export {default as SkillToolConfig} from "./config/skill-tool-config.mjs"; export {default as SkillsConfig} from "./config/skills-config.mjs"; export {default as SpellSlotsConfig} from "./config/spell-slots-config.mjs"; diff --git a/module/applications/actor/base-sheet.mjs b/module/applications/actor/base-sheet.mjs index c624a23fd0..6a0ca9795e 100644 --- a/module/applications/actor/base-sheet.mjs +++ b/module/applications/actor/base-sheet.mjs @@ -20,6 +20,7 @@ import DamagesConfig from "./config/damages-config.mjs"; import HitDiceConfig from "./config/hit-dice-config.mjs"; import HitPointsConfig from "./config/hit-points-config.mjs"; import InitiativeConfig from "./config/initiative-config.mjs"; +import LanguagesConfig from "./config/languages-config.mjs"; import SkillToolConfig from "./config/skill-tool-config.mjs"; import SkillsConfig from "./config/skills-config.mjs"; import SpellSlotsConfig from "./config/spell-slots-config.mjs"; @@ -1239,10 +1240,16 @@ export default class ActorSheet5e extends ActorSheetMixin(ActorSheet) { event.preventDefault(); const trait = event.currentTarget.dataset.trait; const options = { document: this.actor, trait }; - if ( trait === "tool" ) return new ToolsConfig(options).render({ force: true }); - else if ( ["dr", "di", "dv", "dm"].includes(trait) ) return new DamagesConfig(options).render({ force: true }); - else if ( trait === "weapon" ) return new WeaponsConfig(options).render({ force: true }); - return new TraitsConfig(options).render({ force: true }); + switch ( trait ) { + case "di": + case "dm": + case "dr": + case "dv": return new DamagesConfig(options).render({ force: true }); + case "languages": return new LanguagesConfig(options).render({ force: true }); + case "tool": return new ToolsConfig(options).render({ force: true }); + case "weapon": return new WeaponsConfig(options).render({ force: true }); + default: return new TraitsConfig(options).render({ force: true }); + } } /* -------------------------------------------- */ diff --git a/module/applications/actor/config/languages-config.mjs b/module/applications/actor/config/languages-config.mjs new file mode 100644 index 0000000000..5afa0378db --- /dev/null +++ b/module/applications/actor/config/languages-config.mjs @@ -0,0 +1,59 @@ +import TraitsConfig from "./traits-config.mjs"; + +/** + * Configuration application for languages. + */ +export default class LanguagesConfig extends TraitsConfig { + /** @override */ + static DEFAULT_OPTIONS = { + classes: ["languages"], + trait: "languages" + }; + + /* -------------------------------------------- */ + + /** @override */ + static PARTS = { + traits: { + template: "systems/dnd5e/templates/actors/config/languages-config.hbs" + } + }; + + /* -------------------------------------------- */ + /* Properties */ + /* -------------------------------------------- */ + + /** @override */ + get title() { + return game.i18n.localize("DND5E.Languages"); + } + + /* -------------------------------------------- */ + /* Rendering */ + /* -------------------------------------------- */ + + /** @inheritDoc */ + async _preparePartContext(partId, context, options) { + context = await super._preparePartContext(partId, context, options); + + const unitOptions = Object.entries(CONFIG.DND5E.movementUnits).map(([value, { label }]) => ({ value, label })); + context.communication = Object.entries(CONFIG.DND5E.communicationTypes).map(([key, { label }]) => ({ + label, unitOptions, + data: context.data.communication[key], + fields: context.fields.communication.model.fields, + keyPath: `${context.keyPath}.communication.${key}` + })); + + return context; + } + + /* -------------------------------------------- */ + /* Form Submission */ + /* -------------------------------------------- */ + + /** @inheritDoc */ + _processFormData(event, form, formData) { + const submitData = super._processFormData(event, form, formData); + return submitData; + } +} diff --git a/module/applications/actor/sheet-v2-mixin.mjs b/module/applications/actor/sheet-v2-mixin.mjs index 5a01f86d8f..9de5791d20 100644 --- a/module/applications/actor/sheet-v2-mixin.mjs +++ b/module/applications/actor/sheet-v2-mixin.mjs @@ -260,6 +260,14 @@ export default function ActorSheetV2Mixin(Base) { if ( values.length ) traits.dm = values; } + // Display ranged communication + for ( const [key, { label }] of Object.entries(CONFIG.DND5E.communicationTypes) ) { + const data = this.actor.system.traits?.languages?.communication?.[key]; + if ( !data?.value ) return; + traits.languages ??= []; + traits.languages.push({ label: `${label} ${formatDistance(data.value, data.units)}` }); + } + // Display weapon masteries for ( const key of this.actor.system.traits?.weaponProf?.mastery?.value ?? [] ) { let value = traits.weapon?.find(w => w.key === key); diff --git a/module/config.mjs b/module/config.mjs index 2087869374..c5844e81cd 100644 --- a/module/config.mjs +++ b/module/config.mjs @@ -3657,47 +3657,47 @@ DND5E.bloodied = { /** * Languages a character can learn. - * @enum {string} + * @enum {object} */ DND5E.languages = { standard: { - label: "DND5E.LanguagesStandard", + label: "DND5E.Language.Category.Standard", children: { - common: "DND5E.LanguagesCommon", - draconic: "DND5E.LanguagesDraconic", - dwarvish: "DND5E.LanguagesDwarvish", - elvish: "DND5E.LanguagesElvish", - giant: "DND5E.LanguagesGiant", - gnomish: "DND5E.LanguagesGnomish", - goblin: "DND5E.LanguagesGoblin", - halfling: "DND5E.LanguagesHalfling", - orc: "DND5E.LanguagesOrc", - sign: "DND5E.LanguagesCommonSign" + common: "DND5E.Language.Dialect.Common", + draconic: "DND5E.Language.Dialect.Draconic", + dwarvish: "DND5E.Language.Dialect.Dwarvish", + elvish: "DND5E.Language.Dialect.Elvish", + giant: "DND5E.Language.Dialect.Giant", + gnomish: "DND5E.Language.Dialect.Gnomish", + goblin: "DND5E.Language.Dialect.Goblin", + halfling: "DND5E.Language.Dialect.Halfling", + orc: "DND5E.Language.Dialect.Orc", + sign: "DND5E.Language.Dialect.CommonSign" } }, exotic: { - label: "DND5E.LanguagesExotic", + label: "DND5E.Language.Category.Rare", children: { - aarakocra: "DND5E.LanguagesAarakocra", - abyssal: "DND5E.LanguagesAbyssal", - cant: "DND5E.LanguagesThievesCant", - celestial: "DND5E.LanguagesCelestial", - deep: "DND5E.LanguagesDeepSpeech", - druidic: "DND5E.LanguagesDruidic", - gith: "DND5E.LanguagesGith", - gnoll: "DND5E.LanguagesGnoll", - infernal: "DND5E.LanguagesInfernal", + aarakocra: "DND5E.Language.Dialect.Aarakocra", + abyssal: "DND5E.Language.Dialect.Abyssal", + cant: "DND5E.Language.Dialect.ThievesCant", + celestial: "DND5E.Language.Dialect.Celestial", + deep: "DND5E.Language.Dialect.DeepSpeech", + druidic: "DND5E.Language.Dialect.Druidic", + gith: "DND5E.Language.Dialect.Gith", + gnoll: "DND5E.Language.Dialect.Gnoll", + infernal: "DND5E.Language.Dialect.Infernal", primordial: { - label: "DND5E.LanguagesPrimordial", + label: "DND5E.Language.Dialect.Primordial", children: { - aquan: "DND5E.LanguagesAquan", - auran: "DND5E.LanguagesAuran", - ignan: "DND5E.LanguagesIgnan", - terran: "DND5E.LanguagesTerran" + aquan: "DND5E.Language.Dialect.Aquan", + auran: "DND5E.Language.Dialect.Auran", + ignan: "DND5E.Language.Dialect.Ignan", + terran: "DND5E.Language.Dialect.Terran" } }, - sylvan: "DND5E.LanguagesSylvan", - undercommon: "DND5E.LanguagesUndercommon" + sylvan: "DND5E.Language.Dialect.Sylvan", + undercommon: "DND5E.Language.Dialect.Undercommon" } } }; @@ -3708,6 +3708,19 @@ preLocalize("languages.exotic.children.primordial.children", { sort: true }); /* -------------------------------------------- */ +/** + * Communication types that take ranges such as telepathy. + * @enum {{ label: string }} + */ +DND5E.communicationTypes = { + telepathy: { + label: "DND5E.Language.Communication.Telepathy" + } +}; +preLocalize("communicationTypes", { key: "label" }); + +/* -------------------------------------------- */ + /** * Maximum allowed character level. * @type {number} diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index ff6af4dc99..ab89a681ee 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -5,6 +5,7 @@ import FormulaField from "../fields/formula-field.mjs"; import LocalDocumentField from "../fields/local-document-field.mjs"; import CreatureTypeField from "../shared/creature-type-field.mjs"; import RollConfigField from "../shared/roll-config-field.mjs"; +import SimpleTraitField from "./fields/simple-trait-field.mjs"; import AttributesFields from "./templates/attributes.mjs"; import CreatureTemplate from "./templates/creature.mjs"; import DetailsFields from "./templates/details.mjs"; @@ -14,6 +15,10 @@ const { ArrayField, BooleanField, HTMLField, IntegerSortField, NumberField, SchemaField, SetField, StringField } = foundry.data.fields; +/** + * @typedef {import("./fields/simple-trait.mjs").SimpleTraitData} SimpleTraitData + */ + /** * @typedef {object} ActorFavorites5e * @property {"activity"|"effect"|"item"|"skill"|"slots"|"tool"} type The favorite type. @@ -152,15 +157,13 @@ export default class CharacterData extends CreatureTemplate { traits: new SchemaField({ ...TraitsFields.common, ...TraitsFields.creature, - weaponProf: TraitsFields.makeSimpleTrait({ label: "DND5E.TraitWeaponProf" }, { - extraFields: { - mastery: new SchemaField({ - value: new SetField(new StringField()), - bonus: new SetField(new StringField()) - }) - } - }), - armorProf: TraitsFields.makeSimpleTrait({ label: "DND5E.TraitArmorProf" }) + weaponProf: new SimpleTraitField({ + mastery: new SchemaField({ + value: new SetField(new StringField()), + bonus: new SetField(new StringField()) + }) + }, { label: "DND5E.TraitWeaponProf" }), + armorProf: new SimpleTraitField({}, { label: "DND5E.TraitArmorProf" }) }, { label: "DND5E.Traits" }), resources: new SchemaField({ primary: makeResourceField({ label: "DND5E.ResourcePrimary" }), diff --git a/module/data/actor/fields/damage-trait-field.mjs b/module/data/actor/fields/damage-trait-field.mjs new file mode 100644 index 0000000000..4714ffc670 --- /dev/null +++ b/module/data/actor/fields/damage-trait-field.mjs @@ -0,0 +1,22 @@ +import SimpleTraitField from "./simple-trait-field.mjs"; +const { SetField, StringField } = foundry.data.fields; + +/** + * Data structure for a damage actor trait. + * + * @typedef {import("./simple-trait.mjs").SimpleTraitData} DamageTraitData + * @property {Set} bypasses Keys for physical weapon properties that cause resistances to be bypassed. + */ + +/** + * Field for storing damage resistances, immunities, and vulnerabilities data. + */ +export default class DamageTraitField extends SimpleTraitField { + constructor(fields={}, { initialBypasses=[], ...options }={}) { + super({ + bypasses: new SetField(new StringField(), { + label: "DND5E.DamagePhysicalBypass", hint: "DND5E.DamagePhysicalBypassHint", initial: initialBypasses + }) + }, options); + } +} diff --git a/module/data/actor/fields/simple-trait-field.mjs b/module/data/actor/fields/simple-trait-field.mjs new file mode 100644 index 0000000000..b4669c9992 --- /dev/null +++ b/module/data/actor/fields/simple-trait-field.mjs @@ -0,0 +1,24 @@ +const { SchemaField, SetField, StringField } = foundry.data.fields; + +/** + * Data structure for a standard actor trait. + * + * @typedef {object} SimpleTraitData + * @property {Set} value Keys for currently selected traits. + * @property {string} custom Semicolon-separated list of custom traits. + */ + +/** + * Field for storing standard trait data. + */ +export default class SimpleTraitField extends SchemaField { + constructor(fields={}, { initialValue=[], ...options }={}) { + fields = { + value: new SetField(new StringField(), { label: "DND5E.TraitsChosen", initial: initialValue }), + custom: new StringField({ required: true, label: "DND5E.Special" }), + ...fields + }; + Object.entries(fields).forEach(([k, v]) => !v ? delete fields[k] : null); + super(fields, options); + } +} diff --git a/module/data/actor/templates/traits.mjs b/module/data/actor/templates/traits.mjs index e1549eeb22..81296e725e 100644 --- a/module/data/actor/templates/traits.mjs +++ b/module/data/actor/templates/traits.mjs @@ -1,36 +1,39 @@ +import { defaultUnits } from "../../../utils.mjs"; import FormulaField from "../../fields/formula-field.mjs"; import MappingField from "../../fields/mapping-field.mjs"; +import DamageTraitField from "../fields/damage-trait-field.mjs"; +import SimpleTraitField from "../fields/simple-trait-field.mjs"; -const { SchemaField, SetField, StringField } = foundry.data.fields; +const { NumberField, SchemaField, SetField, StringField } = foundry.data.fields; /** - * Shared contents of the traits schema between various actor types. + * @typedef {import("../fields/damage-trait.mjs").DamageTraitData} DamageTraitData + * @typedef {import("../fields/simple-trait.mjs").SimpleTraitData} SimpleTraitData */ -export default class TraitsField { - /** - * Data structure for a standard actor trait. - * - * @typedef {object} SimpleTraitData - * @property {Set} value Keys for currently selected traits. - * @property {string} custom Semicolon-separated list of custom traits. - */ - /** - * Data structure for a damage actor trait. - * - * @typedef {object} DamageTraitData - * @property {Set} value Keys for currently selected traits. - * @property {Set} bypasses Keys for physical weapon properties that cause resistances to be bypassed. - * @property {string} custom Semicolon-separated list of custom traits. - */ +/** + * Data structure for a damage actor trait. + * + * @typedef {object} DamageModificationData + * @property {Record} amount Damage boost or reduction by damage type. + * @property {Set} bypasses Keys for physical properties that cause modification to be bypassed. + */ - /** - * Data structure for a damage actor trait. - * - * @typedef {object} DamageModificationData - * @property {{[key: string]: string}} amount Damage boost or reduction by damage type. - * @property {Set} bypasses Keys for physical properties that cause modification to be bypassed. - */ +/** + * @typedef {SimpleTraitData} LanguageTraitData + * @property {Record} communication Measured communication ranges (e.g. telepathy). + */ + +/** + * @typedef {object} LanguageCommunicationData + * @property {string} units Units used to measure range. + * @property {number} value Range to which this ability can be used. + */ + +/** + * Shared contents of the traits schema between various actor types. + */ +export default class TraitsField { /* -------------------------------------------- */ @@ -47,17 +50,17 @@ export default class TraitsField { */ static get common() { return { - size: new StringField({required: true, initial: "med", label: "DND5E.Size"}), - di: this.makeDamageTrait({label: "DND5E.DamImm"}), - dr: this.makeDamageTrait({label: "DND5E.DamRes"}), - dv: this.makeDamageTrait({label: "DND5E.DamVuln"}), + size: new StringField({ required: true, initial: "med", label: "DND5E.Size" }), + di: new DamageTraitField({}, { label: "DND5E.DamImm" }), + dr: new DamageTraitField({}, { label: "DND5E.DamRes" }), + dv: new DamageTraitField({}, { label: "DND5E.DamVuln" }), dm: new SchemaField({ - amount: new MappingField(new FormulaField({deterministic: true}), {label: "DND5E.DamMod"}), + amount: new MappingField(new FormulaField({ deterministic: true }), { label: "DND5E.DamMod" }), bypasses: new SetField(new StringField(), { label: "DND5E.DamagePhysicalBypass", hint: "DND5E.DamagePhysicalBypassHint" }) }), - ci: this.makeSimpleTrait({label: "DND5E.ConImm"}) + ci: new SimpleTraitField({}, { label: "DND5E.ConImm" }) }; } @@ -71,7 +74,12 @@ export default class TraitsField { */ static get creature() { return { - languages: this.makeSimpleTrait({label: "DND5E.Languages"}) + languages: new SimpleTraitField({ + communication: new MappingField(new SchemaField({ + units: new StringField({ initial: () => defaultUnits("length") }), + value: new NumberField({ min: 0 }) + })) + }, { label: "DND5E.Languages" }) }; } @@ -83,16 +91,14 @@ export default class TraitsField { * @param {object} [options={}] * @param {string[]} [options.initial={}] The initial value for the value set. * @param {object} [options.extraFields={}] Additional fields added to schema. - * @returns {SchemaField} + * @returns {SimpleTraitField} */ static makeSimpleTrait(schemaOptions={}, {initial=[], extraFields={}}={}) { - return new SchemaField({ - ...extraFields, - value: new SetField( - new StringField(), {label: "DND5E.TraitsChosen", initial} - ), - custom: new StringField({required: true, label: "DND5E.Special"}) - }, schemaOptions); + foundry.utils.logCompatibilityWarning( + "The `makeSimpleTrait` method on `TraitsField` has been deprecated and replaced with `SimpleTraitField`.", + { since: "DnD5e 4.2", until: "DnD5e 4.4" } + ); + return new SimpleTraitField(extraFields, { initialValue: initial, ...schemaOptions }); } /* -------------------------------------------- */ @@ -103,15 +109,14 @@ export default class TraitsField { * @param {object} [options={}] * @param {string[]} [options.initial={}] The initial value for the value set. * @param {object} [options.extraFields={}] Additional fields added to schema. - * @returns {SchemaField} + * @returns {DamageTraitField} */ static makeDamageTrait(schemaOptions={}, {initial=[], initialBypasses=[], extraFields={}}={}) { - return this.makeSimpleTrait(schemaOptions, {initial, extraFields: { - ...extraFields, - bypasses: new SetField(new StringField(), { - label: "DND5E.DamagePhysicalBypass", hint: "DND5E.DamagePhysicalBypassHint", initial: initialBypasses - }) - }}); + foundry.utils.logCompatibilityWarning( + "The `makeDamageTrait` method on `TraitsField` has been deprecated and replaced with `DamageTraitField`.", + { since: "DnD5e 4.2", until: "DnD5e 4.4" } + ); + return new DamageTraitField(extraFields, { initialValue: initial, initialBypasses, ...schemaOptions }); } /* -------------------------------------------- */ diff --git a/module/data/actor/vehicle.mjs b/module/data/actor/vehicle.mjs index 16a4888d3e..69da50019e 100644 --- a/module/data/actor/vehicle.mjs +++ b/module/data/actor/vehicle.mjs @@ -1,5 +1,7 @@ import FormulaField from "../fields/formula-field.mjs"; import SourceField from "../shared/source-field.mjs"; +import DamageTraitField from "./fields/damage-trait-field.mjs"; +import SimpleTraitField from "./fields/simple-trait-field.mjs"; import AttributesFields from "./templates/attributes.mjs"; import CommonTemplate from "./templates/common.mjs"; import DetailsFields from "./templates/details.mjs"; @@ -117,8 +119,8 @@ export default class VehicleData extends CommonTemplate { traits: new SchemaField({ ...TraitsFields.common, size: new StringField({ required: true, initial: "lg", label: "DND5E.Size" }), - di: TraitsFields.makeDamageTrait({ label: "DND5E.DamImm" }, { initial: ["poison", "psychic"] }), - ci: TraitsFields.makeSimpleTrait({ label: "DND5E.ConImm" }, { initial: [ + di: new DamageTraitField({}, { label: "DND5E.DamImm", initialValue: ["poison", "psychic"] }), + ci: new SimpleTraitField({}, { label: "DND5E.ConImm", initialValue: [ "blinded", "charmed", "deafened", "frightened", "paralyzed", "petrified", "poisoned", "stunned", "unconscious" ] }), diff --git a/templates/actors/config/languages-config.hbs b/templates/actors/config/languages-config.hbs new file mode 100644 index 0000000000..dcd0261a9d --- /dev/null +++ b/templates/actors/config/languages-config.hbs @@ -0,0 +1,26 @@ +
+ {{#each choices}} +
+ {{ label }} + {{> "dnd5e.traits-list" choices=children keyPath=(concat @root.keyPath ".value.") topLevel=true + field=@root.checkbox input=@root.inputs.createCheckboxInput }} +
+ {{/each}} +
+ {{ localize "DND5E.Language.Communication.Label" }} + {{#each communication}} +
+ +
+ {{ formInput fields.value name=(concat keyPath ".value") value=data.value }} + {{ formInput fields.units name=(concat keyPath ".units") value=data.units options=unitOptions }} +
+
+ {{/each}} +
+
+ {{ localize "DND5E.Special" }} + {{ formInput fields.custom value=data.custom }} +

{{ localize "DND5E.SpecialHint" }}

+
+
From 094fcc473881fc96c046807fd111433ec223cf91 Mon Sep 17 00:00:00 2001 From: Jeff Hitchcock Date: Fri, 13 Dec 2024 11:04:48 -0800 Subject: [PATCH 2/2] [#1982] Add communication to embedded stat blocks --- module/data/actor/npc.mjs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/module/data/actor/npc.mjs b/module/data/actor/npc.mjs index 901d621312..80a4a2ee95 100644 --- a/module/data/actor/npc.mjs +++ b/module/data/actor/npc.mjs @@ -1,7 +1,7 @@ import Actor5e from "../../documents/actor/actor.mjs"; import Proficiency from "../../documents/actor/proficiency.mjs"; import * as Trait from "../../documents/actor/trait.mjs"; -import { defaultUnits, formatCR, formatNumber, splitSemicolons } from "../../utils.mjs"; +import { defaultUnits, formatCR, formatDistance, formatNumber, splitSemicolons } from "../../utils.mjs"; import FormulaField from "../fields/formula-field.mjs"; import CreatureTypeField from "../shared/creature-type-field.mjs"; import RollConfigField from "../shared/roll-config-field.mjs"; @@ -450,10 +450,8 @@ export default class NPCData extends CreatureTemplate { */ async _prepareEmbedContext() { const formatter = game.i18n.getListFormatter({ type: "unit" }); - const prepareMeasured = (value, units, label) => { - value = `${formatNumber(value)} ${units}.`; - return label ? `${label} ${value}` : value; - }; + const prepareMeasured = (value, units, label) => label ? `${label} ${formatDistance(value, units)}` + : formatDistance(value, units); const prepareTrait = ({ value, custom }, trait) => formatter.format([ ...Array.from(value).map(t => Trait.keyLabel(t, { trait })).filter(_ => _), ...splitSemicolons(custom ?? "") @@ -501,7 +499,13 @@ export default class NPCData extends CreatureTemplate { formatNumber(this.attributes.init.score)})`, // Languages (e.g. `Common, Draconic`) - languages: prepareTrait(this.traits.languages, "languages") || game.i18n.localize("None"), + languages: [ + prepareTrait(this.traits.languages, "languages"), + ...Object.entries(CONFIG.DND5E.communicationTypes).map(([key, { label }]) => { + const data = this.traits.languages.communication[key]; + return data?.value ? `${label} ${formatDistance(data.value, data.units)}`.toLowerCase() : null; + }).filter(_ => _) + ].join("; ") || game.i18n.localize("None"), // Senses (e.g. `Blindsight 60 ft., Darkvision 120 ft.; Passive Perception 27`) senses: [