From f80a849b7354a8ada9575fb7b8487353fe68bb41 Mon Sep 17 00:00:00 2001
From: WBHarry <89362246+WBHarry@users.noreply.github.com>
Date: Sun, 15 Jun 2025 13:19:48 +0200
Subject: [PATCH] 140/141 - Class/Subclass Actions/Effects (#142)
* Added Actions and effects
* Added class hopeFeatures and classFeatures
---
daggerheart.mjs | 11 +-
lang/en.json | 9 +-
module/applications/config/Action.mjs | 6 +-
module/applications/sheets/items/class.mjs | 73 +++++++++-
module/applications/sheets/items/subclass.mjs | 136 ++++++++++++------
module/data/action/action.mjs | 5 +-
module/data/item/class.mjs | 8 +-
module/data/item/subclass.mjs | 15 +-
styles/daggerheart.css | 1 -
styles/less/global/feature-section.less | 1 -
.../global/partials/feature-section-item.hbs | 6 +-
templates/sheets/items/class/features.hbs | 26 ++--
templates/sheets/items/subclass/features.hbs | 6 +-
.../items/subclass/parts/subclass-feature.hbs | 30 ++++
.../subclass/parts/subclass-features.hbs | 22 +++
15 files changed, 281 insertions(+), 74 deletions(-)
create mode 100644 templates/sheets/items/subclass/parts/subclass-feature.hbs
create mode 100644 templates/sheets/items/subclass/parts/subclass-features.hbs
diff --git a/daggerheart.mjs b/daggerheart.mjs
index 747fd490..96b407d3 100644
--- a/daggerheart.mjs
+++ b/daggerheart.mjs
@@ -106,8 +106,13 @@ Hooks.once('init', () => {
Hooks.on('ready', () => {
ui.resources = new CONFIG.ui.resources();
- if(game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.DisplayFear) !== 'hide') ui.resources.render({ force: true });
- document.body.classList.toggle('theme-colorful', game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).dualityColorScheme === DualityRollColor.colorful.value);
+ if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.DisplayFear) !== 'hide')
+ ui.resources.render({ force: true });
+ document.body.classList.toggle(
+ 'theme-colorful',
+ game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).dualityColorScheme ===
+ DualityRollColor.colorful.value
+ );
});
Hooks.once('dicesoniceready', () => {});
@@ -269,6 +274,8 @@ const preloadHandlebarsTemplates = async function () {
'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/sheets/items/subclass/parts/subclass-features.hbs',
+ 'systems/daggerheart/templates/sheets/items/subclass/parts/subclass-feature.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 6f3810a5..71dbd08a 100755
--- a/lang/en.json
+++ b/lang/en.json
@@ -825,7 +825,8 @@
"Input": "Input",
"Dice": "Dice"
},
- "Max": "Max"
+ "Max": "Max",
+ "NewEffect": "New Effect"
},
"FeatureType": {
"Normal": "Normal",
@@ -1145,6 +1146,8 @@
"Appearance": "Appearance",
"settings": "Settings"
},
+ "HopeFeatures": "Hope Features",
+ "Class Features": "Class Features",
"Domains": "Domains",
"DamageThresholds": {
"Title": "Damage Thresholds",
@@ -1254,7 +1257,9 @@
"Description": "Description",
"SubclassFeature": {
"Description": "Description",
- "Abilities": "Abilities"
+ "Abilities": "Abilities",
+ "Actions": "Actions",
+ "Effects": "Effects"
}
},
"Weapon": {
diff --git a/module/applications/config/Action.mjs b/module/applications/config/Action.mjs
index 6453f896..d8118fa3 100644
--- a/module/applications/config/Action.mjs
+++ b/module/applications/config/Action.mjs
@@ -86,11 +86,11 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
static async updateForm(event, _, formData) {
const submitData = this._prepareSubmitData(event, formData),
data = foundry.utils.expandObject(foundry.utils.mergeObject(this.action.toObject(), submitData)),
- newActions = this.action.parent.actions.map(x => x.toObject()); // Find better way
+ newActions = foundry.utils.getProperty(this.action.parent, this.action.systemPath).map(x => x.toObject()); // Find better way
if (!newActions.findSplice(x => x._id === data._id, data)) newActions.push(data);
- const updates = await this.action.parent.parent.update({ 'system.actions': newActions });
+ const updates = await this.action.parent.parent.update({ [`system.${this.action.systemPath}`]: newActions });
if (!updates) return;
- this.action = updates.system.actions[this.action.index];
+ this.action = foundry.utils.getProperty(updates.system, this.action.systemPath)[this.action.index];
this.render();
}
diff --git a/module/applications/sheets/items/class.mjs b/module/applications/sheets/items/class.mjs
index c8f5c1e1..54f29361 100644
--- a/module/applications/sheets/items/class.mjs
+++ b/module/applications/sheets/items/class.mjs
@@ -1,8 +1,11 @@
+import { actionsTypes } from '../../../data/_module.mjs';
import { tagifyElement } from '../../../helpers/utils.mjs';
+import DHActionConfig from '../../config/Action.mjs';
import DaggerheartSheet from '../daggerheart-sheet.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
const { TextEditor } = foundry.applications.ux;
+
export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
@@ -11,8 +14,9 @@ export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) {
actions: {
removeSubclass: this.removeSubclass,
viewSubclass: this.viewSubclass,
- deleteFeature: this.deleteFeature,
+ addFeature: this.addFeature,
editFeature: this.editFeature,
+ deleteFeature: this.deleteFeature,
removeItem: this.removeItem,
viewItem: this.viewItem,
removePrimaryWeapon: this.removePrimaryWeapon,
@@ -151,6 +155,69 @@ export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) {
await this.document.update({ 'system.characterGuide.suggestedArmor': null }, { diff: false });
}
+ async selectActionType() {
+ const content = await foundry.applications.handlebars.renderTemplate(
+ 'systems/daggerheart/templates/views/actionType.hbs',
+ { types: SYSTEM.ACTIONS.actionTypes }
+ ),
+ title = 'Select Action Type',
+ type = 'form',
+ data = {};
+ return Dialog.prompt({
+ title,
+ label: title,
+ content,
+ type,
+ callback: html => {
+ const form = html[0].querySelector('form'),
+ fd = new foundry.applications.ux.FormDataExtended(form);
+ foundry.utils.mergeObject(data, fd.object, { inplace: true });
+
+ return data;
+ },
+ rejectClose: false
+ });
+ }
+
+ getActionPath(type) {
+ return type === 'hope' ? 'hopeFeatures' : 'classFeatures';
+ }
+
+ static async addFeature(_, target) {
+ const actionPath = this.getActionPath(target.dataset.type);
+ const actionType = await this.selectActionType();
+ const cls = actionsTypes[actionType?.type] ?? actionsTypes.attack,
+ action = new cls(
+ {
+ _id: foundry.utils.randomID(),
+ systemPath: actionPath,
+ type: actionType.type,
+ name: game.i18n.localize(SYSTEM.ACTIONS.actionTypes[actionType.type].name),
+ ...cls.getSourceConfig(this.document)
+ },
+ {
+ parent: this.document
+ }
+ );
+ await this.document.update({ [`system.${actionPath}`]: [...this.document.system[actionPath], action] });
+ }
+
+ static async editFeature(_, target) {
+ const action = this.document.system[this.getActionPath(target.dataset.type)].find(
+ x => x._id === target.dataset.feature
+ );
+ await new DHActionConfig(action).render(true);
+ }
+
+ static async deleteFeature(_, target) {
+ const actionPath = this.getActionPath(target.dataset.type);
+ await this.document.update({
+ [`system.${actionPath}`]: this.document.system[actionPath].filter(
+ action => action._id !== target.dataset.feature
+ )
+ });
+ }
+
async _onDrop(event) {
const data = TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid);
@@ -158,10 +225,6 @@ export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) {
await this.document.update({
'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.map(x => x.uuid), item.uuid]
- });
} else if (item.type === 'weapon') {
if (event.currentTarget.classList.contains('primary-weapon-section')) {
if (!this.document.system.characterGuide.suggestedPrimaryWeapon && !item.system.secondary)
diff --git a/module/applications/sheets/items/subclass.mjs b/module/applications/sheets/items/subclass.mjs
index 5869d84d..e6e9725f 100644
--- a/module/applications/sheets/items/subclass.mjs
+++ b/module/applications/sheets/items/subclass.mjs
@@ -1,28 +1,24 @@
-import DaggerheartSheet from '../daggerheart-sheet.mjs';
+import { actionsTypes } from '../../../data/_module.mjs';
+import DHActionConfig from '../../config/Action.mjs';
+import DhpApplicationMixin from '../daggerheart-sheet.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
-const { TextEditor } = foundry.applications.ux;
-const { duplicate, getProperty } = foundry.utils;
-export default class SubclassSheet extends DaggerheartSheet(ItemSheetV2) {
+export default class SubclassSheet extends DhpApplicationMixin(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'subclass'],
position: { width: 600 },
window: { resizable: false },
actions: {
- editAbility: this.editAbility,
- deleteFeatureAbility: this.deleteFeatureAbility
+ addFeature: this.addFeature,
+ editFeature: this.editFeature,
+ deleteFeature: this.deleteFeature
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
- },
- dragDrop: [
- { dragSelector: null, dropSelector: '.foundation-tab' },
- { dragSelector: null, dropSelector: '.specialization-tab' },
- { dragSelector: null, dropSelector: '.mastery-tab' }
- ]
+ }
};
static PARTS = {
@@ -80,41 +76,99 @@ export default class SubclassSheet extends DaggerheartSheet(ItemSheetV2) {
this.render();
}
- static async editAbility(_, button) {
- const feature = await fromUuid(button.dataset.ability);
- feature.sheet.render(true);
+ static addFeature(_, target) {
+ if (target.dataset.type === 'action') this.addAction(target.dataset.level);
+ else this.addEffect(target.dataset.level);
}
- static async deleteFeatureAbility(event, button) {
- event.preventDefault();
- event.stopPropagation();
-
- const feature = button.dataset.feature;
- const newAbilities = this.document.system[`${feature}Feature`].abilities.filter(
- x => x.uuid !== button.dataset.ability
- );
- const path = `system.${feature}Feature.abilities`;
-
- await this.document.update({ [path]: newAbilities });
+ static async editFeature(_, target) {
+ if (target.dataset.type === 'action') this.editAction(target.dataset.level, target.dataset.feature);
+ else this.editEffect(target.dataset.feature);
}
- async _onDrop(event) {
- event.preventDefault();
- const data = TextEditor.getDragEventData(event);
- const item = await fromUuid(data.uuid);
- if (!(item.type === 'feature' && item.system.type === SYSTEM.ITEM.featureTypes.subclass.id)) return;
+ static async deleteFeature(_, target) {
+ if (target.dataset.type === 'action') this.removeAction(target.dataset.level, target.dataset.feature);
+ else this.removeEffect(target.dataset.level, target.dataset.feature);
+ }
- let featureField;
- if (event.currentTarget.classList.contains('foundation-tab')) featureField = 'foundation';
- else if (event.currentTarget.classList.contains('specialization-tab')) featureField = 'specialization';
- else if (event.currentTarget.classList.contains('mastery-tab')) featureField = 'mastery';
- else return;
+ async #selectActionType() {
+ const content = await foundry.applications.handlebars.renderTemplate(
+ 'systems/daggerheart/templates/views/actionType.hbs',
+ { types: SYSTEM.ACTIONS.actionTypes }
+ ),
+ title = 'Select Action Type',
+ type = 'form',
+ data = {};
+ return Dialog.prompt({
+ title,
+ label: title,
+ content,
+ type,
+ callback: html => {
+ const form = html[0].querySelector('form'),
+ fd = new foundry.applications.ux.FormDataExtended(form);
+ foundry.utils.mergeObject(data, fd.object, { inplace: true });
+ return data;
+ },
+ rejectClose: false
+ });
+ }
- const path = `system.${featureField}Feature.abilities`;
- const abilities = duplicate(getProperty(this.document, path)) || [];
- const featureData = { name: item.name, img: item.img, uuid: item.uuid };
- abilities.push(featureData);
+ async addAction(level) {
+ const actionType = await this.#selectActionType();
+ const cls = actionsTypes[actionType?.type] ?? actionsTypes.attack,
+ action = new cls(
+ {
+ _id: foundry.utils.randomID(),
+ systemPath: `${level}.actions`,
+ type: actionType.type,
+ name: game.i18n.localize(SYSTEM.ACTIONS.actionTypes[actionType.type].name),
+ ...cls.getSourceConfig(this.document)
+ },
+ {
+ parent: this.document
+ }
+ );
+ await this.document.update({ [`system.${level}.actions`]: [...this.document.system[level].actions, action] });
+ await new DHActionConfig(
+ this.document.system[level].actions[this.document.system[level].actions.length - 1]
+ ).render(true);
+ }
- await this.document.update({ [path]: abilities });
+ async addEffect(level) {
+ const embeddedItems = await this.document.createEmbeddedDocuments('ActiveEffect', [
+ { name: game.i18n.localize('DAGGERHEART.Feature.NewEffect') }
+ ]);
+ await this.document.update({
+ [`system.${level}.effects`]: [
+ ...this.document.system[level].effects.map(x => x.uuid),
+ embeddedItems[0].uuid
+ ]
+ });
+ }
+
+ async editAction(level, id) {
+ const action = this.document.system[level].actions.find(x => x._id === id);
+ await new DHActionConfig(action).render(true);
+ }
+
+ async editEffect(id) {
+ const effect = this.document.effects.get(id);
+ effect.sheet.render(true);
+ }
+
+ async removeAction(level, id) {
+ await this.document.update({
+ [`system.${level}.actions`]: this.document.system[level].actions.filter(action => action._id !== id)
+ });
+ }
+
+ async removeEffect(level, id) {
+ await this.document.effects.get(id).delete();
+ await this.document.update({
+ [`system.${level}.effects`]: this.document.system[level].effects
+ .filter(x => x && x.id !== id)
+ .map(effect => effect.uuid)
+ });
}
}
diff --git a/module/data/action/action.mjs b/module/data/action/action.mjs
index 7b877415..16f74945 100644
--- a/module/data/action/action.mjs
+++ b/module/data/action/action.mjs
@@ -55,6 +55,7 @@ export class DHBaseAction extends foundry.abstract.DataModel {
static defineSchema() {
return {
_id: new fields.DocumentIdField(),
+ systemPath: new fields.StringField({ required: true, initial: 'actions' }),
type: new fields.StringField({ initial: undefined, readonly: true, required: true }),
name: new fields.StringField({ initial: undefined }),
img: new fields.FilePathField({ initial: undefined, categories: ['IMAGE'], base64: false }),
@@ -93,7 +94,7 @@ export class DHBaseAction extends foundry.abstract.DataModel {
prepareData() {}
get index() {
- return this.parent.actions.indexOf(this);
+ return foundry.utils.getProperty(this.parent, this.systemPath).indexOf(this);
}
get item() {
@@ -203,7 +204,7 @@ export class DHAttackAction extends DHBaseAction {
static getRollType() {
return 'weapon';
}
-
+
get chatTitle() {
return game.i18n.format('DAGGERHEART.Chat.AttackRoll.Title', {
attack: this.item.name
diff --git a/module/data/item/class.mjs b/module/data/item/class.mjs
index 335014b9..47acb712 100644
--- a/module/data/item/class.mjs
+++ b/module/data/item/class.mjs
@@ -1,5 +1,6 @@
import BaseDataItem from './base.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
+import ActionField from '../fields/actionField.mjs';
export default class DHClass extends BaseDataItem {
/** @inheritDoc */
@@ -19,7 +20,8 @@ export default class DHClass extends BaseDataItem {
domains: new fields.ArrayField(new fields.StringField(), { max: 2 }),
classItems: new fields.ArrayField(new ForeignDocumentUUIDField({ type: 'Item' })),
evasion: new fields.NumberField({ initial: 0, integer: true }),
- features: new fields.ArrayField(new ForeignDocumentUUIDField({ type: 'Item' })),
+ hopeFeatures: new foundry.data.fields.ArrayField(new ActionField()),
+ classFeatures: new foundry.data.fields.ArrayField(new ActionField()),
subclasses: new fields.ArrayField(
new ForeignDocumentUUIDField({ type: 'Item', required: false, nullable: true, initial: undefined })
),
@@ -51,6 +53,10 @@ export default class DHClass extends BaseDataItem {
};
}
+ get hopeFeature() {
+ return this.hopeFeatures.length > 0 ? this.hopeFeatures[0] : null;
+ }
+
async _preCreate(data, options, user) {
const allowed = await super._preCreate(data, options, user);
if (allowed === false) return;
diff --git a/module/data/item/subclass.mjs b/module/data/item/subclass.mjs
index bb315fcd..1e236ff4 100644
--- a/module/data/item/subclass.mjs
+++ b/module/data/item/subclass.mjs
@@ -1,6 +1,15 @@
+import ActionField from '../fields/actionField.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import BaseDataItem from './base.mjs';
+const featureSchema = () => {
+ return new foundry.data.fields.SchemaField({
+ name: new foundry.data.fields.StringField({ required: true }),
+ effects: new foundry.data.fields.ArrayField(new ForeignDocumentUUIDField({ type: 'ActiveEffect' })),
+ actions: new foundry.data.fields.ArrayField(new ActionField())
+ });
+};
+
export default class DHSubclass extends BaseDataItem {
/** @inheritDoc */
static get metadata() {
@@ -22,9 +31,9 @@ export default class DHSubclass extends BaseDataItem {
nullable: true,
initial: null
}),
- foundationFeature: new ForeignDocumentUUIDField({ type: 'Item' }),
- specializationFeature: new ForeignDocumentUUIDField({ type: 'Item' }),
- masteryFeature: new ForeignDocumentUUIDField({ type: 'Item' }),
+ foundationFeature: featureSchema(),
+ specializationFeature: featureSchema(),
+ masteryFeature: featureSchema(),
featureState: new fields.NumberField({ required: true, initial: 1, min: 1 }),
isMulticlass: new fields.BooleanField({ initial: false })
};
diff --git a/styles/daggerheart.css b/styles/daggerheart.css
index c39c6df6..e5d27cb5 100755
--- a/styles/daggerheart.css
+++ b/styles/daggerheart.css
@@ -3579,7 +3579,6 @@ div.daggerheart.views.multiclass {
}
.sheet.daggerheart.dh-style.item .tab.features {
padding: 0 10px;
- max-height: 265px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: light-dark(#18162e, #f3c267) transparent;
diff --git a/styles/less/global/feature-section.less b/styles/less/global/feature-section.less
index a294926f..db1c117a 100644
--- a/styles/less/global/feature-section.less
+++ b/styles/less/global/feature-section.less
@@ -4,7 +4,6 @@
.sheet.daggerheart.dh-style.item {
.tab.features {
padding: 0 10px;
- max-height: 265px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: light-dark(@dark-blue, @golden) transparent;
diff --git a/templates/sheets/global/partials/feature-section-item.hbs b/templates/sheets/global/partials/feature-section-item.hbs
index ebaabefe..7bcf7736 100644
--- a/templates/sheets/global/partials/feature-section-item.hbs
+++ b/templates/sheets/global/partials/feature-section-item.hbs
@@ -9,7 +9,8 @@
@@ -17,7 +18,8 @@
diff --git a/templates/sheets/items/class/features.hbs b/templates/sheets/items/class/features.hbs
index dfa386d1..8b615121 100644
--- a/templates/sheets/items/class/features.hbs
+++ b/templates/sheets/items/class/features.hbs
@@ -3,15 +3,25 @@
data-tab='{{tabs.features.id}}'
data-group='{{tabs.features.group}}'
>
+