Initial v14 fixes

This commit is contained in:
WBHarry 2026-01-29 18:46:39 +01:00
parent b374070809
commit 1a928e950c
19 changed files with 197 additions and 180 deletions

View file

@ -33,6 +33,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' },
changes: {
template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs',
templates: ['systems/daggerheart/templates/sheets/activeEffect/change.hbs'],
scrollable: ['ol[data-changes]']
},
footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' }
@ -121,8 +122,41 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
}));
}
break;
case 'changes':
const fields = this.document.system.schema.fields.changes.element.fields;
partContext.changes = await Promise.all(
foundry.utils
.deepClone(context.source.changes)
.map((c, i) => this._prepareChangeContext(c, i, fields))
);
break;
}
return partContext;
}
_prepareChangeContext(change, index, fields) {
if (typeof change.value !== 'string') change.value = JSON.stringify(change.value);
const defaultPriority = game.system.api.documents.DhActiveEffect.CHANGE_TYPES[change.type]?.defaultPriority;
Object.assign(
change,
['key', 'type', 'value', 'priority'].reduce((paths, fieldName) => {
paths[`${fieldName}Path`] = `system.changes.${index}.${fieldName}`;
return paths;
}, {})
);
return (
game.system.api.documents.DhActiveEffect.CHANGE_TYPES[change.type].render?.(
change,
index,
defaultPriority
) ??
renderTemplate('systems/daggerheart/templates/sheets/activeEffect/change.hbs', {
change,
index,
defaultPriority,
fields
})
);
}
}

View file

@ -72,18 +72,6 @@ const typeSettingsMap = {
*/
export default function DHApplicationMixin(Base) {
class DHSheetV2 extends HandlebarsApplicationMixin(Base) {
/**
* @param {DHSheetV2Configuration} [options={}]
*/
constructor(options = {}) {
super(options);
/**
* @type {foundry.applications.ux.DragDrop[]}
* @private
*/
this._dragDrop = this._createDragDropHandlers();
}
#nonHeaderAttribution = ['environment', 'ancestry', 'community', 'domainCard'];
/**
@ -177,7 +165,7 @@ export default function DHApplicationMixin(Base) {
/**@inheritdoc */
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
this._dragDrop.forEach(d => d.bind(htmlElement));
// this._dragDrop.forEach(d => d.bind(htmlElement));
// Handle delta inputs
for (const deltaInput of htmlElement.querySelectorAll('input[data-allow-delta]')) {
@ -350,21 +338,6 @@ export default function DHApplicationMixin(Base) {
/* Drag and Drop */
/* -------------------------------------------- */
/**
* Creates drag-drop handlers from the configured options.
* @returns {foundry.applications.ux.DragDrop[]}
* @private
*/
_createDragDropHandlers() {
return this.options.dragDrop.map(d => {
d.callbacks = {
dragstart: this._onDragStart.bind(this),
drop: this._onDrop.bind(this)
};
return new foundry.applications.ux.DragDrop.implementation(d);
});
}
/**
* Handle dragStart event.
* @param {DragEvent} event

View file

@ -52,10 +52,6 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
}
};
get element() {
return document.body.querySelector('.daggerheart.dh-style.countdowns');
}
/**@inheritdoc */
async _renderFrame(options) {
const frame = await super._renderFrame(options);
@ -68,6 +64,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
const header = frame.querySelector('.window-header');
header.querySelector('button[data-action="close"]').remove();
header.querySelector('button[data-action="toggleControls"]').remove();
if (game.user.isGM) {
const editTooltip = game.i18n.localize('DAGGERHEART.APPLICATIONS.CountdownEdit.editTitle');
@ -278,10 +275,8 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
return acc;
}, {})
};
await emitAsGM(GMUpdateEvent.UpdateCountdowns,
DhCountdowns.gmSetSetting.bind(settings),
settings, null, {
refreshType: RefreshType.Countdown
await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
refreshType: RefreshType.Countdown
});
}

View file

@ -6,7 +6,7 @@ export default class DhTemplateLayer extends foundry.canvas.layers.TemplateLayer
order: 2,
title: 'CONTROLS.GroupMeasure',
icon: 'fa-solid fa-ruler-combined',
visible: game.user.can('TEMPLATE_CREATE'),
visible: game.user.can('REGION_CREATE'),
onChange: (event, active) => {
if (active) canvas.templates.activate();
},

View file

@ -70,8 +70,12 @@ export const range = {
}
};
/* circle|cone|rect|ray used to be CONST.MEASURED_TEMPLATE_TYPES. Hardcoded for now */
export const templateTypes = {
...CONST.MEASURED_TEMPLATE_TYPES,
CIRCLE: 'circle',
CONE: 'cone',
RECTANGLE: 'rect',
RAY: 'ray',
EMANATION: 'emanation',
INFRONT: 'inFront'
};
@ -737,3 +741,36 @@ export const sceneRangeMeasurementSetting = {
label: 'Custom'
}
};
export const activeEffectModes = {
custom: {
id: 'custom',
priority: 0,
label: 'EFFECT.CHANGES.TYPES.custom'
},
multiply: {
id: 'multiply',
priority: 10,
label: 'EFFECT.CHANGES.TYPES.multiply'
},
add: {
id: 'add',
priority: 20,
label: 'EFFECT.CHANGES.TYPES.add'
},
downgrade: {
id: 'downgrade',
priority: 30,
label: 'EFFECT.CHANGES.TYPES.downgrade'
},
upgrade: {
id: 'upgrade',
priority: 40,
label: 'EFFECT.CHANGES.TYPES.upgrade'
},
override: {
id: 'override',
priority: 50,
label: 'EFFECT.CHANGES.TYPES.override'
}
};

View file

@ -2,84 +2,4 @@ import DHBaseAction from './baseAction.mjs';
export default class DhBeastformAction extends DHBaseAction {
static extraSchemas = [...super.extraSchemas, 'beastform'];
/* async use(event, options) {
const beastformConfig = this.prepareBeastformConfig();
const abort = await this.handleActiveTransformations();
if (abort) return;
const calcCosts = game.system.api.fields.ActionFields.CostField.calcCosts.call(this, this.cost);
const hasCost = game.system.api.fields.ActionFields.CostField.hasCost.call(this, calcCosts);
if (!hasCost) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.insufficientResources'));
return;
}
const { selected, evolved, hybrid } = await BeastformDialog.configure(beastformConfig, this.item);
if (!selected) return;
const result = await super.use(event, options);
if (!result) return;
await this.transform(selected, evolved, hybrid);
}
prepareBeastformConfig(config) {
const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers;
const actorLevel = this.actor.system.levelData.level.current;
const actorTier =
Object.values(settingsTiers).find(
tier => actorLevel >= tier.levels.start && actorLevel <= tier.levels.end
) ?? 1;
return {
tierLimit: this.beastform.tierAccess.exact ?? actorTier
};
}
async transform(selectedForm, evolvedData, hybridData) {
const formData = evolvedData?.form ? evolvedData.form.toObject() : selectedForm.toObject();
const beastformEffect = formData.effects.find(x => x.type === 'beastform');
if (!beastformEffect) {
ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect');
return;
}
if (evolvedData?.form) {
const evolvedForm = selectedForm.effects.find(x => x.type === 'beastform');
if (!evolvedForm) {
ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect');
return;
}
beastformEffect.changes = [...beastformEffect.changes, ...evolvedForm.changes];
formData.system.features = [...formData.system.features, ...selectedForm.system.features.map(x => x.uuid)];
}
if (selectedForm.system.beastformType === CONFIG.DH.ITEM.beastformTypes.hybrid.id) {
formData.system.advantageOn = Object.values(hybridData.advantages).reduce((advantages, formCategory) => {
Object.keys(formCategory).forEach(advantageKey => {
advantages[advantageKey] = formCategory[advantageKey];
});
return advantages;
}, {});
formData.system.features = [
...formData.system.features,
...Object.values(hybridData.features).flatMap(x => Object.keys(x))
];
}
this.actor.createEmbeddedDocuments('Item', [formData]);
}
async handleActiveTransformations() {
const beastformEffects = this.actor.effects.filter(x => x.type === 'beastform');
const existingEffects = beastformEffects.length > 0;
await this.actor.deleteEmbeddedDocuments(
'ActiveEffect',
beastformEffects.map(x => x.id)
);
return existingEffects;
} */
}

View file

@ -12,11 +12,27 @@
* "Anything that uses another data model value as its value": +1 - Effects that increase traits have to be calculated first at Base priority. (EX: Raise evasion by half your agility)
*/
export default class BaseEffect extends foundry.abstract.TypeDataModel {
export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
changes: new fields.ArrayField(
new fields.SchemaField({
key: new fields.StringField({ required: true }),
type: new fields.StringField({
required: true,
blank: false,
choices: CONFIG.DH.GENERAL.activeEffectModes,
initial: CONFIG.DH.GENERAL.activeEffectModes.add.id,
validate: BaseEffect.#validateType
}),
value: new fields.AnyField({ required: true, nullable: true, serializable: true, initial: '' }),
phase: new fields.StringField({ required: true, blank: false, initial: 'initial' }),
priority: new fields.NumberField()
})
),
rangeDependence: new fields.SchemaField({
enabled: new fields.BooleanField({
required: true,
@ -45,6 +61,23 @@ export default class BaseEffect extends foundry.abstract.TypeDataModel {
};
}
/**
* Validate that an {@link EffectChangeData#type} string is well-formed.
* @param {string} type The string to be validated
* @returns {true}
* @throws {Error} An error if the type string is malformed
*/
static #validateType(type) {
if (type.length < 3) throw new Error('must be at least three characters long');
if (!/^custom\.-?\d+$/.test(type) && !type.split('.').every(s => /^[a-z0-9]+$/i.test(s))) {
throw new Error(
'A change type must either be a sequence of dot-delimited, alpha-numeric substrings or of the form' +
' "custom.{number}"'
);
}
return true;
}
static getDefaultObject() {
return {
name: 'New Effect',

View file

@ -5,6 +5,7 @@ export default class BeastformEffect extends BaseEffect {
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
characterTokenData: new fields.SchemaField({
usesDynamicToken: new fields.BooleanField({ initial: false }),
tokenImg: new fields.FilePathField({

View file

@ -111,9 +111,17 @@ export class ActionField extends foundry.data.fields.ObjectField {
* @param {object} sourceData Candidate source data of the root model.
* @param {any} fieldData The value of this field within the source data.
*/
migrateSource(sourceData, fieldData) {
const cls = this.getModel(fieldData);
if (cls) cls.migrateDataSafe(fieldData);
_migrate(sourceData, _fieldData) {
const source = sourceData ?? this.options.initial;
if (!source) return sourceData;
const cls = this.getModel(source);
if (cls) {
cls.migrateDataSafe(source);
return source;
}
return sourceData;
}
}

View file

@ -101,12 +101,13 @@ export default class DHBeastform extends BaseDataItem {
const effect = this.parent.effects.find(x => x.type === 'beastform');
if (!effect) return null;
const traitBonus = effect.changes.find(x => x.key === `system.traits.${this.mainTrait}.value`)?.value ?? 0;
const evasionBonus = effect.changes.find(x => x.key === 'system.evasion')?.value ?? 0;
const traitBonus =
effect.system.changes.find(x => x.key === `system.traits.${this.mainTrait}.value`)?.value ?? 0;
const evasionBonus = effect.system.changes.find(x => x.key === 'system.evasion')?.value ?? 0;
const damageDiceIndex = effect.changes.find(x => x.key === 'system.rules.attack.damage.diceIndex');
const damageDiceIndex = effect.system.changes.find(x => x.key === 'system.rules.attack.damage.diceIndex');
const damageDice = damageDiceIndex ? Object.keys(CONFIG.DH.GENERAL.diceTypes)[damageDiceIndex.value] : null;
const damageBonus = effect.changes.find(x => x.key === 'system.rules.attack.damage.bonus')?.value ?? 0;
const damageBonus = effect.system.changes.find(x => x.key === 'system.rules.attack.damage.bonus')?.value ?? 0;
return {
trait: game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.mainTrait].label),
@ -169,17 +170,17 @@ export default class DHBeastform extends BaseDataItem {
const beastformEffect = this.parent.effects.find(x => x.type === 'beastform');
await beastformEffect.updateSource({
changes: [
...beastformEffect.changes,
{
key: 'system.advantageSources',
mode: 2,
value: Object.values(this.advantageOn)
.map(x => x.value)
.join(', ')
}
],
system: {
changes: [
...beastformEffect.system.changes,
{
key: 'system.advantageSources',
mode: 2,
value: Object.values(this.advantageOn)
.map(x => x.value)
.join(', ')
}
],
characterTokenData: {
usesDynamicToken: this.parent.parent.prototypeToken.ring.enabled,
tokenImg: this.parent.parent.prototypeToken.texture.src,

View file

@ -51,6 +51,9 @@ export default class DHSubclass extends BaseDataItem {
}
async _preCreate(data, options, user) {
const allowed = await super._preCreate(data, options, user);
if (allowed === false) return;
if (this.actor?.type === 'character') {
const dataUuid = data.uuid ?? data._stats.compendiumSource ?? `Item.${data._id}`;
if (this.actor.system.class.subclass) {
@ -85,8 +88,5 @@ export default class DHSubclass extends BaseDataItem {
}
}
}
const allowed = await super._preCreate(data, options, user);
if (allowed === false) return;
}
}

View file

@ -191,7 +191,7 @@ export default class DamageRoll extends DHRoll {
if (config.data.parent.appliedEffects) {
// Bardic Rally
const rallyChoices = config.data?.parent?.appliedEffects.reduce((a, c) => {
const change = c.changes.find(ch => ch.key === 'system.bonuses.rally');
const change = c.system.changes.find(ch => ch.key === 'system.bonuses.rally');
if (change) a.push({ value: c.id, label: change.value });
return a;
}, []);

View file

@ -249,12 +249,12 @@ export default class DHRoll extends Roll {
const changeKeys = this.getActionChangeKeys();
return (
this.options.effects?.reduce((acc, effect) => {
if (effect.changes.some(x => changeKeys.some(key => x.key.includes(key)))) {
if (effect.system.changes.some(x => changeKeys.some(key => x.key.includes(key)))) {
acc[effect.id] = {
id: effect.id,
name: effect.name,
description: effect.description,
changes: effect.changes,
changes: effect.system.changes,
origEffect: effect,
selected: !effect.disabled
};

View file

@ -67,7 +67,7 @@ export default class DualityRoll extends D20Roll {
setRallyChoices() {
return this.data?.parent?.appliedEffects.reduce((a, c) => {
const change = c.changes.find(ch => ch.key === 'system.bonuses.rally');
const change = c.system.changes.find(ch => ch.key === 'system.bonuses.rally');
if (change) a.push({ value: c.id, label: change.value });
return a;
}, []);
@ -179,7 +179,7 @@ export default class DualityRoll extends D20Roll {
static async buildConfigure(config = {}, message = {}) {
config.dialog ??= {};
config.guaranteedCritical = config.data?.parent?.appliedEffects.reduce((a, c) => {
const change = c.changes.find(ch => ch.key === 'system.rules.roll.guaranteedCritical');
const change = c.system.changes.find(ch => ch.key === 'system.rules.roll.guaranteedCritical');
if (change) a = true;
return a;
}, false);

View file

@ -109,17 +109,25 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
/**@inheritdoc*/
static applyField(model, change, field) {
change.value = DhActiveEffect.getChangeValue(model, change, change.effect);
change.key = DhActiveEffect.getChangeKey(model, change, change.effect);
super.applyField(model, change, field);
}
/** */
static getChangeKey(model, change, effect) {
return DhActiveEffect.parseValue(change.key, model, change, effect);
}
static getChangeValue(model, change, effect) {
let value = change.value;
const isOriginTarget = value.toLowerCase().includes('origin.@');
return DhActiveEffect.parseValue(change.value, model, change, effect);
}
static parseValue(value, model, change, effect) {
let key = value;
const isOriginTarget = key.toLowerCase().includes('origin.@');
let parseModel = model;
if (isOriginTarget && effect.origin) {
value = change.value.replaceAll(/origin\.@/gi, '@');
key = change.key.replaceAll(/origin\.@/gi, '@');
try {
const originEffect = foundry.utils.fromUuidSync(effect.origin);
const doc =
@ -130,8 +138,8 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
} catch (_) {}
}
const evalValue = this.effectSafeEval(itemAbleRollParse(value, parseModel, effect.parent));
return evalValue ?? value;
const evalValue = this.effectSafeEval(itemAbleRollParse(key, parseModel, effect.parent));
return evalValue ?? key;
}
/**

View file

@ -415,7 +415,12 @@ export async function createEmbeddedItemWithEffects(actor, baseData, update) {
...baseData,
id: data.id,
uuid: data.uuid,
effects: data.effects?.map(effect => effect.toObject())
_uuid: data.uuid,
effects: data.effects?.map(effect => effect.toObject()),
_stats: {
...data._stats,
compendiumSource: data.pack ? `Compendium.${data.pack}.Item.${data.id}` : null
}
}
]);

View file

@ -2,11 +2,11 @@
"id": "daggerheart",
"title": "Daggerheart",
"description": "An unofficial implementation of the Daggerheart system",
"version": "1.6.1",
"version": "2.0.0",
"compatibility": {
"minimum": "13.346",
"verified": "13.351",
"maximum": "13"
"minimum": "14.353",
"verified": "14.353",
"maximum": "14"
},
"authors": [
{

View file

@ -0,0 +1,17 @@
<li data-index="{{index}}">
<div class="key">
{{formInput fields.key name=change.keyPath value=change.key}}
</div>
<div class="type">
{{formInput fields.type name=change.typePath value=change.type localize=true}}
</div>
<div class="value">
{{formInput fields.value name=change.valuePath value=change.value elementType="input"}}
</div>
<div class="priority">
{{formInput fields.priority name=change.priorityPath value=change.priority placeholder=defaultPriority}}
</div>
<div class="controls">
<button type="button" class="inline-control icon fa-solid fa-trash" data-action="deleteChange"></button>
</div>
</li>

View file

@ -1,31 +1,16 @@
<section class="tab changes{{#if tab.active}} active{{/if}}" data-group="{{tab.group}}" data-tab="{{tab.id}}">
<header>
<div class="key">{{localize "EFFECT.ChangeKey"}}</div>
<div class="mode">{{localize "EFFECT.ChangeMode"}}</div>
<div class="value">{{localize "EFFECT.ChangeValue"}}</div>
<div class="priority">{{localize "EFFECT.ChangePriority"}}</div>
<div class="controls"><a data-action="addChange"><i class="fa-regular fa-square-plus"></i></a></div>
<div class="key">{{localize "EFFECT.FIELDS.changes.element.key.label"}}</div>
<div class="type">{{localize "EFFECT.FIELDS.changes.element.type.label"}}</div>
<div class="value">{{localize "EFFECT.FIELDS.changes.element.value.label"}}</div>
<div class="priority">{{localize "EFFECT.FIELDS.changes.element.priority.label"}}</div>
<div class="controls">
<button type="button" class="inline-control icon fa-regular fa-square-plus" data-action="addChange"></button>
</div>
</header>
<ol class="scrollable" data-changes>
{{#each source.changes as |change i|}}
{{#with ../fields.changes.element.fields as |changeFields|}}
<li data-index="{{i}}">
<div class="key">
<input type="text" class="effect-change-input" name="{{concat "changes." i ".key"}}" value="{{change.key}}" />
</div>
<div class="mode">
{{formInput changeFields.mode name=(concat "changes." i ".mode") value=change.mode choices=@root.modes}}
</div>
<div class="value">
{{formInput changeFields.value name=(concat "changes." i ".value") value=change.value}}
</div>
<div class="priority">
{{formInput changeFields.priority name=(concat "changes." i ".priority") value=change.priority
placeholder=(lookup ../../priorities change.mode)}}
</div>
<div class="controls"><a data-action="deleteChange"><i class="fa-solid fa-trash"></i></a></div>
</li>
{{/with}}
{{#each changes as |change|}}
{{{change}}}
{{/each}}
</ol>
</section>