From 0c9da9b746ae080415f1a52c7d32fd3078819408 Mon Sep 17 00:00:00 2001 From: Jeff Hitchcock Date: Mon, 23 Oct 2023 12:28:24 -0700 Subject: [PATCH] [#342] Turn senses into full object, rework how config apps are called --- module/applications/actor/movement-config.mjs | 13 ++- module/applications/actor/senses-config.mjs | 28 +++---- module/applications/actor/type-config.mjs | 17 +--- module/applications/item/item-sheet.mjs | 8 +- module/data/actor/npc.mjs | 8 +- module/data/actor/templates/attributes.mjs | 39 +-------- module/data/item/race.mjs | 84 +++++++++++-------- module/data/shared/_module.mjs | 3 + module/data/shared/creature-type-field.mjs | 16 ++++ module/data/shared/movement-field.mjs | 20 +++++ module/data/shared/senses-field.mjs | 19 +++++ template.json | 19 +---- templates/apps/senses-config.hbs | 8 +- templates/items/race.hbs | 21 +++-- 14 files changed, 163 insertions(+), 140 deletions(-) create mode 100644 module/data/shared/creature-type-field.mjs create mode 100644 module/data/shared/movement-field.mjs create mode 100644 module/data/shared/senses-field.mjs diff --git a/module/applications/actor/movement-config.mjs b/module/applications/actor/movement-config.mjs index 49cc06838f..84b8a5b4a1 100644 --- a/module/applications/actor/movement-config.mjs +++ b/module/applications/actor/movement-config.mjs @@ -12,7 +12,8 @@ export default class ActorMovementConfig extends BaseConfigSheet { template: "systems/dnd5e/templates/apps/movement-config.hbs", width: 300, height: "auto", - sheetConfig: false + sheetConfig: false, + keyPath: "system.attributes.movement" }); } @@ -28,11 +29,9 @@ export default class ActorMovementConfig extends BaseConfigSheet { /** @inheritdoc */ getData(options={}) { const source = this.document.toObject(); - const isActor = this.document instanceof Actor; - const keyPath = isActor ? "system.attributes.movement" : "system.movement"; // Current movement values - const movement = foundry.utils.getProperty(source, keyPath) ?? {}; + const movement = foundry.utils.getProperty(source, this.options.keyPath) ?? {}; for ( let [k, v] of Object.entries(movement) ) { if ( ["units", "hover"].includes(k) ) continue; movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : null; @@ -55,10 +54,10 @@ export default class ActorMovementConfig extends BaseConfigSheet { return { speeds, movement, - selectUnits: source.type !== "group", - canHover: isActor && (source.type !== "group"), + selectUnits: Object.hasOwn(movement, "units"), + canHover: Object.hasOwn(movement, "hover"), units: CONFIG.DND5E.movementUnits, - keyPath + keyPath: this.options.keyPath }; } } diff --git a/module/applications/actor/senses-config.mjs b/module/applications/actor/senses-config.mjs index 30e6e380b5..6493fc1a46 100644 --- a/module/applications/actor/senses-config.mjs +++ b/module/applications/actor/senses-config.mjs @@ -11,7 +11,8 @@ export default class ActorSensesConfig extends BaseConfigSheet { classes: ["dnd5e"], template: "systems/dnd5e/templates/apps/senses-config.hbs", width: 300, - height: "auto" + height: "auto", + keyPath: "system.attributes.senses" }); } @@ -26,19 +27,16 @@ export default class ActorSensesConfig extends BaseConfigSheet { /** @inheritdoc */ getData(options) { - const source = this.document.toObject().system.attributes?.senses || {}; - const data = { - senses: {}, - special: source.special ?? "", - units: source.units, movementUnits: CONFIG.DND5E.movementUnits - }; - for ( let [name, label] of Object.entries(CONFIG.DND5E.senses) ) { - const v = Number(source[name]); - data.senses[name] = { - label: game.i18n.localize(label), - value: Number.isNumeric(v) ? v.toNearest(0.1) : 0 - }; - } - return data; + const source = this.document.toObject(); + const senses = foundry.utils.getProperty(source, this.options.keyPath) ?? {}; + return foundry.utils.mergeObject(super.getData(options), { + senses: Object.entries(CONFIG.DND5E.senses).reduce((obj, [k, label]) => { + obj[k] = { label, value: senses[k] ?? 0 }; + return obj; + }, {}), + special: senses.special ?? "", + units: senses.units, movementUnits: CONFIG.DND5E.movementUnits, + keyPath: this.options.keyPath + }); } } diff --git a/module/applications/actor/type-config.mjs b/module/applications/actor/type-config.mjs index c725216729..94e53c1c8d 100644 --- a/module/applications/actor/type-config.mjs +++ b/module/applications/actor/type-config.mjs @@ -16,7 +16,8 @@ export default class ActorTypeConfig extends DocumentSheet { allowCustom: true, minimum: 0, maximum: null, - sheetConfig: false + sheetConfig: false, + keyPath: "system.details.type" }); } @@ -36,20 +37,10 @@ export default class ActorTypeConfig extends DocumentSheet { /* -------------------------------------------- */ - /** - * Key path to where the type is stored on the document. - * @type {string} - */ - get keyPath() { - return this.document instanceof Actor ? "system.details.type" : "system.type"; - } - - /* -------------------------------------------- */ - /** @inheritdoc */ getData(options={}) { // Get current value or new default - let attr = foundry.utils.getProperty(this.object, this.keyPath); + let attr = foundry.utils.getProperty(this.object, this.options.keyPath); if ( foundry.utils.getType(attr) !== "Object" ) attr = { value: (attr in CONFIG.DND5E.creatureTypes) ? attr : "humanoid", subtype: "", @@ -91,7 +82,7 @@ export default class ActorTypeConfig extends DocumentSheet { /** @override */ async _updateObject(event, formData) { const typeObject = foundry.utils.expandObject(formData); - return this.object.update({[this.keyPath]: typeObject}); + return this.object.update({[this.options.keyPath]: typeObject}); } /* -------------------------------------------- */ diff --git a/module/applications/item/item-sheet.mjs b/module/applications/item/item-sheet.mjs index 5862430026..cbc349f8a7 100644 --- a/module/applications/item/item-sheet.mjs +++ b/module/applications/item/item-sheet.mjs @@ -1,4 +1,5 @@ import ActorMovementConfig from "../actor/movement-config.mjs"; +import ActorSensesConfig from "../actor/senses-config.mjs"; import ActorTypeConfig from "../actor/type-config.mjs"; import AdvancementManager from "../advancement/advancement-manager.mjs"; import AdvancementMigrationDialog from "../advancement/advancement-migration-dialog.mjs"; @@ -503,10 +504,13 @@ export default class ItemSheet5e extends ItemSheet { let app; switch ( button.dataset.action ) { case "movement": - app = new ActorMovementConfig(this.item); + app = new ActorMovementConfig(this.item, { keyPath: "system.movement" }); + break; + case "senses": + app = new ActorSensesConfig(this.item, { keyPath: "system.senses" }); break; case "type": - app = new ActorTypeConfig(this.item); + app = new ActorTypeConfig(this.item, { keyPath: "system.type" }); break; } app?.render(true); diff --git a/module/data/actor/npc.mjs b/module/data/actor/npc.mjs index 439060865d..352773866a 100644 --- a/module/data/actor/npc.mjs +++ b/module/data/actor/npc.mjs @@ -3,6 +3,7 @@ import AttributesFields from "./templates/attributes.mjs"; import CreatureTemplate from "./templates/creature.mjs"; import DetailsFields from "./templates/details.mjs"; import TraitsFields from "./templates/traits.mjs"; +import CreatureTypeField from "../shared/creature-type-field.mjs"; /** * System data definition for NPCs. @@ -72,12 +73,7 @@ export default class NPCData extends CreatureTemplate { details: new foundry.data.fields.SchemaField({ ...DetailsFields.common, ...DetailsFields.creature, - type: new foundry.data.fields.SchemaField({ - value: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.CreatureType"}), - subtype: new foundry.data.fields.StringField({required: true, label: "DND5E.CreatureTypeSelectorSubtype"}), - swarm: new foundry.data.fields.StringField({required: true, blank: true, label: "DND5E.CreatureSwarmSize"}), - custom: new foundry.data.fields.StringField({required: true, label: "DND5E.CreatureTypeSelectorCustom"}) - }, {label: "DND5E.CreatureType"}), + type: new CreatureTypeField(), environment: new foundry.data.fields.StringField({required: true, label: "DND5E.Environment"}), cr: new foundry.data.fields.NumberField({ required: true, nullable: false, min: 0, initial: 1, label: "DND5E.ChallengeRating" diff --git a/module/data/actor/templates/attributes.mjs b/module/data/actor/templates/attributes.mjs index 12f078cd60..06e64372b0 100644 --- a/module/data/actor/templates/attributes.mjs +++ b/module/data/actor/templates/attributes.mjs @@ -1,4 +1,6 @@ import { FormulaField } from "../../fields.mjs"; +import MovementField from "../../shared/movement-field.mjs"; +import SensesField from "../../shared/senses-field.mjs"; /** * Shared contents of the attributes schema between various actor types. @@ -26,25 +28,7 @@ export default class AttributesFields { ability: new foundry.data.fields.StringField({label: "DND5E.AbilityModifier"}), bonus: new FormulaField({label: "DND5E.InitiativeBonus"}) }, { label: "DND5E.Initiative" }), - movement: new foundry.data.fields.SchemaField({ - burrow: new foundry.data.fields.NumberField({ - nullable: false, min: 0, step: 0.1, initial: 0, label: "DND5E.MovementBurrow" - }), - climb: new foundry.data.fields.NumberField({ - nullable: false, min: 0, step: 0.1, initial: 0, label: "DND5E.MovementClimb" - }), - fly: new foundry.data.fields.NumberField({ - nullable: false, min: 0, step: 0.1, initial: 0, label: "DND5E.MovementFly" - }), - swim: new foundry.data.fields.NumberField({ - nullable: false, min: 0, step: 0.1, initial: 0, label: "DND5E.MovementSwim" - }), - walk: new foundry.data.fields.NumberField({ - nullable: false, min: 0, step: 0.1, initial: 30, label: "DND5E.MovementWalk" - }), - units: new foundry.data.fields.StringField({initial: "ft", label: "DND5E.MovementUnits"}), - hover: new foundry.data.fields.BooleanField({label: "DND5E.MovementHover"}) - }, {label: "DND5E.Movement"}) + movement: new MovementField() }; } @@ -72,22 +56,7 @@ export default class AttributesFields { required: true, nullable: false, integer: true, min: 0, initial: 3, label: "DND5E.AttunementMax" }) }, {label: "DND5E.Attunement"}), - senses: new foundry.data.fields.SchemaField({ - darkvision: new foundry.data.fields.NumberField({ - required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.SenseDarkvision" - }), - blindsight: new foundry.data.fields.NumberField({ - required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.SenseBlindsight" - }), - tremorsense: new foundry.data.fields.NumberField({ - required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.SenseTremorsense" - }), - truesight: new foundry.data.fields.NumberField({ - required: true, nullable: false, integer: true, min: 0, initial: 0, label: "DND5E.SenseTruesight" - }), - units: new foundry.data.fields.StringField({required: true, initial: "ft", label: "DND5E.SenseUnits"}), - special: new foundry.data.fields.StringField({required: true, label: "DND5E.SenseSpecial"}) - }, {label: "DND5E.Senses"}), + senses: new SensesField(), spellcasting: new foundry.data.fields.StringField({ required: true, blank: true, initial: "int", label: "DND5E.SpellAbility" }) diff --git a/module/data/item/race.mjs b/module/data/item/race.mjs index f974fbc87c..5aa4ca9381 100644 --- a/module/data/item/race.mjs +++ b/module/data/item/race.mjs @@ -1,5 +1,7 @@ +import Actor5e from "../../documents/actor/actor.mjs"; import SystemDataModel from "../abstract.mjs"; import { AdvancementField, IdentifierField } from "../fields.mjs"; +import { CreatureTypeField, MovementField, SensesField } from "../shared/_module.mjs"; import ItemDescriptionTemplate from "./templates/item-description.mjs"; /** @@ -8,17 +10,9 @@ import ItemDescriptionTemplate from "./templates/item-description.mjs"; * * @property {string} identifier Identifier slug for this race. * @property {object[]} advancement Advancement objects for this race. - * @property {number} darkvision Darkvision distance. - * @property {object} movement - * @property {number} movement.burrow - * @property {number} movement.climb - * @property {number} movement.fly - * @property {number} movement.swim - * @property {number} movement.walk - * @property {string} movement.units Units used to measure movement. - * @property {object} type - * @property {string} type.value Creature type (e.g. "humanoid", "construct"). - * @property {string} type.subtype Additional type data (e.g. "elf" for Eladrin) + * @property {MovementField} movement + * @property {SensesField} senses + * @property {CreatureType} type */ export default class RaceData extends SystemDataModel.mixin(ItemDescriptionTemplate) { /** @inheritdoc */ @@ -26,29 +20,9 @@ export default class RaceData extends SystemDataModel.mixin(ItemDescriptionTempl return this.mergeSchema(super.defineSchema(), { identifier: new IdentifierField({label: "DND5E.Identifier"}), advancement: new foundry.data.fields.ArrayField(new AdvancementField(), {label: "DND5E.AdvancementTitle"}), - darkvision: new foundry.data.fields.NumberField({min: 0, integer: true, label: "DND5E.SenseDarkvision"}), - movement: new foundry.data.fields.SchemaField({ - burrow: new foundry.data.fields.NumberField({ - initial: null, min: 0, integer: true, label: "DND5E.MovementBurrow" - }), - climb: new foundry.data.fields.NumberField({ - initial: null, min: 0, integer: true, label: "DND5E.MovementClimb" - }), - fly: new foundry.data.fields.NumberField({ - initial: null, min: 0, integer: true, label: "DND5E.MovementFly" - }), - swim: new foundry.data.fields.NumberField({ - initial: null, min: 0, integer: true, label: "DND5E.MovementSwim" - }), - walk: new foundry.data.fields.NumberField({ - initial: 30, min: 0, integer: true, label: "DND5E.MovementWalk" - }), - units: new foundry.data.fields.StringField({initial: "ft", label: "DND5E.MovementUnits"}) - }), - type: new foundry.data.fields.SchemaField({ - value: new foundry.data.fields.StringField({initial: "humanoid", label: "DND5E.CreatureType"}), - subtype: new foundry.data.fields.StringField({label: "DND5E.CreatureTypeSelectorSubtype"}) - }) + movement: new MovementField(), + senses: new SensesField(), + type: new CreatureTypeField({ swarm: false }) }); } @@ -70,4 +44,46 @@ export default class RaceData extends SystemDataModel.mixin(ItemDescriptionTempl labels.movement[key] = `${label} ${value} ${units}`; } } + + /* -------------------------------------------- */ + /* Properties */ + /* -------------------------------------------- */ + + /** + * Sheet labels for a race's movement. + * @returns {Object} + */ + get movementLabels() { + const units = CONFIG.DND5E.movementUnits[this.movement.units]; + return Object.entries(CONFIG.DND5E.movementTypes).reduce((obj, [k, label]) => { + const value = this.movement[k]; + if ( value ) obj[k] = `${label} ${value} ${units}`; + return obj; + }, {}); + } + + /* -------------------------------------------- */ + + /** + * Sheet labels for a race's senses. + * @returns {Object} + */ + get sensesLabels() { + const units = CONFIG.DND5E.movementUnits[this.senses.units]; + return Object.entries(CONFIG.DND5E.senses).reduce((arr, [k, label]) => { + const value = this.senses[k]; + if ( value ) arr.push(`${label} ${value} ${units}`); + return arr; + }, []).concat(this.senses.special.split(";").filter(l => l)); + } + + /* -------------------------------------------- */ + + /** + * Sheet label for a race's creature type. + * @returns {Object} + */ + get typeLabel() { + return Actor5e.formatCreatureType(this.type); + } } diff --git a/module/data/shared/_module.mjs b/module/data/shared/_module.mjs index 8f634b36b9..22675cba3c 100644 --- a/module/data/shared/_module.mjs +++ b/module/data/shared/_module.mjs @@ -1 +1,4 @@ +export {default as CreatureTypeField} from "./creature-type-field.mjs"; export {default as CurrencyTemplate} from "./currency.mjs"; +export {default as MovementField} from "./movement-field.mjs"; +export {default as SensesField} from "./senses-field.mjs"; diff --git a/module/data/shared/creature-type-field.mjs b/module/data/shared/creature-type-field.mjs new file mode 100644 index 0000000000..c03b670fc4 --- /dev/null +++ b/module/data/shared/creature-type-field.mjs @@ -0,0 +1,16 @@ +/** + * Field for storing creature type data. + */ +export default class CreatureTypeField extends foundry.data.fields.SchemaField { + constructor(fields={}, options={}) { + fields = { + value: new foundry.data.fields.StringField({blank: true, label: "DND5E.CreatureType"}), + subtype: new foundry.data.fields.StringField({label: "DND5E.CreatureTypeSelectorSubtype"}), + swarm: new foundry.data.fields.StringField({blank: true, label: "DND5E.CreatureSwarmSize"}), + custom: new foundry.data.fields.StringField({label: "DND5E.CreatureTypeSelectorCustom"}), + ...fields + }; + Object.entries(fields).forEach(([k, v]) => !v ? delete fields[k] : null); + super(fields, { label: "DND5E.CreatureType", ...options }); + } +} diff --git a/module/data/shared/movement-field.mjs b/module/data/shared/movement-field.mjs new file mode 100644 index 0000000000..c2adf7c85f --- /dev/null +++ b/module/data/shared/movement-field.mjs @@ -0,0 +1,20 @@ +/** + * Field for storing movement data. + */ +export default class MovementField extends foundry.data.fields.SchemaField { + constructor(fields={}, options={}) { + const numberConfig = { nullable: false, min: 0, step: 0.1, initial: 0 }; + fields = { + burrow: new foundry.data.fields.NumberField({ ...numberConfig, label: "DND5E.MovementBurrow" }), + climb: new foundry.data.fields.NumberField({ ...numberConfig, label: "DND5E.MovementClimb" }), + fly: new foundry.data.fields.NumberField({ ...numberConfig, label: "DND5E.MovementFly" }), + swim: new foundry.data.fields.NumberField({ ...numberConfig, label: "DND5E.MovementSwim" }), + walk: new foundry.data.fields.NumberField({ ...numberConfig, initial: 30, label: "DND5E.MovementWalk" }), + units: new foundry.data.fields.StringField({initial: "ft", label: "DND5E.MovementUnits"}), + hover: new foundry.data.fields.BooleanField({label: "DND5E.MovementHover"}), + ...fields + }; + Object.entries(fields).forEach(([k, v]) => !v ? delete fields[k] : null); + super(fields, { label: "DND5E.Movement", ...options }); + } +} diff --git a/module/data/shared/senses-field.mjs b/module/data/shared/senses-field.mjs new file mode 100644 index 0000000000..9429d23c37 --- /dev/null +++ b/module/data/shared/senses-field.mjs @@ -0,0 +1,19 @@ +/** + * Field for storing senses data. + */ +export default class SensesField extends foundry.data.fields.SchemaField { + constructor(fields={}, options={}) { + const numberConfig = { required: true, nullable: false, integer: true, min: 0, initial: 0 }; + fields = { + darkvision: new foundry.data.fields.NumberField({ ...numberConfig, label: "DND5E.SenseDarkvision" }), + blindsight: new foundry.data.fields.NumberField({ ...numberConfig, label: "DND5E.SenseBlindsight" }), + tremorsense: new foundry.data.fields.NumberField({ ...numberConfig, label: "DND5E.SenseTremorsense" }), + truesight: new foundry.data.fields.NumberField({ ...numberConfig, label: "DND5E.SenseTruesight" }), + units: new foundry.data.fields.StringField({required: true, initial: "ft", label: "DND5E.SenseUnits"}), + special: new foundry.data.fields.StringField({required: true, label: "DND5E.SenseSpecial"}), + ...fields + }; + Object.entries(fields).forEach(([k, v]) => !v ? delete fields[k] : null); + super(fields, { label: "DND5E.Senses", ...options }); + } +} diff --git a/template.json b/template.json index efcbd53f73..357f3a05bb 100644 --- a/template.json +++ b/template.json @@ -676,24 +676,7 @@ "proficient": null, "bonus": "" }, - "race": { - "templates": ["itemDescription"], - "identifier": "", - "advancement": [], - "darkvision": null, - "movement": { - "burrow": null, - "climb": null, - "fly": null, - "swim": null, - "walk": 30, - "units": "ft" - }, - "type": { - "value": "humanoid", - "subtype": "" - } - }, + "race": {}, "spell": { "templates": ["itemDescription", "activatedEffect", "action"], "level": 1, diff --git a/templates/apps/senses-config.hbs b/templates/apps/senses-config.hbs index 9f9e7d34f2..1161c52409 100644 --- a/templates/apps/senses-config.hbs +++ b/templates/apps/senses-config.hbs @@ -1,20 +1,20 @@ -
+{{log this}}

{{localize "DND5E.SensesConfigHint"}}

{{#each senses as |sense name|}}
- +
{{/each}}
- {{selectOptions movementUnits selected=units}}
- +
diff --git a/templates/items/race.hbs b/templates/items/race.hbs index 17c954a76e..ab20fb7425 100644 --- a/templates/items/race.hbs +++ b/templates/items/race.hbs @@ -39,21 +39,30 @@ {{#if editable}}{{/if}}
    -
  1. {{labels.type}}
  2. +
  3. {{item.system.typeLabel}}

{{localize "DND5E.Movement"}} {{#if editable}}{{/if}}

    - {{#each labels.movement}} + {{#each item.system.movementLabels}}
  1. {{this}}
  2. + {{else}} +
  3. {{localize "DND5E.None"}}
  4. + {{/each}} +
+

+ {{localize "DND5E.Senses"}} + {{#if editable}}{{/if}} +

+
    + {{#each item.system.sensesLabels}} +
  1. {{this}}
  2. + {{else}} +
  3. {{localize "DND5E.None"}}
  4. {{/each}}
-
- - {{numberInput system.darkvision name="system.darkvision" step=1 min=0 placeholder="None"}} -
{{editor descriptionHTML target="system.description.value" button=true editable=editable