From 3186468f286abd019db739a79831288dcc577d8b Mon Sep 17 00:00:00 2001 From: WBHarry Date: Thu, 3 Jul 2025 14:13:49 +0200 Subject: [PATCH] Fixed basic beastform --- daggerheart.mjs | 2 + lang/en.json | 18 +++- .../applications/sheets/actors/character.mjs | 17 ++-- module/data/_module.mjs | 1 + module/data/action/action.mjs | 29 ++++++- module/data/activeEffect/_module.mjs | 7 ++ module/data/activeEffect/beastformEffect.mjs | 18 ++++ module/data/item/beastform.mjs | 83 ++++++++++++++++++- module/dialogs/beastformDialog.mjs | 10 ++- module/documents/activeEffect.mjs | 23 +++++ module/helpers/utils.mjs | 14 ++++ styles/daggerheart.css | 2 +- styles/less/applications/beastform.less | 2 +- system.json | 3 + .../sheets/global/partials/inventory-item.hbs | 2 +- templates/sheets/items/beastform/settings.hbs | 11 +++ templates/tooltip/beastform.hbs | 6 ++ templates/views/beastformDialog.hbs | 4 +- 18 files changed, 231 insertions(+), 21 deletions(-) create mode 100644 module/data/activeEffect/_module.mjs create mode 100644 module/data/activeEffect/beastformEffect.mjs create mode 100644 templates/tooltip/beastform.hbs diff --git a/daggerheart.mjs b/daggerheart.mjs index e9af1a1f..eff0fcd4 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -91,6 +91,8 @@ Hooks.once('init', () => { Actors.registerSheet(SYSTEM.id, applications.DhpEnvironment, { types: ['environment'], makeDefault: true }); CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect; + CONFIG.ActiveEffect.dataModels = models.activeEffects.config; + foundry.applications.apps.DocumentSheetConfig.unregisterSheet( CONFIG.ActiveEffect.documentClass, 'core', diff --git a/lang/en.json b/lang/en.json index 093b63e9..a4aa7c2f 100755 --- a/lang/en.json +++ b/lang/en.json @@ -23,7 +23,9 @@ "DAGGERHEART": { "UI": { "notifications": { - "adversaryMissing": "The linked adversary doesn't exist in the world." + "adversaryMissing": "The linked adversary doesn't exist in the world.", + "beastformInapplicable": "A beastform can only be applied to a Character.", + "beastformAlreadyApplied": "The character already has a beastform applied!" } }, "Settings": { @@ -1473,13 +1475,21 @@ } }, "Beastform": { - "DialogTitle": "Beastform Selection", "FIELDS": { "tier": { "label": "Tier" }, "examples": { "label": "Examples" }, - "advantageOn": { "label": "Gain Advantage On" } + "advantageOn": { "label": "Gain Advantage On" }, + "tokenImg": { "label": "Token Image" }, + "tokenSize": { + "placeholder": "Using character dimensions", + "height": { "label": "Height" }, + "width": { "label": "Width" } + } }, - "Transform": "Transform" + "dialogTitle": "Beastform Selection", + "tokenTitle": "Beastform Token", + "transform": "Transform", + "beastformEffect": "Beastform Transformation" }, "Global": { "Actions": "Actions", diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 8be31690..d82ac900 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -304,11 +304,14 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { getItem(element) { const listElement = (element.target ?? element).closest('[data-item-id]'); - const document = listElement.dataset.companion ? this.document.system.companion : this.document; - - const itemId = listElement.dataset.itemId, - item = document.items.get(itemId); - return item; + const itemId = listElement.dataset.itemId; + if (listElement.dataset.type === 'effect') { + return this.document.effects.get(itemId); + } else if (listElement.dataset.type === 'features') { + return this.document.items.get(itemId); + } else { + return this.document.system[listElement.dataset.type].system.actions.find(x => x.id === itemId); + } } static triggerContextMenu(event, button) { @@ -615,8 +618,8 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { if (!item) return; // Should dandle its actions. Or maybe they'll be separate buttons as per an Issue on the board - if (item.type === 'feature') { - item.toChat(); + if (item.type === 'feature' || item instanceof ActiveEffect) { + item.toChat(this); } else { const wasUsed = await item.use(event); if (wasUsed && item.type === 'weapon') { diff --git a/module/data/_module.mjs b/module/data/_module.mjs index 4284bc41..45a1d558 100644 --- a/module/data/_module.mjs +++ b/module/data/_module.mjs @@ -6,3 +6,4 @@ export * as items from './item/_module.mjs'; export { actionsTypes } from './action/_module.mjs'; export * as messages from './chat-message/_modules.mjs'; export * as fields from './fields/_module.mjs'; +export * as activeEffects from './activeEffect/_module.mjs'; diff --git a/module/data/action/action.mjs b/module/data/action/action.mjs index ff38ae3f..f6c56bf7 100644 --- a/module/data/action/action.mjs +++ b/module/data/action/action.mjs @@ -271,8 +271,13 @@ export class DHBaseAction extends foundry.abstract.DataModel { } if (this instanceof DhBeastformAction) { - config = await BeastformDialog.configure(config); - if (!config) return; + const abort = await this.handleActiveTransformations(); + if (abort) return; + + const beastformUuid = await BeastformDialog.configure(config); + if (!beastformUuid) return; + + await this.transform(beastformUuid); } if (this.doFollowUp()) { @@ -774,4 +779,24 @@ export class DhBeastformAction extends DHBaseAction { }) }; } + + async transform(beastformUuid) { + const beastform = await foundry.utils.fromUuid(beastformUuid); + this.actor.createEmbeddedDocuments('Item', [beastform.toObject()]); + } + + async handleActiveTransformations() { + const activeBeastforms = this.actor.items.filter(x => x.type === 'beastform'); + if (activeBeastforms.length > 0) { + for (let form of activeBeastforms) { + await form.delete(); + } + + this.actor.effects.filter(x => x.type === 'beastform').forEach(x => x.delete()); + + return true; + } + + return false; + } } diff --git a/module/data/activeEffect/_module.mjs b/module/data/activeEffect/_module.mjs new file mode 100644 index 00000000..f4627f0c --- /dev/null +++ b/module/data/activeEffect/_module.mjs @@ -0,0 +1,7 @@ +import beastformEffect from './beastformEffect.mjs'; + +export { beastformEffect }; + +export const config = { + beastform: beastformEffect +}; diff --git a/module/data/activeEffect/beastformEffect.mjs b/module/data/activeEffect/beastformEffect.mjs new file mode 100644 index 00000000..19ed9efa --- /dev/null +++ b/module/data/activeEffect/beastformEffect.mjs @@ -0,0 +1,18 @@ +export default class BeastformEffect extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields; + return { + isBeastform: new fields.BooleanField({ initial: false }) + }; + } + + async _preDelete() { + if (this.parent.parent.type === 'character') { + for (let item of this.parent.parent.items) { + if (item.type === 'beastform') { + await item.delete(); + } + } + } + } +} diff --git a/module/data/item/beastform.mjs b/module/data/item/beastform.mjs index 8df5dd0e..22b8e30a 100644 --- a/module/data/item/beastform.mjs +++ b/module/data/item/beastform.mjs @@ -1,4 +1,4 @@ -import ActionField from '../fields/actionField.mjs'; +import { updateActorTokens } from '../../helpers/utils.mjs'; import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; import BaseDataItem from './base.mjs'; @@ -24,9 +24,90 @@ export default class DHBeastform extends BaseDataItem { choices: SYSTEM.GENERAL.tiers, initial: SYSTEM.GENERAL.tiers.tier1.id }), + tokenImg: new fields.FilePathField({ + initial: 'icons/svg/mystery-man.svg', + categories: ['IMAGE'], + base64: false + }), + tokenSize: new fields.SchemaField({ + height: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }), + width: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }) + }), + characterTokenData: new fields.SchemaField({ + tokenImg: new fields.FilePathField({ + categories: ['IMAGE'], + base64: false, + nullable: true, + initial: null + }), + tokenSize: new fields.SchemaField({ + height: new fields.NumberField({ integer: true, initial: null, nullable: true }), + width: new fields.NumberField({ integer: true, initial: null, nullable: true }) + }) + }), examples: new fields.StringField(), advantageOn: new fields.ArrayField(new fields.StringField()), features: new ForeignDocumentUUIDArrayField({ type: 'Item' }) }; } + + async _preCreate(data, options, user) { + const allowed = await super._preCreate(data, options, user); + if (allowed === false) return; + + if (this.actor?.type !== 'character') { + ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.beastformInapplicable')); + return; + } + + if (this.actor.items.find(x => x.type === 'beastform')) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.beastformAlreadyApplied')); + return; + } + + await this.updateSource({ + characterTokenData: { + tokenImg: this.parent.parent.prototypeToken.texture.src, + tokenSize: { + height: this.parent.parent.prototypeToken.height, + width: this.parent.parent.prototypeToken.width + } + } + }); + } + + _onCreate(data, options, userId) { + super._onCreate(data, options, userId); + + const update = { + height: this.tokenSize.height, + width: this.tokenSize.width, + texture: { + src: this.tokenImg + } + }; + updateActorTokens(this.parent.parent, update); + + this.parent.parent.createEmbeddedDocuments('ActiveEffect', [ + { + type: 'beastform', + name: game.i18n.localize('DAGGERHEART.Sheets.Beastform.beastformEffect'), + img: 'icons/creatures/abilities/paw-print-pair-purple.webp', + system: { + isBeastform: true + } + } + ]); + } + + async _preDelete() { + const update = { + height: this.characterTokenData.tokenSize.height, + width: this.characterTokenData.tokenSize.width, + texture: { + src: this.characterTokenData.tokenImg + } + }; + await updateActorTokens(this.parent.parent, update); + } } diff --git a/module/dialogs/beastformDialog.mjs b/module/dialogs/beastformDialog.mjs index d9699b59..4bb606ef 100644 --- a/module/dialogs/beastformDialog.mjs +++ b/module/dialogs/beastformDialog.mjs @@ -19,6 +19,7 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat height: 'auto' }, actions: { + selectBeastform: this.selectBeastform, submitBeastform: this.submitBeastform }, form: { @@ -29,7 +30,7 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat }; get title() { - return game.i18n.localize('DAGGERHEART.Sheets.Beastform.DialogTitle'); + return game.i18n.localize('DAGGERHEART.Sheets.Beastform.dialogTitle'); } /** @override */ @@ -64,6 +65,11 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat this.render(); } + static selectBeastform(_, target) { + this.selected = this.selected === target.dataset.uuid ? null : target.dataset.uuid; + this.render(); + } + static async submitBeastform() { await this.close({ submitted: true }); } @@ -76,7 +82,7 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat static async configure(configData) { return new Promise(resolve => { const app = new this(configData); - app.addEventListener('close', () => resolve(app.config), { once: true }); + app.addEventListener('close', () => resolve(app.selected), { once: true }); app.render({ force: true }); }); } diff --git a/module/documents/activeEffect.mjs b/module/documents/activeEffect.mjs index ce73441b..2a2aa33e 100644 --- a/module/documents/activeEffect.mjs +++ b/module/documents/activeEffect.mjs @@ -28,4 +28,27 @@ export default class DhActiveEffect extends ActiveEffect { change.value = Roll.safeEval(Roll.replaceFormulaData(change.value, change.effect.parent)); super.applyField(model, change, field); } + + async toChat(origin) { + const cls = getDocumentClass('ChatMessage'); + const systemData = { + title: game.i18n.localize('DAGGERHEART.ActionType.action'), + origin: origin, + img: this.img, + name: this.name, + description: this.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()); + } } diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 990d0b35..6744f34b 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -294,3 +294,17 @@ export const adjustRange = (rangeVal, decrease) => { const newIndex = decrease ? Math.max(index - 1, 0) : Math.min(index + 1, rangeKeys.length - 1); return range[rangeKeys[newIndex]]; }; + +export const updateActorTokens = async (actor, update) => { + await actor.prototypeToken.update(update); + + /* Update the tokens in all scenes belonging to Actor */ + for (let scene of game.scenes) { + for (let token of scene.tokens) { + const actor = token.baseActor ?? token.actor; + if (actor?.id === actor.id) { + await token.update(update); + } + } + } +}; diff --git a/styles/daggerheart.css b/styles/daggerheart.css index c1fd3028..cee88d55 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -5006,7 +5006,7 @@ div.daggerheart.views.multiclass { border-radius: 6px; cursor: pointer; } -.application.daggerheart.dh-style.views.beastform-selection .beastforms-container .beastforms-tier .beastform-container.disabled { +.application.daggerheart.dh-style.views.beastform-selection .beastforms-container .beastforms-tier .beastform-container.inactive { opacity: 0.4; } .application.daggerheart.dh-style.views.beastform-selection .beastforms-container .beastforms-tier .beastform-container img { diff --git a/styles/less/applications/beastform.less b/styles/less/applications/beastform.less index e809bedd..37069bdb 100644 --- a/styles/less/applications/beastform.less +++ b/styles/less/applications/beastform.less @@ -23,7 +23,7 @@ border-radius: 6px; cursor: pointer; - &.disabled { + &.inactive { opacity: 0.4; } diff --git a/system.json b/system.json index 864e540e..99bdd747 100644 --- a/system.json +++ b/system.json @@ -250,6 +250,9 @@ }, "beastform": {} }, + "ActiveEffect": { + "beastform": {} + }, "Combat": { "combat": {} }, diff --git a/templates/sheets/global/partials/inventory-item.hbs b/templates/sheets/global/partials/inventory-item.hbs index b69f1a2d..061cb125 100644 --- a/templates/sheets/global/partials/inventory-item.hbs +++ b/templates/sheets/global/partials/inventory-item.hbs @@ -1,4 +1,4 @@ -
  • +
  • {{item.name}}
    diff --git a/templates/sheets/items/beastform/settings.hbs b/templates/sheets/items/beastform/settings.hbs index 68dc8d39..a9052122 100644 --- a/templates/sheets/items/beastform/settings.hbs +++ b/templates/sheets/items/beastform/settings.hbs @@ -13,4 +13,15 @@ {{!-- {{formGroup systemFields.examples value=source.system.examples localize=true}} --}} + +
    + {{localize "DAGGERHEART.Sheets.Beastform.tokenTitle"}} + +
    + {{formGroup systemFields.tokenImg value=source.system.tokenImg localize=true}} +
    + + {{formGroup systemFields.tokenSize.fields.height value=source.system.tokenSize.height localize=true placeholder=(localize "DAGGERHEART.Sheets.Beastform.FIELDS.tokenSize.placeholder") }} + {{formGroup systemFields.tokenSize.fields.width value=source.system.tokenSize.width localize=true placeholder=(localize "DAGGERHEART.Sheets.Beastform.FIELDS.tokenSize.placeholder")}} +
    \ No newline at end of file diff --git a/templates/tooltip/beastform.hbs b/templates/tooltip/beastform.hbs new file mode 100644 index 00000000..3af49969 --- /dev/null +++ b/templates/tooltip/beastform.hbs @@ -0,0 +1,6 @@ +
    +
    {{name}}
    + +
    {{{system.examples}}}
    +
    {{system.advantageOn}}
    +
    \ No newline at end of file diff --git a/templates/views/beastformDialog.hbs b/templates/views/beastformDialog.hbs index adb0c01b..a9a0f757 100644 --- a/templates/views/beastformDialog.hbs +++ b/templates/views/beastformDialog.hbs @@ -4,7 +4,7 @@
    {{tier.label}} {{#each tier.values as |form uuid|}} -
    {{!-- data-tooltip="{{concat "#item#" uuid}}" --}} +
    {{form.value.name}}
    @@ -13,6 +13,6 @@ {{/each}}
    \ No newline at end of file