Fixed Feature flow

This commit is contained in:
WBHarry 2025-07-01 03:27:05 +02:00
parent 71fec10788
commit c406974cb5
17 changed files with 100 additions and 427 deletions

View file

@ -1,5 +1,5 @@
import BaseLevelUp from './levelup.mjs';
import { defaultCompanionTier } from '../../data/levelTier.mjs';
import { defaultCompanionTier, LevelOptionType } from '../../data/levelTier.mjs';
import { DhLevelup } from '../../data/levelup.mjs';
import { diceTypes, range } from '../../config/generalConfig.mjs';
@ -108,6 +108,12 @@ export default class DhCompanionLevelUp extends BaseLevelUp {
: actorKey;
advancement[choiceKey][checkbox.data[0]] =
options[keys[Math.min(currentIndex + 1, keys.length - 1)]];
default:
if (!advancement.simple) advancement.simple = {};
advancement.simple[choiceKey] = game.i18n.localize(
LevelOptionType[checkbox.type].label
);
break;
}
}
}
@ -140,7 +146,8 @@ export default class DhCompanionLevelUp extends BaseLevelUp {
new: game.i18n.localize(advancement.vicious.range.label)
}
: null
}
},
simple: advancement.simple ?? {}
};
context.advancements.statistics.stress.shown =

View file

@ -6,8 +6,6 @@ import DaggerheartSheet from './daggerheart-sheet.mjs';
import { abilities } from '../../config/actorConfig.mjs';
import DhCharacterlevelUp from '../levelup/characterLevelup.mjs';
import DhCharacterCreation from '../characterCreation.mjs';
import DHActionConfig from '../config/Action.mjs';
import { DHBaseAction } from '../../data/action/action.mjs';
const { ActorSheetV2 } = foundry.applications.sheets;
const { TextEditor } = foundry.applications.ux;
@ -53,7 +51,6 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
levelManagement: this.levelManagement,
editImage: this._onEditImage,
triggerContextMenu: this.triggerContextMenu
// editAction: this.editAction,
},
window: {
resizable: true
@ -307,20 +304,13 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
getItem(element) {
const listElement = (element.target ?? element).closest('[data-item-id]');
if (listElement.dataset.isAction) return this.getAction(listElement);
const document = listElement.dataset.companion ? this.document.system.companion : this.document;
const itemId = listElement.dataset.itemId,
item = this.document.items.get(itemId);
item = document.items.get(itemId);
return item;
}
getAction(listElement) {
const target = listElement.dataset.partner === 'true' ? this.document.system.companion : this.document;
if (!target) return null;
return target.system.actions.find(x => x.id === listElement.dataset.itemId);
}
static triggerContextMenu(event, button) {
return CONFIG.ux.ContextMenu.triggerContextMenu(event);
}
@ -623,9 +613,15 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
static async useItem(event, button) {
const item = this.getItem(button);
if (!item) return;
const wasUsed = await item.use(event);
if (wasUsed && item.type === 'weapon') {
Hooks.callAll(SYSTEM.HOOKS.characterAttack, {});
// Should dandle its actions. Or maybe they'll be separate buttons as per an Issue on the board
if (item.type === 'feature') {
item.toChat();
} else {
const wasUsed = await item.use(event);
if (wasUsed && item.type === 'weapon') {
Hooks.callAll(SYSTEM.HOOKS.characterAttack, {});
}
}
}
@ -633,11 +629,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
const item = this.getItem(event);
if (!item) return;
if (item instanceof DHBaseAction) {
new DHActionConfig(item).render({ force: true });
} else {
item.sheet.render(true);
}
item.sheet.render(true);
}
editItem(event) {
@ -694,12 +686,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
const item = this.getItem(event);
if (!item) return;
if (item instanceof DHBaseAction) {
const newActions = item.parent.actions.filter(x => x.id !== item.id);
await item.parent.parent.update({ ['system.actions']: newActions });
} else {
await item.delete();
}
await item.delete();
}
static async setItemQuantity(button, value) {
@ -735,30 +722,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
}
static async toChat(event, button) {
const item = event.dataset ? event : button.closest(['[data-item-id']);
if (item?.dataset.isAction) {
const action = this.getAction(item);
const cls = getDocumentClass('ChatMessage');
const systemData = {
title: action.name,
origin: this,
img: action.img,
name: action.name,
description: action.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());
} else if (button?.dataset?.type === 'experience') {
if (button?.dataset?.type === 'experience') {
const experience = this.document.system.experiences[button.dataset.uuid];
const cls = getDocumentClass('ChatMessage');
const systemData = {

View file

@ -6,11 +6,7 @@ export default class FeatureSheet extends DHBaseItemSheet {
id: 'daggerheart-feature',
classes: ['feature'],
position: { height: 600 },
window: { resizable: true },
actions: {
addEffect: this.addEffect,
removeEffect: this.removeEffect
}
window: { resizable: true }
};
/**@override */
@ -22,27 +18,16 @@ export default class FeatureSheet extends DHBaseItemSheet {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs',
scrollable: ['.actions']
},
settings: {
template: 'systems/daggerheart/templates/sheets/items/feature/settings.hbs',
scrollable: ['.settings']
},
effects: {
template: 'systems/daggerheart/templates/sheets/items/feature/effects.hbs',
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs',
scrollable: ['.effects']
}
};
/**
* Internally tracks the selected effect type from the select.
* @type {String}
* @private
*/
_selectedEffectType;
/**@override */
static TABS = {
primary: {
tabs: [{ id: 'description' }, { id: 'actions' }, { id: 'settings' }, { id: 'effects' }],
tabs: [{ id: 'description' }, { id: 'actions' }, { id: 'effects' }],
initial: 'description',
labelPrefix: 'DAGGERHEART.Sheets.TABS'
}
@ -50,68 +35,10 @@ export default class FeatureSheet extends DHBaseItemSheet {
/* -------------------------------------------- */
/**@inheritdoc*/
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
if (partId === 'effects')
htmlElement.querySelector('.effect-select')?.addEventListener('change', this._effectSelect.bind(this));
}
/**
* Handles selection of a new effect type.
* @param {Event} event - Change Event
*/
_effectSelect(event) {
const value = event.currentTarget.value;
this._selectedEffectType = value;
this.render({ parts: ['effects'] });
}
/* -------------------------------------------- */
/**@inheritdoc */
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.properties = CONFIG.daggerheart.ACTOR.featureProperties;
context.dice = CONFIG.daggerheart.GENERAL.diceTypes;
context.effectConfig = CONFIG.daggerheart.EFFECTS;
context.selectedEffectType = this._selectedEffectType;
return context;
}
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
/**
* Adds a new effect to the item, based on the selected effect type.
* @param {PointerEvent} _event - The originating click event
* @param {HTMLElement} _target - The capturing HTML element which defines the [data-action]
* @returns
*/
static async addEffect(_event, _target) {
const type = this._selectedEffectType;
if (!type) return;
const { id, name, ...rest } = CONFIG.daggerheart.EFFECTS.effectTypes[type];
await this.item.update({
[`system.effects.${foundry.utils.randomID()}`]: {
type,
value: '',
...rest
}
});
}
/**
* Removes an effect from the item.
* @param {PointerEvent} _event - The originating click event
* @param {HTMLElement} target - The capturing HTML element which defines the [data-action]
* @returns
*/
static async removeEffect(_event, target) {
const path = `system.effects.-=${target.dataset.effect}`;
await this.item.update({ [path]: null });
}
}

View file

@ -157,15 +157,12 @@ export default class DhCharacter extends BaseDataActor {
return this.parent.items.find(x => x.type === 'community') ?? null;
}
// get actions() {
// const generalActions = []; // Add in things like Sprint etc
// const levelupActions = this.levelData.actions.filter(x => !x.partner).map(x => x.value);
get features() {
return this.parent.items.filter(x => x.type === 'feature') ?? [];
}
// return [...generalActions, ...levelupActions];
// }
get companionActions() {
return this.companion ? this.companion.system.actions : [];
get companionFeatures() {
return this.companion ? this.companion.items.filter(x => x.type === 'feature') : [];
}
get needsCharacterSetup() {

View file

@ -1,4 +1,3 @@
import { getTier } from '../../helpers/utils.mjs';
import BaseDataItem from './base.mjs';
import ActionField from '../fields/actionField.mjs';
@ -17,135 +16,7 @@ export default class DHFeature extends BaseDataItem {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
//A type of feature seems unnecessary
type: new fields.StringField({ choices: SYSTEM.ITEM.featureTypes }),
//TODO: remove actionType field
actionType: new fields.StringField({
choices: SYSTEM.ITEM.actionTypes,
initial: SYSTEM.ITEM.actionTypes.passive.id
}),
//TODO: remove featureType field
featureType: new fields.SchemaField({
type: new fields.StringField({
choices: SYSTEM.ITEM.valueTypes,
initial: Object.keys(SYSTEM.ITEM.valueTypes).find(x => x === 'normal')
}),
data: new fields.SchemaField({
value: new fields.StringField({}),
property: new fields.StringField({
choices: SYSTEM.ACTOR.featureProperties,
initial: Object.keys(SYSTEM.ACTOR.featureProperties).find(x => x === 'spellcastingTrait')
}),
max: new fields.NumberField({ initial: 1, integer: true }),
numbers: new fields.TypedObjectField(
new fields.SchemaField({
value: new fields.NumberField({ integer: true }),
used: new fields.BooleanField({ initial: false })
})
)
})
}),
refreshData: new fields.SchemaField(
{
type: new fields.StringField({ choices: SYSTEM.GENERAL.refreshTypes }),
uses: new fields.NumberField({ initial: 1, integer: true }),
//TODO: remove refreshed field
refreshed: new fields.BooleanField({ initial: true })
},
{ nullable: true, initial: null }
),
//TODO: remove refreshed field
multiclass: new fields.NumberField({ initial: null, nullable: true, integer: true }),
disabled: new fields.BooleanField({ initial: false }),
//TODO: re do it completely or just remove it
effects: new fields.TypedObjectField(
new fields.SchemaField({
type: new fields.StringField({ choices: SYSTEM.EFFECTS.effectTypes }),
valueType: new fields.StringField({ choices: SYSTEM.EFFECTS.valueTypes }),
parseType: new fields.StringField({ choices: SYSTEM.EFFECTS.parseTypes }),
initiallySelected: new fields.BooleanField({ initial: true }),
options: new fields.ArrayField(
new fields.SchemaField({
name: new fields.StringField({}),
value: new fields.StringField({})
}),
{ nullable: true, initial: null }
),
dataField: new fields.StringField({}),
appliesOn: new fields.StringField(
{
choices: SYSTEM.EFFECTS.applyLocations
},
{ nullable: true, initial: null }
),
applyLocationChoices: new fields.TypedObjectField(new fields.StringField({}), {
nullable: true,
initial: null
}),
valueData: new fields.SchemaField({
value: new fields.StringField({}),
fromValue: new fields.StringField({ initial: null, nullable: true }),
type: new fields.StringField({ initial: null, nullable: true }),
hopeIncrease: new fields.StringField({ initial: null, nullable: true })
})
})
),
actions: new fields.ArrayField(new ActionField())
};
}
get multiclassTier() {
return getTier(this.multiclass);
}
async refresh() {
if (this.refreshData) {
if (this.featureType.type === SYSTEM.ITEM.valueTypes.dice.id) {
const update = { 'system.refreshData.refreshed': true };
Object.keys(this.featureType.data.numbers).forEach(
x => (update[`system.featureType.data.numbers.-=${x}`] = null)
);
await this.parent.update(update);
} else {
await this.parent.update({ 'system.refreshData.refreshed': true });
}
}
}
get effectData() {
const effectValues = Object.values(this.effects);
const effectCategories = Object.keys(SYSTEM.EFFECTS.effectTypes).reduce((acc, effectType) => {
acc[effectType] = effectValues.reduce((acc, effect) => {
if (effect.type === effectType) {
acc.push({ ...effect, valueData: this.#parseValues(effect.parseType, effect.valueData) });
}
return acc;
}, []);
return acc;
}, {});
return effectCategories;
}
#parseValues(parseType, values) {
return Object.keys(values).reduce((acc, prop) => {
acc[prop] = this.#parseValue(parseType, values[prop]);
return acc;
}, {});
}
#parseValue(parseType, value) {
switch (parseType) {
case SYSTEM.EFFECTS.parseTypes.number.id:
return Number.parseInt(value);
default:
return value;
}
}
}

View file

@ -43,7 +43,7 @@ export default class DhLevelData extends foundry.abstract.DataModel {
data: new fields.ArrayField(new fields.StringField({ required: true })),
secondaryData: new fields.TypedObjectField(new fields.StringField({ required: true })),
itemUuid: new fields.StringField({ required: true }),
actionIds: new fields.ArrayField(new fields.StringField())
featureIds: new fields.ArrayField(new fields.StringField())
})
)
})

View file

@ -66,12 +66,10 @@ export const CompanionLevelOptionType = {
creatureComfort: {
id: 'creatureComfort',
label: 'Creature Comfort',
actions: [
features: [
{
name: 'DAGGERHEART.LevelUp.Actions.CreatureComfort.Name',
img: 'icons/magic/life/heart-cross-purple-orange.webp',
type: 'attack',
actionType: 'passive',
description: 'DAGGERHEART.LevelUp.Actions.CreatureComfort.Description'
}
]
@ -79,12 +77,10 @@ export const CompanionLevelOptionType = {
armored: {
id: 'armored',
label: 'Armored',
actions: [
features: [
{
name: 'DAGGERHEART.LevelUp.Actions.Armored.Name',
img: 'icons/equipment/shield/kite-wooden-oak-glow.webp',
type: 'attack',
actionType: 'passive',
description: 'DAGGERHEART.LevelUp.Actions.Armored.Description'
}
]
@ -100,12 +96,10 @@ export const CompanionLevelOptionType = {
bonded: {
id: 'bonded',
label: 'Bonded',
actions: [
features: [
{
name: 'DAGGERHEART.LevelUp.Actions.Bonded.Name',
img: 'icons/magic/life/heart-red-blue.webp',
type: 'attack',
actionType: 'passive',
description: 'DAGGERHEART.LevelUp.Actions.Bonded.Description'
}
]

View file

@ -1,8 +1,8 @@
import DamageSelectionDialog from '../applications/damageSelectionDialog.mjs';
import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs';
import DamageReductionDialog from '../applications/damageReductionDialog.mjs';
import { actionsTypes } from '../data/_module.mjs';
import { LevelOptionType } from '../data/levelTier.mjs';
import DHFeature from '../data/item/feature.mjs';
export default class DhpActor extends Actor {
async _preCreate(data, options, user) {
@ -43,6 +43,7 @@ export default class DhpActor extends Actor {
return acc;
}, {});
const featureIds = [];
const domainCards = [];
const experiences = [];
const subclassFeatureState = { class: null, multiclass: null };
@ -55,6 +56,7 @@ export default class DhpActor extends Actor {
const advancementCards = level.selections.filter(x => x.type === 'domainCard').map(x => x.itemUuid);
domainCards.push(...achievementCards, ...advancementCards);
experiences.push(...Object.keys(level.achievements.experiences));
featureIds.push(...level.selections.flatMap(x => x.featureIds));
const subclass = level.selections.find(x => x.type === 'subclass');
if (subclass) {
@ -68,6 +70,10 @@ export default class DhpActor extends Actor {
multiclass = level.selections.find(x => x.type === 'multiclass');
});
for (let featureId of featureIds) {
this.items.get(featureId).delete();
}
if (experiences.length > 0) {
const getUpdate = () => ({
'system.experiences': experiences.reduce((acc, key) => {
@ -153,7 +159,7 @@ export default class DhpActor extends Actor {
}
let multiclass = null;
const actionIds = [];
const featureAdditions = [];
const domainCards = [];
const subclassFeatureState = { class: null, multiclass: null };
const selections = [];
@ -163,27 +169,17 @@ export default class DhpActor extends Actor {
const checkbox = selection[checkboxNr];
const tierOption = LevelOptionType[checkbox.type];
for (var actionData of tierOption.actions ?? []) {
const cls = actionsTypes[actionData.type];
const actionId = foundry.utils.randomID();
actionIds.push(actionId);
actions.push(
new cls(
{
// ...cls.getSourceConfig(target),
...actionData,
_id: actionId,
name: game.i18n.localize(actionData.name),
description: game.i18n.localize(actionData.description)
},
{
parent: this
}
)
);
}
if (checkbox.type === 'multiclass') {
if (tierOption.features?.length > 0) {
featureAdditions.push({
checkbox: {
...checkbox,
level: Number(levelKey),
optionKey: optionKey,
checkboxNr: Number(checkboxNr)
},
features: tierOption.features
});
} else if (checkbox.type === 'multiclass') {
multiclass = {
...checkbox,
level: Number(levelKey),
@ -216,6 +212,28 @@ export default class DhpActor extends Actor {
}
}
for (var addition of featureAdditions) {
for (var featureData of addition.features) {
const feature = new DHFeature({
...featureData,
description: game.i18n.localize(featureData.description)
});
const embeddedItem = await this.createEmbeddedDocuments('Item', [
{
...featureData,
name: game.i18n.localize(featureData.name),
type: 'feature',
system: feature
}
]);
addition.checkbox.featureIds = !addition.checkbox.featureIds
? [embeddedItem[0].id]
: [...addition.checkbox.featureIds, embeddedItem[0].id];
}
selections.push(addition.checkbox);
}
if (multiclass) {
const subclassItem = await foundry.utils.fromUuid(multiclass.secondaryData.subclass);
const subclassData = subclassItem.toObject();

View file

@ -117,7 +117,9 @@ export default class DHItem extends foundry.documents.Item {
? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.AncestryTitle')
: this.type === 'community'
? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.CommunityTitle')
: game.i18n.localize('DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle'),
: this.type === 'feature'
? game.i18n.localize('TYPES.Item.feature')
: game.i18n.localize('DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle'),
origin: origin,
img: this.img,
name: this.name,