From 746e0f239a90d6be324594aea7b419041ea8d89e Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sat, 7 Jun 2025 20:30:12 +0200 Subject: [PATCH 1/7] 110 - Class Data Model (#111) * Added PreCreate/Create/Delete logic for Class/Subclass and set it as foreignUUID fields in PC * Moved methods into TypedModelData * Simplified Subclass --- lang/en.json | 8 ++ module/applications/levelup.mjs | 6 +- .../applications/sheets/daggerheart-sheet.mjs | 2 +- module/applications/sheets/items/class.mjs | 30 +++---- module/applications/sheets/pc.mjs | 86 ++++--------------- .../data/fields/foreignDocumentUUIDField.mjs | 65 +++++++------- module/data/item/class.mjs | 70 +++++++++++---- module/data/item/subclass.mjs | 85 +++++++++--------- module/data/pc.mjs | 17 ++-- module/documents/item.mjs | 11 +-- .../global/partials/feature-section-item.hbs | 4 +- templates/sheets/items/subclass/features.hbs | 12 +-- templates/sheets/parts/features.hbs | 6 +- templates/sheets/pc/pc.hbs | 12 +-- 14 files changed, 190 insertions(+), 224 deletions(-) diff --git a/lang/en.json b/lang/en.json index 39d5225c..eb5ecd62 100755 --- a/lang/en.json +++ b/lang/en.json @@ -1178,6 +1178,14 @@ "Description": "Description" } }, + "Item": { + "Errors": { + "MissingClass": "The character is missing a class", + "SubclassNotInClass": "The subclass does not belong to the character's class", + "ClassAlreadySelected": "The character already has a class", + "SubclassAlreadySelected": "The character already has a subclass for that class." + } + }, "Effects": { "Types": { "health": { diff --git a/module/applications/levelup.mjs b/module/applications/levelup.mjs index 7e69a6a1..22c6d33e 100644 --- a/module/applications/levelup.mjs +++ b/module/applications/levelup.mjs @@ -230,7 +230,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) domains: multiclass?.system?.domains.map(key => { const domain = domains[key]; - const alreadySelected = this.actor.system.class.system.domains.includes(key); + const alreadySelected = this.actor.system.class.value.system.domains.includes(key); return { ...domain, @@ -480,7 +480,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) const target = event.target.closest('.card-preview-container'); if (item.type === 'domainCard') { if ( - !this.actor.system.class.system.domains.includes(item.system.domain) && + !this.actor.system.class.value.system.domains.includes(item.system.domain) && this.levelup.classUpgradeChoices?.multiclass?.domain !== item.system.domain ) { ui.notifications.error( @@ -522,7 +522,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) } else if (event.target.closest('.multiclass-cards')) { const target = event.target.closest('.multiclass-cards'); if (item.type === 'class') { - if (item.name === this.actor.system.class.name) { + if (item.name === this.actor.system.class.value.name) { ui.notifications.error( game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.error.alreadySelectedClass') ); diff --git a/module/applications/sheets/daggerheart-sheet.mjs b/module/applications/sheets/daggerheart-sheet.mjs index 635d2434..4810b0a7 100644 --- a/module/applications/sheets/daggerheart-sheet.mjs +++ b/module/applications/sheets/daggerheart-sheet.mjs @@ -27,7 +27,7 @@ export default function DhpApplicationMixin(Base) { async _prepareContext(_options, objectPath = 'document') { const context = await super._prepareContext(_options); - context.source = this[objectPath].toObject(); + context.source = this[objectPath]; context.fields = this[objectPath].schema.fields; context.systemFields = this[objectPath].system ? this[objectPath].system.schema.fields : {}; diff --git a/module/applications/sheets/items/class.mjs b/module/applications/sheets/items/class.mjs index 978a3f9a..b0d8bd1d 100644 --- a/module/applications/sheets/items/class.mjs +++ b/module/applications/sheets/items/class.mjs @@ -11,8 +11,8 @@ export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) { actions: { removeSubclass: this.removeSubclass, viewSubclass: this.viewSubclass, - removeFeature: this.removeFeature, - viewFeature: this.viewFeature, + deleteFeature: this.deleteFeature, + editFeature: this.editFeature, removeItem: this.removeItem, viewItem: this.viewItem, removePrimaryWeapon: this.removePrimaryWeapon, @@ -153,13 +153,13 @@ export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) { subclass.sheet.render(true); } - static async removeFeature(_, button) { + static async deleteFeature(_, button) { await this.document.update({ - 'system.features': this.document.system.features.filter(x => x.uuid !== button.dataset.feature) + 'system.features': this.document.system.features.map(x => x.uuid).filter(x => x !== button.dataset.feature) }); } - static async viewFeature(_, button) { + static async editFeature(_, button) { const feature = await fromUuid(button.dataset.feature); feature.sheet.render(true); } @@ -198,15 +198,11 @@ export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) { const item = await fromUuid(data.uuid); if (item.type === 'subclass') { await this.document.update({ - 'system.subclasses': [ - ...this.document.system.subclasses, item.uuid - ] + 'system.subclasses': [...this.document.system.subclasses.map(x => x.uuid), item.uuid] }); } else if (item.type === 'feature') { await this.document.update({ - 'system.features': [ - ...this.document.system.features, item.uuid - ] + 'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid] }); } else if (item.type === 'weapon') { if (event.currentTarget.classList.contains('primary-weapon-section')) { @@ -231,25 +227,19 @@ export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) { if (item.type === 'miscellaneous' || item.type === 'consumable') { if (this.document.system.inventory.choiceA.length < 2) await this.document.update({ - 'system.inventory.choiceA': [ - ...this.document.system.inventory.choiceA, item.uuid - ] + 'system.inventory.choiceA': [...this.document.system.inventory.choiceA, item.uuid] }); } } else if (item.type === 'miscellaneous') { if (event.currentTarget.classList.contains('take-section')) { if (this.document.system.inventory.take.length < 3) await this.document.update({ - 'system.inventory.take': [ - ...this.document.system.inventory.take, item.uuid - ] + 'system.inventory.take': [...this.document.system.inventory.take, item.uuid] }); } else if (event.currentTarget.classList.contains('choice-b-section')) { if (this.document.system.inventory.choiceB.length < 2) await this.document.update({ - 'system.inventory.choiceB': [ - ...this.document.system.inventory.choiceB, item.uuid - ] + 'system.inventory.choiceB': [...this.document.system.inventory.choiceB, item.uuid] }); } } diff --git a/module/applications/sheets/pc.mjs b/module/applications/sheets/pc.mjs index b38195d5..7433e3f3 100644 --- a/module/applications/sheets/pc.mjs +++ b/module/applications/sheets/pc.mjs @@ -215,13 +215,13 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { context.abilityScoresFinished = context.abilityScoreArray.every(x => x.value === 0); //FIXME: - context.domains = this.document.system.class + context.domains = this.document.system.class.value ? { - first: this.document.system.class.system.domains[0] - ? SYSTEM.DOMAIN.domains[this.document.system.class.system.domains[0]].src + first: this.document.system.class.value.system.domains[0] + ? SYSTEM.DOMAIN.domains[this.document.system.class.value.system.domains[0]].src : null, - second: this.document.system.class.system.domains[1] - ? SYSTEM.DOMAIN.domains[this.document.system.class.system.domains[1]].src + second: this.document.system.class.value.system.domains[1] + ? SYSTEM.DOMAIN.domains[this.document.system.class.value.system.domains[1]].src : null } : {}; @@ -361,7 +361,7 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { } mapAdvancementFeatures(actor, config) { - if (!actor.system.subclass) return { foundation: null, advancements: [] }; + if (!actor.system.class.value || !actor.system.class.subclass) return { foundation: null, advancements: [] }; const { subclass, multiclassSubclass } = actor.system.subclassFeatures; @@ -370,8 +370,8 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { multiclass: false, img: actor.system.subclass.img, subtitle: game.i18n.localize('DAGGERHEART.Sheets.PC.DomainCard.FoundationTitle'), - domains: actor.system.class.system.domains.map(x => config.DOMAIN.domains[x].src), - className: actor.system.class.name, + domains: actor.system.class.value.system.domains.map(x => config.DOMAIN.domains[x].src), + className: actor.system.class.value.name, subclassUuid: actor.system.subclass.uuid, subclassName: actor.system.subclass.name, spellcast: config.ACTOR.abilities[actor.system.subclass.system.spellcastingTrait]?.name ?? null, @@ -402,9 +402,9 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { : game.i18n.localize('DAGGERHEART.Sheets.PC.DomainCard.FoundationTitle'), domains: firstKey === 'sub' - ? actor.system.class.system.domains.map(x => config.DOMAIN.domains[x].src) + ? actor.system.class.value.system.domains.map(x => config.DOMAIN.domains[x].src) : actor.system.multiclass.system.domains.map(x => config.DOMAIN.domains[x].src), - className: firstKey === 'sub' ? actor.system.class.name : actor.system.multiclass.name, + className: firstKey === 'sub' ? actor.system.class.value.name : actor.system.multiclass.name, subclassUuid: firstBase.uuid, subclassName: firstBase.name, spellcast: @@ -456,9 +456,9 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { : game.i18n.localize('DAGGERHEART.Sheets.PC.DomainCard.FoundationTitle'), domains: secondKey === 'sub' - ? actor.system.class.system.domains.map(x => config.DOMAIN.domains[x].src) + ? actor.system.class.value.system.domains.map(x => config.DOMAIN.domains[x].src) : actor.system.multiclass.system.domains.map(x => config.DOMAIN.domains[x].src), - className: secondKey === 'sub' ? actor.system.class.name : actor.system.multiclass.name, + className: secondKey === 'sub' ? actor.system.class.value.name : actor.system.multiclass.name, subclassUuid: secondBase.uuid, subclassName: secondBase.name, spellcast: @@ -644,7 +644,7 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { } openLevelUp() { - if (!this.document.system.class || !this.document.system.subclass) { + if (!this.document.system.class.value || !this.document.system.subclass) { ui.notifications.error(game.i18n.localize('DAGGERHEART.Sheets.PC.Errors.missingClassOrSubclass')); return; } @@ -1144,7 +1144,7 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { const createdItems = []; if (item.type === 'domainCard') { - if (!this.document.system.class) { + if (!this.document.system.class.value) { ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.NoClassSelected')); return; } @@ -1173,63 +1173,7 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { return createdItem; } else { - if (!item.system.multiclass && ['class', 'subclass', 'ancestry', 'community'].includes(item.type)) { - const existing = this.document.items.find(x => x.type === item.type); - await existing?.delete(); - } - - if (item.type === 'subclass') { - if (!item.system.multiclass) { - if (!this.document.system.class) { - ui.notifications.info( - game.i18n.localize('DAGGERHEART.Notification.Info.SelectClassBeforeSubclass') - ); - return; - } else if (!this.document.system.class.system.subclasses.some(x => x.uuid === item.uuid)) { - ui.notifications.info(game.i18n.localize('DAGGERHEART.Notification.Info.SubclassNotOfClass')); - return; - } - - for (var feature of this.document.items.filter( - x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.subclass.id - )) { - await feature.delete(); - } - } - - const features = [ - itemData.system.foundationFeature, - itemData.system.specializationFeature, - itemData.system.masteryFeature - ]; - for (var i = 0; i < features.length; i++) { - const feature = features[i]; - for (var ability of feature.abilities) { - const data = (await fromUuid(ability.uuid)).toObject(); - if (i > 0) data.system.disabled = true; - data.uuid = itemData.uuid; - - const abilityData = await this._onDropItemCreate(data); - ability.uuid = abilityData[0].uuid; - - createdItems.push(abilityData); - } - } - } else if (item.type === 'class') { - if (!item.system.multiclass) { - for (var feature of this.document.items.filter( - x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.class.id - )) { - await feature.delete(); - } - } - - for (var feature of item.system.features) { - const data = (await fromUuid(feature.uuid)).toObject(); - const itemData = await this._onDropItemCreate(data); - createdItems.push(itemData); - } - } else if (item.type === 'ancestry') { + if (item.type === 'ancestry') { for (var feature of this.document.items.filter( x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.ancestry.id )) { diff --git a/module/data/fields/foreignDocumentUUIDField.mjs b/module/data/fields/foreignDocumentUUIDField.mjs index 13bbc80d..29cfaff3 100644 --- a/module/data/fields/foreignDocumentUUIDField.mjs +++ b/module/data/fields/foreignDocumentUUIDField.mjs @@ -3,40 +3,39 @@ * that resolves to either the document, the index(for items in compenidums) or the UUID string. */ export default class ForeignDocumentUUIDField extends foundry.data.fields.DocumentUUIDField { + /** + * @param {foundry.data.types.DocumentUUIDFieldOptions} [options] Options which configure the behavior of the field + * @param {foundry.data.types.DataFieldContext} [context] Additional context which describes the field + */ + constructor(options, context) { + super(options, context); + } -/** - * @param {foundry.data.types.DocumentUUIDFieldOptions} [options] Options which configure the behavior of the field - * @param {foundry.data.types.DataFieldContext} [context] Additional context which describes the field - */ - constructor(options, context) { - super(options, context); - } + /** @inheritdoc */ + static get _defaults() { + return foundry.utils.mergeObject(super._defaults, { + nullable: true, + readonly: false, + idOnly: false + }); + } - /** @inheritdoc */ - static get _defaults() { - return foundry.utils.mergeObject(super._defaults, { - nullable: true, - readonly: false, - idOnly: false, - }); - } + /**@override */ + initialize(value, _model, _options = {}) { + if (this.idOnly) return value; + return (() => { + try { + const doc = fromUuidSync(value); + return doc; + } catch (error) { + console.error(error); + return value ?? null; + } + })(); + } - /**@override */ - initialize(value, _model, _options = {}) { - if (this.idOnly) return value; - return () => { - try { - const doc = fromUuidSync(value); - return doc; - } catch (error) { - console.error(error); - return value ?? null; - } - }; - } - - /**@override */ - toObject(value) { - return value?.uuid ?? value; - } + /**@override */ + toObject(value) { + return value?.uuid ?? value; + } } diff --git a/module/data/item/class.mjs b/module/data/item/class.mjs index 8d9d62e9..423ad8a5 100644 --- a/module/data/item/class.mjs +++ b/module/data/item/class.mjs @@ -1,4 +1,3 @@ -import { getTier } from '../../helpers/utils.mjs'; import BaseDataItem from './base.mjs'; import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; @@ -6,9 +5,9 @@ export default class DHClass extends BaseDataItem { /** @inheritDoc */ static get metadata() { return foundry.utils.mergeObject(super.metadata, { - label: "TYPES.Item.class", - type: "class", - hasDescription: true, + label: 'TYPES.Item.class', + type: 'class', + hasDescription: true }); } @@ -19,15 +18,22 @@ export default class DHClass extends BaseDataItem { ...super.defineSchema(), domains: new fields.ArrayField(new fields.StringField(), { max: 2 }), - classItems: new fields.ArrayField(new ForeignDocumentUUIDField({ type: "Item" })), + classItems: new fields.ArrayField(new ForeignDocumentUUIDField({ type: 'Item' })), evasion: new fields.NumberField({ initial: 0, integer: true }), - features: new fields.ArrayField(new ForeignDocumentUUIDField({ type: "Item" })), - - subclasses: new fields.ArrayField(new ForeignDocumentUUIDField({ type: "Item", required: false, nullable: true, initial: undefined })), + features: new fields.ArrayField(new ForeignDocumentUUIDField({ type: 'Item' })), + subclasses: new fields.ArrayField( + new ForeignDocumentUUIDField({ type: 'Item', required: false, nullable: true, initial: undefined }) + ), inventory: new fields.SchemaField({ - take: new fields.ArrayField(new ForeignDocumentUUIDField({ type: "Item", required: false, nullable: true, initial: undefined })), - choiceA: new fields.ArrayField(new ForeignDocumentUUIDField({ type: "Item", required: false, nullable: true, initial: undefined })), - choiceB: new fields.ArrayField(new ForeignDocumentUUIDField({ type: "Item", required: false, nullable: true, initial: undefined })), + take: new fields.ArrayField( + new ForeignDocumentUUIDField({ type: 'Item', required: false, nullable: true, initial: undefined }) + ), + choiceA: new fields.ArrayField( + new ForeignDocumentUUIDField({ type: 'Item', required: false, nullable: true, initial: undefined }) + ), + choiceB: new fields.ArrayField( + new ForeignDocumentUUIDField({ type: 'Item', required: false, nullable: true, initial: undefined }) + ) }), characterGuide: new fields.SchemaField({ suggestedTraits: new fields.SchemaField({ @@ -38,15 +44,45 @@ export default class DHClass extends BaseDataItem { presence: new fields.NumberField({ initial: 0, integer: true }), knowledge: new fields.NumberField({ initial: 0, integer: true }) }), - suggestedPrimaryWeapon: new ForeignDocumentUUIDField({ type: "Item" }), - suggestedSecondaryWeapon: new ForeignDocumentUUIDField({ type: "Item" }), - suggestedArmor: new ForeignDocumentUUIDField({ type: "Item" }), + suggestedPrimaryWeapon: new ForeignDocumentUUIDField({ type: 'Item' }), + suggestedSecondaryWeapon: new ForeignDocumentUUIDField({ type: 'Item' }), + suggestedArmor: new ForeignDocumentUUIDField({ type: 'Item' }) }), - multiclass: new fields.NumberField({ initial: null, nullable: true, integer: true }), + isMulticlass: new fields.BooleanField({ initial: false }) }; } - get multiclassTier() { - return getTier(this.multiclass, true); + async _preCreate(data, options, user) { + const allowed = await super._preCreate(data, options, user); + if (allowed === false) return; + + if (this.actor?.type === 'pc') { + const path = data.system.isMulticlass ? 'system.multiclass.value' : 'system.class.value'; + if (foundry.utils.getProperty(this.actor, path)) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.ClassAlreadySelected')); + return false; + } + } + } + + _onCreate(data, options, userId) { + super._onCreate(data, options, userId); + if (options.parent?.type === 'pc') { + const path = `system.${data.system.isMulticlass ? 'multiclass.value' : 'class.value'}`; + options.parent.update({ [path]: `${options.parent.uuid}.Item.${data._id}` }); + } + } + + _onDelete(options, userId) { + super._onDelete(options, userId); + + if (options.parent?.type === 'pc') { + const path = `system.${this.isMulticlass ? 'multiclass' : 'class'}`; + options.parent.update({ + [`${path}.value`]: null + }); + + foundry.utils.getProperty(options.parent, `${path}.subclass`)?.delete(); + } } } diff --git a/module/data/item/subclass.mjs b/module/data/item/subclass.mjs index ff92db53..883df064 100644 --- a/module/data/item/subclass.mjs +++ b/module/data/item/subclass.mjs @@ -1,13 +1,13 @@ -import { getTier } from '../../helpers/utils.mjs'; +import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; import BaseDataItem from './base.mjs'; export default class DHSubclass extends BaseDataItem { /** @inheritDoc */ static get metadata() { return foundry.utils.mergeObject(super.metadata, { - label: "TYPES.Item.subclass", - type: "subclass", - hasDescription: true, + label: 'TYPES.Item.subclass', + type: 'subclass', + hasDescription: true }); } @@ -22,45 +22,48 @@ export default class DHSubclass extends BaseDataItem { nullable: true, initial: null }), - foundationFeature: new fields.SchemaField({ - description: new fields.HTMLField({}), - abilities: new fields.ArrayField( - new fields.SchemaField({ - name: new fields.StringField({}), - img: new fields.StringField({}), - uuid: new fields.StringField({}) - }) - ) - }), - specializationFeature: new fields.SchemaField({ - unlocked: new fields.BooleanField({ initial: false }), - tier: new fields.NumberField({ initial: null, nullable: true, integer: true }), - description: new fields.HTMLField({}), - abilities: new fields.ArrayField( - new fields.SchemaField({ - name: new fields.StringField({}), - img: new fields.StringField({}), - uuid: new fields.StringField({}) - }) - ) - }), - masteryFeature: new fields.SchemaField({ - unlocked: new fields.BooleanField({ initial: false }), - tier: new fields.NumberField({ initial: null, nullable: true, integer: true }), - description: new fields.HTMLField({}), - abilities: new fields.ArrayField( - new fields.SchemaField({ - name: new fields.StringField({}), - img: new fields.StringField({}), - uuid: new fields.StringField({}) - }) - ) - }), - multiclass: new fields.NumberField({ initial: null, nullable: true, integer: true }) + foundationFeature: new ForeignDocumentUUIDField({ type: 'Item' }), + specializationFeature: new ForeignDocumentUUIDField({ type: 'Item' }), + masteryFeature: new ForeignDocumentUUIDField({ type: 'Item' }), + isMulticlass: new fields.BooleanField({ initial: false }) }; } - get multiclassTier() { - return getTier(this.multiclass); + async _preCreate(data, options, user) { + const allowed = await super._preCreate(data, options, user); + if (allowed === false) return; + + if (this.actor?.type === 'pc') { + const path = data.system.isMulticlass ? 'system.multiclass' : 'system.class'; + const classData = foundry.utils.getProperty(this.actor, path); + if (!classData.value) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.MissingClass')); + return false; + } else if (classData.subclass) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.SubclassAlreadySelected')); + return false; + } else if (classData.value.system.subclasses.every(x => x.uuid !== `Item.${data._id}`)) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.SubclassNotInClass')); + return false; + } + } + } + + _onCreate(data, options, userId) { + super._onCreate(data, options, userId); + + if (options.parent?.type === 'pc') { + const path = `system.${data.system.isMulticlass ? 'multiclass.subclass' : 'class.subclass'}`; + options.parent.update({ [path]: `${options.parent.uuid}.Item.${data._id}` }); + } + } + + _onDelete(options, userId) { + super._onDelete(options, userId); + + if (options.parent?.type === 'pc') { + const path = `system.${this.isMulticlass ? 'multiclass.subclass' : 'class.subclass'}`; + options.parent.update({ [path]: null }); + } } } diff --git a/module/data/pc.mjs b/module/data/pc.mjs index 7589321a..740b51eb 100644 --- a/module/data/pc.mjs +++ b/module/data/pc.mjs @@ -1,4 +1,5 @@ import { getPathValue } from '../helpers/utils.mjs'; +import ForeignDocumentUUIDField from './fields/foreignDocumentUUIDField.mjs'; import { LevelOptionType } from './levelTier.mjs'; const fields = foundry.data.fields; @@ -96,6 +97,14 @@ export default class DhpPC extends foundry.abstract.TypeDataModel { max: new fields.NumberField({ initial: 6, integer: true }), value: new fields.NumberField({ initial: 0, integer: true }) }), + class: new fields.SchemaField({ + value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }), + subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }) + }), + multiclass: new fields.SchemaField({ + value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }), + subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }) + }), levelData: new fields.EmbeddedDataField(DhPCLevelData) }; } @@ -108,14 +117,6 @@ export default class DhpPC extends foundry.abstract.TypeDataModel { return this.parent.items.find(x => x.type === 'ancestry') ?? null; } - get class() { - return this.parent.items.find(x => x.type === 'class' && !x.system.multiclass) ?? null; - } - - get multiclass() { - return this.parent.items.find(x => x.type === 'class' && x.system.multiclass) ?? null; - } - get multiclassSubclass() { return this.parent.items.find(x => x.type === 'subclass' && x.system.multiclass) ?? null; } diff --git a/module/documents/item.mjs b/module/documents/item.mjs index 955c2c27..65dafc51 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -1,21 +1,12 @@ export default class DhpItem extends Item { - _preCreate(data, changes, user) { - super._preCreate(data, changes, user); - } - prepareData() { super.prepareData(); - - if (this.type === 'class') { - // Bad. Make this better. - // this.system.domains = CONFIG.daggerheart.DOMAIN.classDomainMap[Object.keys(CONFIG.daggerheart.DOMAIN.classDomainMap).find(x => x === this.name.toLowerCase())]; - } } /** * @inheritdoc * @param {object} options - Options which modify the getRollData method. - * @returns + * @returns */ getRollData(options = {}) { let data; diff --git a/templates/sheets/global/partials/feature-section-item.hbs b/templates/sheets/global/partials/feature-section-item.hbs index adc029e9..ebaabefe 100644 --- a/templates/sheets/global/partials/feature-section-item.hbs +++ b/templates/sheets/global/partials/feature-section-item.hbs @@ -9,7 +9,7 @@ @@ -17,7 +17,7 @@ diff --git a/templates/sheets/items/subclass/features.hbs b/templates/sheets/items/subclass/features.hbs index eb2010c4..d9ebb3c1 100644 --- a/templates/sheets/items/subclass/features.hbs +++ b/templates/sheets/items/subclass/features.hbs @@ -5,22 +5,16 @@ >
{{localize "DAGGERHEART.Sheets.Subclass.Tabs.Foundation"}} - {{#each source.system.foundationFeature.abilities as |feature key|}} - {{> 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs' feature=feature}} - {{/each}} + {{> 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs' feature=source.system.foundationFeature}}
{{localize "DAGGERHEART.Sheets.Subclass.Tabs.Specialization"}} - {{#each source.system.specializationFeature.abilities as |feature key|}} - {{> 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs' feature=feature}} - {{/each}} + {{> 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs' feature=source.system.specializationFeature}}
{{localize "DAGGERHEART.Sheets.Subclass.Tabs.Mastery"}} - {{#each source.system.masteryFeature.abilities as |feature key|}} - {{> 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs' feature=feature}} - {{/each}} + {{> 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs' feature=source.system.masteryFeature}}
\ No newline at end of file diff --git a/templates/sheets/parts/features.hbs b/templates/sheets/parts/features.hbs index ac9f8195..4fb37890 100644 --- a/templates/sheets/parts/features.hbs +++ b/templates/sheets/parts/features.hbs @@ -1,12 +1,12 @@
- {{#if this.document.system.class}} - {{#if this.document.system.multiclass}} + {{#if this.document.system.class.value}} + {{#if this.document.system.multiclass.value}} {{this.document.system.class.name}} {{localize "DAGGERHEART.General.Features"}}{{this.document.system.multiclass.name}} {{localize "DAGGERHEART.General.Features"}} {{else}} - {{this.document.system.class.name}} {{localize "DAGGERHEART.General.Features"}} + {{this.document.system.class.value.name}} {{localize "DAGGERHEART.General.Features"}} {{/if}} {{else}} {{localize "DAGGERHEART.Sheets.PC.Features.Title"}} diff --git a/templates/sheets/pc/pc.hbs b/templates/sheets/pc/pc.hbs index 25704b15..bf28caf0 100644 --- a/templates/sheets/pc/pc.hbs +++ b/templates/sheets/pc/pc.hbs @@ -4,17 +4,17 @@
{{document.name}} - {{#if document.system.class}} + {{#if document.system.class.value}}
-

+

- {{document.system.class.name}} + {{document.system.class.value.name}}

- {{document.system.class.system.domains.[0]}} + {{document.system.class.value.system.domains.[0]}} and - {{document.system.class.system.domains.[1]}} + {{document.system.class.value.system.domains.[1]}}
{{else}}
@@ -56,7 +56,7 @@ {{/objectSelector}} {{#objectSelector title="Subclass" ids=(join document.system.subclass.uuid) values=(join document.system.subclass.name) titleFontSize=14}} - + {{/objectSelector}}
From ad0acd62cdce8e2a151737e62eb8be955a15316f Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Mon, 9 Jun 2025 13:25:43 +0200 Subject: [PATCH 2/7] Fixed up data model and a basic placeholder template (#117) --- lang/en.json | 56 +++++- module/applications/sheets/environment.mjs | 165 +++++++----------- module/config/actorConfig.mjs | 19 ++ module/config/generalConfig.mjs | 24 ++- module/data/adversary.mjs | 5 +- module/data/environment.mjs | 36 ++-- module/helpers/handlebarsHelper.mjs | 5 + styles/daggerheart.css | 25 +++ styles/less/global/elements.less | 4 + styles/sheets/environment.less | 27 +++ styles/sheets/sheets.less | 1 + system.json | 16 +- .../sheets/actors/environment/header.hbs | 9 + .../sheets/actors/environment/information.hbs | 16 ++ templates/sheets/actors/environment/main.hbs | 37 ++++ 15 files changed, 300 insertions(+), 145 deletions(-) create mode 100644 styles/sheets/environment.less create mode 100644 templates/sheets/actors/environment/header.hbs create mode 100644 templates/sheets/actors/environment/information.hbs create mode 100644 templates/sheets/actors/environment/main.hbs diff --git a/lang/en.json b/lang/en.json index eb5ecd62..26cedccb 100755 --- a/lang/en.json +++ b/lang/en.json @@ -261,6 +261,12 @@ "Description": "When an effect makes a creature Restrained, it means they cannot move until this condition is cleared.\nThey can still take actions from their current position." } }, + "Tiers": { + "tier1": "Tier 1", + "tier2": "Tier 2", + "tier3": "Tier 3", + "tier4": "Tier 4" + }, "Adversary": { "Bruiser": { "Name": "Bruiser", @@ -320,6 +326,26 @@ } } }, + "Environment": { + "Type": { + "Exploration": { + "label": "Exploration", + "description": "" + }, + "Social": { + "label": "Social", + "description": "" + }, + "Traversal": { + "label": "Traversal", + "description": "" + }, + "Event": { + "label": "Event", + "description": "" + } + } + }, "Domains": { "Arcana": { "Description": "This is the domain of the innate or instinctual use of magic. Those who walk this path tap into the raw, enigmatic forces of the realms to manipulate both the elements and their own energy. Arcana offers wielders a volatile power, but it is incredibly potent when correctly channeled." @@ -1030,9 +1056,33 @@ "NewFeature": "New Feature" }, "Environment": { - "ToneAndFeel": "Tone And feel", - "PotentialAdversaries": "Potential Adversaries", - "NewFeature": "New Feature" + "FIELDS": { + "tier": { + "label": "Tier" + }, + "type": { + "label": "Type" + }, + "difficulty": { + "label": "Difficulty" + } + }, + "Tabs": { + "Main": "Data", + "Information": "Information" + }, + "general": "General", + "newAdversary": "New Adversary", + "newFeature": "New feature", + "description": "Description", + "impulses": "Impulses", + "potentialAdversaries": { + "label": "Potential Adversaries", + "placeholder": "Optionally drag and drop adversaries here" + }, + "features": { + "label": "Features" + } }, "Armor": { "baseScore": "Base Score", diff --git a/module/applications/sheets/environment.mjs b/module/applications/sheets/environment.mjs index 8799d41a..7c59fd55 100644 --- a/module/applications/sheets/environment.mjs +++ b/module/applications/sheets/environment.mjs @@ -1,78 +1,60 @@ import DaggerheartSheet from './daggerheart-sheet.mjs'; -const { DocumentSheetV2 } = foundry.applications.api; -export default class DhpEnvironment extends DaggerheartSheet(DocumentSheetV2) { - constructor(options) { - super(options); - - this.editMode = false; - } - +const { ActorSheetV2 } = foundry.applications.sheets; +export default class DhpEnvironment extends DaggerheartSheet(ActorSheetV2) { static DEFAULT_OPTIONS = { tag: 'form', - classes: ['daggerheart', 'sheet', 'adversary', 'environment'], + classes: ['daggerheart', 'sheet', 'actor', 'dh-style', 'environment'], position: { - width: 600, - height: 'auto' + width: 450, + height: 1000 }, actions: { - toggleSlider: this.toggleSlider, - viewFeature: this.viewFeature, + addAdversary: this.addAdversary, addFeature: this.addFeature, - removeFeature: this.removeFeature, - addTone: this.addTone, - removeTone: this.removeTone, - useFeature: this.useFeature + deleteProperty: this.deleteProperty, + viewAdversary: this.viewAdversary }, form: { handler: this._updateForm, - closeOnSubmit: false, - submitOnChange: true - } + submitOnChange: true, + closeOnSubmit: false + }, + dragDrop: [{ dragSelector: null, dropSelector: '.adversary-container' }] }; - /** @override */ static PARTS = { - form: { - id: 'form', - template: 'systems/daggerheart/templates/sheets/environment.hbs' - } + header: { template: 'systems/daggerheart/templates/sheets/actors/environment/header.hbs' }, + tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, + main: { template: 'systems/daggerheart/templates/sheets/actors/environment/main.hbs' }, + information: { template: 'systems/daggerheart/templates/sheets/actors/environment/information.hbs' } }; - /* -------------------------------------------- */ - - /** @inheritDoc */ - get title() { - return `${game.i18n.localize('Environment')} - ${this.document.name}`; - } + static TABS = { + main: { + active: true, + cssClass: '', + group: 'primary', + id: 'main', + icon: null, + label: 'DAGGERHEART.Sheets.Environment.Tabs.Main' + }, + information: { + active: false, + cssClass: '', + group: 'primary', + id: 'information', + icon: null, + label: 'DAGGERHEART.Sheets.Environment.Tabs.Information' + } + }; async _prepareContext(_options) { - return { - title: `${this.document.name} - ${game.i18n.localize(SYSTEM.ACTOR.adversaryTypes[this.document.system.type].name)}`, - user: this.document, - source: this.document.toObject(), - fields: this.document.schema.fields, - data: { - type: game.i18n.localize(SYSTEM.ACTOR.adversaryTypes[this.document.system.type].name), - features: this.document.items.reduce((acc, x) => { - if (x.type === 'feature') { - const feature = x.toObject(); - acc.push({ - ...feature, - system: { - ...feature.system, - actionType: game.i18n.localize(SYSTEM.ITEM.actionTypes[feature.system.actionType].name) - }, - uuid: x.uuid - }); - } + const context = await super._prepareContext(_options); + context.document = this.document; + context.tabs = super._getTabs(this.constructor.TABS); - return acc; - }, []) - }, - editMode: this.editMode, - config: SYSTEM - }; + return context; } static async _updateForm(event, _, formData) { @@ -80,60 +62,41 @@ export default class DhpEnvironment extends DaggerheartSheet(DocumentSheetV2) { this.render(); } - static toggleSlider() { - this.editMode = !this.editMode; + static async addAdversary() { + await this.document.update({ + [`system.potentialAdversaries.${foundry.utils.randomID()}.label`]: game.i18n.localize( + 'DAGGERHEART.Sheets.Environment.newAdversary' + ) + }); this.render(); } - static async viewFeature(_, button) { - const move = await fromUuid(button.dataset.feature); - move.sheet.render(true); - } - static async addFeature() { - const result = await this.document.createEmbeddedDocuments('Item', [ - { - name: game.i18n.localize('DAGGERHEART.Sheets.Environment.NewFeature'), - type: 'feature' - } - ]); - - await result[0].sheet.render(true); + ui.notifications.error('Not Implemented yet. Awaiting datamodel rework'); } - static async removeFeature(_, button) { - await this.document.items.find(x => x.uuid === button.dataset.feature).delete(); + static async deleteProperty(_, target) { + await this.document.update({ [`${target.dataset.path}.-=${target.id}`]: null }); + this.render(); } - static async addTone() { - await this.document.update({ 'system.toneAndFeel': [...this.document.system.toneAndFeel, ''] }); + static async viewAdversary(_, button) { + const adversary = foundry.utils.getProperty( + this.document.system.potentialAdversaries, + `${button.dataset.potentialAdversary}.adversaries.${button.dataset.adversary}` + ); + adversary.sheet.render(true); } - static async removeTone(button) { - await this.document.update({ - 'system.toneAndFeel': this.document.system.toneAndFeel.filter( - (_, index) => index !== Number.parseInt(button.dataset.tone) - ) - }); - } - - static async useFeature(_, button) { - const item = this.document.items.find(x => x.uuid === button.dataset.feature); - - const cls = getDocumentClass('ChatMessage'); - const msg = new cls({ - user: game.user.id, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/ability-use.hbs', - { - title: game.i18n.format('DAGGERHEART.Chat.EnvironmentTitle', { - actionType: button.dataset.actionType - }), - card: { name: item.name, img: item.img, description: item.system.description } - } - ) - }); - - cls.create(msg.toObject()); + async _onDrop(event) { + const data = TextEditor.getDragEventData(event); + const item = await fromUuid(data.uuid); + if (item.type === 'adversary') { + const target = event.target.closest('.adversary-container'); + const path = `system.potentialAdversaries.${target.dataset.potentialAdversary}.adversaries.${item.id}`; + await this.document.update({ + [path]: item.uuid + }); + } } } diff --git a/module/config/actorConfig.mjs b/module/config/actorConfig.mjs index 4db5ca9c..bcb6ee8b 100644 --- a/module/config/actorConfig.mjs +++ b/module/config/actorConfig.mjs @@ -123,6 +123,25 @@ export const adversaryTypes = { } }; +export const environmentTypes = { + exploration: { + label: 'DAGGERHEART.Environment.Type.Exploration.label', + description: 'DAGGERHEART.Environment.Type.Exploration.description' + }, + social: { + label: 'DAGGERHEART.Environment.Type.Social.label', + description: 'DAGGERHEART.Environment.Type.Social.description' + }, + traversal: { + label: 'DAGGERHEART.Environment.Type.Traversal.label', + description: 'DAGGERHEART.Environment.Type.Traversal.description' + }, + event: { + label: 'DAGGERHEART.Environment.Type.Event.label', + description: 'DAGGERHEART.Environment.Type.Event.description' + } +}; + export const adversaryTraits = { relentless: { name: 'DAGGERHEART.Adversary.Trait..Name', diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index fb596347..47736284 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -175,25 +175,21 @@ export const deathMoves = { }; export const tiers = { - 0: { - key: 0, - id: 'tier0', - name: 'DAGGERHEART.General.Tier.0' - }, - 1: { - key: 1, + tier1: { id: 'tier1', - name: 'DAGGERHEART.General.Tier.1' + label: 'DAGGERHEART.Tiers.tier1' }, - 2: { - key: 2, + tier2: { id: 'tier2', - name: 'DAGGERHEART.General.Tier.2' + label: 'DAGGERHEART.Tiers.tier2' }, - 3: { - key: 3, + tier3: { id: 'tier3', - name: 'DAGGERHEART.General.Tier.3' + label: 'DAGGERHEART.Tiers.tier3' + }, + tier4: { + id: 'tier4', + label: 'DAGGERHEART.Tiers.tier4' } }; diff --git a/module/data/adversary.mjs b/module/data/adversary.mjs index 3e8cdaf6..0f25b62d 100644 --- a/module/data/adversary.mjs +++ b/module/data/adversary.mjs @@ -14,7 +14,10 @@ export default class DhpAdversary extends foundry.abstract.TypeDataModel { max: new fields.NumberField({ initial: 0, integer: true }) }) }), - tier: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.tiers), integer: false }), + tier: new fields.StringField({ + choices: Object.keys(SYSTEM.GENERAL.tiers), + initial: SYSTEM.GENERAL.tiers.tier1.id + }), type: new fields.StringField({ choices: Object.keys(SYSTEM.ACTOR.adversaryTypes), integer: false, diff --git a/module/data/environment.mjs b/module/data/environment.mjs index 23e9cd25..a2cd7529 100644 --- a/module/data/environment.mjs +++ b/module/data/environment.mjs @@ -1,22 +1,28 @@ -export default class DhpEnvironment extends foundry.abstract.TypeDataModel { +import { environmentTypes } from '../config/actorConfig.mjs'; +import ForeignDocumentUUIDField from './fields/foreignDocumentUUIDField.mjs'; + +export default class DhEnvironment extends foundry.abstract.TypeDataModel { + static LOCALIZATION_PREFIXES = ['DAGGERHEART.Sheets.Environment']; + static defineSchema() { const fields = foundry.data.fields; return { - resources: new fields.SchemaField({}), - tier: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.tiers), integer: false }), - type: new fields.StringField({ - choices: Object.keys(SYSTEM.ACTOR.adversaryTypes), - integer: false, - initial: Object.keys(SYSTEM.ACTOR.adversaryTypes).find(x => x === 'standard') + tier: new fields.StringField({ + required: true, + choices: SYSTEM.GENERAL.tiers, + initial: SYSTEM.GENERAL.tiers.tier1.id }), - description: new fields.StringField({}), - toneAndFeel: new fields.StringField({}), - difficulty: new fields.NumberField({ initial: 1, integer: true }), - potentialAdversaries: new fields.StringField({}) + type: new fields.StringField({ choices: environmentTypes }), + description: new fields.HTMLField(), + impulses: new fields.HTMLField(), + difficulty: new fields.NumberField({ required: true, initial: 11, integer: true }), + potentialAdversaries: new fields.TypedObjectField( + new fields.SchemaField({ + label: new fields.StringField(), + adversaries: new fields.TypedObjectField(new ForeignDocumentUUIDField({ type: 'Actor' })) + }) + ) + /* Features pending datamodel rework */ }; } - - get features() { - return this.parent.items.filter(x => x.type === 'feature'); - } } diff --git a/module/helpers/handlebarsHelper.mjs b/module/helpers/handlebarsHelper.mjs index 87d1fb7f..25dd0e5e 100644 --- a/module/helpers/handlebarsHelper.mjs +++ b/module/helpers/handlebarsHelper.mjs @@ -11,6 +11,7 @@ export default class RegisterHandlebarsHelpers { includes: this.includes, debug: this.debug, signedNumber: this.signedNumber, + length: this.length, switch: this.switch, case: this.case }); @@ -82,6 +83,10 @@ export default class RegisterHandlebarsHelpers { return number >= 0 ? `+${number}` : number; } + static length(obj) { + return Object.keys(obj).length; + } + static switch(value, options) { this.switch_value = value; this.switch_break = false; diff --git a/styles/daggerheart.css b/styles/daggerheart.css index 78af5ecc..cb4ede19 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -2593,6 +2593,28 @@ div.daggerheart.views.multiclass { width: 40px; background: white; } +.daggerheart.sheet.actor.environment .potential-adversary-container { + width: 100%; + height: 50px; +} +.daggerheart.sheet.actor.environment .potential-adversary-container .adversary-placeholder { + font-style: italic; + text-align: center; + opacity: 0.6; +} +.daggerheart.sheet.actor.environment .potential-adversary-container .adversaries-container { + display: flex; + gap: 8px; +} +.daggerheart.sheet.actor.environment .potential-adversary-container .adversaries-container .adversary-container { + border: 1px solid var(--color-dark-5); + border-radius: 6px; + padding: 0 2px; + font-weight: bold; + cursor: pointer; + background-image: url(../assets/parchments/dh-parchment-dark.png); + color: var(--color-light-3); +} .daggerheart.sheet .title-container { display: flex; gap: 8px; @@ -3354,6 +3376,9 @@ div.daggerheart.views.multiclass { grid-template-columns: 1fr 2fr; gap: 10px; } +.application.sheet.dh-style fieldset.two-columns.even { + grid-template-columns: 1fr 1fr; +} .application.sheet.dh-style fieldset legend { font-family: 'Montserrat', sans-serif; font-weight: bold; diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index 077d2226..14345ca6 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -103,6 +103,10 @@ display: grid; grid-template-columns: 1fr 2fr; gap: 10px; + + &.even { + grid-template-columns: 1fr 1fr; + } } legend { diff --git a/styles/sheets/environment.less b/styles/sheets/environment.less new file mode 100644 index 00000000..d534de38 --- /dev/null +++ b/styles/sheets/environment.less @@ -0,0 +1,27 @@ +.daggerheart.sheet.actor.environment { + .potential-adversary-container { + width: 100%; + height: 50px; + + .adversary-placeholder { + font-style: italic; + text-align: center; + opacity: 0.6; + } + + .adversaries-container { + display: flex; + gap: 8px; + + .adversary-container { + border: 1px solid var(--color-dark-5); + border-radius: 6px; + padding: 0 2px; + font-weight: bold; + cursor: pointer; + background-image: url(../assets/parchments/dh-parchment-dark.png); + color: var(--color-light-3); + } + } + } +} diff --git a/styles/sheets/sheets.less b/styles/sheets/sheets.less index 19c76980..3ce23be8 100644 --- a/styles/sheets/sheets.less +++ b/styles/sheets/sheets.less @@ -1,6 +1,7 @@ @import './heritage.less'; @import './class.less'; @import './adversary.less'; +@import './environment.less'; .daggerheart.sheet { .title-container { diff --git a/system.json b/system.json index f90f53c8..1717e6e6 100644 --- a/system.json +++ b/system.json @@ -163,10 +163,7 @@ "name": "Daggerheart", "sorting": "m", "color": "#08718c", - "packs": [ - "adversaries", - "environments" - ], + "packs": ["adversaries", "environments"], "folders": [ { "name": "Character Options", @@ -186,12 +183,7 @@ "name": "Items", "sorting": "m", "color": "#000000", - "packs": [ - "weapons", - "armors", - "consumables", - "general-items" - ] + "packs": ["weapons", "armors", "consumables", "general-items"] } ] } @@ -213,7 +205,9 @@ "Actor": { "pc": {}, "adversary": {}, - "environment": {} + "environment": { + "htmlFields": ["description", "impulses"] + } }, "Item": { "ancestry": { diff --git a/templates/sheets/actors/environment/header.hbs b/templates/sheets/actors/environment/header.hbs new file mode 100644 index 00000000..a21f5f64 --- /dev/null +++ b/templates/sheets/actors/environment/header.hbs @@ -0,0 +1,9 @@ +
+ +
+

+
+

{{localize 'TYPES.Actor.environment'}}

+
+
+
\ No newline at end of file diff --git a/templates/sheets/actors/environment/information.hbs b/templates/sheets/actors/environment/information.hbs new file mode 100644 index 00000000..3b49a933 --- /dev/null +++ b/templates/sheets/actors/environment/information.hbs @@ -0,0 +1,16 @@ +
+
+ {{localize "DAGGERHEART.Sheets.Environment.description"}} + + {{formInput systemFields.description value=source.system.description }} +
+
+ {{localize "DAGGERHEART.Sheets.Environment.impulses"}} + + {{formInput systemFields.impulses value=source.system.impulses }} +
+
\ No newline at end of file diff --git a/templates/sheets/actors/environment/main.hbs b/templates/sheets/actors/environment/main.hbs new file mode 100644 index 00000000..a973cdc9 --- /dev/null +++ b/templates/sheets/actors/environment/main.hbs @@ -0,0 +1,37 @@ +
+
+ {{localize "DAGGERHEART.Sheets.Environment.general"}} + + {{formGroup systemFields.tier value=source.system.tier localize=true }} + {{formGroup systemFields.type value=source.system.type localize=true }} + {{formGroup systemFields.difficulty value=source.system.difficulty localize=true }} +
+ +
+ {{localize "DAGGERHEART.Sheets.Environment.potentialAdversaries.label"}} + + {{#each source.system.potentialAdversaries}} +
+ + {{#if (eq (length this.adversaries) 0)}} +
{{localize "DAGGERHEART.Sheets.Environment.potentialAdversaries.placeholder"}}
+ {{else}} +
+ {{#each this.adversaries as |adversary id|}} +
{{adversary.name}}
+ {{/each}} +
+ {{/if}} +
+ {{/each}} +
+ +
+ {{localize "DAGGERHEART.Sheets.Environment.features.label"}} + +
+
\ No newline at end of file From 02f16f7363fa3d2d360e9a96ea3d355c90977e31 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Mon, 9 Jun 2025 13:33:33 +0200 Subject: [PATCH 3/7] 118 - adversary data model (#119) * Fixed datamodel and set up basic template in new style * Added in a temp attack button, because why not * Restored HitPoints counting up --- lang/en.json | 145 ++++---- module/applications/chatMessage.mjs | 6 +- module/applications/sheets/adversary.mjs | 322 ++---------------- module/config/actorConfig.mjs | 34 +- module/config/generalConfig.mjs | 5 + module/data/adversary.mjs | 71 ++-- styles/daggerheart.css | 273 ++------------- styles/daggerheart.less | 3 + styles/less/actors/adversary.less | 5 + .../{sheets => less/actors}/environment.less | 0 styles/less/global/elements.less | 4 + styles/sheets/adversary.less | 280 --------------- styles/sheets/sheets.less | 2 - system.json | 4 +- templates/chat/adversary-attack-roll.hbs | 2 +- templates/sheets/actors/adversary/header.hbs | 9 + .../sheets/actors/adversary/information.hbs | 17 + templates/sheets/actors/adversary/main.hbs | 60 ++++ 18 files changed, 307 insertions(+), 935 deletions(-) create mode 100644 styles/less/actors/adversary.less rename styles/{sheets => less/actors}/environment.less (100%) delete mode 100644 styles/sheets/adversary.less create mode 100644 templates/sheets/actors/adversary/header.hbs create mode 100644 templates/sheets/actors/adversary/information.hbs create mode 100644 templates/sheets/actors/adversary/main.hbs diff --git a/lang/en.json b/lang/en.json index 26cedccb..78532b49 100755 --- a/lang/en.json +++ b/lang/en.json @@ -268,45 +268,47 @@ "tier4": "Tier 4" }, "Adversary": { - "Bruiser": { - "Name": "Bruiser", - "Description": "Tough adversaries with powerful attacks." - }, - "Horde": { - "Name": "Horde", - "Description": "A Horde represents a number of foes working in a group." - }, - "Leader": { - "Name": "Leader", - "Description": "Adversaries that command and summon other adversaries." - }, - "Minion": { - "Name": "Minion", - "Description": "Basic enemies that are easily dispatched but dangerous in numbers." - }, - "Ranged": { - "Name": "Ranged", - "Description": "Adversaries that attack from a distance." - }, - "Skulker": { - "Name": "Skulker", - "Description": "Adversaries that maneuver and exploit opportunities to ambush their opponents." - }, - "Social": { - "Name": "Social", - "Description": "Adversaries that are primarily interpersonal threats or challenges." - }, - "Solo": { - "Name": "Solo", - "Description": "Designed to present a challenge to a whole party." - }, - "Standard": { - "Name": "Standard", - "Description": "Rank and File adversaries." - }, - "Support": { - "Name": "Support", - "Description": "Enemies that enhance their allies and/or disrupt their opponents." + "Type": { + "Bruiser": { + "label": "Bruiser", + "Description": "Tough adversaries with powerful attacks." + }, + "Horde": { + "label": "Horde", + "Description": "A Horde represents a number of foes working in a group." + }, + "Leader": { + "label": "Leader", + "Description": "Adversaries that command and summon other adversaries." + }, + "Minion": { + "label": "Minion", + "Description": "Basic enemies that are easily dispatched but dangerous in numbers." + }, + "Ranged": { + "label": "Ranged", + "Description": "Adversaries that attack from a distance." + }, + "Skulk": { + "label": "Skulk", + "Description": "Adversaries that maneuver and exploit opportunities to ambush their opponents." + }, + "Social": { + "label": "Social", + "Description": "Adversaries that are primarily interpersonal threats or challenges." + }, + "Solo": { + "label": "Solo", + "Description": "Designed to present a challenge to a whole party." + }, + "Standard": { + "label": "Standard", + "Description": "Rank and File adversaries." + }, + "Support": { + "label": "Support", + "Description": "Enemies that enhance their allies and/or disrupt their opponents." + } }, "Trait": { "Relentless": { @@ -1025,35 +1027,52 @@ } }, "Adversary": { - "Description": "Description", - "MotivesAndTactics": "Motives & Tactics", - "Tier": "Tier", - "Type": "Type", - "Attack": { - "Title": "Attack", - "Modifier": "Attack Modifier", - "Name": "Name", - "Range": "Range", - "Damage": { - "Title": "Damage", - "Value": "Value", - "Type": "Type" + "FIELDS": { + "tier": { "label": "Tier" }, + "type": { "label": "Type" }, + "description": { "label": "Description" }, + "motivesAndTactics": { "label": "Motives & Tactics" }, + "difficulty": { "label": "Difficulty" }, + "damageThresholds": { + "major": { "label": "Major" }, + "severe": { "label": "Severe" } + }, + "resources": { + "hitPoints": { + "value": { "label": "Current" }, + "max": { "label": "Max" } + }, + "stress": { + "value": { "label": "Current" }, + "max": { "label": "Max" } + } + }, + "experiences": { + "element": { + "name": { "label": "Name" }, + "value": { "label": "Modifier" } + } + }, + "attack": { + "name": { "label": "Name" }, + "modifier": { "label": "Modifier" }, + "range": { "label": "Range" }, + "damage": { + "value": { "label": "Damage" }, + "type": { "label": "Damage Type" } + } } }, - "Difficulty": "Difficulty", - "Reaction": "Reaction Roll", - "DamageThresholds": { - "Title": "Damage Thresholds", - "Minor": "Minor", - "Major": "Major", - "Severe": "Severe" + "Tabs": { + "Main": "Data", + "Information": "Information" }, - "HP": "HP", + "General": "General", + "DamageThresholds": "Damage Thresholds", + "HitPoints": "Hit Points", "Stress": "Stress", - "Experience": "Experience", "Experiences": "Experiences", - "Features": "Features", - "NewFeature": "New Feature" + "Attack": "Attack" }, "Environment": { "FIELDS": { diff --git a/module/applications/chatMessage.mjs b/module/applications/chatMessage.mjs index 30bd0a27..aef05bae 100644 --- a/module/applications/chatMessage.mjs +++ b/module/applications/chatMessage.mjs @@ -1,12 +1,8 @@ import { DualityRollColor } from '../data/settings/Appearance.mjs'; -import DHDualityRoll from "../data/chat-message/dualityRoll.mjs"; +import DHDualityRoll from '../data/chat-message/dualityRoll.mjs'; export default class DhpChatMessage extends foundry.documents.ChatMessage { async renderHTML() { - if (this.type === 'dualityRoll' || this.type === 'adversaryRoll' || this.type === 'abilityUse') { - this.content = await foundry.applications.handlebars.renderTemplate(this.content, this.system); - } - /* We can change to fully implementing the renderHTML function if needed, instead of augmenting it. */ const html = await super.renderHTML(); diff --git a/module/applications/sheets/adversary.mjs b/module/applications/sheets/adversary.mjs index b9180bf3..8345b532 100644 --- a/module/applications/sheets/adversary.mjs +++ b/module/applications/sheets/adversary.mjs @@ -1,219 +1,12 @@ -// import DhpApplicationMixin from '../daggerheart-sheet.mjs'; - -// export class Teest extends DhpApplicationMixin(ActorSheet) { -// static documentType = "adversary"; - -// constructor(options){ -// super(options); - -// this.editMode = false; -// } - -// /** @override */ -// static get defaultOptions() { -// return foundry.utils.mergeObject(super.defaultOptions, { -// classes: ["daggerheart", "sheet", "adversary"], -// width: 600, -// height: 'auto', -// resizable: false, -// }); -// } - -// async getData() { -// const context = super.getData(); -// context.config = SYSTEM; -// context.editMode = this.editMode; -// context.title = `${this.actor.name} - ${game.i18n.localize(SYSTEM.ACTOR.adversaryTypes[this.actor.system.type].name)}`; - -// context.data = { -// description: this.object.system.description, -// motivesAndTactics: this.object.system.motivesAndTactics.join(', '), -// tier: this.object.system.tier, -// type: game.i18n.localize(SYSTEM.ACTOR.adversaryTypes[this.object.system.type].name), -// attack: { -// name: this.object.system.attack.name, -// attackModifier: this.object.system.attackModifier, -// range: this.object.system.attack.range ? game.i18n.localize(SYSTEM.GENERAL.range[this.object.system.attack.range].name) : null, -// damage: { -// value: this.object.system.attack.damage.value, -// type: this.object.system.attack.damage.type, -// typeName: this.object.system.attack.damage.type ? game.i18n.localize(SYSTEM.GENERAL.damageTypes[this.object.system.attack.damage.type].abbreviation).toLowerCase() : null, -// }, -// }, -// damageThresholds: this.object.system.damageThresholds, -// difficulty: this.object.system.difficulty, -// hp: { ...this.object.system.resources.health, lastRowIndex: Math.floor(this.object.system.resources.health.max/5)*5 }, -// stress: { ...this.object.system.resources.stress, lastRowIndex: Math.floor(this.object.system.resources.stress.max/5)*5 }, -// moves: this.object.system.moves, -// }; - -// return context; -// } - -// async _handleAction(action, event, button) { -// switch(action){ -// case 'viewMove': -// await this.viewMove(button); -// break; -// case 'addMove': -// this.addMove(); -// break; -// case 'removeMove': -// await this.removeMove(button); -// break; -// case 'toggleSlider': -// this.toggleEditMode(); -// break; -// case 'addMotive': -// await this.addMotive(); -// break; -// case 'removeMotive': -// await this.removeMotive(button); -// break; -// case 'reactionRoll': -// await this.reactionRoll(event); -// break; -// case 'attackRoll': -// await this.attackRoll(event); -// break; -// case 'addExperience': -// await this.addExperience(); -// break; -// case 'removeExperience': -// await this.removeExperience(button); -// break; -// case 'toggleHP': -// await this.toggleHP(button); -// break; -// case 'toggleStress': -// await this.toggleStress(button); -// break; -// } -// } - -// async viewMove(button){ -// const move = await fromUuid(button.dataset.move); -// move.sheet.render(true); -// } - -// async addMove(){ -// const result = await this.object.createEmbeddedDocuments("Item", [{ -// name: game.i18n.localize('DAGGERHEART.Sheets.Adversary.NewMove'), -// type: 'feature', -// }]); - -// await result[0].sheet.render(true); -// } - -// async removeMove(button){ -// await this.object.items.find(x => x.uuid === button.dataset.move).delete(); -// } - -// toggleEditMode(){ -// this.editMode = !this.editMode; -// this.render(); -// } - -// async addMotive(){ -// await this.object.update({ "system.motivesAndTactics": [...this.object.system.motivesAndTactics, ''] }); -// } - -// async removeMotive(button){ -// await this.object.update({ "system.motivesAndTactics": this.object.system.motivesAndTactics.filter((_, index) => index !== Number.parseInt(button.dataset.motive) )}); -// } - -// async reactionRoll(event){ -// const { roll, diceResults, modifiers } = await this.actor.diceRoll({ title: `${this.actor.name} - Reaction Roll`, value: 0 }, event.shiftKey); - -// const cls = getDocumentClass("ChatMessage"); -// const msg = new cls({ -// type: 'adversaryRoll', -// system: { -// roll: roll._formula, -// total: roll._total, -// modifiers: modifiers, -// diceResults: diceResults, -// }, -// content: "systems/daggerheart/templates/chat/adversary-roll.hbs", -// rolls: [roll] -// }); - -// cls.create(msg.toObject()); -// } - -// async attackRoll(event){ -// const modifier = Number.parseInt(event.currentTarget.dataset.value); - -// const { roll, diceResults, modifiers } = await this.actor.diceRoll({ title: `${this.actor.name} - Attack Roll`, value: modifier }, event.shiftKey); - -// const targets = Array.from(game.user.targets).map(x => ({ -// id: x.id, -// name: x.actor.name, -// img: x.actor.img, -// difficulty: x.actor.system.difficulty, -// evasion: x.actor.system.evasion, -// })); - -// const cls = getDocumentClass("ChatMessage"); -// const msg = new cls({ -// type: 'adversaryRoll', -// system: { -// roll: roll._formula, -// total: roll._total, -// modifiers: modifiers, -// diceResults: diceResults, -// targets: targets, -// damage: { value: event.currentTarget.dataset.damage, type: event.currentTarget.dataset.damageType }, -// }, -// content: "systems/daggerheart/templates/chat/adversary-attack-roll.hbs", -// rolls: [roll] -// }); - -// cls.create(msg.toObject()); -// } - -// async addExperience(){ -// await this.object.update({ "system.experiences": [...this.object.system.experiences, { name: 'Experience', value: 1 }] }); -// } - -// async removeExperience(button){ -// await this.object.update({ "system.experiences": this.object.system.experiences.filter((_, index) => index !== Number.parseInt(button.dataset.experience) )}); -// } - -// async toggleHP(button){ -// const index = Number.parseInt(button.dataset.index); -// const newHP = index < this.object.system.resources.health.value ? index : index+1; -// await this.object.update({ "system.resources.health.value": newHP }); -// } - -// async toggleStress(button){ -// const index = Number.parseInt(button.dataset.index); -// const newStress = index < this.object.system.resources.stress.value ? index : index+1; -// await this.object.update({ "system.resources.stress.value": newStress }); -// } -// } - import DaggerheartSheet from './daggerheart-sheet.mjs'; const { ActorSheetV2 } = foundry.applications.sheets; export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) { - constructor(options = {}) { - super(options); - - this.editMode = false; - } - static DEFAULT_OPTIONS = { tag: 'form', - classes: ['daggerheart', 'sheet', 'adversary'], - position: { width: 600 }, + classes: ['daggerheart', 'sheet', 'actor', 'dh-style', 'adversary'], + position: { width: 450, height: 1000 }, actions: { - viewMove: this.viewMove, - addMove: this.addMove, - removeMove: this.removeMove, - toggleSlider: this.toggleEditMode, - addMotive: this.addMotive, - removeMotive: this.removeMotive, reactionRoll: this.reactionRoll, attackRoll: this.attackRoll, addExperience: this.addExperience, @@ -229,54 +22,35 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) { }; static PARTS = { - form: { - id: 'feature', - template: 'systems/daggerheart/templates/sheets/adversary.hbs' + header: { template: 'systems/daggerheart/templates/sheets/actors/adversary/header.hbs' }, + tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, + main: { template: 'systems/daggerheart/templates/sheets/actors/adversary/main.hbs' }, + information: { template: 'systems/daggerheart/templates/sheets/actors/adversary/information.hbs' } + }; + + static TABS = { + main: { + active: true, + cssClass: '', + group: 'primary', + id: 'main', + icon: null, + label: 'DAGGERHEART.Sheets.Adversary.Tabs.Main' + }, + information: { + active: false, + cssClass: '', + group: 'primary', + id: 'information', + icon: null, + label: 'DAGGERHEART.Sheets.Adversary.Tabs.Information' } }; async _prepareContext(_options) { const context = await super._prepareContext(_options); context.document = this.document; - context.config = SYSTEM; - context.editMode = this.editMode; - context.title = `${this.actor.name} - ${game.i18n.localize(SYSTEM.ACTOR.adversaryTypes[this.actor.system.type].name)}`; - - context.data = { - description: this.document.system.description, - motivesAndTactics: this.document.system.motivesAndTactics.join(', '), - tier: this.document.system.tier, - type: game.i18n.localize(SYSTEM.ACTOR.adversaryTypes[this.document.system.type].name), - attack: { - name: this.document.system.attack.name, - attackModifier: this.document.system.attackModifier, - range: this.document.system.attack.range - ? game.i18n.localize(SYSTEM.GENERAL.range[this.document.system.attack.range].name) - : null, - damage: { - value: this.document.system.attack.damage.value, - type: this.document.system.attack.damage.type, - typeName: this.document.system.attack.damage.type - ? game.i18n - .localize( - SYSTEM.GENERAL.damageTypes[this.document.system.attack.damage.type].abbreviation - ) - .toLowerCase() - : null - } - }, - damageThresholds: this.document.system.damageThresholds, - difficulty: this.document.system.difficulty, - hp: { - ...this.document.system.resources.health, - lastRowIndex: Math.floor(this.document.system.resources.health.max / 5) * 5 - }, - stress: { - ...this.document.system.resources.stress, - lastRowIndex: Math.floor(this.document.system.resources.stress.max / 5) * 5 - }, - moves: this.document.system.moves - }; + context.tabs = super._getTabs(this.constructor.TABS); return context; } @@ -286,43 +60,6 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) { this.render(); } - static async viewMove(_, button) { - const move = await fromUuid(button.dataset.move); - move.sheet.render(true); - } - - static async addMove() { - const result = await this.document.createEmbeddedDocuments('Item', [ - { - name: game.i18n.localize('DAGGERHEART.Sheets.Adversary.NewMove'), - type: 'feature' - } - ]); - - await result[0].sheet.render(true); - } - - static async removeMove(_, button) { - await this.document.items.find(x => x.uuid === button.dataset.move).delete(); - } - - static toggleEditMode() { - this.editMode = !this.editMode; - this.render(); - } - - static async addMotive() { - await this.document.update({ 'system.motivesAndTactics': [...this.document.system.motivesAndTactics, ''] }); - } - - static async removeMotive(button) { - await this.document.update({ - 'system.motivesAndTactics': this.document.system.motivesAndTactics.filter( - (_, index) => index !== Number.parseInt(button.dataset.motive) - ) - }); - } - static async reactionRoll(event) { const { roll, diceResults, modifiers } = await this.actor.diceRoll( { title: `${this.actor.name} - Reaction Roll`, value: 0 }, @@ -349,9 +86,8 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) { cls.create(msg.toObject()); } - static async attackRoll(event, button) { - const modifier = Number.parseInt(button.dataset.value); - + static async attackRoll() { + const { modifier, damage, name: attackName } = this.actor.system.attack; const { roll, dice, advantageState, modifiers } = await this.actor.diceRoll( { title: `${this.actor.name} - Attack Roll`, value: modifier }, event.shiftKey @@ -367,7 +103,7 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) { const cls = getDocumentClass('ChatMessage'); const systemData = { - title: button.dataset.name, + title: attackName, origin: this.document.id, roll: roll._formula, advantageState, @@ -375,7 +111,7 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) { modifiers: modifiers, dice: dice, targets: targets, - damage: { value: button.dataset.damage, type: button.dataset.damageType } + damage: { value: damage.value, type: damage.type } }; const msg = new cls({ type: 'adversaryRoll', diff --git a/module/config/actorConfig.mjs b/module/config/actorConfig.mjs index bcb6ee8b..dcd85cdb 100644 --- a/module/config/actorConfig.mjs +++ b/module/config/actorConfig.mjs @@ -82,43 +82,53 @@ export const featureProperties = { export const adversaryTypes = { bruiser: { - name: 'DAGGERHEART.Adversary.Bruiser.Name', + id: 'bruiser', + label: 'DAGGERHEART.Adversary.Type.Bruiser.label', description: 'DAGGERHEART.Adversary.Bruiser.Description' }, horde: { - name: 'DAGGERHEART.Adversary.Horde.Name', + id: 'horde', + label: 'DAGGERHEART.Adversary.Type.Horde.label', description: 'DAGGERHEART.Adversary.Horde.Description' }, leader: { - name: 'DAGGERHEART.Adversary.Leader.Name', + id: 'leader', + label: 'DAGGERHEART.Adversary.Type.Leader.label', description: 'DAGGERHEART.Adversary.Leader.Description' }, minion: { - name: 'DAGGERHEART.Adversary.Minion.Name', + id: 'minion', + label: 'DAGGERHEART.Adversary.Type.Minion.label', description: 'DAGGERHEART.Adversary.Minion.Description' }, ranged: { - name: 'DAGGERHEART.Adversary.Ranged.Name', + id: 'ranged', + label: 'DAGGERHEART.Adversary.Type.Ranged.label', description: 'DAGGERHEART.Adversary.Ranged.Description' }, - skulker: { - name: 'DAGGERHEART.Adversary.Skulker.Name', - description: 'DAGGERHEART.Adversary.Skulker.Description' + skulk: { + id: 'skulk', + label: 'DAGGERHEART.Adversary.Type.Skulk.label', + description: 'DAGGERHEART.Adversary.Skulk.Description' }, social: { - name: 'DAGGERHEART.Adversary.Social.Name', + id: 'social', + label: 'DAGGERHEART.Adversary.Type.Social.label', description: 'DAGGERHEART.Adversary.Social.Description' }, solo: { - name: 'DAGGERHEART.Adversary.Solo.Name', + id: 'solo', + label: 'DAGGERHEART.Adversary.Type.Solo.label', description: 'DAGGERHEART.Adversary.Solo.Description' }, standard: { - name: 'DAGGERHEART.Adversary.Standard.Name', + id: 'standard', + label: 'DAGGERHEART.Adversary.Type.Standard.label', description: 'DAGGERHEART.Adversary.Standard.Description' }, support: { - name: 'DAGGERHEART.Adversary.Support.Name', + id: 'support', + label: 'DAGGERHEART.Adversary.Type.Support.label', description: 'DAGGERHEART.Adversary.Support.Description' } }; diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 47736284..fecb939e 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -1,25 +1,30 @@ export const range = { melee: { + id: 'melee', label: 'DAGGERHEART.Range.melee.name', description: 'DAGGERHEART.Range.melee.description', distance: 1 }, veryClose: { + id: 'veryClose', label: 'DAGGERHEART.Range.veryClose.name', description: 'DAGGERHEART.Range.veryClose.description', distance: 3 }, close: { + id: 'close', label: 'DAGGERHEART.Range.close.name', description: 'DAGGERHEART.Range.close.description', distance: 10 }, far: { + id: 'far', label: 'DAGGERHEART.Range.far.name', description: 'DAGGERHEART.Range.far.description', distance: 20 }, veryFar: { + id: 'veryFar', label: 'DAGGERHEART.Range.veryFar.name', description: 'DAGGERHEART.Range.veryFar.description', distance: 30 diff --git a/module/data/adversary.mjs b/module/data/adversary.mjs index 0f25b62d..7e4c28f1 100644 --- a/module/data/adversary.mjs +++ b/module/data/adversary.mjs @@ -1,55 +1,60 @@ +const resourceField = () => + new foundry.data.fields.SchemaField({ + value: new foundry.data.fields.NumberField({ initial: 0, integer: true }), + max: new foundry.data.fields.NumberField({ initial: 0, integer: true }) + }); + export default class DhpAdversary extends foundry.abstract.TypeDataModel { + static LOCALIZATION_PREFIXES = ['DAGGERHEART.Sheets.Adversary']; + static defineSchema() { const fields = foundry.data.fields; return { - resources: new fields.SchemaField({ - health: new fields.SchemaField({ - value: new fields.NumberField({ initial: 0, integer: true }), - min: new fields.NumberField({ initial: 0, integer: true }), - max: new fields.NumberField({ initial: 0, integer: true }) - }), - stress: new fields.SchemaField({ - value: new fields.NumberField({ initial: 0, integer: true }), - min: new fields.NumberField({ initial: 0, integer: true }), - max: new fields.NumberField({ initial: 0, integer: true }) - }) - }), tier: new fields.StringField({ - choices: Object.keys(SYSTEM.GENERAL.tiers), + required: true, + choices: SYSTEM.GENERAL.tiers, initial: SYSTEM.GENERAL.tiers.tier1.id }), type: new fields.StringField({ - choices: Object.keys(SYSTEM.ACTOR.adversaryTypes), - integer: false, - initial: Object.keys(SYSTEM.ACTOR.adversaryTypes).find(x => x === 'standard') + required: true, + choices: SYSTEM.ACTOR.adversaryTypes, + initial: SYSTEM.ACTOR.adversaryTypes.standard.id + }), + description: new fields.HTMLField(), + motivesAndTactics: new fields.HTMLField(), + difficulty: new fields.NumberField({ required: true, initial: 1, integer: true }), + damageThresholds: new fields.SchemaField({ + major: new fields.NumberField({ required: true, initial: 0, integer: true }), + severe: new fields.NumberField({ required: true, initial: 0, integer: true }) + }), + resources: new fields.SchemaField({ + hitPoints: resourceField(), + stress: resourceField() }), - description: new fields.StringField({}), - motivesAndTactics: new fields.ArrayField(new fields.StringField({})), - attackModifier: new fields.NumberField({ integer: true, nullabe: true, initial: null }), attack: new fields.SchemaField({ name: new fields.StringField({}), - range: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.range), integer: false }), + modifier: new fields.NumberField({ required: true, integer: true, initial: 0 }), + range: new fields.StringField({ + required: true, + choices: SYSTEM.GENERAL.range, + initial: SYSTEM.GENERAL.range.melee.id + }), damage: new fields.SchemaField({ - value: new fields.StringField({}), - type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.damageTypes), integer: false }) + value: new fields.StringField(), + type: new fields.StringField({ + required: true, + choices: SYSTEM.GENERAL.damageTypes, + initial: SYSTEM.GENERAL.damageTypes.physical.id + }) }) }), - difficulty: new fields.NumberField({ initial: 1, integer: true }), - damageThresholds: new fields.SchemaField({ - major: new fields.NumberField({ initial: 0, integer: true }), - severe: new fields.NumberField({ initial: 0, integer: true }) - }), experiences: new fields.TypedObjectField( new fields.SchemaField({ - id: new fields.StringField({ required: true }), name: new fields.StringField(), - value: new fields.NumberField({ integer: true, nullable: true, initial: null }) + value: new fields.NumberField({ required: true, integer: true, initial: 1 }) }) ) + /* Features waiting on pseudo-document data model addition */ }; } - - get features() { - return this.parent.items.filter(x => x.type === 'feature'); - } } diff --git a/styles/daggerheart.css b/styles/daggerheart.css index cb4ede19..96b949fc 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -2370,251 +2370,6 @@ div.daggerheart.views.multiclass { align-items: center; gap: 5px; } -.daggerheart.sheet.adversary .adversary-header-container { - position: relative; - background-color: grey; - display: flex; -} -.daggerheart.sheet.adversary .adversary-header-container .adversary-header { - flex: 1; -} -.daggerheart.sheet.adversary .adversary-header-container .adversary-header img { - height: 60px; - width: 60px; -} -.daggerheart.sheet.adversary .adversary-header-container .adversary-header .adversary-title { - display: flex; - align-items: center; - text-align: center; - font-size: 28px; -} -.daggerheart.sheet.adversary .adversary-header-container .adversary-header .adversary-title .title-text { - width: 100%; -} -.daggerheart.sheet.adversary .adversary-header-container .adversary-header .adversary-title input { - font-size: 28px; - border: 0; - height: 100%; -} -.daggerheart.sheet.adversary .adversary-header-container .adversary-toggle { - position: absolute; - top: 0; - right: 0; - background-color: white; - color: black; - flex: 0; -} -.daggerheart.sheet.adversary .motive-container { - background: lightgrey; - margin-bottom: 8px; - padding-bottom: 4px; -} -.daggerheart.sheet.adversary .motive-container .motive-title { - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; -} -.daggerheart.sheet.adversary .motive-container .motive-title .motive-title-base { - font-size: 21px; -} -.daggerheart.sheet.adversary .motive-container .motive-title .motive-title-value { - font-style: italic; - position: relative; - top: 2px; -} -.daggerheart.sheet.adversary .motive-container .motive-title i { - margin-left: 4px; - cursor: pointer; -} -.daggerheart.sheet.adversary .motive-container .motive-title i:hover { - filter: drop-shadow(0 0 3px red); -} -.daggerheart.sheet.adversary .adversary-content-container { - display: flex; - align-items: baseline; -} -.daggerheart.sheet.adversary .adversary-statistics-container { - flex: 1; - margin-right: 24px; - display: flex; - flex-direction: column; - gap: 12px; -} -.daggerheart.sheet.adversary .adversary-statistics-container .statistic-title { - flex: 0; - white-space: nowrap; - font-weight: bold; -} -.daggerheart.sheet.adversary .adversary-statistics-container .statistic-row { - display: flex; - align-items: center; -} -.daggerheart.sheet.adversary .adversary-statistics-container .statistic-row .statistic-value { - flex: 0; - white-space: nowrap; - margin-left: 4px; -} -.daggerheart.sheet.adversary .adversary-statistics-container .statistic-row .adversary-roll { - border: 0; - width: 16px; - margin-left: 4px; - align-self: baseline; - transition: transform 0.2s; -} -.daggerheart.sheet.adversary .adversary-statistics-container .statistic-row .adversary-roll:hover { - transform: rotate(30deg); - filter: drop-shadow(0px 0px 3px red); - cursor: pointer; -} -.daggerheart.sheet.adversary .adversary-statistics-container .statistic-resource-container { - display: flex; - align-items: center; -} -.daggerheart.sheet.adversary .adversary-statistics-container .statistic-resource-container label { - min-width: 44px; -} -.daggerheart.sheet.adversary .adversary-statistics-container .statistic-resource-container .statistic-resource-inner-container { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 4px; -} -.daggerheart.sheet.adversary .adversary-statistics-container .statistic-resource-container .resource-title { - align-self: center; - font-weight: bold; -} -.daggerheart.sheet.adversary .adversary-statistics-container .statistic-resource-container .statistic-resource-input { - margin: 0; - flex: 0; - min-width: 16px; -} -.daggerheart.sheet.adversary .adversary-statistics-container .attack-container { - border: 1px solid black dotted; -} -.daggerheart.sheet.adversary .adversary-statistics-container .experience-row { - display: flex; -} -.daggerheart.sheet.adversary .adversary-statistics-container .experience-row * { - flex: 0; - white-space: nowrap; -} -.daggerheart.sheet.adversary .adversary-statistics-container .experience-container i { - margin-left: 4px; - cursor: pointer; -} -.daggerheart.sheet.adversary .adversary-statistics-container .experience-container i:hover { - filter: drop-shadow(0 0 3px red); -} -.daggerheart.sheet.adversary .adversary-statistics-container .experience-chip { - border: 2px solid #708090; - border-radius: 6px; - display: flex; - align-items: center; - padding: 4px; - margin-bottom: 6px; -} -.daggerheart.sheet.adversary .adversary-statistics-container .experience-chip .experience-text { - flex: 1; -} -.daggerheart.sheet.adversary .adversary-statistics-container .experience-chip .experience-value { - flex: 0; - min-width: 26px; - margin: 0 4px; -} -.daggerheart.sheet.adversary .adversary-statistics-container .experience-chip .experience-button { - flex: 0; - border-radius: 50%; - height: 20px; - width: 20px; - display: flex; - align-items: center; - justify-content: center; - padding: 12px; -} -.daggerheart.sheet.adversary .adversary-damage-threshold-container input { - min-width: 26px; -} -.daggerheart.sheet.adversary .adversary-moves-container { - flex: 2.5; -} -.daggerheart.sheet.adversary .adversary-moves-container .moves-title { - text-decoration: underline; - font-weight: bold; -} -.daggerheart.sheet.adversary .adversary-moves-container .move-container { - cursor: pointer; -} -.daggerheart.sheet.adversary .adversary-moves-container .move-container:hover { - background: #2f4f4f40; -} -.daggerheart.sheet.adversary .adversary-moves-container .move-container .moves-name { - font-weight: bold; - text-decoration: none; -} -.daggerheart.sheet.adversary .adversary-moves-container .move-container .move-description p { - margin-top: 0; -} -.daggerheart.sheet.adversary .adversary-moves-container .moves-edit-container i { - margin-left: 4px; - cursor: pointer; -} -.daggerheart.sheet.adversary .adversary-moves-container .moves-edit-container i:hover { - filter: drop-shadow(0 0 3px red); -} -.daggerheart.sheet.adversary .chip-container { - display: flex; - align-items: center; - justify-content: space-between; - background: #778899; - padding: 8px; - border: 2px solid black; - border-radius: 6px; -} -.daggerheart.sheet.adversary .chip-container:not(:last-child) { - margin-bottom: 8px; -} -.daggerheart.sheet.adversary .chip-container .chip-inner-container { - display: flex; - align-items: center; -} -.daggerheart.sheet.adversary .chip-container .chip-inner-container img { - height: 40px; - width: 40px; - margin-right: 8px; -} -.daggerheart.sheet.adversary .chip-container .chip-inner-container .chip-title { - font-size: 22px; - font-weight: bold; - font-style: italic; -} -.daggerheart.sheet.adversary .chip-container button { - height: 40px; - width: 40px; - background: white; -} -.daggerheart.sheet.actor.environment .potential-adversary-container { - width: 100%; - height: 50px; -} -.daggerheart.sheet.actor.environment .potential-adversary-container .adversary-placeholder { - font-style: italic; - text-align: center; - opacity: 0.6; -} -.daggerheart.sheet.actor.environment .potential-adversary-container .adversaries-container { - display: flex; - gap: 8px; -} -.daggerheart.sheet.actor.environment .potential-adversary-container .adversaries-container .adversary-container { - border: 1px solid var(--color-dark-5); - border-radius: 6px; - padding: 0 2px; - font-weight: bold; - cursor: pointer; - background-image: url(../assets/parchments/dh-parchment-dark.png); - color: var(--color-light-3); -} .daggerheart.sheet .title-container { display: flex; gap: 8px; @@ -3100,6 +2855,31 @@ div.daggerheart.views.multiclass { #resources:has(.fear-bar) { min-width: 200px; } +.application.sheet.daggerheart.actor.dh-style.adversary .window-content { + overflow: auto; +} +.daggerheart.sheet.actor.environment .potential-adversary-container { + width: 100%; + height: 50px; +} +.daggerheart.sheet.actor.environment .potential-adversary-container .adversary-placeholder { + font-style: italic; + text-align: center; + opacity: 0.6; +} +.daggerheart.sheet.actor.environment .potential-adversary-container .adversaries-container { + display: flex; + gap: 8px; +} +.daggerheart.sheet.actor.environment .potential-adversary-container .adversaries-container .adversary-container { + border: 1px solid var(--color-dark-5); + border-radius: 6px; + padding: 0 2px; + font-weight: bold; + cursor: pointer; + background-image: url(../assets/parchments/dh-parchment-dark.png); + color: var(--color-light-3); +} .application.sheet.daggerheart.dh-style.feature .item-sheet-header { display: flex; } @@ -3379,6 +3159,9 @@ div.daggerheart.views.multiclass { .application.sheet.dh-style fieldset.two-columns.even { grid-template-columns: 1fr 1fr; } +.application.sheet.dh-style fieldset.two-columns .full-width { + grid-column: span 2; +} .application.sheet.dh-style fieldset legend { font-family: 'Montserrat', sans-serif; font-weight: bold; diff --git a/styles/daggerheart.less b/styles/daggerheart.less index 6869e316..8ec94054 100755 --- a/styles/daggerheart.less +++ b/styles/daggerheart.less @@ -13,6 +13,9 @@ @import './resources.less'; // new styles imports +@import './less/actors/adversary.less'; +@import './less/actors/environment.less'; + @import './less/items/feature.less'; @import './less/items/domainCard.less'; @import './less/items/class.less'; diff --git a/styles/less/actors/adversary.less b/styles/less/actors/adversary.less new file mode 100644 index 00000000..5b4feb26 --- /dev/null +++ b/styles/less/actors/adversary.less @@ -0,0 +1,5 @@ +.application.sheet.daggerheart.actor.dh-style.adversary { + .window-content { + overflow: auto; + } +} diff --git a/styles/sheets/environment.less b/styles/less/actors/environment.less similarity index 100% rename from styles/sheets/environment.less rename to styles/less/actors/environment.less diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index 14345ca6..797b2328 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -107,6 +107,10 @@ &.even { grid-template-columns: 1fr 1fr; } + + .full-width { + grid-column: span 2; + } } legend { diff --git a/styles/sheets/adversary.less b/styles/sheets/adversary.less deleted file mode 100644 index 657ce218..00000000 --- a/styles/sheets/adversary.less +++ /dev/null @@ -1,280 +0,0 @@ -.daggerheart.sheet.adversary { - .adversary-header-container { - position: relative; - background-color: grey; - display: flex; - - .adversary-header { - flex: 1; - - img { - height: 60px; - width: 60px; - } - - .adversary-title { - display: flex; - align-items: center; - text-align: center; - font-size: 28px; - - .title-text { - width: 100%; - } - - input { - font-size: 28px; - border: 0; - height: 100%; - } - } - } - - .adversary-toggle { - position: absolute; - top: 0; - right: 0; - background-color: white; - color: black; - flex: 0; - } - } - - .motive-container { - background: lightgrey; - margin-bottom: @fullMargin; - padding-bottom: @fullPadding; - - .motive-title { - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - - .motive-title-base { - font-size: 21px; - } - - .motive-title-value { - font-style: italic; - position: relative; - top: 2px; - } - - i { - margin-left: 4px; - cursor: pointer; - - &:hover { - filter: drop-shadow(0 0 3px red); - } - } - } - } - - .adversary-content-container { - display: flex; - align-items: baseline; - } - - .adversary-statistics-container { - flex: 1; - margin-right: 24px; - display: flex; - flex-direction: column; - gap: @mediumMargin; - - .statistic-title { - flex: 0; - white-space: nowrap; - font-weight: bold; - } - - .statistic-row { - display: flex; - align-items: center; - - .statistic-value { - flex: 0; - white-space: nowrap; - margin-left: 4px; - } - - .adversary-roll { - border: 0; - width: 16px; - margin-left: 4px; - align-self: baseline; - transition: transform 0.2s; - - &:hover { - transform: rotate(30deg); - filter: drop-shadow(0px 0px 3px red); - cursor: pointer; - } - } - } - - .statistic-resource-container { - display: flex; - align-items: center; - - label { - min-width: 44px; - } - - .statistic-resource-inner-container { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: @halfMargin; - } - - .resource-title { - align-self: center; - font-weight: bold; - } - - .statistic-resource-input { - margin: 0; - flex: 0; - min-width: 16px; - } - } - - .attack-container { - border: 1px solid black dotted; - } - - .experience-row { - display: flex; - - * { - flex: 0; - white-space: nowrap; - } - } - - .experience-container { - i { - margin-left: 4px; - cursor: pointer; - - &:hover { - filter: drop-shadow(0 0 3px red); - } - } - } - - .experience-chip { - border: 2px solid @secondaryAccent; - border-radius: 6px; - display: flex; - align-items: center; - padding: 4px; - margin-bottom: 6px; - - .experience-text { - flex: 1; - } - - .experience-value { - flex: 0; - min-width: @inputSingleMinWidth; - margin: 0 4px; - } - - .experience-button { - flex: 0; - border-radius: 50%; - height: 20px; - width: 20px; - display: flex; - align-items: center; - justify-content: center; - padding: 12px; - } - } - } - - .adversary-damage-threshold-container { - input { - min-width: @inputSingleMinWidth; - } - } - - .adversary-moves-container { - flex: 2.5; - - .moves-title { - text-decoration: underline; - font-weight: bold; - } - .move-container { - cursor: pointer; - - &:hover { - background: @hoverBackground; - } - - .moves-name { - font-weight: bold; - text-decoration: none; - } - - .move-description { - p { - margin-top: 0; - } - } - } - - .moves-edit-container { - i { - margin-left: 4px; - cursor: pointer; - - &:hover { - filter: drop-shadow(0 0 3px red); - } - } - } - } - - .chip-container { - display: flex; - align-items: center; - justify-content: space-between; - background: @primaryAccent; - padding: 8px; - border: 2px solid black; - border-radius: 6px; - - &:not(:last-child) { - margin-bottom: 8px; - } - - .chip-inner-container { - display: flex; - align-items: center; - - img { - height: 40px; - width: 40px; - margin-right: 8px; - } - - .chip-title { - font-size: 22px; - font-weight: bold; - font-style: italic; - } - } - - button { - height: 40px; - width: 40px; - background: white; - } - } -} diff --git a/styles/sheets/sheets.less b/styles/sheets/sheets.less index 3ce23be8..5a49a010 100644 --- a/styles/sheets/sheets.less +++ b/styles/sheets/sheets.less @@ -1,7 +1,5 @@ @import './heritage.less'; @import './class.less'; -@import './adversary.less'; -@import './environment.less'; .daggerheart.sheet { .title-container { diff --git a/system.json b/system.json index 1717e6e6..c402b837 100644 --- a/system.json +++ b/system.json @@ -204,7 +204,9 @@ "documentTypes": { "Actor": { "pc": {}, - "adversary": {}, + "adversary": { + "htmlFields": ["description", "motivesAndTactics"] + }, "environment": { "htmlFields": ["description", "impulses"] } diff --git a/templates/chat/adversary-attack-roll.hbs b/templates/chat/adversary-attack-roll.hbs index 3e805de9..d7cd9ecc 100644 --- a/templates/chat/adversary-attack-roll.hbs +++ b/templates/chat/adversary-attack-roll.hbs @@ -39,7 +39,7 @@
{{/if}}
- +
\ No newline at end of file diff --git a/templates/sheets/actors/adversary/header.hbs b/templates/sheets/actors/adversary/header.hbs new file mode 100644 index 00000000..e3914c32 --- /dev/null +++ b/templates/sheets/actors/adversary/header.hbs @@ -0,0 +1,9 @@ +
+ +
+

+
+

{{localize 'TYPES.Actor.adversary'}}

+
+
+
\ No newline at end of file diff --git a/templates/sheets/actors/adversary/information.hbs b/templates/sheets/actors/adversary/information.hbs new file mode 100644 index 00000000..6d6efb21 --- /dev/null +++ b/templates/sheets/actors/adversary/information.hbs @@ -0,0 +1,17 @@ +
+
+ {{localize "DAGGERHEART.Sheets.Adversary.Description" }} + + {{formGroup systemFields.description value=source.system.description}} +
+ +
+ {{localize "DAGGERHEART.Sheets.Adversary.MotivesAndTactics" }} + + {{formGroup systemFields.motivesAndTactics value=source.system.motivesAndTactics}} +
+
diff --git a/templates/sheets/actors/adversary/main.hbs b/templates/sheets/actors/adversary/main.hbs new file mode 100644 index 00000000..788af5ea --- /dev/null +++ b/templates/sheets/actors/adversary/main.hbs @@ -0,0 +1,60 @@ +
+
+
+ {{localize "DAGGERHEART.Sheets.Adversary.General"}} + + {{formGroup systemFields.tier value=source.system.tier localize=true}} + {{formGroup systemFields.type value=source.system.type localize=true}} +
{{formGroup systemFields.difficulty value=source.system.difficulty}}
+ +
+ {{localize "DAGGERHEART.Sheets.Adversary.DamageThresholds"}} + + {{formGroup systemFields.damageThresholds.fields.major value=source.system.damageThresholds.major}} + {{formGroup systemFields.damageThresholds.fields.severe value=source.system.damageThresholds.severe}} +
+ +
+ {{localize "DAGGERHEART.Sheets.Adversary.HitPoints"}} + + {{formGroup systemFields.resources.fields.hitPoints.fields.value value=source.system.resources.hitPoints.value}} + {{formGroup systemFields.resources.fields.hitPoints.fields.max value=source.system.resources.hitPoints.max}} +
+ +
+ {{localize "DAGGERHEART.Sheets.Adversary.Stress"}} + + {{formGroup systemFields.resources.fields.stress.fields.value value=source.system.resources.stress.value}} + {{formGroup systemFields.resources.fields.stress.fields.max value=source.system.resources.stress.max}} +
+ +
+ {{localize "DAGGERHEART.Sheets.Adversary.Experiences"}} + + {{#each source.system.experiences}} +
+ {{this.name}} + + {{formGroup @root.systemFields.experiences.element.fields.name name=(concat "system.experiences." @key ".name") value=this.name }} + {{formGroup @root.systemFields.experiences.element.fields.value name=(concat "system.experiences." @key ".value") value=this.value }} +
+ {{/each}} +
+
+ +
+ {{localize "DAGGERHEART.Sheets.Adversary.Attack"}} + + {{formGroup systemFields.attack.fields.name value=source.system.attack.name}} + + {{formGroup systemFields.attack.fields.modifier value=source.system.attack.modifier}} + {{formGroup systemFields.attack.fields.range value=source.system.attack.range localize=true}} + {{formGroup systemFields.attack.fields.damage.fields.value value=source.system.attack.damage.value}} + {{formGroup systemFields.attack.fields.damage.fields.type value=source.system.attack.damage.type localize=true}} +
+
+
From 70382df63c6553649bc5e7eb063cbcce1d7776e0 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Mon, 9 Jun 2025 13:36:48 +0200 Subject: [PATCH 4/7] 113 - Character Data Model (#114) * Improved Character datamodel * Removed additional unneccessary getters * Preliminary cleanup in the class sheet * Cleanup of 'pc' references * Corrected Duality rolling from Character * Fix to damage roll --- daggerheart.mjs | 86 +- lang/en.json | 22 +- module/applications/_module.mjs | 2 +- module/applications/levelup.mjs | 30 +- module/applications/multiclassDialog.mjs | 115 -- module/applications/rollSelectionDialog.mjs | 63 +- module/applications/sheets/adversary.mjs | 2 +- module/applications/sheets/character.mjs | 703 ++++++++++ module/applications/sheets/pc.mjs | 1241 ----------------- module/config/actorConfig.mjs | 2 +- module/config/generalConfig.mjs | 4 +- module/data/_module.mjs | 2 +- module/data/character.mjs | 233 ++++ module/data/item/class.mjs | 6 +- module/data/item/domainCard.mjs | 39 +- module/data/item/subclass.mjs | 6 +- module/data/pc.mjs | 413 ------ module/data/settings/VariantRules.mjs | 5 +- module/documents/actor.mjs | 29 +- module/helpers/utils.mjs | 2 +- styles/daggerheart.less | 1 + styles/less/actors/character.less | 0 system.json | 4 +- templates/settings/variant-rules.hbs | 2 + .../{pc/pc.hbs => character/character.hbs} | 10 +- .../parts/advancementCard.hbs | 0 .../{pc => character}/parts/heritageCard.hbs | 0 .../{pc => character}/sections/inventory.hbs | 0 .../{pc => character}/sections/loadout.hbs | 10 +- templates/sheets/parts/attributes.hbs | 6 +- templates/sheets/parts/defense.hbs | 24 +- templates/sheets/parts/experience.hbs | 8 +- templates/sheets/parts/weapons.hbs | 32 +- templates/views/rollSelection.hbs | 34 - 34 files changed, 1135 insertions(+), 2001 deletions(-) delete mode 100644 module/applications/multiclassDialog.mjs create mode 100644 module/applications/sheets/character.mjs delete mode 100644 module/applications/sheets/pc.mjs create mode 100644 module/data/character.mjs delete mode 100644 module/data/pc.mjs create mode 100644 styles/less/actors/character.less rename templates/sheets/{pc/pc.hbs => character/character.hbs} (92%) rename templates/sheets/{pc => character}/parts/advancementCard.hbs (100%) rename templates/sheets/{pc => character}/parts/heritageCard.hbs (100%) rename templates/sheets/{pc => character}/sections/inventory.hbs (100%) rename templates/sheets/{pc => character}/sections/loadout.hbs (95%) diff --git a/daggerheart.mjs b/daggerheart.mjs index 58600ad4..e223865d 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -13,6 +13,7 @@ import { dualityRollEnricher } from './module/enrichers/DualityRollEnricher.mjs' import { getCommandTarget, rollCommandToJSON, setDiceSoNiceForDualityRoll } from './module/helpers/utils.mjs'; import { abilities } from './module/config/actorConfig.mjs'; import Resources from './module/applications/resources.mjs'; +import DHDualityRoll from './module/data/chat-message/dualityRoll.mjs'; globalThis.SYSTEM = SYSTEM; @@ -40,7 +41,7 @@ Hooks.once('init', () => { CONFIG.Item.dataModels = models.items.config; const { Items, Actors } = foundry.documents.collections; - Items.unregisterSheet('core', foundry.appv1.sheets.ItemSheet); + Items.unregisterSheet('core', foundry.applications.sheets.ItemSheetV2); Items.registerSheet(SYSTEM.id, applications.DhpAncestry, { types: ['ancestry'], makeDefault: true }); Items.registerSheet(SYSTEM.id, applications.DhpCommunity, { types: ['community'], makeDefault: true }); Items.registerSheet(SYSTEM.id, applications.DhpClassSheet, { types: ['class'], makeDefault: true }); @@ -54,12 +55,12 @@ Hooks.once('init', () => { CONFIG.Actor.documentClass = documents.DhpActor; CONFIG.Actor.dataModels = { - pc: models.DhpPC, + character: models.DhCharacter, adversary: models.DhpAdversary, environment: models.DhpEnvironment }; - Actors.unregisterSheet('core', foundry.appv1.sheets.ActorSheet); - Actors.registerSheet(SYSTEM.id, applications.DhpPCSheet, { types: ['pc'], makeDefault: true }); + Actors.unregisterSheet('core', foundry.applications.sheets.ActorSheetV2); + Actors.registerSheet(SYSTEM.id, applications.DhCharacterSheet, { types: ['character'], makeDefault: true }); Actors.registerSheet(SYSTEM.id, applications.DhpAdversarySheet, { types: ['adversary'], makeDefault: true }); Actors.registerSheet(SYSTEM.id, applications.DhpEnvironment, { types: ['environment'], makeDefault: true }); @@ -137,22 +138,28 @@ const renderDualityButton = async event => { title: button.dataset.label, value: rollModifier }); + + const systemData = new DHDualityRoll({ + title: button.dataset.label, + origin: target.id, + roll: roll._formula, + modifiers: modifiers, + hope: hope, + fear: fear, + advantage: advantage, + disadvantage: disadvantage + }); + const cls = getDocumentClass('ChatMessage'); const msgData = { type: 'dualityRoll', sound: CONFIG.sounds.dice, - system: { - title: button.dataset.label, - origin: target.id, - roll: roll._formula, - modifiers: modifiers, - hope: hope, - fear: fear, - advantage: advantage, - disadvantage: disadvantage - }, + system: systemData, user: game.user.id, - content: 'systems/daggerheart/templates/chat/duality-roll.hbs', + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/chat/duality-roll.hbs', + systemData + ), rolls: [roll] }; @@ -226,29 +233,34 @@ Hooks.on('chatMessage', (_, message) => { : undefined, title }); - }).then(({ roll, attribute, title }) => { + }).then(async ({ roll, attribute, title }) => { const cls = getDocumentClass('ChatMessage'); + const systemData = new DHDualityRoll({ + title: title, + origin: target?.id, + roll: roll._formula, + modifiers: attribute ? [attribute] : [], + hope: { dice: rollCommand.hope ?? 'd12', value: roll.dice[0].total }, + fear: { dice: rollCommand.fear ?? 'd12', value: roll.dice[1].total }, + advantage: + rollCommand.advantage && !rollCommand.disadvantage + ? { dice: 'd6', value: roll.dice[2].total } + : undefined, + disadvantage: + rollCommand.disadvantage && !rollCommand.advantage + ? { dice: 'd6', value: roll.dice[2].total } + : undefined + }); + const msgData = { type: 'dualityRoll', sound: CONFIG.sounds.dice, - system: { - title: title, - origin: target?.id, - roll: roll._formula, - modifiers: attribute ? [attribute] : [], - hope: { dice: rollCommand.hope ?? 'd12', value: roll.dice[0].total }, - fear: { dice: rollCommand.fear ?? 'd12', value: roll.dice[1].total }, - advantage: - rollCommand.advantage && !rollCommand.disadvantage - ? { dice: 'd6', value: roll.dice[2].total } - : undefined, - disadvantage: - rollCommand.disadvantage && !rollCommand.advantage - ? { dice: 'd6', value: roll.dice[2].total } - : undefined - }, + system: systemData, user: game.user.id, - content: 'systems/daggerheart/templates/chat/duality-roll.hbs', + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/chat/duality-roll.hbs', + systemData + ), rolls: [roll] }; @@ -275,10 +287,10 @@ const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/sheets/parts/heritage.hbs', 'systems/daggerheart/templates/sheets/parts/subclassFeature.hbs', 'systems/daggerheart/templates/sheets/parts/effects.hbs', - 'systems/daggerheart/templates/sheets/pc/sections/inventory.hbs', - 'systems/daggerheart/templates/sheets/pc/sections/loadout.hbs', - 'systems/daggerheart/templates/sheets/pc/parts/heritageCard.hbs', - 'systems/daggerheart/templates/sheets/pc/parts/advancementCard.hbs', + 'systems/daggerheart/templates/sheets/character/sections/inventory.hbs', + 'systems/daggerheart/templates/sheets/character/sections/loadout.hbs', + 'systems/daggerheart/templates/sheets/character/parts/heritageCard.hbs', + 'systems/daggerheart/templates/sheets/character/parts/advancementCard.hbs', 'systems/daggerheart/templates/components/card-preview.hbs', 'systems/daggerheart/templates/views/levelup/parts/selectable-card-preview.hbs', 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs', diff --git a/lang/en.json b/lang/en.json index 78532b49..a843e2c6 100755 --- a/lang/en.json +++ b/lang/en.json @@ -13,7 +13,7 @@ "armor": "Armor" }, "Actor": { - "pc": "PC", + "character": "Character", "npc": "NPC", "adversary": "Adversary", "environment": "Environment" @@ -110,8 +110,14 @@ }, "VariantRules": { "ActionTokens": { - "Name": "Action Tokens", - "Hint": "Give each player action tokens to use in combat" + "label": "Action Tokens", + "hint": "Give each player action tokens to use in combat" + }, + "FIELDS": { + "useCoins": { + "label": "Use Coins", + "hint": "test" + } } }, "DualityRollColor": { @@ -132,10 +138,6 @@ "AttackTargetDoesNotExist": "The target token no longer exists" }, "Error": { - "NoClassSelected": "Your character has no class selected!", - "LacksDomain": "Your character doesn't have the domain of the card!", - "MaxLoadoutReached": "You can't have any more domain cards at this level!", - "DuplicateDomainCard": "You already have a domain card with that name!", "ActionRequiresTarget": "The action requires at least one target", "NoAssignedPlayerCharacter": "You have no assigned character.", "NoSelectedToken": "You have no selected token", @@ -1252,7 +1254,11 @@ "MissingClass": "The character is missing a class", "SubclassNotInClass": "The subclass does not belong to the character's class", "ClassAlreadySelected": "The character already has a class", - "SubclassAlreadySelected": "The character already has a subclass for that class." + "SubclassAlreadySelected": "The character already has a subclass for that class.", + "NoClassSelected": "Your character has no class selected!", + "LacksDomain": "Your character doesn't have the domain of the card!", + "MaxLoadoutReached": "You can't have any more domain cards at this level!", + "DuplicateDomainCard": "You already have a domain card with that name!" } }, "Effects": { diff --git a/module/applications/_module.mjs b/module/applications/_module.mjs index 5c031db0..079fe062 100644 --- a/module/applications/_module.mjs +++ b/module/applications/_module.mjs @@ -1,4 +1,4 @@ -export { default as DhpPCSheet } from './sheets/pc.mjs'; +export { default as DhCharacterSheet } from './sheets/character.mjs'; export { default as DhpAdversarySheet } from './sheets/adversary.mjs'; export { default as DhpClassSheet } from './sheets/items/class.mjs'; export { default as DhpSubclass } from './sheets/items/subclass.mjs'; diff --git a/module/applications/levelup.mjs b/module/applications/levelup.mjs index 22c6d33e..61a85677 100644 --- a/module/applications/levelup.mjs +++ b/module/applications/levelup.mjs @@ -149,7 +149,10 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) const experienceIncreaseValues = experienceIncreases .filter(exp => exp.data.length > 0) .flatMap(exp => - exp.data.map(data => this.actor.system.experiences.find(x => x.id === data).description) + exp.data.map(data => { + const experience = Object.keys(this.actor.system.experiences).find(x => x === data); + return this.actor.system.experiences[experience].description; + }) ); context.experienceIncreases = { values: experienceIncreaseValues, @@ -201,7 +204,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) const multiclassSubclass = this.actor.system.multiclass?.system?.subclasses?.[0]; const possibleSubclasses = [ - this.actor.system.subclass, + this.actor.system.class.subclass, ...(multiclassSubclass ? [multiclassSubclass] : []) ]; const selectedSubclasses = possibleSubclasses.filter(x => subclassSelections.includes(x.uuid)); @@ -274,8 +277,8 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) context.achievements = { proficiency: { - old: this.actor.system.proficiency.value, - new: this.actor.system.proficiency.value + achivementProficiency, + old: this.actor.system.proficiency, + new: this.actor.system.proficiency + achivementProficiency, shown: achivementProficiency > 0 }, damageThresholds: { @@ -328,10 +331,12 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) break; case 'experience': if (!advancement[choiceKey]) advancement[choiceKey] = []; - const data = checkbox.data.map( - data => - this.actor.system.experiences.find(x => x.id === data)?.description ?? '' - ); + const data = checkbox.data.map(data => { + const experience = Object.keys(this.actor.system.experiences).find( + x => x === data + ); + return this.actor.system.experiences[experience]?.description ?? ''; + }); advancement[choiceKey].push({ data: data, value: checkbox.value }); break; } @@ -354,8 +359,8 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) new: this.actor.system.resources.stress.max + (advancement.stress ?? 0) }, evasion: { - old: this.actor.system.evasion.value, - new: this.actor.system.evasion.value + (advancement.evasion ?? 0) + old: this.actor.system.evasion, + new: this.actor.system.evasion + (advancement.evasion ?? 0) } }, traits: @@ -421,8 +426,9 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) if (experienceIncreaseTagify) { tagifyElement( experienceIncreaseTagify, - this.actor.system.experiences.reduce((acc, experience) => { - acc[experience.id] = { label: experience.description }; + Object.keys(this.actor.system.experiences).reduce((acc, id) => { + const experience = this.actor.system.experiences[id]; + acc[id] = { label: experience.description }; return acc; }, {}), diff --git a/module/applications/multiclassDialog.mjs b/module/applications/multiclassDialog.mjs deleted file mode 100644 index f9e53c17..00000000 --- a/module/applications/multiclassDialog.mjs +++ /dev/null @@ -1,115 +0,0 @@ -const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; - -export default class DhpMulticlassDialog extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(actorName, actorClass, resolve) { - super({}); - - this.actorName = actorName; - this.actorClass = actorClass; - this.resolve = resolve; - - this.classChoices = Array.from( - game.items.reduce((acc, x) => { - if (x.type === 'class' && x.name !== actorClass.name) { - acc.add(x); - } - - return acc; - }, new Set()) - ); - this.subclassChoices = []; - this.domainChoices = []; - - this.data = { - class: null, - subclass: null, - domain: null - }; - } - - get title() { - return `${this.actorName} - Multiclass`; - } - - static DEFAULT_OPTIONS = { - classes: ['daggerheart', 'views', 'multiclass'], - position: { width: 600, height: 'auto' }, - actions: { - selectClass: this.selectClass, - selectSubclass: this.selectSubclass, - selectDomain: this.selectDomain, - finish: this.finish - } - }; - - static PARTS = { - form: { - id: 'levelup', - template: 'systems/daggerheart/templates/views/multiclass.hbs' - } - }; - - async _prepareContext(_options) { - const context = await super._prepareContext(_options); - context.classChoices = this.classChoices; - context.subclassChoices = this.subclassChoices; - context.domainChoices = this.domainChoices; - context.disabledFinish = !this.data.class || !this.data.subclass || !this.data.domain; - context.data = this.data; - - return context; - } - - static async selectClass(_, button) { - const oldClass = this.data.class; - this.data.class = this.data.class?.uuid === button.dataset.class ? null : await fromUuid(button.dataset.class); - if (oldClass !== button.dataset.class) { - this.data.subclass = null; - this.data.domain = null; - this.subclassChoices = this.data.class ? this.data.class.system.subclasses : []; - - //FIXME - this.domainChoices = this.data.class - ? this.data.class.system.domains.map(x => { - const config = SYSTEM.DOMAIN.domains[x]; - return { - name: game.i18n.localize(config.name), - id: config.id, - img: config.src, - disabled: this.actorClass.system.domains.includes(config.id) - }; - }) - : []; - } - - this.render(true); - } - - static async selectSubclass(_, button) { - this.data.subclass = - this.data.subclass?.uuid === button.dataset.subclass - ? null - : this.subclassChoices.find(x => x.uuid === button.dataset.subclass); - this.render(true); - } - - static async selectDomain(_, button) { - const domain = - this.data.domain?.id === button.dataset.domain - ? null - : this.domainChoices.find(x => x.id === button.dataset.domain); - if (domain?.disabled) return; - - this.data.domain = domain; - this.render(true); - } - - static finish() { - this.close({}, this.data); - } - - async close(options = {}, data = null) { - this.resolve(data); - super.close(options); - } -} diff --git a/module/applications/rollSelectionDialog.mjs b/module/applications/rollSelectionDialog.mjs index 3230cf72..e97d7cf1 100644 --- a/module/applications/rollSelectionDialog.mjs +++ b/module/applications/rollSelectionDialog.mjs @@ -1,7 +1,7 @@ const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; export default class RollSelectionDialog extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(experiences, bonusDamage, hopeResource, resolve, isNpc) { + constructor(experiences, hopeResource, resolve) { super({}, {}); this.experiences = experiences; @@ -17,23 +17,13 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl fear: ['d12'], advantage: null, disadvantage: null, - bonusDamage: bonusDamage.reduce((acc, x) => { - if (x.appliesOn === SYSTEM.EFFECTS.applyLocations.attackRoll.id) { - acc.push({ - ...x, - hopeUses: 0 - }); - } - - return acc; - }, []), hopeResource: hopeResource }; } static DEFAULT_OPTIONS = { tag: 'form', - id: 'roll-selection', //Having an id causes a new instance to overwrite previous. + id: 'roll-selection', classes: ['daggerheart', 'views', 'roll-selection'], position: { width: 400, @@ -41,8 +31,6 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl }, actions: { selectExperience: this.selectExperience, - decreaseHopeUse: this.decreaseHopeUse, - increaseHopeUse: this.increaseHopeUse, setAdvantage: this.setAdvantage, setDisadvantage: this.setDisadvantage, finish: this.finish @@ -74,27 +62,14 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl context.fear = this.data.fear; context.advantage = this.data.advantage; context.disadvantage = this.data.disadvantage; - context.experiences = this.experiences.map(x => ({ - ...x, - selected: this.selectedExperiences.find(selected => selected.id === x.id) - })); - context.bonusDamage = this.data.bonusDamage; + context.experiences = Object.keys(this.experiences).map(id => ({ id, ...this.experiences[id] })); context.hopeResource = this.data.hopeResource + 1; - context.hopeUsed = this.getHopeUsed(); return context; } static updateSelection(event, _, formData) { - const { bonusDamage, ...rest } = foundry.utils.expandObject(formData.object); - - for (var index in bonusDamage) { - this.data.bonusDamage[index].initiallySelected = bonusDamage[index].initiallySelected; - if (bonusDamage[index].hopeUses) { - const value = Number.parseInt(bonusDamage[index].hopeUses); - if (!Number.isNaN(value)) this.data.bonusDamage[index].hopeUses = value; - } - } + const { ...rest } = foundry.utils.expandObject(formData.object); this.data = foundry.utils.mergeObject(this.data, rest); this.render(); @@ -104,35 +79,12 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl if (this.selectedExperiences.find(x => x.id === button.dataset.key)) { this.selectedExperiences = this.selectedExperiences.filter(x => x.id !== button.dataset.key); } else { - this.selectedExperiences = [ - ...this.selectedExperiences, - this.experiences.find(x => x.id === button.dataset.key) - ]; + this.selectedExperiences = [...this.selectedExperiences, button.dataset.key]; } this.render(); } - getHopeUsed() { - return this.data.bonusDamage.reduce((acc, x) => acc + x.hopeUses, 0); - } - - static decreaseHopeUse(_, button) { - const index = Number.parseInt(button.dataset.index); - if (this.data.bonusDamage[index].hopeUses - 1 >= 0) { - this.data.bonusDamage[index].hopeUses -= 1; - this.render(true); - } - } - - static increaseHopeUse(_, button) { - const index = Number.parseInt(button.dataset.index); - if (this.data.bonusDamage[index].hopeUses <= this.data.hopeResource + 1) { - this.data.bonusDamage[index].hopeUses += 1; - this.render(true); - } - } - static setAdvantage() { this.data.advantage = this.data.advantage ? null : 'd6'; this.data.disadvantage = null; @@ -149,11 +101,10 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl static async finish() { const { diceOptions, ...rest } = this.data; + this.resolve({ ...rest, - experiences: this.selectedExperiences, - hopeUsed: this.getHopeUsed(), - bonusDamage: this.data.bonusDamage.reduce((acc, x) => acc.concat(` + ${1 + x.hopeUses}${x.value}`), '') + experiences: this.selectedExperiences.map(x => ({ id: x, ...this.experiences[x] })) }); this.close(); } diff --git a/module/applications/sheets/adversary.mjs b/module/applications/sheets/adversary.mjs index 8345b532..0196a8c8 100644 --- a/module/applications/sheets/adversary.mjs +++ b/module/applications/sheets/adversary.mjs @@ -98,7 +98,7 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) { name: x.actor.name, img: x.actor.img, difficulty: x.actor.system.difficulty, - evasion: x.actor.system.evasion.value + evasion: x.actor.system.evasion })); const cls = getDocumentClass('ChatMessage'); diff --git a/module/applications/sheets/character.mjs b/module/applications/sheets/character.mjs new file mode 100644 index 00000000..8eaf67aa --- /dev/null +++ b/module/applications/sheets/character.mjs @@ -0,0 +1,703 @@ +import { capitalize } from '../../helpers/utils.mjs'; +import DhpDeathMove from '../deathMove.mjs'; +import DhpDowntime from '../downtime.mjs'; +import AncestrySelectionDialog from '../ancestrySelectionDialog.mjs'; +import DaggerheartSheet from './daggerheart-sheet.mjs'; +import { abilities } from '../../config/actorConfig.mjs'; +import DhlevelUp from '../levelup.mjs'; +import DHDualityRoll from '../../data/chat-message/dualityRoll.mjs'; + +const { ActorSheetV2 } = foundry.applications.sheets; +const { TextEditor } = foundry.applications.ux; +export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { + constructor(options = {}) { + super(options); + } + + static DEFAULT_OPTIONS = { + tag: 'form', + classes: ['daggerheart', 'sheet', 'pc'], + position: { width: 810, height: 1080 }, + actions: { + attributeRoll: this.rollAttribute, + toggleMarks: this.toggleMarks, + toggleHP: this.toggleHP, + toggleStress: this.toggleStress, + toggleHope: this.toggleHope, + toggleGold: this.toggleGold, + attackRoll: this.attackRoll, + useDomainCard: this.useDomainCard, + removeCard: this.removeDomainCard, + selectClass: this.selectClass, + selectSubclass: this.selectSubclass, + selectAncestry: this.selectAncestry, + selectCommunity: this.selectCommunity, + viewObject: this.viewObject, + useFeature: this.useFeature, + takeShortRest: this.takeShortRest, + takeLongRest: this.takeLongRest, + deleteItem: this.deleteItem, + addScar: this.addScar, + deleteScar: this.deleteScar, + makeDeathMove: this.makeDeathMove, + itemQuantityDecrease: (_, button) => this.setItemQuantity(button, -1), + itemQuantityIncrease: (_, button) => this.setItemQuantity(button, 1), + useAbility: this.useAbility, + useAdvancementCard: this.useAdvancementCard, + useAdvancementAbility: this.useAdvancementAbility, + toggleEquipItem: this.toggleEquipItem, + levelup: this.openLevelUp + }, + window: { + minimizable: false, + resizable: true + }, + form: { + handler: this.updateForm, + submitOnChange: true, + closeOnSubmit: false + }, + dragDrop: [ + { dragSelector: null, dropSelector: '.weapon-section' }, + { dragSelector: null, dropSelector: '.armor-section' }, + { dragSelector: '.item-list .item', dropSelector: null } + ] + }; + + static PARTS = { + form: { + id: 'character', + template: 'systems/daggerheart/templates/sheets/character/character.hbs' + } + }; + + _getTabs() { + const setActive = tabs => { + for (const v of Object.values(tabs)) { + v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active; + v.cssClass = v.active ? 'active' : ''; + } + }; + + const primaryTabs = { + features: { + active: true, + cssClass: '', + group: 'primary', + id: 'features', + icon: null, + label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Features') + }, + loadout: { + active: false, + cssClass: '', + group: 'primary', + id: 'loadout', + icon: null, + label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Loadout') + }, + inventory: { + active: false, + cssClass: '', + group: 'primary', + id: 'inventory', + icon: null, + label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Inventory') + }, + story: { + active: false, + cssClass: '', + group: 'primary', + id: 'story', + icon: null, + label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Story') + } + }; + const secondaryTabs = { + foundation: { + active: true, + cssClass: '', + group: 'secondary', + id: 'foundation', + icon: null, + label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Foundation') + }, + loadout: { + active: false, + cssClass: '', + group: 'secondary', + id: 'loadout', + icon: null, + label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Loadout') + }, + vault: { + active: false, + cssClass: '', + group: 'secondary', + id: 'vault', + icon: null, + label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Vault') + } + }; + + setActive(primaryTabs); + setActive(secondaryTabs); + + return { primary: primaryTabs, secondary: secondaryTabs }; + } + + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + htmlElement.querySelector('.level-value').addEventListener('change', this.onLevelChange.bind(this)); + } + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.document = this.document; + context.tabs = this._getTabs(); + + context.config = SYSTEM; + + const selectedAttributes = Object.values(this.document.system.traits).map(x => x.base); + context.abilityScoreArray = JSON.parse( + await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.AbilityArray) + ).reduce((acc, x) => { + const selectedIndex = selectedAttributes.indexOf(x); + if (selectedIndex !== -1) { + selectedAttributes.splice(selectedIndex, 1); + } else { + acc.push({ name: x, value: x }); + } + + return acc; + }, []); + if (!context.abilityScoreArray.includes(0)) context.abilityScoreArray.push({ name: 0, value: 0 }); + context.abilityScoresFinished = context.abilityScoreArray.every(x => x.value === 0); + + context.attributes = Object.keys(this.document.system.traits).reduce((acc, key) => { + acc[key] = { + ...this.document.system.traits[key], + name: game.i18n.localize(SYSTEM.ACTOR.abilities[key].name), + verbs: SYSTEM.ACTOR.abilities[key].verbs.map(x => game.i18n.localize(x)) + }; + + return acc; + }, {}); + + const ancestry = await this.mapFeatureType( + this.document.system.ancestry ? [this.document.system.ancestry] : [], + SYSTEM.GENERAL.objectTypes + ); + const community = await this.mapFeatureType( + this.document.system.community ? [this.document.system.community] : [], + SYSTEM.GENERAL.objectTypes + ); + const foundation = { + ancestry: ancestry[0], + community: community[0], + advancement: {} + }; + + const nrLoadoutCards = this.document.system.domainCards.loadout.length; + const loadout = await this.mapFeatureType(this.document.system.domainCards.loadout, SYSTEM.DOMAIN.cardTypes); + const vault = await this.mapFeatureType(this.document.system.domainCards.vault, SYSTEM.DOMAIN.cardTypes); + context.abilities = { + foundation: foundation, + loadout: { + top: loadout.slice(0, Math.min(2, nrLoadoutCards)), + bottom: nrLoadoutCards > 2 ? loadout.slice(2, Math.min(5, nrLoadoutCards)) : [], + nrTotal: nrLoadoutCards + }, + vault: vault.map(x => ({ + ...x, + uuid: x.uuid, + sendToLoadoutDisabled: this.document.system.domainCards.loadout.length >= 5 + })) + }; + + context.inventory = { + consumable: { + titles: { + name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.ConsumableTitle'), + quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle') + }, + items: this.document.items.filter(x => x.type === 'consumable') + }, + miscellaneous: { + titles: { + name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.MiscellaneousTitle'), + quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle') + }, + items: this.document.items.filter(x => x.type === 'miscellaneous') + }, + weapons: { + titles: { + name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.WeaponsTitle'), + quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle') + }, + items: this.document.items.filter(x => x.type === 'weapon') + }, + armor: { + titles: { + name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.ArmorsTitle'), + quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle') + }, + items: this.document.items.filter(x => x.type === 'armor') + } + }; + + if (context.inventory.length === 0) { + context.inventory = Array(1).fill(Array(5).fill([])); + } + + return context; + } + + static async updateForm(event, _, formData) { + await this.document.update(formData.object); + this.render(); + } + + async mapFeatureType(data, configType) { + return await Promise.all( + data.map(async x => { + const abilities = x.system.abilities + ? await Promise.all(x.system.abilities.map(async x => await fromUuid(x.uuid))) + : []; + + return { + ...x, + uuid: x.uuid, + system: { + ...x.system, + abilities: abilities, + type: game.i18n.localize(configType[x.system.type ?? x.type].label) + } + }; + }) + ); + } + + static async rollAttribute(event, button) { + const { roll, hope, fear, advantage, disadvantage, modifiers } = await this.document.dualityRoll( + { title: game.i18n.localize(abilities[button.dataset.attribute].label), value: button.dataset.value }, + event.shiftKey + ); + + const cls = getDocumentClass('ChatMessage'); + + const systemContent = new DHDualityRoll({ + title: game.i18n.format('DAGGERHEART.Chat.DualityRoll.AbilityCheckTitle', { + ability: game.i18n.localize(abilities[button.dataset.attribute].label) + }), + origin: this.document.id, + roll: roll._formula, + modifiers: modifiers, + hope: hope, + fear: fear, + advantage: advantage, + disadvantage: disadvantage + }); + + await cls.create({ + type: 'dualityRoll', + sound: CONFIG.sounds.dice, + system: systemContent, + user: game.user.id, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/chat/duality-roll.hbs', + systemContent + ), + rolls: [roll] + }); + } + + static async toggleMarks(_, button) { + const markValue = Number.parseInt(button.dataset.value); + const newValue = this.document.system.armor.system.marks.value >= markValue ? markValue - 1 : markValue; + await this.document.system.armor.update({ 'system.marks.value': newValue }); + } + + static async toggleHP(_, button) { + const healthValue = Number.parseInt(button.dataset.value); + const newValue = this.document.system.resources.hitPoints.value >= healthValue ? healthValue - 1 : healthValue; + await this.document.update({ 'system.resources.hitPoints.value': newValue }); + } + + static async toggleStress(_, button) { + const healthValue = Number.parseInt(button.dataset.value); + const newValue = this.document.system.resources.stress.value >= healthValue ? healthValue - 1 : healthValue; + await this.document.update({ 'system.resources.stress.value': newValue }); + } + + static async toggleHope(_, button) { + const hopeValue = Number.parseInt(button.dataset.value); + const newValue = this.document.system.resources.hope.value >= hopeValue ? hopeValue - 1 : hopeValue; + await this.document.update({ 'system.resources.hope.value': newValue }); + } + + static async toggleGold(_, button) { + const goldValue = Number.parseInt(button.dataset.value); + const goldType = button.dataset.type; + const newValue = this.document.system.gold[goldType] >= goldValue ? goldValue - 1 : goldValue; + + const update = `system.gold.${goldType}`; + await this.document.update({ [update]: newValue }); + } + + static async attackRoll(event, button) { + const weapon = await fromUuid(button.dataset.weapon); + const damage = { + value: `${this.document.system.proficiency}${weapon.system.damage.value}`, + type: weapon.system.damage.type + }; + const modifier = this.document.system.traits[weapon.system.trait].value; + + const { roll, hope, fear, advantage, disadvantage, modifiers } = await this.document.dualityRoll( + { title: game.i18n.localize(abilities[weapon.system.trait].label), value: modifier }, + event.shiftKey + ); + + const targets = Array.from(game.user.targets).map(x => ({ + id: x.id, + name: x.actor.name, + img: x.actor.img, + difficulty: x.actor.system.difficulty, + evasion: x.actor.system.evasion + })); + + const systemData = new DHDualityRoll({ + title: weapon.name, + origin: this.document.id, + roll: roll._formula, + modifiers: modifiers, + hope: hope, + fear: fear, + advantage: advantage, + disadvantage: disadvantage, + damage: damage, + targets: targets + }); + + const cls = getDocumentClass('ChatMessage'); + const msg = new cls({ + type: 'dualityRoll', + sound: CONFIG.sounds.dice, + system: systemData, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/chat/attack-roll.hbs', + systemData + ), + rolls: [roll] + }); + + await cls.create(msg.toObject()); + } + + static openLevelUp() { + if (!this.document.system.class.value || !this.document.system.class.subclass) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.Sheets.PC.Errors.missingClassOrSubclass')); + return; + } + + new DhlevelUp(this.document).render(true); + } + + static async useDomainCard(_, button) { + const card = this.document.items.find(x => x.uuid === button.dataset.key); + + const cls = getDocumentClass('ChatMessage'); + const systemData = { + title: `${game.i18n.localize('DAGGERHEART.Chat.DomainCard.Title')} - ${capitalize(button.dataset.domain)}`, + origin: this.document.id, + img: card.img, + name: card.name, + description: card.system.effect, + actions: card.system.actions + }; + const msg = new cls({ + type: 'abilityUse', + user: game.user.id, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/chat/ability-use.hbs', + systemData + ), + system: systemData + }); + + cls.create(msg.toObject()); + } + + static async removeDomainCard(_, button) { + if (button.dataset.type === 'domainCard') { + const card = this.document.items.find(x => x.uuid === button.dataset.key); + await card.delete(); + } + } + + static async selectClass() { + (await game.packs.get('daggerheart.classes'))?.render(true); + } + + static async selectSubclass() { + (await game.packs.get('daggerheart.subclasses'))?.render(true); + } + + static async selectAncestry() { + const dialogClosed = new Promise((resolve, _) => { + new AncestrySelectionDialog(resolve).render(true); + }); + const result = await dialogClosed; + + for (var ancestry of this.document.items.filter(x => x => x.type === 'ancestry')) { + await ancestry.delete(); + } + + const createdItems = []; + for (var feature of this.document.items.filter( + x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.ancestry.id + )) { + await feature.delete(); + } + + createdItems.push(result.data); + + await this.document.createEmbeddedDocuments('Item', createdItems); + } + + static async selectCommunity() { + (await game.packs.get('daggerheart.communities'))?.render(true); + } + + static async viewObject(_, button) { + const object = await fromUuid(button.dataset.value); + if (!object) return; + + const tab = button.dataset.tab; + if (tab && object.sheet._tabs) object.sheet._tabs[0].active = tab; + + if (object.sheet.editMode) object.sheet.editMode = false; + + object.sheet.render(true); + } + + static async takeShortRest() { + await new DhpDowntime(this.document, true).render(true); + await this.minimize(); + } + + static async takeLongRest() { + await new DhpDowntime(this.document, false).render(true); + await this.minimize(); + } + + static async addScar() { + if (this.document.system.story.scars.length === 5) return; + + await this.document.update({ + 'system.story.scars': [ + ...this.document.system.story.scars, + { name: game.i18n.localize('DAGGERHEART.Sheets.PC.NewScar'), description: '' } + ] + }); + } + + static async deleteScar(event, button) { + event.stopPropagation(); + await this.document.update({ + 'system.story.scars': this.document.system.story.scars.filter( + (_, index) => index !== Number.parseInt(button.currentTarget.dataset.scar) + ) + }); + } + + static async makeDeathMove() { + if (this.document.system.resources.hitPoints.value === this.document.system.resources.hitPoints.max) { + await new DhpDeathMove(this.document).render(true); + await this.minimize(); + } + } + + async itemUpdate(event) { + const name = event.currentTarget.dataset.item; + const item = await fromUuid($(event.currentTarget).closest('[data-item-id]')[0].dataset.itemId); + await item.update({ [name]: event.currentTarget.value }); + } + + async onLevelChange(event) { + await this.document.updateLevel(Number(event.currentTarget.value)); + this.render(); + } + + static async deleteItem(_, button) { + const item = await fromUuid($(button).closest('[data-item-id]')[0].dataset.itemId); + await item.delete(); + } + + static async setItemQuantity(button, value) { + const item = await fromUuid($(button).closest('[data-item-id]')[0].dataset.itemId); + await item.update({ 'system.quantity': Math.max(item.system.quantity + value, 1) }); + } + + static async useFeature(_, button) { + const item = await fromUuid(button.dataset.id); + + const cls = getDocumentClass('ChatMessage'); + const systemData = { + title: game.i18n.localize('DAGGERHEART.Chat.FeatureTitle'), + origin: this.document.id, + img: item.img, + name: item.name, + description: item.system.description, + actions: item.system.actions + }; + const msg = new cls({ + type: 'abilityUse', + user: game.user.id, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/chat/ability-use.hbs', + systemData + ), + system: systemData + }); + + cls.create(msg.toObject()); + } + + static async useAbility(_, button) { + const item = await fromUuid(button.dataset.feature); + const type = button.dataset.type; + + const cls = getDocumentClass('ChatMessage'); + const systemData = { + title: + type === 'ancestry' + ? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.AncestryTitle') + : type === 'community' + ? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.CommunityTitle') + : game.i18n.localize('DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle'), + origin: this.document.id, + img: item.img, + name: item.name, + description: item.system.description, + actions: [] + }; + const msg = new cls({ + type: 'abilityUse', + user: game.user.id, + system: systemData, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/chat/ability-use.hbs', + systemData + ) + }); + + cls.create(msg.toObject()); + } + + static async useAdvancementCard(_, button) { + const item = + button.dataset.multiclass === 'true' + ? this.document.system.multiclass.subclass + : this.document.system.class.subclass; + const ability = item.system[`${button.dataset.key}Feature`]; + const title = `${item.name} - ${game.i18n.localize(`DAGGERHEART.Sheets.PC.DomainCard.${capitalize(button.dataset.key)}Title`)}`; + + const cls = getDocumentClass('ChatMessage'); + const systemData = { + title: game.i18n.localize('DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle'), + origin: this.document.id, + name: title, + img: item.img, + description: ability.description + }; + const msg = new cls({ + type: 'abilityUse', + user: game.user.id, + system: systemData, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/chat/ability-use.hbs', + systemData + ) + }); + + cls.create(msg.toObject()); + } + + static async useAdvancementAbility(_, button) { + const item = this.document.items.find(x => x.uuid === button.dataset.id); + + const cls = getDocumentClass('ChatMessage'); + const systemData = { + title: game.i18n.localize('DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle'), + origin: this.document.id, + name: item.name, + img: item.img, + description: item.system.description + }; + const msg = new cls({ + user: game.user.id, + system: systemData, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/chat/ability-use.hbs', + systemData + ) + }); + + cls.create(msg.toObject()); + } + + static async toggleEquipItem(_, button) { + const item = this.document.items.get(button.id); + if (item.system.equipped) { + await item.update({ 'system.equipped': false }); + return; + } + + switch (item.type) { + case 'armor': + const currentArmor = this.document.system.armor; + if (currentArmor) { + await currentArmor.update({ 'system.equipped': false }); + } + + await item.update({ 'system.equipped': true }); + break; + case 'weapon': + await this.document.system.constructor.unequipBeforeEquip.bind(this.document.system)(item); + + await item.update({ 'system.equipped': true }); + break; + } + this.render(); + } + + async _onDragStart(_, event) { + super._onDragStart(event); + } + + async _onDrop(event) { + super._onDrop(event); + this._onDropItem(event, TextEditor.getDragEventData(event)); + } + + async _onDropItem(event, data) { + const item = await Item.implementation.fromDropData(data); + const itemData = item.toObject(); + + if (item.type === 'domainCard' && this.document.system.domainCards.loadout.length >= 5) { + itemData.system.inVault = true; + } + + if (this.document.uuid === item.parent?.uuid) return this._onSortItem(event, itemData); + const createdItem = await this._onDropItemCreate(itemData); + + return createdItem; + } + + async _onDropItemCreate(itemData, event) { + itemData = itemData instanceof Array ? itemData : [itemData]; + return this.document.createEmbeddedDocuments('Item', itemData); + } +} diff --git a/module/applications/sheets/pc.mjs b/module/applications/sheets/pc.mjs deleted file mode 100644 index 7433e3f3..00000000 --- a/module/applications/sheets/pc.mjs +++ /dev/null @@ -1,1241 +0,0 @@ -import { capitalize } from '../../helpers/utils.mjs'; -import DhpDeathMove from '../deathMove.mjs'; -import DhpDowntime from '../downtime.mjs'; -import AncestrySelectionDialog from '../ancestrySelectionDialog.mjs'; -import DaggerheartSheet from './daggerheart-sheet.mjs'; -import { abilities } from '../../config/actorConfig.mjs'; -import DhlevelUp from '../levelup.mjs'; - -const { ActorSheetV2 } = foundry.applications.sheets; -const { TextEditor } = foundry.applications.ux; -export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { - constructor(options = {}) { - super(options); - - this.editAttributes = false; - this.onVaultTab = false; - this.currentInventoryPage = 0; - this.selectedScar = null; - this.storyEditor = null; - this.dropItemBlock = false; - this.multiclassFeatureSetSelected = false; - } - - static DEFAULT_OPTIONS = { - tag: 'form', - classes: ['daggerheart', 'sheet', 'pc'], - position: { width: 810, height: 1080 }, - actions: { - toggleEditAttributes: this.toggleEditAttributes, - attributeRoll: this.rollAttribute, - toggleMarks: this.toggleMarks, - toggleAttributeMark: this.toggleAttributeMark, - toggleHP: this.toggleHP, - toggleStress: this.toggleStress, - toggleHope: this.toggleHope, - toggleGold: this.toggleGold, - attackRoll: this.attackRoll, - tabToLoadout: () => this.domainCardsTab(false), - tabToVault: () => this.domainCardsTab(true), - sendToVault: this.moveDomainCard, - sendToLoadout: this.moveDomainCard, - useDomainCard: this.useDomainCard, - removeCard: this.removeDomainCard, - selectClass: this.selectClass, - selectSubclass: this.selectSubclass, - selectAncestry: this.selectAncestry, - selectCommunity: this.selectCommunity, - viewObject: this.viewObject, - useFeature: this.useFeature, - takeShortRest: this.takeShortRest, - takeLongRest: this.takeLongRest, - addMiscItem: this.addMiscItem, - deleteItem: this.deleteItem, - addScar: this.addScar, - selectScar: this.selectScar, - deleteScar: this.deleteScar, - makeDeathMove: this.makeDeathMove, - toggleFeatureDice: this.toggleFeatureDice, - setStoryEditor: this.setStoryEditor, - itemQuantityDecrease: (_, button) => this.setItemQuantity(button, -1), - itemQuantityIncrease: (_, button) => this.setItemQuantity(button, 1), - useAbility: this.useAbility, - useAdvancementCard: this.useAdvancementCard, - useAdvancementAbility: this.useAdvancementAbility, - selectFeatureSet: this.selectFeatureSet, - toggleEquipItem: this.toggleEquipItem - }, - window: { - minimizable: false, - resizable: true - }, - form: { - handler: this.updateForm, - submitOnChange: true, - closeOnSubmit: false - }, - dragDrop: [ - { dragSelector: null, dropSelector: '.weapon-section' }, - { dragSelector: null, dropSelector: '.armor-section' }, - { dragSelector: null, dropSelector: '.inventory-weapon-section-first' }, - { dragSelector: null, dropSelector: '.inventory-weapon-section-second' }, - { dragSelector: '.item-list .item', dropSelector: null } - ] - }; - - static PARTS = { - form: { - id: 'pc', - template: 'systems/daggerheart/templates/sheets/pc/pc.hbs' - } - }; - - _getTabs() { - const setActive = tabs => { - for (const v of Object.values(tabs)) { - v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active; - v.cssClass = v.active ? 'active' : ''; - } - }; - - const primaryTabs = { - features: { - active: true, - cssClass: '', - group: 'primary', - id: 'features', - icon: null, - label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Features') - }, - loadout: { - active: false, - cssClass: '', - group: 'primary', - id: 'loadout', - icon: null, - label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Loadout') - }, - inventory: { - active: false, - cssClass: '', - group: 'primary', - id: 'inventory', - icon: null, - label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Inventory') - }, - story: { - active: false, - cssClass: '', - group: 'primary', - id: 'story', - icon: null, - label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Story') - } - }; - const secondaryTabs = { - foundation: { - active: true, - cssClass: '', - group: 'secondary', - id: 'foundation', - icon: null, - label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Foundation') - }, - loadout: { - active: false, - cssClass: '', - group: 'secondary', - id: 'loadout', - icon: null, - label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Loadout') - }, - vault: { - active: false, - cssClass: '', - group: 'secondary', - id: 'vault', - icon: null, - label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Vault') - } - }; - - setActive(primaryTabs); - setActive(secondaryTabs); - - return { primary: primaryTabs, secondary: secondaryTabs }; - } - - _attachPartListeners(partId, htmlElement, options) { - super._attachPartListeners(partId, htmlElement, options); - htmlElement - .querySelectorAll('.attribute-value') - .forEach(element => element.addEventListener('change', this.attributeChange.bind(this))); - htmlElement - .querySelectorAll('.tab-selector') - .forEach(element => element.addEventListener('click', this.tabSwitch.bind(this))); - htmlElement.querySelector('.level-title.levelup')?.addEventListener('click', this.openLevelUp.bind(this)); - htmlElement - .querySelectorAll('.feature-input') - .forEach(element => element.addEventListener('change', this.onFeatureInputBlur.bind(this))); - htmlElement - .querySelectorAll('.experience-description') - .forEach(element => element.addEventListener('change', this.experienceDescriptionChange.bind(this))); - htmlElement - .querySelectorAll('.experience-value') - .forEach(element => element.addEventListener('change', this.experienceValueChange.bind(this))); - htmlElement.querySelector('.level-value').addEventListener('change', this.onLevelChange.bind(this)); - } - - async _prepareContext(_options) { - const context = await super._prepareContext(_options); - context.document = this.document; - context.tabs = this._getTabs(); - - context.config = SYSTEM; - context.editAttributes = this.editAttributes; - context.onVaultTab = this.onVaultTab; - context.selectedScar = this.selectedScar; - context.storyEditor = this.storyEditor; - context.multiclassFeatureSetSelected = this.multiclassFeatureSetSelected; - - const selectedAttributes = Object.values(this.document.system.traits).map(x => x.base); - context.abilityScoreArray = JSON.parse( - await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.AbilityArray) - ).reduce((acc, x) => { - const selectedIndex = selectedAttributes.indexOf(x); - if (selectedIndex !== -1) { - selectedAttributes.splice(selectedIndex, 1); - } else { - acc.push({ name: x, value: x }); - } - - return acc; - }, []); - if (!context.abilityScoreArray.includes(0)) context.abilityScoreArray.push({ name: 0, value: 0 }); - context.abilityScoresFinished = context.abilityScoreArray.every(x => x.value === 0); - - //FIXME: - context.domains = this.document.system.class.value - ? { - first: this.document.system.class.value.system.domains[0] - ? SYSTEM.DOMAIN.domains[this.document.system.class.value.system.domains[0]].src - : null, - second: this.document.system.class.value.system.domains[1] - ? SYSTEM.DOMAIN.domains[this.document.system.class.value.system.domains[1]].src - : null - } - : {}; - - context.attributes = Object.keys(this.document.system.traits).reduce((acc, key) => { - acc[key] = { - ...this.document.system.traits[key], - name: game.i18n.localize(SYSTEM.ACTOR.abilities[key].name), - verbs: SYSTEM.ACTOR.abilities[key].verbs.map(x => game.i18n.localize(x)) - }; - - return acc; - }, {}); - - const ancestry = await this.mapFeatureType( - this.document.system.ancestry ? [this.document.system.ancestry] : [], - SYSTEM.GENERAL.objectTypes - ); - const community = await this.mapFeatureType( - this.document.system.community ? [this.document.system.community] : [], - SYSTEM.GENERAL.objectTypes - ); - const foundation = { - ancestry: ancestry[0], - community: community[0], - advancement: { - ...this.mapAdvancementFeatures(this.document, SYSTEM) - } - }; - - const nrLoadoutCards = this.document.system.domainCards.loadout.length; - const loadout = await this.mapFeatureType(this.document.system.domainCards.loadout, SYSTEM.DOMAIN.cardTypes); - const vault = await this.mapFeatureType(this.document.system.domainCards.vault, SYSTEM.DOMAIN.cardTypes); - context.abilities = { - foundation: foundation, - loadout: { - top: loadout.slice(0, Math.min(2, nrLoadoutCards)), - bottom: nrLoadoutCards > 2 ? loadout.slice(2, Math.min(5, nrLoadoutCards)) : [], - nrTotal: nrLoadoutCards - }, - vault: vault.map(x => ({ - ...x, - uuid: x.uuid, - sendToLoadoutDisabled: - this.document.system.domainCards.loadout.length >= this.document.system.domainData.maxLoadout - })) - }; - - context.inventory = { - consumable: { - titles: { - name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.ConsumableTitle'), - quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle') - }, - items: this.document.items.filter(x => x.type === 'consumable') - }, - miscellaneous: { - titles: { - name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.MiscellaneousTitle'), - quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle') - }, - items: this.document.items.filter(x => x.type === 'miscellaneous') - }, - weapons: { - titles: { - name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.WeaponsTitle'), - quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle') - }, - items: this.document.items.filter(x => x.type === 'weapon') - }, - armor: { - titles: { - name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.ArmorsTitle'), - quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle') - }, - items: this.document.items.filter(x => x.type === 'armor') - } - }; - - if (context.inventory.length === 0) { - context.inventory = Array(1).fill(Array(5).fill([])); - } - - context.classFeatures = ( - this.multiclassFeatureSetSelected - ? this.document.system.multiclassFeatures - : this.document.system.classFeatures - ).map(x => { - if (x.system.featureType.type !== 'dice') { - return x; - } - - return { - ...x, - uuid: x.uuid, - system: { - ...x.system, - featureType: { - ...x.system.featureType, - data: { - ...x.system.featureType.data, - property: this.document.system.subclass - ? SYSTEM.ACTOR.featureProperties[x.system.featureType.data.property].path(this.document) - : 0 - } - } - } - }; - }); - - return context; - } - - static async updateForm(event, _, formData) { - await this.document.update(formData.object); - this.render(); - } - - async mapFeatureType(data, configType) { - return await Promise.all( - data.map(async x => { - const abilities = x.system.abilities - ? await Promise.all(x.system.abilities.map(async x => await fromUuid(x.uuid))) - : []; - - return { - ...x, - uuid: x.uuid, - system: { - ...x.system, - abilities: abilities, - type: game.i18n.localize(configType[x.system.type ?? x.type].label) - } - }; - }) - ); - } - - mapAdvancementFeatures(actor, config) { - if (!actor.system.class.value || !actor.system.class.subclass) return { foundation: null, advancements: [] }; - - const { subclass, multiclassSubclass } = actor.system.subclassFeatures; - - const foundation = { - type: 'foundation', - multiclass: false, - img: actor.system.subclass.img, - subtitle: game.i18n.localize('DAGGERHEART.Sheets.PC.DomainCard.FoundationTitle'), - domains: actor.system.class.value.system.domains.map(x => config.DOMAIN.domains[x].src), - className: actor.system.class.value.name, - subclassUuid: actor.system.subclass.uuid, - subclassName: actor.system.subclass.name, - spellcast: config.ACTOR.abilities[actor.system.subclass.system.spellcastingTrait]?.name ?? null, - description: actor.system.subclass.system.foundationFeature.description, - abilities: subclass.foundation, - abilityKey: 'foundationFeature' - }; - - const firstKey = - actor.system.subclass.system.specializationFeature.unlocked && - actor.system.subclass.system.specializationFeature.tier === 2 - ? 'sub' - : actor.system.multiclass?.system?.multiclassTier === 2 - ? 'multi' - : null; - const firstType = firstKey === 'sub' ? 'specialization' : 'foundation'; - const firstBase = - firstKey === 'sub' ? actor.system.subclass : firstKey === 'multi' ? actor.system.multiclassSubclass : null; - const first = !firstBase - ? null - : { - type: firstType, - multiclass: firstKey === 'multi', - img: firstBase.img, - subtitle: - firstKey === 'sub' - ? game.i18n.localize('DAGGERHEART.Sheets.PC.DomainCard.SpecializationTitle') - : game.i18n.localize('DAGGERHEART.Sheets.PC.DomainCard.FoundationTitle'), - domains: - firstKey === 'sub' - ? actor.system.class.value.system.domains.map(x => config.DOMAIN.domains[x].src) - : actor.system.multiclass.system.domains.map(x => config.DOMAIN.domains[x].src), - className: firstKey === 'sub' ? actor.system.class.value.name : actor.system.multiclass.name, - subclassUuid: firstBase.uuid, - subclassName: firstBase.name, - spellcast: - firstKey === 'sub' - ? null - : (config.ACTOR.abilities[firstBase.system.spellcastingTrait]?.name ?? null), - description: - firstKey === 'sub' - ? firstBase.system.specializationFeature.description - : firstBase.system.foundationFeature.description, - abilities: firstKey === 'sub' ? subclass.specialization : multiclassSubclass.foundation, - abilityKey: firstKey === 'sub' ? 'specializationFeature' : 'foundationFeature' - }; - - const secondKey = - (actor.system.subclass.system.specializationFeature.unlocked && - actor.system.subclass.system.specializationFeature.tier === 3) || - (actor.system.subclass.system.masteryFeature.unlocked && - actor.system.subclass.system.masteryFeature.tier === 3) - ? 'sub' - : actor.system.multiclass?.system?.multiclassTier === 3 || - actor.system.multiclassSubclass?.system?.specializationFeature?.unlocked - ? 'multi' - : null; - const secondBase = - secondKey === 'sub' - ? actor.system.subclass - : secondKey === 'multi' - ? actor.system.multiclassSubclass - : null; - const secondAbilities = secondKey === 'sub' ? subclass : multiclassSubclass; - const secondType = secondBase - ? secondBase.system.masteryFeature.unlocked - ? 'mastery' - : secondBase.system.specializationFeature.unlocked - ? 'specialization' - : 'foundation' - : null; - const second = !secondBase - ? null - : { - type: secondType, - multiclass: secondKey === 'multi', - img: secondBase.img, - subtitle: secondBase.system.masteryFeature.unlocked - ? game.i18n.localize('DAGGERHEART.Sheets.PC.DomainCard.MasteryTitle') - : secondBase.system.specializationFeature.unlocked - ? game.i18n.localize('DAGGERHEART.Sheets.PC.DomainCard.SpecializationTitle') - : game.i18n.localize('DAGGERHEART.Sheets.PC.DomainCard.FoundationTitle'), - domains: - secondKey === 'sub' - ? actor.system.class.value.system.domains.map(x => config.DOMAIN.domains[x].src) - : actor.system.multiclass.system.domains.map(x => config.DOMAIN.domains[x].src), - className: secondKey === 'sub' ? actor.system.class.value.name : actor.system.multiclass.name, - subclassUuid: secondBase.uuid, - subclassName: secondBase.name, - spellcast: - secondKey === 'sub' || secondBase.system.specializationFeature.unlocked - ? null - : (config.ACTOR.abilities[firstBase.system.spellcastingTrait]?.name ?? null), - description: secondBase.system.masteryFeature.unlocked - ? secondBase.system.masteryFeature.description - : secondBase.system.specializationFeature.unlocked - ? secondBase.system.specializationFeature.description - : firstBase.system.foundationFeature.description, - abilities: secondBase.system.masteryFeature.unlocked - ? secondAbilities.mastery - : secondBase.system.specializationFeature.unlocked - ? secondAbilities.specialization - : secondAbilities.foundation, - abilityKey: secondBase.system.masteryFeature.unlocked - ? 'masteryFeature' - : secondBase.system.specializationFeature.unlocked - ? 'specializationFeature' - : 'foundationFeature' - }; - - return { - foundation: foundation, - first: first, - second: second - }; - } - - async attributeChange(event) { - const path = `system.traits.${event.currentTarget.dataset.attribute}.base`; - await this.document.update({ [path]: event.currentTarget.value }); - } - - static toggleEditAttributes() { - this.editAttributes = !this.editAttributes; - this.render(); - } - - static async rollAttribute(event, button) { - const { roll, hope, fear, advantage, disadvantage, modifiers } = await this.document.dualityRoll( - { title: game.i18n.localize(abilities[button.dataset.attribute].label), value: button.dataset.value }, - event.shiftKey - ); - - const cls = getDocumentClass('ChatMessage'); - - const systemContent = { - title: game.i18n.format('DAGGERHEART.Chat.DualityRoll.AbilityCheckTitle', { - ability: game.i18n.localize(abilities[button.dataset.attribute].label) - }), - origin: this.document.id, - roll: roll._formula, - modifiers: modifiers, - hope: hope, - fear: fear, - advantage: advantage, - disadvantage: disadvantage - }; - - const msg = new cls({ - type: 'dualityRoll', - sound: CONFIG.sounds.dice, - system: systemContent, - user: game.user.id, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/duality-roll.hbs', - systemContent - ), - rolls: [roll] - }); - - await cls.create(msg.toObject()); - } - - static async toggleMarks(_, button) { - const markValue = Number.parseInt(button.dataset.value); - const newValue = this.document.system.armor.system.marks.value >= markValue ? markValue - 1 : markValue; - await this.document.system.armor.update({ 'system.marks.value': newValue }); - } - - static async toggleAttributeMark(_, button) { - const attribute = this.document.system.traits[button.dataset.attribute]; - const newMark = this.document.system.availableAttributeMarks - .filter(x => x > Math.max.apply(null, this.document.system.traits[button.dataset.attribute].levelMarks)) - .sort((a, b) => (a > b ? 1 : -1))[0]; - - if (attribute.levelMark || !newMark) return; - - const path = `system.traits.${button.dataset.attribute}.levelMarks`; - await this.document.update({ [path]: [...attribute.levelMarks, newMark] }); - } - - static async toggleHP(_, button) { - const healthValue = Number.parseInt(button.dataset.value); - const newValue = this.document.system.resources.hitPoints.value >= healthValue ? healthValue - 1 : healthValue; - await this.document.update({ 'system.resources.hitPoints.value': newValue }); - } - - static async toggleStress(_, button) { - const healthValue = Number.parseInt(button.dataset.value); - const newValue = this.document.system.resources.stress.value >= healthValue ? healthValue - 1 : healthValue; - await this.document.update({ 'system.resources.stress.value': newValue }); - } - - static async toggleHope(_, button) { - const hopeValue = Number.parseInt(button.dataset.value); - const newValue = this.document.system.resources.hope.value >= hopeValue ? hopeValue - 1 : hopeValue; - await this.document.update({ 'system.resources.hope.value': newValue }); - } - - static async toggleGold(_, button) { - const goldValue = Number.parseInt(button.dataset.value); - const goldType = button.dataset.type; - const newValue = this.document.system.gold[goldType] >= goldValue ? goldValue - 1 : goldValue; - - const update = `system.gold.${goldType}`; - await this.document.update({ [update]: newValue }); - } - - static async attackRoll(event, button) { - const weapon = await fromUuid(button.dataset.weapon); - const damage = { - value: `${this.document.system.proficiency.value}${weapon.system.damage.value}`, - type: weapon.system.damage.type, - bonusDamage: this.document.system.bonuses.damage - }; - const modifier = this.document.system.traits[weapon.system.trait].value; - - const { roll, hope, fear, advantage, disadvantage, modifiers, bonusDamageString } = - await this.document.dualityRoll( - { title: game.i18n.localize(abilities[weapon.system.trait].label), value: modifier }, - event.shiftKey, - damage.bonusDamage - ); - - damage.value = damage.value.concat(bonusDamageString); - - const targets = Array.from(game.user.targets).map(x => ({ - id: x.id, - name: x.actor.name, - img: x.actor.img, - difficulty: x.actor.system.difficulty, - evasion: x.actor.system.evasion.value - })); - - const systemData = { - title: weapon.name, - origin: this.document.id, - roll: roll._formula, - modifiers: modifiers, - hope: hope, - fear: fear, - advantage: advantage, - disadvantage: disadvantage, - damage: damage, - targets: targets - }; - - const cls = getDocumentClass('ChatMessage'); - const msg = new cls({ - type: 'dualityRoll', - sound: CONFIG.sounds.dice, - system: systemData, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/attack-roll.hbs', - systemData - ), - rolls: [roll] - }); - - await cls.create(msg.toObject()); - } - - tabSwitch(event) { - const tab = event.currentTarget.dataset.tab; - if (tab !== 'loadout') { - this.onVaultTab = false; - } - - this.render(); - } - - openLevelUp() { - if (!this.document.system.class.value || !this.document.system.subclass) { - ui.notifications.error(game.i18n.localize('DAGGERHEART.Sheets.PC.Errors.missingClassOrSubclass')); - return; - } - - new DhlevelUp(this.document).render(true); - } - - static domainCardsTab(toVault) { - this.onVaultTab = toVault; - this.render(); - } - - static async moveDomainCard(_, button) { - const toVault = button.dataset.action === 'sendToVault'; - if (!toVault && this.document.system.domainCards.loadout.length >= this.document.system.domainData.maxLoadout) { - return; - } - - const card = this.document.items.find(x => x.uuid === button.dataset.domain); - await card.update({ 'system.inVault': toVault }); - } - - static async useDomainCard(_, button) { - const card = this.document.items.find(x => x.uuid === button.dataset.key); - - const cls = getDocumentClass('ChatMessage'); - const systemData = { - title: `${game.i18n.localize('DAGGERHEART.Chat.DomainCard.Title')} - ${capitalize(button.dataset.domain)}`, - origin: this.document.id, - img: card.img, - name: card.name, - description: card.system.effect, - actions: card.system.actions - }; - const msg = new cls({ - type: 'abilityUse', - user: game.user.id, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/ability-use.hbs', - systemData - ), - system: systemData - }); - - cls.create(msg.toObject()); - } - - static async removeDomainCard(_, button) { - if (button.dataset.type === 'domainCard') { - const card = this.document.items.find(x => x.uuid === button.dataset.key); - await card.delete(); - } - } - - static async selectClass() { - (await game.packs.get('daggerheart.classes'))?.render(true); - } - - static async selectSubclass() { - (await game.packs.get('daggerheart.subclasses'))?.render(true); - } - - static async selectAncestry() { - const dialogClosed = new Promise((resolve, _) => { - new AncestrySelectionDialog(resolve).render(true); - }); - const result = await dialogClosed; - - // await this.emulateItemDrop({ type: 'item', data: result }); - for (var ancestry of this.document.items.filter(x => x => x.type === 'ancestry')) { - await ancestry.delete(); - } - - const createdItems = []; - for (var feature of this.document.items.filter( - x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.ancestry.id - )) { - await feature.delete(); - } - - // createdItems.push(...result.data.system.abilities); - createdItems.push(result.data); - - await this.document.createEmbeddedDocuments('Item', createdItems); - - // await this.document.createEmbeddedDocuments("Item", [result.toObject()]); - // (await game.packs.get('daggerheart.playtest-ancestries'))?.render(true); - } - - static async selectCommunity() { - (await game.packs.get('daggerheart.communities'))?.render(true); - } - - static async viewObject(_, button) { - const object = await fromUuid(button.dataset.value); - if (!object) return; - - const tab = button.dataset.tab; - if (tab && object.sheet._tabs) object.sheet._tabs[0].active = tab; - - if (object.sheet.editMode) object.sheet.editMode = false; - - object.sheet.render(true); - } - - static async takeShortRest() { - await new DhpDowntime(this.document, true).render(true); - await this.minimize(); - } - - static async takeLongRest() { - await new DhpDowntime(this.document, false).render(true); - await this.minimize(); - } - - static async addMiscItem() { - const result = await this.document.createEmbeddedDocuments('Item', [ - { - name: game.i18n.localize('DAGGERHEART.Sheets.PC.NewItem'), - type: 'miscellaneous' - } - ]); - - await result[0].sheet.render(true); - } - - static async addScar() { - if (this.document.system.story.scars.length === 5) return; - - await this.document.update({ - 'system.story.scars': [ - ...this.document.system.story.scars, - { name: game.i18n.localize('DAGGERHEART.Sheets.PC.NewScar'), description: '' } - ] - }); - } - - static async selectScar(_, button) { - this.selectedScar = Number.parseInt(button.dataset.value); - this.render(); - } - - static async deleteScar(event, button) { - event.stopPropagation(); - await this.document.update({ - 'system.story.scars': this.document.system.story.scars.filter( - (_, index) => index !== Number.parseInt(button.currentTarget.dataset.scar) - ) - }); - } - - static async makeDeathMove() { - if (this.document.system.resources.hitPoints.value === this.document.system.resources.hitPoints.max) { - await new DhpDeathMove(this.document).render(true); - await this.minimize(); - } - } - - static async toggleFeatureDice(_, button) { - const index = Number.parseInt(button.dataset.index); - const feature = this.document.system.classFeatures.find(x => x.uuid === button.dataset.feature); - const path = `system.featureType.data.numbers.${index}`; - if (feature.system.featureType.data.numbers[index]?.used) return; - - if (Object.keys(feature.system.featureType.data.numbers).length <= index) { - const roll = new Roll(feature.system.featureType.data.value); - const rollData = await roll.evaluate(); - const cls = getDocumentClass('ChatMessage'); - const msg = new cls({ - user: game.user.id, - rolls: [roll] - }); - - await cls.create(msg.toObject()); - - await feature.update({ [path]: { value: Number.parseInt(rollData.total), used: false } }); - } else { - await Dialog.confirm({ - title: game.i18n.localize('Confirm feature use'), - content: `Are you sure you want to use ${feature.name}?`, - yes: async () => { - await feature.update({ [path]: { used: true } }); - - const cls = getDocumentClass('ChatMessage'); - const msg = new cls({ - user: game.user.id, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/ability-use.hbs', - { - title: game.i18n.localize('DAGGERHEART.Chat.FeatureTitle'), - card: { - name: `${feature.name} - Roll Of ${feature.system.featureType.data.numbers[index].value}`, - img: feature.img - } - } - ) - }); - - cls.create(msg.toObject()); - }, - no: () => { - return; - }, - defaultYes: false - }); - } - } - - async onFeatureInputBlur(event) { - const feature = this.document.system.classFeatures.find(x => x.uuid === event.currentTarget.dataset.feature); - const value = Number.parseInt(event.currentTarget.value); - if (!Number.isNaN(value)) await feature?.update({ 'system.featureType.data.value': value }); - } - - async experienceDescriptionChange(event) { - const newExperiences = [...this.document.system.experiences]; - newExperiences[event.currentTarget.dataset.index].description = event.currentTarget.value; - await this.document.update({ 'system.experiences': newExperiences }); - } - - async experienceValueChange(event) { - const newExperiences = [...this.document.system.experiences]; - newExperiences[event.currentTarget.dataset.index].value = event.currentTarget.value; - await this.document.update({ 'system.experiences': newExperiences }); - } - - static setStoryEditor(_, button) { - this.storyEditor = this.storyEditor === button.dataset.value ? null : button.dataset.value; - this.render(); - } - - async itemUpdate(event) { - const name = event.currentTarget.dataset.item; - const item = await fromUuid($(event.currentTarget).closest('[data-item-id]')[0].dataset.itemId); - await item.update({ [name]: event.currentTarget.value }); - } - - async onLevelChange(event) { - await this.document.updateLevel(Number(event.currentTarget.value)); - this.render(); - } - - static async deleteItem(_, button) { - const item = await fromUuid($(button).closest('[data-item-id]')[0].dataset.itemId); - await item.delete(); - } - - static async setItemQuantity(button, value) { - const item = await fromUuid($(button).closest('[data-item-id]')[0].dataset.itemId); - await item.update({ 'system.quantity': Math.max(item.system.quantity + value, 1) }); - } - - static async useFeature(_, button) { - const item = await fromUuid(button.dataset.id); - - const cls = getDocumentClass('ChatMessage'); - const systemData = { - title: game.i18n.localize('DAGGERHEART.Chat.FeatureTitle'), - origin: this.document.id, - img: item.img, - name: item.name, - description: item.system.description, - actions: item.system.actions - }; - const msg = new cls({ - type: 'abilityUse', - user: game.user.id, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/ability-use.hbs', - systemData - ), - system: systemData - }); - - cls.create(msg.toObject()); - } - - static async useAbility(_, button) { - const item = await fromUuid(button.dataset.feature); - const type = button.dataset.type; - - const cls = getDocumentClass('ChatMessage'); - const systemData = { - title: - type === 'ancestry' - ? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.AncestryTitle') - : type === 'community' - ? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.CommunityTitle') - : game.i18n.localize('DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle'), - origin: this.document.id, - img: item.img, - name: item.name, - description: item.system.description, - actions: [] - }; - const msg = new cls({ - type: 'abilityUse', - user: game.user.id, - system: systemData, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/ability-use.hbs', - systemData - ) - }); - - cls.create(msg.toObject()); - } - - static async useAdvancementCard(_, button) { - const item = - button.dataset.multiclass === 'true' - ? this.document.system.multiclassSubclass - : this.document.system.subclass; - const ability = item.system[`${button.dataset.key}Feature`]; - const title = `${item.name} - ${game.i18n.localize(`DAGGERHEART.Sheets.PC.DomainCard.${capitalize(button.dataset.key)}Title`)}`; - - const cls = getDocumentClass('ChatMessage'); - const systemData = { - title: game.i18n.localize('DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle'), - origin: this.document.id, - name: title, - img: item.img, - description: ability.description - }; - const msg = new cls({ - type: 'abilityUse', - user: game.user.id, - system: systemData, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/ability-use.hbs', - systemData - ) - }); - - cls.create(msg.toObject()); - } - - static async useAdvancementAbility(_, button) { - // const item = await fromUuid(button.dataset.id); - const item = this.document.items.find(x => x.uuid === button.dataset.id); - - const cls = getDocumentClass('ChatMessage'); - const systemData = { - title: game.i18n.localize('DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle'), - origin: this.document.id, - name: item.name, - img: item.img, - description: item.system.description - }; - const msg = new cls({ - user: game.user.id, - system: systemData, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/ability-use.hbs', - systemData - ) - }); - - cls.create(msg.toObject()); - } - - static async selectFeatureSet(_, button) { - const multiclass = button.dataset.multiclass === 'true'; - this.multiclassFeatureSetSelected = multiclass; - this.render(); - } - - static async toggleEquipItem(_, button) { - const item = this.document.items.get(button.id); - if (item.system.equipped) { - await item.update({ 'system.equipped': false }); - return; - } - - switch (item.type) { - case 'armor': - const currentArmor = this.document.system.armor; - if (currentArmor) { - await currentArmor.update({ 'system.equipped': false }); - } - - await item.update({ 'system.equipped': true }); - break; - case 'weapon': - await this.document.system.constructor.unequipBeforeEquip.bind(this.document.system)(item); - - await item.update({ 'system.equipped': true }); - break; - } - this.render(); - } - - static async close(options) { - this.onVaultTab = false; - super.close(options); - } - - async _onDragStart(_, event) { - if (event.currentTarget.classList.contains('inventory-item')) { - if (!['weapon', 'armor'].includes(event.currentTarget.dataset.type)) { - return; - } - - const targets = { - weapon: ['weapon-section', 'inventory-weapon-section'], - armor: ['armor-section', 'inventory-armor-section'] - }; - - event.dataTransfer.setData( - 'text/plain', - JSON.stringify({ - uuid: event.currentTarget.dataset.item, - internal: true, - targets: targets[event.currentTarget.dataset.type] - }) - ); - } - - super._onDragStart(event); - } - - async _onDrop(event) { - const itemData = event.dataTransfer?.getData('text/plain'); - const item = itemData ? JSON.parse(itemData) : null; - if (item?.internal) { - let target = null; - event.currentTarget.classList.forEach(x => { - if (item.targets.some(target => target === x)) { - target = x; - } - }); - if (target) { - const itemObject = await fromUuid(item.uuid); - switch (target) { - case 'weapon-section': - if ( - itemObject.system.secondary && - this.document.system.equippedWeapons.burden === 'twoHanded' - ) { - ui.notifications.info( - game.i18n.localize('DAGGERHEART.Notification.Info.SecondaryEquipWhileTwohanded') - ); - return; - } else if ( - itemObject.system.burden === 'twoHanded' && - this.document.system.equippedWeapons.secondary - ) { - ui.notifications.info( - game.i18n.localize('DAGGERHEART.Notification.Info.TwohandedEquipWhileSecondary') - ); - return; - } - - const existingWeapon = this.document.items.find( - x => x.system.active && x.system.secondary === itemObject.system.secondary - ); - await existingWeapon?.update({ 'system.active': false }); - await itemObject.update({ 'system.active': true }); - break; - case 'armor-section': - const existingArmor = this.document.items.find(x => x.type === 'armor' && x.system.active); - await existingArmor?.update({ 'system.active': false }); - await itemObject.update({ 'system.active': true }); - break; - case 'inventory-weapon-section': - /* FIXME inventoryWeapon is no longer a field - const existingInventoryWeapon = this.document.items.find(x => x.system.inventoryWeapon); - await existingInventoryWeapon?.update({ 'system.inventoryWeapon': false }); - await itemObject.update({ 'system.inventoryWeapon': true }); - break; - */ - case 'inventory-armor-section': - const existingInventoryArmor = this.document.items.find(x => x.system.inventoryArmor); - await existingInventoryArmor?.update({ 'system.inventoryArmor': false }); - await itemObject.update({ 'system.inventoryArmor': true }); - break; - } - } - } else { - super._onDrop(event); - this._onDropItem(event, TextEditor.getDragEventData(event)); - } - } - - async _onDropItem(event, data) { - if (this.dropItemBlock) { - return; - } else { - this.dropItemBlock = true; - setTimeout(() => (this.dropItemBlock = false), 500); - } - - const element = event.currentTarget; - const item = await Item.implementation.fromDropData(data); - const itemData = item.toObject(); - - const createdItems = []; - - if (item.type === 'domainCard') { - if (!this.document.system.class.value) { - ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.NoClassSelected')); - return; - } - - if (!this.document.system.domains.find(x => x === item.system.domain)) { - ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.LacksDomain')); - return; - } - - if (this.document.system.domainCards.total.length === this.document.system.domainData.maxCards) { - ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.MaxLoadoutReached')); - return; - } - - if (this.document.system.domainCards.total.find(x => x.name === item.name)) { - ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.DuplicateDomainCard')); - return; - } - - if (this.document.system.domainCards.loadout.length >= this.document.system.domainData.maxLoadout) { - itemData.system.inVault = true; - } - - if (this.document.uuid === item.parent?.uuid) return this._onSortItem(event, itemData); - const createdItem = await this._onDropItemCreate(itemData); - - return createdItem; - } else { - if (item.type === 'ancestry') { - for (var feature of this.document.items.filter( - x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.ancestry.id - )) { - await feature.delete(); - } - - for (var feature of item.system.abilities) { - const data = (await fromUuid(feature.uuid)).toObject(); - const itemData = await this._onDropItemCreate(data); - createdItems.push(itemData); - } - } else if (item.type === 'community') { - for (var feature of this.document.items.filter( - x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.community.id - )) { - await feature.delete(); - } - - for (var feature of item.system.abilities) { - const data = (await fromUuid(feature.uuid)).toObject(); - const itemData = await this._onDropItemCreate(data); - createdItems.push(itemData); - } - } - - if (this.document.uuid === item.parent?.uuid) return this._onSortItem(event, item); - - if (item.type === 'weapon') { - if (!element) return; - - if (element.classList.contains('weapon-section')) { - await this.document.system.constructor.unequipBeforeEquip.bind(this.document.system)(itemData); - itemData.system.equipped = true; - } - } - - if (item.type === 'armor') { - if (!element) return; - - if (element.classList.contains('armor-section')) { - const existing = this.document.system.armor - ? await fromUuid(this.document.system.armor.uuid) - : null; - await existing?.update({ 'system.equipped': false }); - itemData.system.equipped = true; - } - } - - const createdItem = await this._onDropItemCreate(itemData); - createdItems.push(createdItem); - - return createdItems; - } - } - - async _onDropItemCreate(itemData, event) { - itemData = itemData instanceof Array ? itemData : [itemData]; - return this.document.createEmbeddedDocuments('Item', itemData); - } - - async emulateItemDrop(data) { - const event = new DragEvent('drop', { altKey: game.keyboard.isModifierActive('Alt') }); - return this._onDropItem(event, data); - } -} diff --git a/module/config/actorConfig.mjs b/module/config/actorConfig.mjs index dcd85cdb..3698d19e 100644 --- a/module/config/actorConfig.mjs +++ b/module/config/actorConfig.mjs @@ -76,7 +76,7 @@ export const featureProperties = { }, spellcastingTrait: { name: 'DAGGERHEART.FeatureProperty.SpellcastingTrait', - path: actor => actor.system.traits[actor.system.subclass.system.spellcastingTrait].data.value + path: actor => actor.system.traits[actor.system.class.subclass.system.spellcastingTrait].data.value } }; diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index fecb939e..6526392f 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -199,8 +199,8 @@ export const tiers = { }; export const objectTypes = { - pc: { - name: 'TYPES.Actor.pc' + character: { + name: 'TYPES.Actor.character' }, npc: { name: 'TYPES.Actor.npc' diff --git a/module/data/_module.mjs b/module/data/_module.mjs index b70b5a23..85dae6dd 100644 --- a/module/data/_module.mjs +++ b/module/data/_module.mjs @@ -1,4 +1,4 @@ -export { default as DhpPC } from './pc.mjs'; +export { default as DhCharacter } from './character.mjs'; export { default as DhClass } from './item/class.mjs'; export { default as DhSubclass } from './item/subclass.mjs'; export { default as DhCombat } from './combat.mjs'; diff --git a/module/data/character.mjs b/module/data/character.mjs new file mode 100644 index 00000000..a3494ecd --- /dev/null +++ b/module/data/character.mjs @@ -0,0 +1,233 @@ +import { burden } from '../config/generalConfig.mjs'; +import ForeignDocumentUUIDField from './fields/foreignDocumentUUIDField.mjs'; +import { LevelOptionType } from './levelTier.mjs'; + +const attributeField = () => + new foundry.data.fields.SchemaField({ + value: new foundry.data.fields.NumberField({ initial: 0, integer: true }), + tierMarked: new foundry.data.fields.BooleanField({ initial: false }) + }); + +const resourceField = max => + new foundry.data.fields.SchemaField({ + value: new foundry.data.fields.NumberField({ initial: 0, integer: true }), + max: new foundry.data.fields.NumberField({ initial: max, integer: true }) + }); + +export default class DhCharacter extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + resources: new fields.SchemaField({ + hitPoints: resourceField(6), + stress: resourceField(6), + hope: resourceField(6) + }), + traits: new fields.SchemaField({ + agility: attributeField(), + strength: attributeField(), + finesse: attributeField(), + instinct: attributeField(), + presence: attributeField(), + knowledge: attributeField() + }), + proficiency: new fields.NumberField({ initial: 1, integer: true }), + evasion: new fields.NumberField({ initial: 0, integer: true }), + experiences: new fields.TypedObjectField( + new fields.SchemaField({ + description: new fields.StringField({}), + value: new fields.NumberField({ integer: true, nullable: true, initial: null }) + }), + { + initial: { + [foundry.utils.randomID()]: { description: '', value: 2 }, + [foundry.utils.randomID()]: { description: '', value: 2 } + } + } + ), + gold: new fields.SchemaField({ + coins: new fields.NumberField({ initial: 0, integer: true }), + handfulls: new fields.NumberField({ initial: 0, integer: true }), + bags: new fields.NumberField({ initial: 0, integer: true }), + chests: new fields.NumberField({ initial: 0, integer: true }) + }), + pronouns: new fields.StringField({}), + scars: new fields.TypedObjectField( + new fields.SchemaField({ + name: new fields.StringField({}), + description: new fields.HTMLField() + }) + ), + story: new fields.HTMLField(), + description: new fields.HTMLField(), + class: new fields.SchemaField({ + value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }), + subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }) + }), + multiclass: new fields.SchemaField({ + value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }), + subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }) + }), + levelData: new fields.EmbeddedDataField(DhPCLevelData) + }; + } + + get ancestry() { + return this.parent.items.find(x => x.type === 'ancestry') ?? null; + } + + get community() { + return this.parent.items.find(x => x.type === 'community') ?? null; + } + + get domains() { + const classDomains = this.class ? this.class.system.domains : []; + const multiclassDomains = this.multiclass ? this.multiclass.system.domains : []; + return [...classDomains, ...multiclassDomains]; + } + + get domainCards() { + const domainCards = this.parent.items.filter(x => x.type === 'domainCard'); + const loadout = domainCards.filter(x => !x.system.inVault); + const vault = domainCards.filter(x => x.system.inVault); + + return { + loadout: loadout, + vault: vault, + total: [...loadout, ...vault] + }; + } + + get armor() { + return this.parent.items.find(x => x.type === 'armor' && x.system.equipped); + } + + get primaryWeapon() { + return this.parent.items.find(x => x.type === 'weapon' && x.system.equipped && !x.system.secondary); + } + + get secondaryWeapon() { + return this.parent.items.find(x => x.type === 'weapon' && x.system.equipped && x.system.secondary); + } + + get getWeaponBurden() { + return this.primaryWeapon?.system?.burden === burden.twoHanded.value || + (this.primaryWeapon && this.secondaryWeapon) + ? burden.twoHanded.value + : this.primaryWeapon || this.secondaryWeapon + ? burden.oneHanded.value + : null; + } + + get refreshableFeatures() { + return this.parent.items.reduce( + (acc, x) => { + if (x.type === 'feature' && x.system.refreshData?.type === 'feature' && x.system.refreshData?.type) { + acc[x.system.refreshData.type].push(x); + } + + return acc; + }, + { shortRest: [], longRest: [] } + ); + } + + static async unequipBeforeEquip(itemToEquip) { + const primary = this.primaryWeapon, + secondary = this.secondaryWeapon; + if (itemToEquip.system.secondary) { + if (primary && primary.burden === SYSTEM.GENERAL.burden.twoHanded.value) { + await primary.update({ 'system.equipped': false }); + } + + if (secondary) { + await secondary.update({ 'system.equipped': false }); + } + } else { + if (secondary && itemToEquip.system.burden === SYSTEM.GENERAL.burden.twoHanded.value) { + await secondary.update({ 'system.equipped': false }); + } + + if (primary) { + await primary.update({ 'system.equipped': false }); + } + } + } + + prepareBaseData() { + for (var attributeKey in this.traits) { + const attribute = this.traits[attributeKey]; + /* Levleup handling */ + } + + const armor = this.armor; + this.damageThresholds = { + major: armor + ? armor.system.baseThresholds.major + this.levelData.level.current + : this.levelData.level.current, + severe: armor + ? armor.system.baseThresholds.severe + this.levelData.level.current + : this.levelData.level.current * 2 + }; + } + + prepareDerivedData() { + this.resources.hope.max -= Object.keys(this.scars).length; + this.resources.hope.value = Math.min(this.resources.hope.value, this.resources.hope.max); + } +} + +class DhPCLevelData extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + level: new fields.SchemaField({ + current: new fields.NumberField({ required: true, integer: true, initial: 1 }), + changed: new fields.NumberField({ required: true, integer: true, initial: 1 }) + }), + levelups: new fields.TypedObjectField( + new fields.SchemaField({ + achievements: new fields.SchemaField( + { + experiences: new fields.TypedObjectField( + new fields.SchemaField({ + name: new fields.StringField({ required: true }), + modifier: new fields.NumberField({ required: true, integer: true }) + }) + ), + domainCards: new fields.ArrayField( + new fields.SchemaField({ + uuid: new fields.StringField({ required: true }), + itemUuid: new fields.StringField({ required: true }) + }) + ), + proficiency: new fields.NumberField({ integer: true }) + }, + { nullable: true, initial: null } + ), + selections: new fields.ArrayField( + new fields.SchemaField({ + tier: new fields.NumberField({ required: true, integer: true }), + level: new fields.NumberField({ required: true, integer: true }), + optionKey: new fields.StringField({ required: true }), + type: new fields.StringField({ required: true, choices: LevelOptionType }), + checkboxNr: new fields.NumberField({ required: true, integer: true }), + value: new fields.NumberField({ integer: true }), + minCost: new fields.NumberField({ integer: true }), + amount: new fields.NumberField({ integer: true }), + data: new fields.ArrayField(new fields.StringField({ required: true })), + secondaryData: new fields.StringField(), + itemUuid: new fields.StringField({ required: true }) + }) + ) + }) + ) + }; + } + + get canLevelUp() { + return this.level.current < this.level.changed; + } +} diff --git a/module/data/item/class.mjs b/module/data/item/class.mjs index 423ad8a5..0584f1db 100644 --- a/module/data/item/class.mjs +++ b/module/data/item/class.mjs @@ -56,7 +56,7 @@ export default class DHClass extends BaseDataItem { const allowed = await super._preCreate(data, options, user); if (allowed === false) return; - if (this.actor?.type === 'pc') { + if (this.actor?.type === 'character') { const path = data.system.isMulticlass ? 'system.multiclass.value' : 'system.class.value'; if (foundry.utils.getProperty(this.actor, path)) { ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.ClassAlreadySelected')); @@ -67,7 +67,7 @@ export default class DHClass extends BaseDataItem { _onCreate(data, options, userId) { super._onCreate(data, options, userId); - if (options.parent?.type === 'pc') { + if (options.parent?.type === 'character') { const path = `system.${data.system.isMulticlass ? 'multiclass.value' : 'class.value'}`; options.parent.update({ [path]: `${options.parent.uuid}.Item.${data._id}` }); } @@ -76,7 +76,7 @@ export default class DHClass extends BaseDataItem { _onDelete(options, userId) { super._onDelete(options, userId); - if (options.parent?.type === 'pc') { + if (options.parent?.type === 'character') { const path = `system.${this.isMulticlass ? 'multiclass' : 'class'}`; options.parent.update({ [`${path}.value`]: null diff --git a/module/data/item/domainCard.mjs b/module/data/item/domainCard.mjs index 6986708f..b5880aad 100644 --- a/module/data/item/domainCard.mjs +++ b/module/data/item/domainCard.mjs @@ -1,13 +1,13 @@ -import DaggerheartAction from "../action.mjs"; -import BaseDataItem from "./base.mjs"; +import DaggerheartAction from '../action.mjs'; +import BaseDataItem from './base.mjs'; export default class DHDomainCard extends BaseDataItem { /** @inheritDoc */ static get metadata() { return foundry.utils.mergeObject(super.metadata, { - label: "TYPES.Item.domainCard", - type: "domainCard", - hasDescription: true, + label: 'TYPES.Item.domainCard', + type: 'domainCard', + hasDescription: true }); } @@ -19,10 +19,37 @@ export default class DHDomainCard extends BaseDataItem { domain: new fields.StringField({ choices: SYSTEM.DOMAIN.domains, required: true, blank: true }), level: new fields.NumberField({ initial: 1, integer: true }), recallCost: new fields.NumberField({ initial: 0, integer: true }), - type: new fields.StringField({ choices: SYSTEM.DOMAIN.cardTypes, required: true, blank: true}), + type: new fields.StringField({ choices: SYSTEM.DOMAIN.cardTypes, required: true, blank: true }), foundation: new fields.BooleanField({ initial: false }), inVault: new fields.BooleanField({ initial: false }), actions: new fields.ArrayField(new fields.EmbeddedDataField(DaggerheartAction)) }; } + + async _preCreate(data, options, user) { + const allowed = await super._preCreate(data, options, user); + if (allowed === false) return; + + if (this.actor?.type === 'character') { + if (!this.actor.system.class.value) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.NoClassSelected')); + return false; + } + + if (!this.actor.system.domains.find(x => x === item.system.domain)) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.LacksDomain')); + return false; + } + + if (this.actor.system.domainCards.total.length === 5) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.MaxLoadoutReached')); + return false; + } + + if (this.actor.system.domainCards.total.find(x => x.name === item.name)) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.DuplicateDomainCard')); + return false; + } + } + } } diff --git a/module/data/item/subclass.mjs b/module/data/item/subclass.mjs index 883df064..ea506efa 100644 --- a/module/data/item/subclass.mjs +++ b/module/data/item/subclass.mjs @@ -33,7 +33,7 @@ export default class DHSubclass extends BaseDataItem { const allowed = await super._preCreate(data, options, user); if (allowed === false) return; - if (this.actor?.type === 'pc') { + if (this.actor?.type === 'character') { const path = data.system.isMulticlass ? 'system.multiclass' : 'system.class'; const classData = foundry.utils.getProperty(this.actor, path); if (!classData.value) { @@ -52,7 +52,7 @@ export default class DHSubclass extends BaseDataItem { _onCreate(data, options, userId) { super._onCreate(data, options, userId); - if (options.parent?.type === 'pc') { + if (options.parent?.type === 'character') { const path = `system.${data.system.isMulticlass ? 'multiclass.subclass' : 'class.subclass'}`; options.parent.update({ [path]: `${options.parent.uuid}.Item.${data._id}` }); } @@ -61,7 +61,7 @@ export default class DHSubclass extends BaseDataItem { _onDelete(options, userId) { super._onDelete(options, userId); - if (options.parent?.type === 'pc') { + if (options.parent?.type === 'character') { const path = `system.${this.isMulticlass ? 'multiclass.subclass' : 'class.subclass'}`; options.parent.update({ [path]: null }); } diff --git a/module/data/pc.mjs b/module/data/pc.mjs deleted file mode 100644 index 740b51eb..00000000 --- a/module/data/pc.mjs +++ /dev/null @@ -1,413 +0,0 @@ -import { getPathValue } from '../helpers/utils.mjs'; -import ForeignDocumentUUIDField from './fields/foreignDocumentUUIDField.mjs'; -import { LevelOptionType } from './levelTier.mjs'; - -const fields = foundry.data.fields; - -const attributeField = () => - new fields.SchemaField({ - bonus: new fields.NumberField({ initial: 0, integer: true }), - base: new fields.NumberField({ initial: 0, integer: true }), - tierMarked: new fields.BooleanField({ required: true, initial: false }) - }); - -const resourceField = max => - new fields.SchemaField({ - value: new fields.NumberField({ initial: 0, integer: true }), - bonus: new fields.NumberField({ initial: 0, integer: true }), - min: new fields.NumberField({ initial: 0, integer: true }), - baseMax: new fields.NumberField({ initial: max, integer: true }) - }); - -export default class DhpPC extends foundry.abstract.TypeDataModel { - static defineSchema() { - return { - resources: new fields.SchemaField({ - hitPoints: resourceField(6), - stress: resourceField(6), - hope: new fields.SchemaField({ - value: new fields.NumberField({ initial: -1, integer: true }), // FIXME. Logic is gte and needs -1 in PC/Hope. Change to 0 - min: new fields.NumberField({ initial: 0, integer: true }) - }) - }), - bonuses: new fields.SchemaField({ - damage: new fields.ArrayField( - new fields.SchemaField({ - value: new fields.NumberField({ integer: true, initial: 0 }), - type: new fields.StringField({ nullable: true }), - initiallySelected: new fields.BooleanField(), - hopeIncrease: new fields.StringField({ initial: null, nullable: true }), - description: new fields.StringField({}) - }) - ) - }), - traits: new fields.SchemaField({ - agility: attributeField(), - strength: attributeField(), - finesse: attributeField(), - instinct: attributeField(), - presence: attributeField(), - knowledge: attributeField() - }), - proficiency: new fields.SchemaField({ - base: new fields.NumberField({ required: true, initial: 1, integer: true }), - bonus: new fields.NumberField({ required: true, initial: 0, integer: true }) - }), - evasion: new fields.SchemaField({ - bonus: new fields.NumberField({ initial: 0, integer: true }) - }), - experiences: new fields.ArrayField( - new fields.SchemaField({ - id: new fields.StringField({ required: true }), - description: new fields.StringField({}), - value: new fields.NumberField({ integer: true, nullable: true, initial: null }) - }), - { - initial: [ - { id: foundry.utils.randomID(), description: '', value: 2 }, - { id: foundry.utils.randomID(), description: '', value: 2 } - ] - } - ), - gold: new fields.SchemaField({ - coins: new fields.NumberField({ initial: 0, integer: true }), - handfulls: new fields.NumberField({ initial: 0, integer: true }), - bags: new fields.NumberField({ initial: 0, integer: true }), - chests: new fields.NumberField({ initial: 0, integer: true }) - }), - pronouns: new fields.StringField({}), - domainData: new fields.SchemaField({ - maxLoadout: new fields.NumberField({ initial: 2, integer: true }), - maxCards: new fields.NumberField({ initial: 2, integer: true }) - }), - story: new fields.SchemaField({ - background: new fields.HTMLField(), - appearance: new fields.HTMLField(), - connections: new fields.HTMLField(), - scars: new fields.ArrayField( - new fields.SchemaField({ - name: new fields.StringField({}), - description: new fields.HTMLField() - }) - ) - }), - description: new fields.StringField({}), - //Temporary until new FoundryVersion fix --> See Armor.Mjs DataPreparation - armorMarks: new fields.SchemaField({ - max: new fields.NumberField({ initial: 6, integer: true }), - value: new fields.NumberField({ initial: 0, integer: true }) - }), - class: new fields.SchemaField({ - value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }), - subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }) - }), - multiclass: new fields.SchemaField({ - value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }), - subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }) - }), - levelData: new fields.EmbeddedDataField(DhPCLevelData) - }; - } - - get tier() { - return this.#getTier(this.levelData.currentLevel); - } - - get ancestry() { - return this.parent.items.find(x => x.type === 'ancestry') ?? null; - } - - get multiclassSubclass() { - return this.parent.items.find(x => x.type === 'subclass' && x.system.multiclass) ?? null; - } - - get subclass() { - return this.parent.items.find(x => x.type === 'subclass' && !x.system.multiclass) ?? null; - } - - get subclassFeatures() { - const subclass = this.subclass; - const multiclass = this.multiclassSubclass; - const subclassItems = this.parent.items.filter(x => x.type === 'feature' && x.system.type === 'subclass'); - return { - subclass: !subclass - ? {} - : { - foundation: subclassItems.filter(x => - subclass.system.foundationFeature.abilities.some(ability => ability.uuid === x.uuid) - ), - specialization: subclassItems.filter(x => - subclass.system.specializationFeature.abilities.some(ability => ability.uuid === x.uuid) - ), - mastery: subclassItems.filter(x => - subclass.system.masteryFeature.abilities.some(ability => ability.uuid === x.uuid) - ) - }, - multiclassSubclass: !multiclass - ? {} - : { - foundation: subclassItems.filter(x => - multiclass.system.foundationFeature.abilities.some(ability => ability.uuid === x.uuid) - ), - specialization: subclassItems.filter(x => - multiclass.system.specializationFeature.abilities.some(ability => ability.uuid === x.uuid) - ), - mastery: subclassItems.filter(x => - multiclass.system.masteryFeature.abilities.some(ability => ability.uuid === x.uuid) - ) - } - }; - } - - get community() { - return this.parent.items.find(x => x.type === 'community') ?? null; - } - - get classFeatures() { - return this.parent.items.filter( - x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.class.id && !x.system.multiclass - ); - } - - get multiclassFeatures() { - return this.parent.items.filter( - x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.class.id && x.system.multiclass - ); - } - - get domains() { - const classDomains = this.class ? this.class.system.domains : []; - const multiclassDomains = this.multiclass ? this.multiclass.system.domains : []; - return [...classDomains, ...multiclassDomains]; - } - - get domainCards() { - const domainCards = this.parent.items.filter(x => x.type === 'domainCard'); - const loadout = domainCards.filter(x => !x.system.inVault); - const vault = domainCards.filter(x => x.system.inVault); - - return { - loadout: loadout, - vault: vault, - total: [...loadout, ...vault] - }; - } - - get armor() { - return this.parent.items.find(x => x.type === 'armor' && x.system.equipped); - } - - get equippedWeapons() { - const primaryWeapon = this.parent.items.find( - x => x.type === 'weapon' && x.system.equipped && !x.system.secondary - ); - const secondaryWeapon = this.parent.items.find( - x => x.type === 'weapon' && x.system.equipped && x.system.secondary - ); - return { - primary: this.#weaponData(primaryWeapon), - secondary: this.#weaponData(secondaryWeapon), - burden: this.getBurden(primaryWeapon, secondaryWeapon) - }; - } - - static async unequipBeforeEquip(itemToEquip) { - const equippedWeapons = this.equippedWeapons; - - if (itemToEquip.system.secondary) { - if (equippedWeapons.primary && equippedWeapons.primary.burden === SYSTEM.GENERAL.burden.twoHanded.value) { - await this.parent.items.get(equippedWeapons.primary.id).update({ 'system.equipped': false }); - } - - if (equippedWeapons.secondary) { - await this.parent.items.get(equippedWeapons.secondary.id).update({ 'system.equipped': false }); - } - } else { - if (equippedWeapons.secondary && itemToEquip.system.burden === SYSTEM.GENERAL.burden.twoHanded.value) { - await this.parent.items.get(equippedWeapons.secondary.id).update({ 'system.equipped': false }); - } - - if (equippedWeapons.primary) { - await this.parent.items.get(equippedWeapons.primary.id).update({ 'system.equipped': false }); - } - } - } - - get effects() { - return this.parent.items.reduce((acc, item) => { - const effects = item.system.effectData; - if (effects && !item.system.disabled) { - for (var key in effects) { - const effect = effects[key]; - for (var effectEntry of effect) { - if (!acc[key]) acc[key] = []; - acc[key].push({ name: item.name, value: effectEntry }); - } - } - } - - return acc; - }, {}); - } - - get refreshableFeatures() { - return this.parent.items.reduce( - (acc, x) => { - if (x.type === 'feature' && x.system.refreshData?.type === 'feature' && x.system.refreshData?.type) { - acc[x.system.refreshData.type].push(x); - } - - return acc; - }, - { shortRest: [], longRest: [] } - ); - } - - //Should not be done in data? - //TODO: REMOVE THIS - #weaponData(weapon) { - return weapon - ? { - id: weapon.id, - name: weapon.name, - trait: game.i18n.localize(CONFIG.daggerheart.ACTOR.abilities[weapon.system.trait].label), - range: CONFIG.daggerheart.GENERAL.range[weapon.system.range], - damage: { - value: weapon.system.damage.value, - type: CONFIG.daggerheart.GENERAL.damageTypes[weapon.system.damage.type] - }, - burden: weapon.system.burden, - feature: CONFIG.daggerheart.ITEM.weaponFeatures[weapon.system.feature], - img: weapon.img, - uuid: weapon.uuid - } - : null; - } - - prepareBaseData() { - this.resources.hitPoints.max = this.resources.hitPoints.baseMax + this.resources.hitPoints.bonus; - this.resources.stress.max = this.resources.stress.baseMax + this.resources.stress.bonus; - this.evasion.value = (this.class?.system?.evasion ?? 0) + this.evasion.bonus; - this.proficiency.value = this.proficiency.base + this.proficiency.bonus; - - for (var attributeKey in this.traits) { - const attribute = this.traits[attributeKey]; - attribute.value = attribute.base + attribute.bonus; - } - } - - prepareDerivedData() { - this.resources.hope.max = 6 - this.story.scars.length; - if (this.resources.hope.value >= this.resources.hope.max) { - this.resources.hope.value = Math.max(this.resources.hope.max - 1, 0); - } - - const armor = this.armor; - this.damageThresholds = { - major: armor - ? armor.system.baseThresholds.major + this.levelData.level.current - : this.levelData.level.current, - severe: armor - ? armor.system.baseThresholds.severe + this.levelData.level.current - : this.levelData.level.current * 2 - }; - - this.applyEffects(); - } - - applyEffects() { - const effects = this.effects; - for (var key in effects) { - const effectType = effects[key]; - for (var effect of effectType) { - switch (key) { - case SYSTEM.EFFECTS.effectTypes.health.id: - this.resources.hitPoints.bonus += effect.value.valueData.value; - break; - case SYSTEM.EFFECTS.effectTypes.stress.id: - this.resources.stress.bonus += effect.value.valueData.value; - break; - case SYSTEM.EFFECTS.effectTypes.damage.id: - this.bonuses.damage.push({ - value: getPathValue(effect.value.valueData.value, this), - type: 'physical', - description: effect.name, - hopeIncrease: effect.value.valueData.hopeIncrease, - initiallySelected: effect.value.initiallySelected, - appliesOn: effect.value.appliesOn - }); - } - } - } - } - - getBurden(primary, secondary) { - const twoHanded = - primary?.system?.burden === 'twoHanded' || - secondary?.system?.burden === 'twoHanded' || - (primary?.system?.burden === 'oneHanded' && secondary?.system?.burden === 'oneHanded'); - const oneHanded = - !twoHanded && (primary?.system?.burden === 'oneHanded' || secondary?.system?.burden === 'oneHanded'); - - return twoHanded ? 'twoHanded' : oneHanded ? 'oneHanded' : null; - } - - #getTier(level) { - if (level >= 8) return 3; - else if (level >= 5) return 2; - else if (level >= 2) return 1; - else return 0; - } -} - -class DhPCLevelData extends foundry.abstract.DataModel { - static defineSchema() { - return { - level: new fields.SchemaField({ - current: new fields.NumberField({ required: true, integer: true, initial: 1 }), - changed: new fields.NumberField({ required: true, integer: true, initial: 1 }) - }), - levelups: new fields.TypedObjectField( - new fields.SchemaField({ - achievements: new fields.SchemaField( - { - experiences: new fields.TypedObjectField( - new fields.SchemaField({ - name: new fields.StringField({ required: true }), - modifier: new fields.NumberField({ required: true, integer: true }) - }) - ), - domainCards: new fields.ArrayField( - new fields.SchemaField({ - uuid: new fields.StringField({ required: true }), - itemUuid: new fields.StringField({ required: true }) - }) - ), - proficiency: new fields.NumberField({ integer: true }) - }, - { nullable: true, initial: null } - ), - selections: new fields.ArrayField( - new fields.SchemaField({ - tier: new fields.NumberField({ required: true, integer: true }), - level: new fields.NumberField({ required: true, integer: true }), - optionKey: new fields.StringField({ required: true }), - type: new fields.StringField({ required: true, choices: LevelOptionType }), - checkboxNr: new fields.NumberField({ required: true, integer: true }), - value: new fields.NumberField({ integer: true }), - minCost: new fields.NumberField({ integer: true }), - amount: new fields.NumberField({ integer: true }), - data: new fields.ArrayField(new fields.StringField({ required: true })), - secondaryData: new fields.StringField(), - itemUuid: new fields.StringField({ required: true }) - }) - ) - }) - ) - }; - } - - get canLevelUp() { - return this.level.current < this.level.changed; - } -} diff --git a/module/data/settings/VariantRules.mjs b/module/data/settings/VariantRules.mjs index 2a1f948d..7d28a1d7 100644 --- a/module/data/settings/VariantRules.mjs +++ b/module/data/settings/VariantRules.mjs @@ -1,11 +1,14 @@ export default class DhVariantRules extends foundry.abstract.DataModel { + static LOCALIZATION_PREFIXES = ['DAGGERHEART.Settings.VariantRules']; + static defineSchema() { const fields = foundry.data.fields; return { actionTokens: new fields.SchemaField({ enabled: new fields.BooleanField({ required: true, initial: false }), tokens: new fields.NumberField({ required: true, integer: true, initial: 3 }) - }) + }), + useCoins: new fields.BooleanField({ initial: false }) }; } diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index bc116550..55b00634 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -10,7 +10,7 @@ export default class DhpActor extends Actor { // Configure prototype token settings const prototypeToken = {}; - if (this.type === 'pc') + if (this.type === 'character') Object.assign(prototypeToken, { sight: { enabled: true }, actorLink: true, @@ -28,7 +28,7 @@ export default class DhpActor extends Actor { } async updateLevel(newLevel) { - if (this.type !== 'pc' || newLevel === this.system.levelData.level.changed) return; + if (this.type !== 'character' || newLevel === this.system.levelData.level.changed) return; if (newLevel > this.system.levelData.level.current) { await this.update({ 'system.levelData.level.changed': newLevel }); @@ -124,7 +124,7 @@ export default class DhpActor extends Actor { } async diceRoll(modifier, shiftKey) { - if (this.type === 'pc') { + if (this.type === 'character') { return await this.dualityRoll(modifier, shiftKey); } else { return await this.npcRoll(modifier, shiftKey); @@ -173,12 +173,11 @@ export default class DhpActor extends Actor { return { roll, dice: dice[0], modifiers, advantageState: advantage === true ? 1 : advantage === false ? 2 : 0 }; } - async dualityRoll(modifier, shiftKey, bonusDamage = []) { + async dualityRoll(modifier, shiftKey) { let hopeDice = 'd12', fearDice = 'd12', advantageDice = null, - disadvantageDice = null, - bonusDamageString = ''; + disadvantageDice = null; const modifiers = modifier.value !== null @@ -195,12 +194,9 @@ export default class DhpActor extends Actor { : []; if (!shiftKey) { const dialogClosed = new Promise((resolve, _) => { - new RollSelectionDialog( - this.system.experiences, - bonusDamage, - this.system.resources.hope.value, - resolve - ).render(true); + new RollSelectionDialog(this.system.experiences, this.system.resources.hope.value, resolve).render( + true + ); }); const result = await dialogClosed; (hopeDice = result.hope), @@ -214,7 +210,6 @@ export default class DhpActor extends Actor { title: x.description }) ); - bonusDamageString = result.bonusDamage; const automateHope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope); @@ -268,8 +263,7 @@ export default class DhpActor extends Actor { fear: { dice: fearDice, value: fear }, advantage: { dice: advantageDice, value: advantage }, disadvantage: { dice: disadvantageDice, value: disadvantage }, - modifiers: modifiers, - bonusDamageString + modifiers: modifiers }; } @@ -401,11 +395,6 @@ export default class DhpActor extends Actor { } } - async emulateItemDrop(data) { - const event = new DragEvent('drop', { altKey: game.keyboard.isModifierActive('Alt') }); - return this.sheet._onDropItem(event, { data: data }); - } - //Move to action-scope? async useAction(action) { const userTargets = Array.from(game.user.targets); diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 3fbe89c3..d764210a 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -114,7 +114,7 @@ export const getCommandTarget = () => { ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.NoSelectedToken')); return null; } - if (target.type !== 'pc') { + if (target.type !== 'character') { ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.OnlyUseableByPC')); return null; } diff --git a/styles/daggerheart.less b/styles/daggerheart.less index 8ec94054..d3ad28b2 100755 --- a/styles/daggerheart.less +++ b/styles/daggerheart.less @@ -13,6 +13,7 @@ @import './resources.less'; // new styles imports +@import './less/actors/character.less'; @import './less/actors/adversary.less'; @import './less/actors/environment.less'; diff --git a/styles/less/actors/character.less b/styles/less/actors/character.less new file mode 100644 index 00000000..e69de29b diff --git a/system.json b/system.json index c402b837..35f69cde 100644 --- a/system.json +++ b/system.json @@ -203,7 +203,9 @@ }, "documentTypes": { "Actor": { - "pc": {}, + "character": { + "htmlFields": ["story", "description", "scars.*.description"] + }, "adversary": { "htmlFields": ["description", "motivesAndTactics"] }, diff --git a/templates/settings/variant-rules.hbs b/templates/settings/variant-rules.hbs index f39cb2a9..2c4d7d30 100644 --- a/templates/settings/variant-rules.hbs +++ b/templates/settings/variant-rules.hbs @@ -8,6 +8,8 @@ + {{formGroup settingFields.schema.fields.useCoins value=settingFields._source.useCoins localize=true }} +
\ No newline at end of file diff --git a/templates/sheets/parts/experience.hbs b/templates/sheets/parts/experience.hbs index 4271d9e0..ace6970e 100644 --- a/templates/sheets/parts/experience.hbs +++ b/templates/sheets/parts/experience.hbs @@ -1,12 +1,12 @@
{{localize "DAGGERHEART.Sheets.PC.Experience.Title"}} - {{#each document.system.experiences as |experience index|}} + {{#each document.system.experiences as |experience id|}}
- -
{{experience.value}}
+ +
{{experience.value}}
{{/each}} - {{#times (subtract 5 document.system.experiences.length)}} + {{#times (subtract 5 (length document.system.experiences))}}
diff --git a/templates/sheets/parts/weapons.hbs b/templates/sheets/parts/weapons.hbs index 53b63c1c..a53fffcb 100644 --- a/templates/sheets/parts/weapons.hbs +++ b/templates/sheets/parts/weapons.hbs @@ -11,39 +11,39 @@
- - + +

{{localize "DAGGERHEART.Sheets.PC.Weapons.PrimaryTitle"}} - {{#if weapons.primary}} - + {{#if primaryWeapon}} + {{/if}}

- - - - + + + +
- +

{{localize "DAGGERHEART.Sheets.PC.Weapons.SecondaryTitle"}} - {{#if weapons.secondary}} - + {{#if secondaryWeapon}} + {{/if}}

- - - - + + + +
- +
\ No newline at end of file diff --git a/templates/views/rollSelection.hbs b/templates/views/rollSelection.hbs index 19c5f658..a103686a 100644 --- a/templates/views/rollSelection.hbs +++ b/templates/views/rollSelection.hbs @@ -15,22 +15,6 @@ - {{!--
- -
- -
-
-
- -
- -
-
--}} {{#if (not this.isNpc)}}
@@ -49,24 +33,6 @@
{{/if}} - {{#each this.bonusDamage as |damage index|}} -
- -
- - - {{#if (and damage.initiallySelected damage.hopeIncrease)}} - - -
- -
{{damage.hopeUses}}
- -
- {{/if}} -
-
- {{/each}}