Merge branch 'refactor/84-data-models-structure' into feature/109-LevelUp-Followup

This commit is contained in:
WBHarry 2025-06-13 14:14:02 +02:00
commit 7a090ea203
81 changed files with 2465 additions and 1032 deletions

0
module/_types.d.ts vendored
View file

View file

@ -13,3 +13,5 @@ export { default as DhpArmor } from './sheets/items/armor.mjs';
export { default as DhpChatMessage } from './chatMessage.mjs';
export { default as DhpEnvironment } from './sheets/environment.mjs';
export { default as DhActiveEffectConfig } from './sheets/activeEffectConfig.mjs';
export * as pseudoDocumentSheet from './sheets/pseudo-documents/_module.mjs';

View file

@ -3,6 +3,10 @@ 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.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();

View file

@ -1,7 +1,7 @@
import DaggerheartSheet from '../sheets/daggerheart-sheet.mjs';
const { ApplicationV2 } = foundry.applications.api;
export default class DaggerheartActionConfig extends DaggerheartSheet(ApplicationV2) {
export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
constructor(action) {
super({});
@ -9,21 +9,25 @@ export default class DaggerheartActionConfig extends DaggerheartSheet(Applicatio
this.openSection = null;
}
// get title(){
// return `Action - ${this.action.name}`;
// }
static DEFAULT_OPTIONS = {
tag: 'form',
id: 'daggerheart-action',
classes: ['daggerheart', 'views', 'action'],
position: { width: 600, height: 'auto' },
actions: {
toggleSection: this.toggleSection
toggleSection: this.toggleSection,
addEffect: this.addEffect,
removeEffect: this.removeEffect,
addElement: this.addElement,
removeElement: this.removeElement,
editEffect: this.editEffect,
addDamage: this.addDamage,
removeDamage: this.removeDamage
},
form: {
handler: this.updateForm,
closeOnSubmit: true
submitOnChange: true,
closeOnSubmit: false
}
};
@ -36,16 +40,9 @@ export default class DaggerheartActionConfig extends DaggerheartSheet(Applicatio
_getTabs() {
const tabs = {
effects: { active: true, cssClass: '', group: 'primary', id: 'effects', icon: null, label: 'Effects' },
useage: { active: false, cssClass: '', group: 'primary', id: 'useage', icon: null, label: 'Useage' },
conditions: {
active: false,
cssClass: '',
group: 'primary',
id: 'conditions',
icon: null,
label: 'Conditions'
}
base: { active: true, cssClass: '', group: 'primary', id: 'base', icon: null, label: 'Base' },
config: { active: false, cssClass: '', group: 'primary', id: 'config', icon: null, label: 'Configuration' },
effect: { active: false, cssClass: '', group: 'primary', id: 'effect', icon: null, label: 'Effect' }
};
for (const v of Object.values(tabs)) {
@ -58,9 +55,13 @@ export default class DaggerheartActionConfig extends DaggerheartSheet(Applicatio
async _prepareContext(_options) {
const context = await super._prepareContext(_options, 'action');
context.source = this.action.toObject(false);
context.openSection = this.openSection;
context.tabs = this._getTabs();
context.config = SYSTEM;
if (!!this.action.effects) context.effects = this.action.effects.map(e => this.action.item.effects.get(e._id));
if (this.action.damage?.hasOwnProperty('includeBase')) context.hasBaseDamage = !!this.action.parent.damage;
context.getRealIndex = this.getRealIndex.bind(this);
return context;
}
@ -69,15 +70,91 @@ export default class DaggerheartActionConfig extends DaggerheartSheet(Applicatio
this.render(true);
}
static async updateForm(event, _, formData) {
const data = foundry.utils.expandObject(
foundry.utils.mergeObject(this.action.toObject(), foundry.utils.expandObject(formData.object))
);
const newActions = this.action.parent.actions.map(x => x.toObject());
if (!newActions.findSplice(x => x.id === data.id, data)) {
newActions.push(data);
}
await this.action.parent.parent.update({ 'system.actions': newActions });
getRealIndex(index) {
const data = this.action.toObject(false);
return data.damage.parts.find(d => d.base) ? index - 1 : index;
}
_prepareSubmitData(event, formData) {
const submitData = foundry.utils.expandObject(formData.object);
// this.element.querySelectorAll("fieldset[disabled] :is(input, select)").forEach(input => {
// foundry.utils.setProperty(submitData, input.name, input.value);
// });
return submitData;
}
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
if (!newActions.findSplice(x => x._id === data._id, data)) newActions.push(data);
const updates = await this.action.parent.parent.update({ 'system.actions': newActions });
if (!updates) return;
this.action = updates.system.actions[this.action.index];
this.render();
}
static addElement(event) {
const data = this.action.toObject(),
key = event.target.closest('.action-category-data').dataset.key;
if (!this.action[key]) return;
data[key].push({});
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static removeElement(event) {
const data = this.action.toObject(),
key = event.target.closest('.action-category-data').dataset.key,
index = event.target.dataset.index;
data[key].splice(index, 1);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static addDamage(event) {
if (!this.action.damage.parts) return;
const data = this.action.toObject();
data.damage.parts.push({});
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static removeDamage(event) {
if (!this.action.damage.parts) return;
const data = this.action.toObject(),
index = event.target.dataset.index;
data.damage.parts.splice(index, 1);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static async addEffect(event) {
if (!this.action.effects) return;
const effectData = this._addEffectData.bind(this)(),
[created] = await this.action.item.createEmbeddedDocuments('ActiveEffect', [effectData], { render: false }),
data = this.action.toObject();
data.effects.push({ _id: created._id });
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
/**
* The data for a newly created applied effect.
* @returns {object}
* @protected
*/
_addEffectData() {
return {
name: this.action.item.name,
img: this.action.item.img,
origin: this.action.item.uuid,
transfer: false
};
}
static removeEffect(event) {
if (!this.action.effects) return;
const index = event.target.dataset.index,
effectId = this.action.effects[index]._id;
this.constructor.removeElement.bind(this)(event);
this.action.item.deleteEmbeddedDocuments('ActiveEffect', [effectId]);
}
static editEffect(event) {}
}

View file

@ -14,7 +14,7 @@ export default class NpcRollSelectionDialog extends HandlebarsApplicationMixin(A
}
get title() {
return game.i18n.localize('DAGGERHEART.Application.Settings.Title');
return game.i18n.localize('DAGGERHEART.Application.RollSelection.Title');
}
static DEFAULT_OPTIONS = {

View file

@ -16,7 +16,6 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl
hope: ['d12'],
fear: ['d12'],
advantage: null,
disadvantage: null,
hopeResource: hopeResource
};
}
@ -30,9 +29,8 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl
height: 'auto'
},
actions: {
updateIsAdvantage: this.updateIsAdvantage,
selectExperience: this.selectExperience,
setAdvantage: this.setAdvantage,
setDisadvantage: this.setDisadvantage,
finish: this.finish
},
form: {
@ -61,7 +59,6 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl
context.hope = this.data.hope;
context.fear = this.data.fear;
context.advantage = this.data.advantage;
context.disadvantage = this.data.disadvantage;
context.experiences = Object.keys(this.experiences).map(id => ({ id, ...this.experiences[id] }));
context.hopeResource = this.data.hopeResource + 1;
@ -85,18 +82,10 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl
this.render();
}
static setAdvantage() {
this.data.advantage = this.data.advantage ? null : 'd6';
this.data.disadvantage = null;
this.render(true);
}
static setDisadvantage() {
this.data.advantage = null;
this.data.disadvantage = this.data.disadvantage ? null : 'd6';
this.render(true);
static updateIsAdvantage(_, button) {
const advantage = Boolean(button.dataset.advantage);
this.data.advantage = this.data.advantage === advantage ? null : advantage;
this.render();
}
static async finish() {

View file

@ -166,8 +166,6 @@ class DhpRangeSettings extends FormApplication {
}
export const registerDHSettings = () => {
// const debouncedReload = foundry.utils.debounce(() => window.location.reload(), 100);
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.AbilityArray, {
name: game.i18n.localize('DAGGERHEART.Settings.General.AbilityArray.Name'),
hint: game.i18n.localize('DAGGERHEART.Settings.General.AbilityArray.Hint'),
@ -274,7 +272,7 @@ export const registerDHSettings = () => {
name: game.i18n.localize('DAGGERHEART.Settings.DualityRollColor.Name'),
hint: game.i18n.localize('DAGGERHEART.Settings.DualityRollColor.Hint'),
scope: 'world',
config: true,
config: false,
type: Number,
choices: Object.values(DualityRollColor),
default: DualityRollColor.colorful.value

View file

@ -54,6 +54,20 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
static async save() {
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance, this.settings.toObject());
const reload = await foundry.applications.api.DialogV2.confirm({
id: 'reload-world-confirm',
modal: true,
rejectClose: false,
window: { title: 'SETTINGS.ReloadPromptTitle' },
position: { width: 400 },
content: `<p>${game.i18n.localize('SETTINGS.ReloadPromptBody')}</p>`
});
if (reload) {
await game.socket.emit('reload');
foundry.utils.debouncedReload();
}
this.close();
}
}

View file

@ -61,70 +61,42 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) {
}
static 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 systemData = {
roll: roll._formula,
total: roll._total,
modifiers: modifiers,
diceResults: diceResults
const config = {
event: event,
title: `${this.actor.name} - Reaction Roll`,
roll: {
modifier: null,
type: 'reaction'
},
chatMessage: {
type: 'adversaryRoll',
template: 'systems/daggerheart/templates/chat/adversary-roll.hbs',
mute: true
}
};
const msg = new cls({
type: 'adversaryRoll',
system: systemData,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/adversary-roll.hbs',
systemData
),
rolls: [roll]
});
cls.create(msg.toObject());
this.actor.diceRoll(config);
}
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
);
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 systemData = {
title: attackName,
origin: this.document.id,
roll: roll._formula,
advantageState,
total: roll._total,
modifiers: modifiers,
dice: dice,
targets: targets,
damage: { value: damage.value, type: damage.type }
};
const msg = new cls({
type: 'adversaryRoll',
sound: CONFIG.sounds.dice,
system: systemData,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/adversary-attack-roll.hbs',
systemData
),
rolls: [roll]
});
cls.create(msg.toObject());
static async attackRoll(event) {
const { modifier, damage, name: attackName } = this.actor.system.attack,
config = {
event: event,
title: attackName,
roll: {
modifier: modifier,
type: 'action'
},
chatMessage: {
type: 'adversaryRoll',
template: 'systems/daggerheart/templates/chat/adversary-attack-roll.hbs'
},
damage: {
value: damage.value,
type: damage.type
},
checkTarget: true
};
this.actor.diceRoll(config);
}
static async addExperience() {

View file

@ -33,6 +33,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
selectAncestry: this.selectAncestry,
selectCommunity: this.selectCommunity,
viewObject: this.viewObject,
useItem: this.useItem,
useFeature: this.useFeature,
takeShortRest: this.takeShortRest,
takeLongRest: this.takeLongRest,
@ -150,6 +151,10 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement.querySelector('.level-value').addEventListener('change', this.onLevelChange.bind(this));
// To Remove when ContextMenu Handler is made
htmlElement
.querySelectorAll('[data-item-id]')
.forEach(element => element.addEventListener('contextmenu', this.editItem.bind(this)));
}
async _prepareContext(_options) {
@ -280,7 +285,24 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
}
static async rollAttribute(event, button) {
const { roll, hope, fear, advantage, disadvantage, modifiers } = await this.document.dualityRoll(
const abilityLabel = game.i18n.localize(abilities[button.dataset.attribute].label);
const config = {
event: event,
title: game.i18n.format('DAGGERHEART.Chat.DualityRoll.AbilityCheckTitle', {
ability: abilityLabel
}),
roll: {
label: abilityLabel,
modifier: button.dataset.value
},
chatMessage: {
template: 'systems/daggerheart/templates/chat/duality-roll.hbs'
}
};
this.document.diceRoll(config);
// Delete when new roll logic test done
/* 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
);
@ -310,7 +332,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
systemContent
),
rolls: [roll]
});
}); */
}
static async toggleMarks(_, button) {
@ -348,51 +370,8 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
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());
if (!weapon) return;
weapon.use(event);
}
static openLevelUp() {
@ -470,6 +449,12 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
(await game.packs.get('daggerheart.communities'))?.render(true);
}
static useItem(event) {
const uuid = event.target.closest('[data-item-id]').dataset.itemId,
item = this.document.items.find(i => i.uuid === uuid);
item.use(event);
}
static async viewObject(_, button) {
const object = await fromUuid(button.dataset.value);
if (!object) return;
@ -482,6 +467,16 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
object.sheet.render(true);
}
editItem(event) {
const uuid = event.target.closest('[data-item-id]').dataset.itemId,
item = this.document.items.find(i => i.uuid === uuid);
if (!item) return;
if (item.sheet.editMode) item.sheet.editMode = false;
item.sheet.render(true);
}
static async takeShortRest() {
await new DhpDowntime(this.document, true).render(true);
await this.minimize();

View file

@ -0,0 +1,132 @@
import DhpApplicationMixin from './daggerheart-sheet.mjs';
import DHActionConfig from '../config/Action.mjs';
import { actionsTypes } from '../../data/_module.mjs';
export default function DHItemMixin(Base) {
return class DHItemSheetV2 extends DhpApplicationMixin(Base) {
constructor(options = {}) {
super(options);
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'item', 'dh-style'],
position: { width: 600 },
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
},
actions: {
addAction: this.addAction,
editAction: this.editAction,
removeAction: this.removeAction
}
};
static TABS = {
description: {
active: true,
cssClass: '',
group: 'primary',
id: 'description',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Description'
},
actions: {
active: false,
cssClass: '',
group: 'primary',
id: 'actions',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Actions'
},
settings: {
active: false,
cssClass: '',
group: 'primary',
id: 'settings',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.config = CONFIG.daggerheart;
context.tabs = super._getTabs(this.constructor.TABS);
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object);
this.render();
}
static 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 });
// if (!data.name?.trim()) data.name = game.i18n.localize(SYSTEM.ACTIONS.actionTypes[data.type].name);
return data;
},
rejectClose: false
});
}
static async addAction() {
const actionType = await DHItemSheetV2.selectActionType(),
actionIndexes = this.document.system.actions.map(x => x._id.split('-')[2]).sort((a, b) => a - b);
try {
const cls = actionsTypes[actionType?.type] ?? actionsTypes.attack,
action = new cls(
{
// id: `${this.document.id}-Action-${actionIndexes.length > 0 ? actionIndexes[0] + 1 : 1}`
_id: foundry.utils.randomID(),
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.actions': [...this.document.system.actions, action] });
await new DHActionConfig(this.document.system.actions[this.document.system.actions.length - 1]).render(
true
);
} catch (error) {
console.log(error);
}
}
static async editAction(_, button) {
const action = this.document.system.actions[button.dataset.index];
await new DHActionConfig(action).render(true);
}
static async removeAction(event, button) {
event.stopPropagation();
await this.document.update({
'system.actions': this.document.system.actions.filter(
(_, index) => index !== Number.parseInt(button.dataset.index)
)
});
}
};
}

View file

@ -1,16 +1,9 @@
import DaggerheartSheet from '../daggerheart-sheet.mjs';
import DHItemSheetV2 from '../item.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
export default class ArmorSheet extends DaggerheartSheet(ItemSheetV2) {
export default class ArmorSheet extends DHItemSheetV2(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'armor'],
position: { width: 600 },
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
},
classes: ['armor'],
dragDrop: [{ dragSelector: null, dropSelector: null }]
};
@ -18,42 +11,13 @@ export default class ArmorSheet extends DaggerheartSheet(ItemSheetV2) {
header: { template: 'systems/daggerheart/templates/sheets/items/armor/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' },
actions: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs',
scrollable: ['.actions']
},
settings: {
template: 'systems/daggerheart/templates/sheets/items/armor/settings.hbs',
scrollable: ['.settings']
}
};
static TABS = {
description: {
active: true,
cssClass: '',
group: 'primary',
id: 'description',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Description'
},
settings: {
active: false,
cssClass: '',
group: 'primary',
id: 'settings',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.config = CONFIG.daggerheart;
context.tabs = super._getTabs(this.constructor.TABS);
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object);
this.render();
}
}

View file

@ -1,57 +1,23 @@
import DaggerheartSheet from '../daggerheart-sheet.mjs';
import DHItemSheetV2 from '../item.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
export default class ConsumableSheet extends DaggerheartSheet(ItemSheetV2) {
export default class ConsumableSheet extends DHItemSheetV2(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'consumable'],
position: { width: 550 },
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
}
classes: ['consumable'],
position: { width: 550 }
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/items/consumable/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' },
actions: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs',
scrollable: ['.actions']
},
settings: {
template: 'systems/daggerheart/templates/sheets/items/consumable/settings.hbs',
scrollable: ['.settings']
}
};
static TABS = {
description: {
active: true,
cssClass: '',
group: 'primary',
id: 'description',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Description'
},
settings: {
active: false,
cssClass: '',
group: 'primary',
id: 'settings',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.tabs = super._getTabs(this.constructor.TABS);
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object);
this.render();
}
}

View file

@ -1,23 +1,10 @@
import DaggerheartAction from '../../../data/action.mjs';
import DaggerheartActionConfig from '../../config/Action.mjs';
import DaggerheartSheet from '../daggerheart-sheet.mjs';
import DHItemSheetV2 from '../item.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
export default class DomainCardSheet extends DaggerheartSheet(ItemSheetV2) {
export default class DomainCardSheet extends DHItemSheetV2(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'domain-card'],
position: { width: 450, height: 700 },
actions: {
addAction: this.addAction,
editAction: this.editAction,
removeAction: this.removeAction
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
}
classes: ['domain-card'],
position: { width: 450, height: 700 }
};
static PARTS = {
@ -33,74 +20,4 @@ export default class DomainCardSheet extends DaggerheartSheet(ItemSheetV2) {
scrollable: ['.settings']
}
};
static TABS = {
description: {
active: true,
cssClass: '',
group: 'primary',
id: 'description',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Description'
},
actions: {
active: false,
cssClass: '',
group: 'primary',
id: 'actions',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Actions'
},
settings: {
active: false,
cssClass: '',
group: 'primary',
id: 'settings',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.config = CONFIG.daggerheart;
context.tabs = super._getTabs(this.constructor.TABS);
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object);
this.render();
}
static async addAction() {
const actionIndexes = this.document.system.actions.map(x => x.id.split('-')[2]).sort((a, b) => a - b);
const action = await new DaggerheartAction(
{
id: `${this.document.id}-Action-${actionIndexes.length > 0 ? actionIndexes[0] + 1 : 1}`
},
{
parent: this.document
}
);
await this.document.update({ 'system.actions': [...this.document.system.actions, action] });
await new DaggerheartActionConfig(this.document.system.actions[this.document.system.actions.length - 1]).render(
true
);
}
static async editAction(_, button) {
const action = this.document.system.actions[button.dataset.index];
await new DaggerheartActionConfig(action).render(true);
}
static async removeAction(event, button) {
event.stopPropagation();
await this.document.update({
'system.actions': this.document.system.actions.filter(
(_, index) => index !== Number.parseInt(button.dataset.index)
)
});
}
}

View file

@ -1,9 +1,7 @@
import DaggerheartAction from '../../../data/action.mjs';
import DaggerheartActionConfig from '../../config/Action.mjs';
import DaggerheartSheet from '../daggerheart-sheet.mjs';
import DHItemSheetV2 from '../item.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
export default class FeatureSheet extends DaggerheartSheet(ItemSheetV2) {
export default class FeatureSheet extends DHItemSheetV2(ItemSheetV2) {
constructor(options = {}) {
super(options);
@ -11,22 +9,13 @@ export default class FeatureSheet extends DaggerheartSheet(ItemSheetV2) {
}
static DEFAULT_OPTIONS = {
tag: 'form',
id: 'daggerheart-feature',
classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'feature'],
classes: ['feature'],
position: { width: 600, height: 600 },
window: { resizable: true },
actions: {
addEffect: this.addEffect,
removeEffect: this.removeEffect,
addAction: this.addAction,
editAction: this.editAction,
removeAction: this.removeAction
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
removeEffect: this.removeEffect
}
};
@ -49,30 +38,7 @@ export default class FeatureSheet extends DaggerheartSheet(ItemSheetV2) {
};
static TABS = {
description: {
active: true,
cssClass: '',
group: 'primary',
id: 'description',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Description'
},
actions: {
active: false,
cssClass: '',
group: 'primary',
id: 'actions',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Actions'
},
settings: {
active: false,
cssClass: '',
group: 'primary',
id: 'settings',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings'
},
...super.TABS,
effects: {
active: false,
cssClass: '',
@ -102,11 +68,6 @@ export default class FeatureSheet extends DaggerheartSheet(ItemSheetV2) {
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object);
this.render();
}
effectSelect(event) {
this.selectedEffectType = event.currentTarget.value;
this.render(true);
@ -130,26 +91,4 @@ export default class FeatureSheet extends DaggerheartSheet(ItemSheetV2) {
const path = `system.effects.-=${button.dataset.effect}`;
await this.item.update({ [path]: null });
}
static async addAction() {
const action = await new DaggerheartAction({ img: this.document.img }, { parent: this.document });
await this.document.update({ 'system.actions': [...this.document.system.actions, action] });
await new DaggerheartActionConfig(this.document.system.actions[this.document.system.actions.length - 1]).render(
true
);
}
static async editAction(_, button) {
const action = this.document.system.actions[button.dataset.index];
await new DaggerheartActionConfig(action).render(true);
}
static async removeAction(event, button) {
event.stopPropagation();
await this.document.update({
'system.actions': this.document.system.actions.filter(
(_, index) => index !== Number.parseInt(button.dataset.index)
)
});
}
}

View file

@ -1,57 +1,23 @@
import DaggerheartSheet from '../daggerheart-sheet.mjs';
import DHItemSheetV2 from '../item.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
export default class MiscellaneousSheet extends DaggerheartSheet(ItemSheetV2) {
export default class MiscellaneousSheet extends DHItemSheetV2(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'miscellaneous'],
position: { width: 550 },
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
}
classes: ['miscellaneous'],
position: { width: 550 }
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/items/miscellaneous/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' },
actions: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs',
scrollable: ['.actions']
},
settings: {
template: 'systems/daggerheart/templates/sheets/items/miscellaneous/settings.hbs',
scrollable: ['.settings']
}
};
static TABS = {
description: {
active: true,
cssClass: '',
group: 'primary',
id: 'description',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Description'
},
settings: {
active: false,
cssClass: '',
group: 'primary',
id: 'settings',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.tabs = super._getTabs(this.constructor.TABS);
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object);
this.render();
}
}

View file

@ -1,58 +1,22 @@
import DaggerheartSheet from '../daggerheart-sheet.mjs';
import DHItemSheetV2 from '../item.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
export default class WeaponSheet extends DaggerheartSheet(ItemSheetV2) {
export default class WeaponSheet extends DHItemSheetV2(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'weapon'],
position: { width: 600 },
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
}
classes: ['weapon']
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/items/weapon/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' },
actions: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs',
scrollable: ['.actions']
},
settings: {
template: 'systems/daggerheart/templates/sheets/items/weapon/settings.hbs',
scrollable: ['.settings']
}
};
static TABS = {
description: {
active: true,
cssClass: '',
group: 'primary',
id: 'description',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Description'
},
settings: {
active: false,
cssClass: '',
group: 'primary',
id: 'settings',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.config = CONFIG.daggerheart;
context.tabs = super._getTabs(this.constructor.TABS);
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object);
this.render();
}
}

View file

@ -0,0 +1 @@
export {default as PseudoDocumentSheet }from "./pseudo-documents-sheet.mjs";

View file

@ -0,0 +1,66 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class PseudoDocumentSheet extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(options) {
super(options);
this.#pseudoDocument = options.document;
}
/**
* The UUID of the associated pseudo-document
* @type {string}
*/
get pseudoUuid() {
return this.pseudoDocument.uuid;
}
#pseudoDocument;
/**
* The pseudo-document instance this sheet represents
* @type {object}
*/
get pseudoDocument() {
return this.#pseudoDocument;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet'],
position: { width: 600 },
form: {
handler: PseudoDocumentSheet.#onSubmitForm,
submitOnChange: true,
closeOnSubmit: false
},
dragDrop: [{ dragSelector: null, dropSelector: null }],
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/pseudo-documents/header.hbs' },
};
/** @inheritDoc */
async _prepareContext(options) {
const context = await super._prepareContext(options);
const document = this.pseudoDocument;
return Object.assign(context, {
document,
source: document._source,
editable: this.isEditable,
user: game.user,
rootId: this.id,
});
}
/**
* Form submission handler
* @param {SubmitEvent | Event} event - The originating form submission or input change event
* @param {HTMLFormElement} form - The form element that was submitted
* @param {foundry.applications.ux.FormDataExtended} formData - Processed data for the submitted form
*/
static async #onSubmitForm(event, form, formData) {
const submitData = foundry.utils.expandObject(formData.object);
await this.pseudoDocument.update(submitData);
}
}

View file

@ -1,7 +1,43 @@
export const actionTypes = {
attack: {
id: 'attack',
name: 'DAGGERHEART.Actions.Types.Attack.Name',
icon: 'fa-swords'
},
spellcast: {
id: 'spellcast',
name: 'DAGGERHEART.Actions.Types.Spellcast.Name',
icon: 'fa-book-sparkles'
},
healing: {
id: 'healing',
name: 'DAGGERHEART.Actions.Types.Healing.Name',
icon: 'fa-kit-medical'
},
resource: {
id: 'resource',
name: 'DAGGERHEART.Actions.Types.Resource.Name',
icon: 'fa-honey-pot'
},
damage: {
id: 'damage',
name: 'DAGGERHEART.Effects.Types.Health.Name'
name: 'DAGGERHEART.Actions.Types.Damage.Name',
icon: 'fa-bone-break'
},
summon: {
id: 'summon',
name: 'DAGGERHEART.Actions.Types.Summon.Name',
icon: 'fa-ghost'
},
effect: {
id: 'effect',
name: 'DAGGERHEART.Actions.Types.Effect.Name',
icon: 'fa-person-rays'
},
macro: {
id: 'macro',
name: 'DAGGERHEART.Actions.Types.Macro.Name',
icon: 'fa-scroll'
}
};

View file

@ -1,4 +1,9 @@
export const range = {
self: {
label: 'DAGGERHEART.Range.self.name',
description: 'DAGGERHEART.Range.self.description',
distance: 0
},
melee: {
id: 'melee',
label: 'DAGGERHEART.Range.melee.name',
@ -248,6 +253,11 @@ export const diceTypes = {
d20: 'd20'
};
export const multiplierTypes = {
proficiency: 'Proficiency',
spellcast: 'Spellcast'
};
export const getDiceSoNicePresets = () => {
const { diceSoNice } = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance);
@ -312,3 +322,18 @@ export const abilityCosts = {
label: 'Stress'
}
};
export const rollTypes = {
weapon: {
id: 'weapon',
label: 'DAGGERHEART.RollTypes.weapon.name'
},
spellcast: {
id: 'spellcast',
label: 'DAGGERHEART.RollTypes.spellcast.name'
},
ability: {
id: 'ability',
label: 'DAGGERHEART.RollTypes.ability.name'
}
};

View file

@ -0,0 +1,17 @@
import { pseudoDocuments } from "../data/_module.mjs";
import { pseudoDocumentSheet } from "../applications/_module.mjs";
//CONFIG.daggerheart.pseudoDocuments
export default {
sheetClass: pseudoDocumentSheet.PseudoDocumentSheet,
feature: {
label: "DAGGERHEART.Feature.Label",
documentClass: pseudoDocuments.feature.BaseFeatureData,
types: {
weapon: {
label: "DAGGERHEART.Feature.Weapon.Label",
documentClass: pseudoDocuments.feature.WeaponFeature,
}
}
}
};

View file

@ -5,6 +5,7 @@ import * as ITEM from './itemConfig.mjs';
import * as SETTINGS from './settingsConfig.mjs';
import * as EFFECTS from './effectConfig.mjs';
import * as ACTIONS from './actionConfig.mjs';
import pseudoDocuments from "./pseudoConfig.mjs";
export const SYSTEM_ID = 'daggerheart';
@ -16,5 +17,6 @@ export const SYSTEM = {
ITEM,
SETTINGS,
EFFECTS,
ACTIONS
ACTIONS,
pseudoDocuments
};

View file

@ -5,4 +5,7 @@ export { default as DhCombatant } from './combatant.mjs';
export * as actors from './actor/_module.mjs';
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 pseudoDocuments from './pseudo-documents/_module.mjs';

View file

@ -1,34 +0,0 @@
export default class DaggerheartAction extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
id: new fields.DocumentIdField(),
name: new fields.StringField({ initial: 'New Action' }),
damage: new fields.SchemaField({
type: new fields.StringField({ choices: SYSTEM.GENERAL.damageTypes, nullable: true, initial: null }),
value: new fields.StringField({})
}),
healing: new fields.SchemaField({
type: new fields.StringField({ choices: SYSTEM.GENERAL.healingTypes, nullable: true, initial: null }),
value: new fields.StringField()
}),
conditions: new fields.ArrayField(
new fields.SchemaField({
name: new fields.StringField(),
icon: new fields.StringField(),
description: new fields.StringField()
})
),
cost: new fields.SchemaField({
type: new fields.StringField({ choices: SYSTEM.GENERAL.abilityCosts, nullable: true, initial: null }),
value: new fields.NumberField({ nullable: true, initial: null })
}),
target: new fields.SchemaField({
type: new fields.StringField({
choices: SYSTEM.ACTIONS.targetTypes,
initial: SYSTEM.ACTIONS.targetTypes.other.id
})
})
};
}
}

View file

@ -0,0 +1,23 @@
import {
DHAttackAction,
DHBaseAction,
DHDamageAction,
DHEffectAction,
DHHealingAction,
DHMacroAction,
DHResourceAction,
DHSpellCastAction,
DHSummonAction
} from './action.mjs';
export const actionsTypes = {
base: DHBaseAction,
attack: DHAttackAction,
spellcast: DHSpellCastAction,
resource: DHResourceAction,
damage: DHDamageAction,
healing: DHHealingAction,
summon: DHSummonAction,
effect: DHEffectAction,
macro: DHMacroAction
};

View file

@ -0,0 +1,386 @@
import { abilities } from '../../config/actorConfig.mjs';
import { DHActionDiceData, DHDamageData, DHDamageField } from './actionDice.mjs';
export default class DHAction extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
id: new fields.DocumentIdField(),
name: new fields.StringField({ initial: 'New Action' }),
damage: new fields.SchemaField({
type: new fields.StringField({ choices: SYSTEM.GENERAL.damageTypes, nullable: true, initial: null }),
value: new fields.StringField({})
}),
healing: new fields.SchemaField({
type: new fields.StringField({ choices: SYSTEM.GENERAL.healingTypes, nullable: true, initial: null }),
value: new fields.StringField()
}),
conditions: new fields.ArrayField(
new fields.SchemaField({
name: new fields.StringField(),
icon: new fields.StringField(),
description: new fields.StringField()
})
),
cost: new fields.SchemaField({
type: new fields.StringField({ choices: SYSTEM.GENERAL.abilityCosts, nullable: true, initial: null }),
value: new fields.NumberField({ nullable: true, initial: null })
}),
target: new fields.SchemaField({
type: new fields.StringField({
choices: SYSTEM.ACTIONS.targetTypes,
initial: SYSTEM.ACTIONS.targetTypes.other.id
})
})
};
}
}
const fields = foundry.data.fields;
/*
ToDo
- Apply ActiveEffect => Add to Chat message like Damage Button ?
- Add Drag & Drop for documentUUID field (Macro & Summon)
- Add optionnal Role for Healing ?
- Handle Roll result as part of formula if needed
- Target Check
- Cost Check
- Range Check
- Area of effect and measurement placement
- Auto use costs and action
*/
export class DHBaseAction extends foundry.abstract.DataModel {
static defineSchema() {
return {
_id: new fields.DocumentIdField(),
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 }),
actionType: new fields.StringField({ choices: SYSTEM.ITEM.actionTypes, initial: 'action', nullable: true }),
cost: new fields.ArrayField(
new fields.SchemaField({
type: new fields.StringField({
choices: SYSTEM.GENERAL.abilityCosts,
nullable: false,
required: true,
initial: 'hope'
}),
value: new fields.NumberField({ nullable: true, initial: 1 }),
scalable: new fields.BooleanField({ initial: false }),
step: new fields.NumberField({ nullable: true, initial: null })
})
),
uses: new fields.SchemaField({
value: new fields.NumberField({ nullable: true, initial: null }),
max: new fields.NumberField({ nullable: true, initial: null }),
recovery: new fields.StringField({
choices: SYSTEM.GENERAL.refreshTypes,
initial: null,
nullable: true
})
}),
range: new fields.StringField({
choices: SYSTEM.GENERAL.range,
required: true,
blank: false,
initial: 'self'
})
};
}
prepareData() {}
get index() {
return this.parent.actions.indexOf(this);
}
get item() {
return this.parent.parent;
}
get actor() {
return this.item?.actor;
}
get chatTemplate() {
return 'systems/daggerheart/templates/chat/attack-roll.hbs';
}
static getRollType() {
return 'ability';
}
static getSourceConfig(parent) {
const updateSource = {};
updateSource.img ??= parent?.img ?? parent?.system?.img;
if (parent?.system?.trait) {
updateSource['roll'] = {
type: this.getRollType(),
trait: parent.system.trait
};
}
if (parent?.system?.range) {
updateSource['range'] = parent?.system?.range;
}
return updateSource;
}
async use(event) {
if (this.roll.type && this.roll.trait) {
const modifierValue = this.actor.system.traits[this.roll.trait].value;
const config = {
event: event,
title: this.item.name,
roll: {
modifier: modifierValue,
label: game.i18n.localize(abilities[this.roll.trait].label),
type: this.actionType,
difficulty: this.roll?.difficulty
},
chatMessage: {
template: this.chatTemplate
}
};
if (this.target?.type) config.checkTarget = true;
if (this.damage.parts.length) {
config.damage = {
value: this.damage.parts.map(p => p.getFormula(this.actor)).join(' + '),
type: this.damage.parts[0].type
};
}
if (this.effects.length) {
// Apply Active Effects. In Chat Message ?
}
return this.actor.diceRoll(config);
}
}
}
const extraDefineSchema = (field, option) => {
return {
[field]: {
// damage: new fields.SchemaField({
// parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData))
// }),
damage: new DHDamageField(option),
roll: new fields.SchemaField({
type: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.GENERAL.rollTypes }),
trait: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.ACTOR.abilities }),
difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 })
}),
target: new fields.SchemaField({
type: new fields.StringField({
choices: SYSTEM.ACTIONS.targetTypes,
initial: SYSTEM.ACTIONS.targetTypes.other.id
})
}),
effects: new fields.ArrayField( // ActiveEffect
new fields.SchemaField({
_id: new fields.DocumentIdField()
})
)
}[field]
};
};
export class DHAttackAction extends DHBaseAction {
static defineSchema() {
return {
...super.defineSchema(),
...extraDefineSchema('damage', true),
...extraDefineSchema('roll'),
...extraDefineSchema('target'),
...extraDefineSchema('effects')
};
}
static getRollType() {
return 'weapon';
}
prepareData() {
super.prepareData();
if (this.damage.includeBase && !!this.item?.system?.damage) {
const baseDamage = this.getParentDamage();
this.damage.parts.unshift(new DHDamageData(baseDamage));
}
}
getParentDamage() {
return {
multiplier: 'proficiency',
dice: this.item?.system?.damage.value,
bonus: this.item?.system?.damage.bonus ?? 0,
type: this.item?.system?.damage.type,
base: true
};
}
// Temporary until full formula parser
// getDamageFormula() {
// return this.damage.parts.map(p => p.formula).join(' + ');
// }
}
export class DHSpellCastAction extends DHBaseAction {
static defineSchema() {
return {
...super.defineSchema(),
...extraDefineSchema('damage'),
...extraDefineSchema('roll'),
...extraDefineSchema('target'),
...extraDefineSchema('effects')
};
}
static getRollType() {
return 'spellcast';
}
}
export class DHDamageAction extends DHBaseAction {
static defineSchema() {
return {
...super.defineSchema(),
...extraDefineSchema('damage', false),
...extraDefineSchema('target'),
...extraDefineSchema('effects')
};
}
async use(event) {
const formula = this.damage.parts.map(p => p.getFormula(this.actor)).join(' + ');
if (!formula || formula == '') return;
let roll = { formula: formula, total: formula };
if (isNaN(formula)) {
roll = await new Roll(formula).evaluate();
}
const cls = getDocumentClass('ChatMessage');
const msg = new cls({
user: game.user.id,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/damage-roll.hbs',
{
roll: roll.formula,
total: roll.total,
type: this.damage.parts.map(p => p.type)
}
)
});
cls.create(msg.toObject());
}
}
export class DHHealingAction extends DHBaseAction {
static defineSchema() {
return {
...super.defineSchema(),
healing: new fields.SchemaField({
type: new fields.StringField({
choices: SYSTEM.GENERAL.healingTypes,
required: true,
blank: false,
initial: SYSTEM.GENERAL.healingTypes.health.id,
label: 'Healing'
}),
value: new fields.EmbeddedDataField(DHActionDiceData)
}),
...extraDefineSchema('target'),
...extraDefineSchema('effects')
};
}
async use(event) {
const formula = this.healing.value.getFormula(this.actor);
if (!formula || formula == '') return;
// const roll = await super.use(event);
let roll = { formula: formula, total: formula };
if (isNaN(formula)) {
roll = await new Roll(formula).evaluate();
}
const cls = getDocumentClass('ChatMessage');
const msg = new cls({
user: game.user.id,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/healing-roll.hbs',
{
roll: roll.formula,
total: roll.total,
type: this.healing.type
}
)
});
cls.create(msg.toObject());
}
get chatTemplate() {
return 'systems/daggerheart/templates/chat/healing-roll.hbs';
}
}
export class DHResourceAction extends DHBaseAction {
static defineSchema() {
return {
...super.defineSchema(),
// ...extraDefineSchema('roll'),
...extraDefineSchema('target'),
...extraDefineSchema('effects'),
resource: new fields.SchemaField({
type: new fields.StringField({
choices: [],
blank: true,
required: false,
initial: '',
label: 'Resource'
}),
value: new fields.NumberField({ initial: 0, label: 'Value' })
})
};
}
}
export class DHSummonAction extends DHBaseAction {
static defineSchema() {
return {
...super.defineSchema(),
documentUUID: new fields.StringField({ blank: true, initial: '', placeholder: 'Enter a Creature UUID' })
};
}
}
export class DHEffectAction extends DHBaseAction {
static defineSchema() {
return {
...super.defineSchema(),
...extraDefineSchema('effects')
};
}
}
export class DHMacroAction extends DHBaseAction {
static defineSchema() {
return {
...super.defineSchema(),
documentUUID: new fields.StringField({ blank: true, initial: '', placeholder: 'Enter a macro UUID' })
};
}
async use(event) {
const fixUUID = !this.documentUUID.includes('Macro.') ? `Macro.${this.documentUUID}` : this.documentUUID,
macro = await fromUuid(fixUUID);
try {
if (!macro) throw new Error(`No macro found for the UUID: ${this.documentUUID}.`);
macro.execute();
} catch (error) {
ui.notifications.error(error);
}
}
}

View file

@ -0,0 +1,55 @@
import FormulaField from '../fields/formulaField.mjs';
const fields = foundry.data.fields;
export class DHActionDiceData extends foundry.abstract.DataModel {
/** @override */
static defineSchema() {
return {
multiplier: new fields.StringField({
choices: SYSTEM.GENERAL.multiplierTypes,
initial: 'proficiency',
label: 'Multiplier'
}),
dice: new fields.StringField({ choices: SYSTEM.GENERAL.diceTypes, initial: 'd6', label: 'Formula' }),
bonus: new fields.NumberField({ nullable: true, initial: null, label: 'Bonus' }),
custom: new fields.SchemaField({
enabled: new fields.BooleanField({ label: 'Custom Formula' }),
formula: new FormulaField({ label: 'Formula' })
})
};
}
getFormula(actor) {
return this.custom.enabled
? this.custom.formula
: `${actor.system[this.multiplier] ?? 1}${this.dice}${this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : ''}`;
}
}
export class DHDamageField extends fields.SchemaField {
constructor(hasBase, options, context = {}) {
const damageFields = {
parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData))
};
if (hasBase) damageFields.includeBase = new fields.BooleanField({ initial: true });
super(damageFields, options, context);
}
}
export class DHDamageData extends DHActionDiceData {
/** @override */
static defineSchema() {
return {
...super.defineSchema(),
base: new fields.BooleanField({ initial: false, readonly: true, label: 'Base' }),
type: new fields.StringField({
choices: SYSTEM.GENERAL.damageTypes,
initial: 'physical',
label: 'Type',
nullable: false,
required: true
})
};
}
}

View file

@ -5,17 +5,19 @@ export default class DHAdversaryRoll extends foundry.abstract.TypeDataModel {
return {
title: new fields.StringField(),
origin: new fields.StringField({ required: true }),
roll: new fields.StringField({}),
total: new fields.NumberField({ integer: true }),
dice: new fields.DataField(),
roll: new fields.DataField(),
modifiers: new fields.ArrayField(
new fields.SchemaField({
value: new fields.NumberField({ integer: true }),
label: new fields.StringField({}),
title: new fields.StringField({})
label: new fields.StringField({})
})
),
advantageState: new fields.NumberField({ required: true, choices: [0, 1, 2], initial: 0 }),
dice: new fields.EmbeddedDataField(DhpAdversaryRollDice),
advantageState: new fields.BooleanField({ nullable: true, initial: null }),
advantage: new fields.SchemaField({
dice: new fields.StringField({}),
value: new fields.NumberField({ integer: true })
}),
targets: new fields.ArrayField(
new fields.SchemaField({
id: new fields.StringField({}),
@ -37,42 +39,8 @@ export default class DHAdversaryRoll extends foundry.abstract.TypeDataModel {
}
prepareDerivedData() {
const diceKeys = Object.keys(this.dice.rolls);
const highestDiceIndex =
diceKeys.length < 2
? null
: this.dice.rolls[diceKeys[0]].value > this.dice.rolls[diceKeys[1]].value
? 0
: 1;
if (highestDiceIndex !== null) {
this.dice.rolls = this.dice.rolls.map((roll, index) => ({
...roll,
discarded: this.advantageState === 1 ? index !== highestDiceIndex : index === highestDiceIndex
}));
}
this.targets.forEach(target => {
target.hit = target.difficulty ? this.total >= target.difficulty : this.total >= target.evasion;
});
}
}
class DhpAdversaryRollDice extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
type: new fields.StringField({ required: true }),
rolls: new fields.ArrayField(
new fields.SchemaField({
value: new fields.NumberField({ required: true, integer: true }),
discarded: new fields.BooleanField({ initial: false })
})
)
};
}
get rollTotal() {
return this.rolls.reduce((acc, roll) => acc + (!roll.discarded ? roll.value : 0), 0);
}
}

View file

@ -9,7 +9,13 @@ export default class DHDamageRoll extends foundry.abstract.TypeDataModel {
total: new fields.NumberField({ required: true, integer: true }),
type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.damageTypes), integer: false })
}),
dice: new fields.ArrayField(new fields.EmbeddedDataField(DhpDamageDice)),
dice: new fields.ArrayField(
new fields.SchemaField({
type: new fields.StringField({ required: true }),
rolls: new fields.ArrayField(new fields.NumberField({ required: true, integer: true })),
total: new fields.NumberField({ integer: true })
})
),
modifiers: new fields.ArrayField(
new fields.SchemaField({
value: new fields.NumberField({ required: true, integer: true }),
@ -26,18 +32,3 @@ export default class DHDamageRoll extends foundry.abstract.TypeDataModel {
};
}
}
class DhpDamageDice extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
type: new fields.StringField({ required: true }),
rolls: new fields.ArrayField(new fields.NumberField({ required: true, integer: true }))
};
}
get rollTotal() {
return this.rolls.reduce((acc, roll) => acc + roll, 0);
}
}

View file

@ -1,4 +1,4 @@
import { DualityRollColor } from "../settings/Appearance.mjs";
import { DualityRollColor } from '../settings/Appearance.mjs';
const fields = foundry.data.fields;
const diceField = () =>
@ -18,18 +18,17 @@ export default class DHDualityRoll extends foundry.abstract.TypeDataModel {
return {
title: new fields.StringField(),
origin: new fields.StringField({ required: true }),
roll: new fields.StringField({}),
roll: new fields.DataField({}),
modifiers: new fields.ArrayField(
new fields.SchemaField({
value: new fields.NumberField({ integer: true }),
label: new fields.StringField({}),
title: new fields.StringField({})
label: new fields.StringField({})
})
),
hope: diceField(),
fear: diceField(),
advantageState: new fields.BooleanField({ nullable: true, initial: null }),
advantage: diceField(),
disadvantage: diceField(),
targets: new fields.ArrayField(
new fields.SchemaField({
id: new fields.StringField({}),
@ -64,15 +63,6 @@ export default class DHDualityRoll extends foundry.abstract.TypeDataModel {
};
}
get total() {
const advantage = this.advantage.value
? this.advantage.value
: this.disadvantage.value
? -this.disadvantage.value
: 0;
return this.diceTotal + advantage + this.modifierTotal.value;
}
get diceTotal() {
return this.hope.value + this.fear.value;
}
@ -112,13 +102,7 @@ export default class DHDualityRoll extends foundry.abstract.TypeDataModel {
}
prepareDerivedData() {
const total = this.total;
this.hope.discarded = this.hope.value < this.fear.value;
this.fear.discarded = this.fear.value < this.hope.value;
this.targets.forEach(target => {
target.hit = target.difficulty ? total >= target.difficulty : total >= target.evasion;
});
}
}

View file

@ -1,2 +1,3 @@
export { default as FormulaField } from "./formulaField.mjs";
export { default as ForeignDocumentUUIDField } from "./foreignDocumentUUIDField.mjs";
export { default as FormulaField } from './formulaField.mjs';
export { default as ForeignDocumentUUIDField } from './foreignDocumentUUIDField.mjs';
export { default as PseudoDocumentsField } from './pseudoDocumentsField.mjs';

View file

@ -0,0 +1,40 @@
import { actionsTypes } from '../action/_module.mjs';
// Temporary Solution
export default class ActionField extends foundry.data.fields.ObjectField {
getModel(value) {
return actionsTypes[value.type] ?? actionsTypes.attack;
}
/* -------------------------------------------- */
/** @override */
_cleanType(value, options) {
if (!(typeof value === 'object')) value = {};
const cls = this.getModel(value);
if (cls) return cls.cleanData(value, options);
return value;
}
/* -------------------------------------------- */
/** @override */
initialize(value, model, options = {}) {
const cls = this.getModel(value);
if (cls) return new cls(value, { parent: model, ...options });
return foundry.utils.deepClone(value);
}
/* -------------------------------------------- */
/**
* Migrate this field's candidate source data.
* @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);
}
}

View file

@ -16,78 +16,78 @@
* Special case StringField which represents a formula.
*/
export default class FormulaField extends foundry.data.fields.StringField {
/**
* @param {FormulaFieldOptions} [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 {FormulaFieldOptions} [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, {
deterministic: false
});
}
/** @inheritDoc */
static get _defaults() {
return foundry.utils.mergeObject(super._defaults, {
deterministic: false
});
}
/* -------------------------------------------- */
/* -------------------------------------------- */
/** @inheritDoc */
_validateType(value) {
const roll = new Roll(value.replace(/@([a-z.0-9_-]+)/gi, '1'));
roll.evaluateSync({ strict: false });
if (this.options.deterministic && !roll.isDeterministic)
throw new Error(`must not contain dice terms: ${value}`);
super._validateType(value);
}
/** @inheritDoc */
_validateType(value) {
const roll = new Roll(value.replace(/@([a-z.0-9_-]+)/gi, "1"));
roll.evaluateSync({ strict: false });
if (this.options.deterministic && !roll.isDeterministic) throw new Error(`must not contain dice terms: ${value}`);
super._validateType(value);
}
/* -------------------------------------------- */
/* Active Effect Integration */
/* -------------------------------------------- */
/* -------------------------------------------- */
/* Active Effect Integration */
/* -------------------------------------------- */
/** @override */
_castChangeDelta(delta) {
return this._cast(delta).trim();
}
/** @override */
_castChangeDelta(delta) {
return this._cast(delta).trim();
}
/* -------------------------------------------- */
/* -------------------------------------------- */
/** @override */
_applyChangeAdd(value, delta, model, change) {
if (!value) return delta;
const operator = delta.startsWith('-') ? '-' : '+';
delta = delta.replace(/^[+-]/, '').trim();
return `${value} ${operator} ${delta}`;
}
/** @override */
_applyChangeAdd(value, delta, model, change) {
if (!value) return delta;
const operator = delta.startsWith("-") ? "-" : "+";
delta = delta.replace(/^[+-]/, "").trim();
return `${value} ${operator} ${delta}`;
}
/* -------------------------------------------- */
/* -------------------------------------------- */
/** @override */
_applyChangeMultiply(value, delta, model, change) {
if (!value) return delta;
const terms = new Roll(value).terms;
if (terms.length > 1) return `(${value}) * ${delta}`;
return `${value} * ${delta}`;
}
/** @override */
_applyChangeMultiply(value, delta, model, change) {
if (!value) return delta;
const terms = new Roll(value).terms;
if (terms.length > 1) return `(${value}) * ${delta}`;
return `${value} * ${delta}`;
}
/* -------------------------------------------- */
/* -------------------------------------------- */
/** @override */
_applyChangeUpgrade(value, delta, model, change) {
if (!value) return delta;
const terms = new Roll(value).terms;
if (terms.length === 1 && terms[0].fn === 'max') return value.replace(/\)$/, `, ${delta})`);
return `max(${value}, ${delta})`;
}
/** @override */
_applyChangeUpgrade(value, delta, model, change) {
if (!value) return delta;
const terms = new Roll(value).terms;
if ((terms.length === 1) && (terms[0].fn === "max")) return value.replace(/\)$/, `, ${delta})`);
return `max(${value}, ${delta})`;
}
/* -------------------------------------------- */
/* -------------------------------------------- */
/** @override */
_applyChangeDowngrade(value, delta, model, change) {
if (!value) return delta;
const terms = new Roll(value).terms;
if ((terms.length === 1) && (terms[0].fn === "min")) return value.replace(/\)$/, `, ${delta})`);
return `min(${value}, ${delta})`;
}
}
/** @override */
_applyChangeDowngrade(value, delta, model, change) {
if (!value) return delta;
const terms = new Roll(value).terms;
if (terms.length === 1 && terms[0].fn === 'min') return value.replace(/\)$/, `, ${delta})`);
return `min(${value}, ${delta})`;
}
}

View file

@ -0,0 +1,56 @@
import PseudoDocument from '../pseudo-documents/base/pseudoDocument.mjs';
const { TypedObjectField, TypedSchemaField } = foundry.data.fields;
/**
* @typedef _PseudoDocumentsFieldOptions
* @property {Number} [max] - The maximum amount of elements (default: `Infinity`)
* @property {String[]} [validTypes] - Allowed pseudo-documents types (default: `[]`)
* @property {Function} [validateKey] - callback for validate keys of the object;
* @typedef {foundry.data.types.DataFieldOptions & _PseudoDocumentsFieldOptions} PseudoDocumentsFieldOptions
*/
export default class PseudoDocumentsField extends TypedObjectField {
/**
* @param {PseudoDocument} model - The PseudoDocument of each entry in this collection.
* @param {PseudoDocumentsFieldOptions} [options] - Options which configure the behavior of the field
* @param {foundry.data.types.DataFieldContext} [context] - Additional context which describes the field
*/
constructor(model, options = {}, context = {}) {
options.validateKey ||= key => foundry.data.validators.isValidId(key);
if (!foundry.utils.isSubclass(model, PseudoDocument)) throw new Error('The model must be a PseudoDocument');
const allTypes = model.TYPES;
const filteredTypes = options.validTypes
? Object.fromEntries(
Object.entries(allTypes).filter(([key]) => options.validTypes.includes(key))
)
: allTypes;
const field = new TypedSchemaField(filteredTypes);
super(field, options, context);
}
/** @inheritdoc */
static get _defaults() {
return Object.assign(super._defaults, {
max: Infinity,
validTypes: []
});
}
/** @override */
_validateType(value, options = {}) {
if (Object.keys(value).length > this.max) throw new Error(`cannot have more than ${this.max} elements`);
return super._validateType(value, options);
}
/** @override */
initialize(value, model, options = {}) {
if (!value) return;
value = super.initialize(value, model, options);
const collection = new foundry.utils.Collection(Object.values(value).map(d => [d._id, d]));
return collection;
}
}

View file

@ -10,27 +10,27 @@ import DHSubclass from './subclass.mjs';
import DHWeapon from './weapon.mjs';
export {
DHAncestry,
DHArmor,
DHClass,
DHCommunity,
DHConsumable,
DHDomainCard,
DHFeature,
DHMiscellaneous,
DHSubclass,
DHWeapon,
}
DHAncestry,
DHArmor,
DHClass,
DHCommunity,
DHConsumable,
DHDomainCard,
DHFeature,
DHMiscellaneous,
DHSubclass,
DHWeapon
};
export const config = {
ancestry: DHAncestry,
armor: DHArmor,
class: DHClass,
community: DHCommunity,
consumable: DHConsumable,
domainCard: DHDomainCard,
feature: DHFeature,
miscellaneous: DHMiscellaneous,
subclass: DHSubclass,
weapon: DHWeapon,
};
ancestry: DHAncestry,
armor: DHArmor,
class: DHClass,
community: DHCommunity,
consumable: DHConsumable,
domainCard: DHDomainCard,
feature: DHFeature,
miscellaneous: DHMiscellaneous,
subclass: DHSubclass,
weapon: DHWeapon
};

View file

@ -5,6 +5,7 @@
* @property {string} type - The system type that this data model represents.
* @property {boolean} hasDescription - Indicates whether items of this type have description field
* @property {boolean} isQuantifiable - Indicates whether items of this type have quantity field
* @property {Record<string,string>} embedded - Record of document names of pseudo-documents and the path to the collection
*/
const fields = foundry.data.fields;
@ -16,7 +17,8 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
label: "Base Item",
type: "base",
hasDescription: false,
isQuantifiable: false
isQuantifiable: false,
embedded: {},
};
}

View file

@ -1,4 +1,4 @@
import DaggerheartAction from '../action.mjs';
import DHAction from '../action/action.mjs';
import BaseDataItem from './base.mjs';
export default class DHDomainCard extends BaseDataItem {
@ -22,7 +22,7 @@ export default class DHDomainCard extends BaseDataItem {
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))
actions: new fields.ArrayField(new fields.EmbeddedDataField(DHAction))
};
}

View file

@ -1,14 +1,14 @@
import { getTier } from '../../helpers/utils.mjs';
import DaggerheartAction from '../action.mjs';
import DHAction from '../action/action.mjs';
import BaseDataItem from './base.mjs';
export default class DHFeature extends BaseDataItem {
/** @inheritDoc */
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {
label: "TYPES.Item.feature",
type: "feature",
hasDescription: true,
label: 'TYPES.Item.feature',
type: 'feature',
hasDescription: true
});
}
@ -17,7 +17,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 }),
@ -26,7 +26,7 @@ export default class DHFeature extends BaseDataItem {
choices: SYSTEM.ITEM.actionTypes,
initial: SYSTEM.ITEM.actionTypes.passive.id
}),
//TODO: remove featureType field
//TODO: remove featureType field
featureType: new fields.SchemaField({
type: new fields.StringField({
choices: SYSTEM.ITEM.valueTypes,
@ -75,9 +75,10 @@ export default class DHFeature extends BaseDataItem {
{ nullable: true, initial: null }
),
dataField: new fields.StringField({}),
appliesOn: new fields.StringField({
choices: SYSTEM.EFFECTS.applyLocations,
},
appliesOn: new fields.StringField(
{
choices: SYSTEM.EFFECTS.applyLocations
},
{ nullable: true, initial: null }
),
applyLocationChoices: new fields.TypedObjectField(new fields.StringField({}), {
@ -92,7 +93,7 @@ export default class DHFeature extends BaseDataItem {
})
})
),
actions: new fields.ArrayField(new fields.EmbeddedDataField(DaggerheartAction))
actions: new fields.ArrayField(new fields.EmbeddedDataField(DHAction))
};
}

View file

@ -1,14 +1,20 @@
import BaseDataItem from "./base.mjs";
import FormulaField from "../fields/formulaField.mjs";
import BaseDataItem from './base.mjs';
import FormulaField from '../fields/formulaField.mjs';
import PseudoDocumentsField from '../fields/pseudoDocumentsField.mjs';
import BaseFeatureData from '../pseudo-documents/feature/baseFeatureData.mjs';
import ActionField from '../fields/actionField.mjs';
export default class DHWeapon extends BaseDataItem {
/** @inheritDoc */
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {
label: "TYPES.Item.weapon",
type: "weapon",
label: 'TYPES.Item.weapon',
type: 'weapon',
hasDescription: true,
isQuantifiable: true,
embedded: {
feature: 'featureTest'
}
});
}
@ -33,8 +39,15 @@ export default class DHWeapon extends BaseDataItem {
initial: 'physical'
})
}),
feature: new fields.StringField({ choices: SYSTEM.ITEM.weaponFeatures, blank: true }),
featureTest: new PseudoDocumentsField(BaseFeatureData, {
required: true,
nullable: true,
max: 1,
validTypes: ['weapon']
}),
// actions: new fields.ArrayField(new fields.EmbeddedDataField(DHAttackAction))
actions: new fields.ArrayField(new ActionField())
};
}
}

View file

@ -0,0 +1,2 @@
export { default as base } from './base/pseudoDocument.mjs';
export * as feature from './feature/_module.mjs';

View file

@ -0,0 +1,213 @@
/**
* @typedef {object} PseudoDocumentMetadata
* @property {string} name - The document name of this pseudo-document
* @property {Record<string, string>} embedded - Record of document names and their collection paths
* @property {typeof foundry.applications.api.ApplicationV2} [sheetClass] - The class used to render this pseudo-document
* @property {string} defaultArtwork - The default image used for newly created documents
*/
/**
* @class Base class for pseudo-documents
* @extends {foundry.abstract.DataModel}
*/
export default class BasePseudoDocument extends foundry.abstract.DataModel {
/**
* Pseudo-document metadata.
* @returns {PseudoDocumentMetadata}
*/
static get metadata() {
return {
name: '',
embedded: {},
defaultArtwork: foundry.documents.Item.DEFAULT_ICON,
sheetClass: CONFIG.daggerheart.pseudoDocuments.sheetClass,
};
}
/** @override */
static LOCALIZATION_PREFIXES = ['DOCUMENT'];
/** @inheritdoc */
static defineSchema() {
const { fields } = foundry.data;
return {
_id: new fields.DocumentIdField({ initial: () => foundry.utils.randomID() }),
name: new fields.StringField({ required: true, blank: false, textSearch: true }),
img: new fields.FilePathField({ categories: ['IMAGE'], initial: this.metadata.defaultArtwork }),
description: new fields.HTMLField({ textSearch: true })
};
}
/* -------------------------------------------- */
/* Instance Properties */
/* -------------------------------------------- */
/**
* The id of this pseudo-document.
* @type {string}
*/
get id() {
return this._id;
}
/* -------------------------------------------- */
/**
* The uuid of this document.
* @type {string}
*/
get uuid() {
let parent = this.parent;
while (!(parent instanceof BasePseudoDocument) && !(parent instanceof foundry.abstract.Document))
parent = parent.parent;
return [parent.uuid, this.constructor.metadata.name, this.id].join('.');
}
/* -------------------------------------------- */
/**
* The parent document of this pseudo-document.
* @type {foundry.abstract.Document}
*/
get document() {
let parent = this;
while (!(parent instanceof foundry.abstract.Document)) parent = parent.parent;
return parent;
}
/* -------------------------------------------- */
/**
* Item to which this PseudoDocument belongs, if applicable.
* @type {foundry.documents.Item|null}
*/
get item() {
return this.parent?.parent instanceof Item ? this.parent.parent : null;
}
/* -------------------------------------------- */
/**
* Actor to which this PseudoDocument's item belongs, if the item is embedded.
* @type {foundry.documents.Actor|null}
*/
get actor() {
return this.item?.parent ?? null;
}
/* -------------------------------------------- */
/**
* The property path to this pseudo document relative to its parent document.
* @type {string}
*/
get fieldPath() {
const fp = this.schema.fieldPath;
let path = fp.slice(0, fp.lastIndexOf('element') - 1);
if (this.parent instanceof BasePseudoDocument) {
path = [this.parent.fieldPath, this.parent.id, path].join('.');
}
return path;
}
/* -------------------------------------------- */
/* Embedded Document Methods */
/* -------------------------------------------- */
/**
* Retrieve an embedded pseudo-document.
* @param {string} embeddedName The document name of the embedded pseudo-document.
* @param {string} id The id of the embedded pseudo-document.
* @param {object} [options] Retrieval options.
* @param {boolean} [options.strinct] Throw an error if the embedded pseudo-document does not exist?
* @returns {PseudoDocument|null}
*/
getEmbeddedDocument(embeddedName, id, { strict = false } = {}) {
const embeds = this.constructor.metadata.embedded ?? {};
if (embeddedName in embeds) {
return foundry.utils.getProperty(this, embeds[embeddedName]).get(id, { strict }) ?? null;
}
return null;
}
/* -------------------------------------------- */
/* CRUD Operations */
/* -------------------------------------------- */
/**
* Does this pseudo-document exist in the document's source?
* @type {boolean}
*/
get isSource() {
const source = foundry.utils.getProperty(this.document._source, this.fieldPath);
if (foundry.utils.getType(source) !== 'Object') {
throw new Error('Source is not an object!');
}
return this.id in source;
}
/**
* Create a new instance of this pseudo-document.
* @param {object} [data] The data used for the creation.
* @param {object} operation The context of the update operation.
* @param {foundry.abstract.Document} operation.parent The parent of this document.
* @returns {Promise<foundry.abstract.Document>} A promise that resolves to the updated document.
*/
static async create(data = {}, { parent, ...operation } = {}) {
if (!parent) {
throw new Error('A parent document must be specified for the creation of a pseudo-document!');
}
const id =
operation.keepId && foundry.data.validators.isValidId(data._id) ? data._id : foundry.utils.randomID();
const fieldPath = parent.system.constructor.metadata.embedded?.[this.metadata.name];
if (!fieldPath) {
throw new Error(
`A ${parent.documentName} of type '${parent.type}' does not support ${this.metadata.name}!`
);
}
const update = { [`system.${fieldPath}.${id}`]: { ...data, _id: id } };
const updatedParent = await parent.update(update, operation);
return foundry.utils.getProperty(updatedParent, `system.${fieldPath}.${id}`);
}
/**
* Delete this pseudo-document.
* @param {object} [operation] The context of the operation.
* @returns {Promise<foundry.abstract.Document>} A promise that resolves to the updated document.
*/
async delete(operation = {}) {
if (!this.isSource) throw new Error('You cannot delete a non-source pseudo-document!');
const update = { [`${this.fieldPath}.-=${this.id}`]: null };
return this.document.update(update, operation);
}
/**
* Duplicate this pseudo-document.
* @returns {Promise<foundry.abstract.Document>} A promise that resolves to the updated document.
*/
async duplicate() {
if (!this.isSource) throw new Error('You cannot duplicate a non-source pseudo-document!');
const docData = foundry.utils.mergeObject(this.toObject(), {
name: game.i18n.format('DOCUMENT.CopyOf', { name: this.name })
});
return this.constructor.create(docData, { parent: this.document });
}
/**
* Update this pseudo-document.
* @param {object} [change] The change to perform.
* @param {object} [operation] The context of the operation.
* @returns {Promise<foundry.abstract.Document>} A promise that resolves to the updated document.
*/
async update(change = {}, operation = {}) {
if (!this.isSource) throw new Error('You cannot update a non-source pseudo-document!');
const path = [this.fieldPath, this.id].join('.');
const update = { [path]: change };
return this.document.update(update, operation);
}
}

View file

@ -0,0 +1,59 @@
import BasePseudoDocument from './base.mjs';
import SheetManagementMixin from './sheetManagementMixin.mjs';
/** @extends BasePseudoDocument */
export default class PseudoDocument extends SheetManagementMixin(BasePseudoDocument) {
static get TYPES() {
const { types } = CONFIG.daggerheart.pseudoDocuments[this.metadata.name];
const typeEntries = Object.entries(types).map(([key, { documentClass }]) => [key, documentClass]);
return (this._TYPES ??= Object.freeze(Object.fromEntries(typeEntries)));
}
static _TYPES;
/**
* The type of this shape.
* @type {string}
*/
static TYPE = '';
/* -------------------------------------------- */
static getTypesChoices(validTypes) {
const { types } = CONFIG.daggerheart.pseudoDocuments[model.metadata.name];
const typeEntries = Object.entries(types)
.map(([key, { label }]) => [key, label])
.filter(([key]) => !validTypes || validTypes.includes(key));
return Object.entries(typeEntries);
}
/* -------------------------------------------- */
/** @override */
static defineSchema() {
const { fields } = foundry.data;
return Object.assign(super.defineSchema(), {
type: new fields.StringField({
required: true,
blank: false,
initial: this.TYPE,
validate: value => value === this.TYPE,
validationError: `must be equal to "${this.TYPE}"`
})
});
}
/** @inheritdoc */
static async create(data = {}, { parent, ...operation } = {}) {
data = foundry.utils.deepClone(data);
if (!data.type) data.type = Object.keys(this.TYPES)[0];
if (!(data.type in this.TYPES)) {
throw new Error(
`The '${data.type}' type is not a valid type for a '${this.metadata.documentName}' pseudo-document!`
);
}
return super.create(data, { parent, ...operation });
}
}

View file

@ -0,0 +1,158 @@
import BasePseudoDocument from './base.mjs';
const { ApplicationV2 } = foundry.applications.api;
/**
* A mixin that adds sheet management capabilities to pseudo-documents
* @template {typeof BasePseudoDocument} T
* @param {T} Base
* @returns {T & typeof PseudoDocumentWithSheets}
*/
export default function SheetManagementMixin(Base) {
class PseudoDocumentWithSheets extends Base {
/**
* Reference to the sheet of this pseudo-document.
* @type {ApplicationV2|null}
*/
get sheet() {
if (this._sheet) return this._sheet;
const cls = this.constructor.metadata.sheetClass ?? ApplicationV2;
if (!foundry.utils.isSubclass(cls, ApplicationV2)) {
return void ui.notifications.error(
'Daggerheart | Error on PseudoDocument | sheetClass must be ApplicationV2'
);
}
const sheet = new cls({ document: this });
this._sheet = sheet;
return sheet;
}
/* -------------------------------------------- */
/* Static Properties */
/* -------------------------------------------- */
/**
* Set of apps what should be re-render.
* @type {Set<ApplicationV2>}
* @internal
*/
_apps = new Set();
/* -------------------------------------------- */
/**
* Existing sheets of a specific type for a specific document.
* @type {ApplicationV2 | null}
*/
_sheet = null;
/* -------------------------------------------- */
/* Display Methods */
/* -------------------------------------------- */
/**
* Render all the Application instances which are connected to this PseudoDocument.
* @param {ApplicationRenderOptions} [options] Rendering options.
*/
render(options) {
for (const app of this._apps ?? []) {
app.render({ window: { title: app.title }, ...options });
}
}
/* -------------------------------------------- */
/**
* Register an application to respond to updates to a certain document.
* @param {ApplicationV2} app Application to update.
* @internal
*/
_registerApp(app) {
this._apps.add(app);
}
/* -------------------------------------------- */
/**
* Remove an application from the render registry.
* @param {ApplicationV2} app Application to stop watching.
*/
_unregisterApp(app) {
this._apps.delete(app);
}
/* -------------------------------------------- */
/* Drag and Drop */
/* -------------------------------------------- */
/**
* Serialize salient information for this PseudoDocument when dragging it.
* @returns {object} An object of drag data.
*/
toDragData() {
const dragData = { type: this.documentName, data: this.toObject() };
if (this.id) dragData.uuid = this.uuid;
return dragData;
}
/* -------------------------------------------- */
/* Dialog Methods */
/* -------------------------------------------- */
/**
* Spawn a dialog for creating a new PseudoDocument.
* @param {object} [data] Data to pre-populate the document with.
* @param {object} context
* @param {foundry.documents.Item} context.parent A parent for the document.
* @param {string[]|null} [context.types] A list of types to restrict the choices to, or null for no restriction.
* @returns {Promise<BasePseudoDocument|null>}
*/
static async createDialog(data = {}, { parent, types = null, ...options } = {}) {
// TODO
}
/**
* Present a Dialog form to confirm deletion of this PseudoDocument.
* @param {object} [options] - Additional options passed to `DialogV2.confirm`;
* @returns {Promise<foundry.abstract.Document>} A Promise which resolves to the deleted PseudoDocument.
*/
async deleteDialog(options = {}) {
const type = game.i18n.localize(this.constructor.metadata.label);
const content = options.content ?? `<p>
<strong>${game.i18n.localize("AreYouSure")}</strong>
${game.i18n.format("SIDEBAR.DeleteWarning", { type })}
</p>`;
return foundry.applications.api.DialogV2.confirm({
content,
yes: { callback: () => this.delete(operation) },
window: {
icon: "fa-solid fa-trash",
title: `${game.i18n.format("DOCUMENT.Delete", { type })}: ${this.name}`
},
...options
});
}
/**
* Gets the default new name for a Document
* @param {object} collection - Collection of Documents
* @returns {string}
*/
static defaultName(collection) {
const documentName = this.metadata.name;
const takenNames = new Set();
for (const document of collection) takenNames.add(document.name);
const config = CONFIG.daggerheart.pseudoDocuments[documentName];
const baseName = game.i18n.localize(config.label);
let name = baseName;
let index = 1;
while (takenNames.has(name)) name = `${baseName} (${++index})`;
return name;
}
}
return PseudoDocumentWithSheets;
}

View file

@ -0,0 +1,2 @@
export { default as BaseFeatureData } from './baseFeatureData.mjs';
export { default as WeaponFeature } from './weaponFeature.mjs';

View file

@ -0,0 +1,24 @@
import PseudoDocument from '../base/pseudoDocument.mjs';
export default class BaseFeatureData extends PseudoDocument {
/**@inheritdoc */
static get metadata() {
return foundry.utils.mergeObject(
super.metadata,
{
name: 'feature',
embedded: {},
//sheetClass: null //TODO: define feature-sheet
},
{ inplace: false }
);
}
static defineSchema() {
const { fields } = foundry.data;
const schema = super.defineSchema();
return Object.assign(schema, {
subtype: new fields.StringField({ initial: 'test' })
});
}
}

View file

@ -0,0 +1,6 @@
import BaseFeatureData from './baseFeatureData.mjs';
export default class WeaponFeature extends BaseFeatureData {
/**@override */
static TYPE = 'weapon';
}

View file

@ -3,6 +3,7 @@ import NpcRollSelectionDialog from '../applications/npcRollSelectionDialog.mjs';
import RollSelectionDialog from '../applications/rollSelectionDialog.mjs';
import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs';
import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
import DHDualityRoll from '../data/chat-message/dualityRoll.mjs';
export default class DhpActor extends Actor {
async _preCreate(data, options, user) {
@ -246,87 +247,52 @@ export default class DhpActor extends Actor {
});
}
async diceRoll(modifier, shiftKey) {
if (this.type === 'character') {
return await this.dualityRoll(modifier, shiftKey);
} else {
return await this.npcRoll(modifier, shiftKey);
}
}
async npcRoll(modifier, shiftKey) {
let advantage = null;
const modifiers = [
{
value: Number.parseInt(modifier.value),
label: modifier.value >= 0 ? `+${modifier.value}` : `-${modifier.value}`,
title: modifier.title
}
];
if (!shiftKey) {
const dialogClosed = new Promise((resolve, _) => {
new NpcRollSelectionDialog(this.system.experiences, resolve).render(true);
});
const result = await dialogClosed;
advantage = result.advantage;
result.experiences.forEach(x =>
modifiers.push({
value: x.value,
label: x.value >= 0 ? `+${x.value}` : `-${x.value}`,
title: x.description
})
);
}
const roll = Roll.create(
`${advantage === true || advantage === false ? 2 : 1}d20${advantage === true ? 'kh' : advantage === false ? 'kl' : ''} ${modifiers.map(x => `+ ${x.value}`).join(' ')}`
);
let rollResult = await roll.evaluate();
const dice = [];
for (var i = 0; i < rollResult.terms.length; i++) {
const term = rollResult.terms[i];
if (term.faces) {
dice.push({ type: `d${term.faces}`, rolls: term.results.map(x => ({ value: x.result })) });
}
}
// There is Only ever one dice term here
return { roll, dice: dice[0], modifiers, advantageState: advantage === true ? 1 : advantage === false ? 2 : 0 };
}
async dualityRoll(modifier, shiftKey) {
/**
* @param {object} config
* @param {Event} config.event
* @param {string} config.title
* @param {object} config.roll
* @param {number} config.roll.modifier
* @param {boolean} [config.roll.simple=false]
* @param {string} [config.roll.type]
* @param {number} [config.roll.difficulty]
* @param {any} [config.damage]
* @param {object} [config.chatMessage]
* @param {string} config.chatMessage.template
* @param {boolean} [config.chatMessage.mute]
* @param {boolean} [config.checkTarget]
*/
async diceRoll(config) {
let hopeDice = 'd12',
fearDice = 'd12',
advantageDice = null,
disadvantageDice = null;
advantageDice = 'd6',
disadvantageDice = 'd6',
advantage = config.event.altKey ? true : config.event.ctrlKey ? false : null,
targets,
damage = config.damage,
modifiers = this.formatRollModifier(config.roll),
rollConfig,
formula,
hope,
fear;
const modifiers =
modifier.value !== null
? [
{
value: modifier.value ? Number.parseInt(modifier.value) : 0,
label:
modifier.value >= 0
? `${modifier.title} +${modifier.value}`
: `${modifier.title} ${modifier.value}`,
title: modifier.title
}
]
: [];
if (!shiftKey) {
if (!config.event.shiftKey && !config.event.altKey && !config.event.ctrlKey) {
const dialogClosed = new Promise((resolve, _) => {
new RollSelectionDialog(this.system.experiences, this.system.resources.hope.value, resolve).render(
true
);
this.type === 'character'
? new RollSelectionDialog(
this.system.experiences,
this.system.resources.hope.value,
resolve
).render(true)
: new NpcRollSelectionDialog(this.system.experiences, resolve).render(true);
});
const result = await dialogClosed;
(hopeDice = result.hope),
(fearDice = result.fear),
(advantageDice = result.advantage),
(disadvantageDice = result.disadvantage);
result.experiences.forEach(x =>
rollConfig = await dialogClosed;
advantage = rollConfig.advantage;
hopeDice = rollConfig.hope;
fearDice = rollConfig.fear;
rollConfig.experiences.forEach(x =>
modifiers.push({
value: x.value,
label: x.value >= 0 ? `+${x.value}` : `-${x.value}`,
@ -334,60 +300,123 @@ export default class DhpActor extends Actor {
})
);
const automateHope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope);
if (this.type === 'character') {
const automateHope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope);
if (automateHope && result.hopeUsed) {
await this.update({
'system.resources.hope.value': this.system.resources.hope.value - result.hopeUsed
});
if (automateHope && result.hopeUsed) {
await this.update({
'system.resources.hope.value': this.system.resources.hope.value - result.hopeUsed
});
}
}
}
const roll = new Roll(
`1${hopeDice} + 1${fearDice}${advantageDice ? ` + 1${advantageDice}` : disadvantageDice ? ` - 1${disadvantageDice}` : ''} ${modifiers.map(x => `+ ${x.value}`).join(' ')}`
);
let rollResult = await roll.evaluate();
setDiceSoNiceForDualityRoll(rollResult, advantageDice, disadvantageDice);
const hope = rollResult.dice[0].results[0].result;
const fear = rollResult.dice[1].results[0].result;
const advantage = advantageDice ? rollResult.dice[2].results[0].result : null;
const disadvantage = disadvantageDice ? rollResult.dice[2].results[0].result : null;
if (disadvantage) {
rollResult = { ...rollResult, total: rollResult.total - Math.max(hope, disadvantage) };
}
if (advantage) {
rollResult = { ...rollResult, total: 'Select Hope Die' };
if (this.type === 'character') {
formula = `1${hopeDice} + 1${fearDice}${advantage === true ? ` + 1d6` : advantage === false ? ` - 1d6` : ''}`;
} else {
formula = `${advantage === true || advantage === false ? 2 : 1}d20${advantage === true ? 'kh' : advantage === false ? 'kl' : ''}`;
}
formula += ` ${modifiers.map(x => `+ ${x.value}`).join(' ')}`;
const roll = await Roll.create(formula).evaluate();
const dice = roll.dice.flatMap(dice => ({
denomination: dice.denomination,
number: dice.number,
total: dice.total,
results: dice.results.map(result => ({ result: result.result, discarded: !result.active }))
}));
const automateHope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope);
if (automateHope && hope > fear) {
await this.update({
'system.resources.hope.value': Math.min(
this.system.resources.hope.value + 1,
this.system.resources.hope.max
)
});
}
if (automateHope && hope === fear) {
await this.update({
'system.resources': {
'hope.value': Math.min(this.system.resources.hope.value + 1, this.system.resources.hope.max),
'stress.value': Math.max(this.system.resources.stress.value - 1, 0)
if (this.type === 'character') {
setDiceSoNiceForDualityRoll(roll, advantage);
hope = roll.dice[0].results[0].result;
fear = roll.dice[1].results[0].result;
if (
game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope) &&
config.roll.type === 'action'
) {
if (hope > fear) {
await this.update({
'system.resources.hope.value': Math.min(
this.system.resources.hope.value + 1,
this.system.resources.hope.max
)
});
} else if (hope === fear) {
await this.update({
'system.resources': {
'hope.value': Math.min(
this.system.resources.hope.value + 1,
this.system.resources.hope.max
),
'stress.value': Math.max(this.system.resources.stress.value - 1, 0)
}
});
}
}
}
if (config.checkTarget) {
targets = Array.from(game.user.targets).map(x => {
const target = {
id: x.id,
name: x.actor.name,
img: x.actor.img,
difficulty: x.actor.system.difficulty,
evasion: x.actor.system.evasion?.value
};
target.hit = target.difficulty ? roll.total >= target.difficulty : roll.total >= target.evasion;
return target;
});
}
return {
roll,
rollResult,
hope: { dice: hopeDice, value: hope },
fear: { dice: fearDice, value: fear },
advantage: { dice: advantageDice, value: advantage },
disadvantage: { dice: disadvantageDice, value: disadvantage },
modifiers: modifiers
};
if (config.chatMessage) {
const configRoll = {
title: config.title,
origin: this.id,
dice,
roll,
modifiers: modifiers.filter(x => x.label),
advantageState: advantage
};
if (this.type === 'character') {
configRoll.hope = { dice: hopeDice, value: hope };
configRoll.fear = { dice: fearDice, value: fear };
configRoll.advantage = { dice: advantageDice, value: roll.dice[2]?.results[0].result ?? null };
}
if (damage) configRoll.damage = damage;
if (targets) configRoll.targets = targets;
const systemData =
this.type === 'character' && !config.roll.simple ? new DHDualityRoll(configRoll) : configRoll,
cls = getDocumentClass('ChatMessage'),
msg = new cls({
type: config.chatMessage.type ?? 'dualityRoll',
sound: config.chatMessage.mute ? null : CONFIG.sounds.dice,
system: systemData,
content: config.chatMessage.template,
rolls: [roll]
});
await cls.create(msg.toObject());
}
return roll;
}
formatRollModifier(roll) {
const modifier = roll.modifier !== null ? Number.parseInt(roll.modifier) : null;
return modifier !== null
? [
{
value: modifier,
label: roll.label
? modifier >= 0
? `${roll.label} +${modifier}`
: `${roll.label} ${modifier}`
: null,
title: roll.label
}
]
: [];
}
async damageRoll(title, damage, targets, shiftKey) {
@ -417,7 +446,11 @@ export default class DhpActor extends Actor {
for (var i = 0; i < rollResult.terms.length; i++) {
const term = rollResult.terms[i];
if (term.faces) {
dice.push({ type: `d${term.faces}`, rolls: term.results.map(x => x.result) });
dice.push({
type: `d${term.faces}`,
rolls: term.results.map(x => x.result),
total: term.results.reduce((acc, x) => acc + x.result, 0)
});
} else if (term.operator) {
} else if (term.number) {
const operator = i === 0 ? '' : rollResult.terms[i - 1].operator;

View file

@ -1,6 +1,18 @@
export default class DhpItem extends Item {
prepareData() {
super.prepareData();
/** @inheritdoc */
getEmbeddedDocument(embeddedName, id, { invalid = false, strict = false } = {}) {
const systemEmbeds = this.system.constructor.metadata.embedded ?? {};
if (embeddedName in systemEmbeds) {
const path = `system.${systemEmbeds[embeddedName]}`;
return foundry.utils.getProperty(this, path).get(id) ?? null;
}
return super.getEmbeddedDocument(embeddedName, id, { invalid, strict });
}
/** @inheritDoc */
prepareEmbeddedDocuments() {
super.prepareEmbeddedDocuments();
for (const action of this.system.actions ?? []) action.prepareData();
}
/**
@ -27,10 +39,6 @@ export default class DhpItem extends Item {
return ['weapon', 'armor', 'miscellaneous', 'consumable'].includes(this.type);
}
_onUpdate(data, options, userId) {
super._onUpdate(data, options, userId);
}
static async createDialog(data = {}, { parent = null, pack = null, ...options } = {}) {
const documentName = this.metadata.name;
const types = game.documentTypes[documentName].filter(t => t !== CONST.BASE_DOCUMENT_TYPE);
@ -90,4 +98,47 @@ export default class DhpItem extends Item {
options
});
}
async selectActionDialog() {
const content = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/views/actionSelect.hbs',
{ actions: this.system.actions }
),
title = 'Select Action',
type = 'div',
data = {};
return Dialog.prompt({
title,
// label: title,
content,
type,
callback: html => {
const form = html[0].querySelector('form'),
fd = new foundry.applications.ux.FormDataExtended(form);
return this.system.actions.find(a => a._id === fd.object.actionId);
},
rejectClose: false
});
}
async use(event) {
const actions = this.system.actions;
let response;
if (actions?.length) {
let action = actions[0];
if (actions.length > 1 && !event?.shiftKey) {
// Actions Choice Dialog
action = await this.selectActionDialog();
}
if (action) response = action.use(event);
// Check Target
// If action.roll => Roll Dialog
// Else If action.cost => Cost Dialog
// Then
// Apply Cost
// Apply Effect
}
// Display Item Card in chat
return response;
}
}

View file

@ -9,21 +9,26 @@ export function dualityRollEnricher(match, _options) {
}
export function getDualityMessage(roll) {
const attributeLabel =
roll.attribute && abilities[roll.attribute]
const traitLabel =
roll.trait && abilities[roll.trait]
? game.i18n.format('DAGGERHEART.General.Check', {
check: game.i18n.localize(abilities[roll.attribute].label)
check: game.i18n.localize(abilities[roll.trait].label)
})
: null;
const label = attributeLabel ?? game.i18n.localize('DAGGERHEART.General.Duality');
const label = traitLabel ?? game.i18n.localize('DAGGERHEART.General.Duality');
const dataLabel = traitLabel
? game.i18n.localize(abilities[roll.trait].label)
: game.i18n.localize('DAGGERHEART.General.Duality');
const dualityElement = document.createElement('span');
dualityElement.innerHTML = `
<button class="duality-roll-button"
data-label="${label}"
data-title="${label}"
data-label="${dataLabel}"
data-hope="${roll.hope ?? 'd12'}"
data-fear="${roll.fear ?? 'd12'}"
${roll.attribute && abilities[roll.attribute] ? `data-attribute="${roll.attribute}"` : ''}
${roll.trait && abilities[roll.trait] ? `data-trait="${roll.trait}"` : ''}
${roll.advantage ? 'data-advantage="true"' : ''}
${roll.disadvantage ? 'data-disadvantage="true"' : ''}
>

View file

@ -122,14 +122,16 @@ export const getCommandTarget = () => {
return target;
};
export const setDiceSoNiceForDualityRoll = (rollResult, advantage, disadvantage) => {
export const setDiceSoNiceForDualityRoll = (rollResult, advantageState) => {
const diceSoNicePresets = getDiceSoNicePresets();
rollResult.dice[0].options.appearance = diceSoNicePresets.hope;
rollResult.dice[1].options.appearance = diceSoNicePresets.fear;
if (advantage) {
rollResult.dice[2].options.appearance = diceSoNicePresets.advantage;
} else if (disadvantage) {
rollResult.dice[2].options.appearance = diceSoNicePresets.disadvantage;
if (rollResult.dice[2]) {
if (advantageState === true) {
rollResult.dice[2].options.appearance = diceSoNicePresets.advantage;
} else if (advantageState === false) {
rollResult.dice[2].options.appearance = diceSoNicePresets.disadvantage;
}
}
};
@ -222,3 +224,14 @@ export const getDeleteKeys = (property, innerProperty, innerPropertyDefaultValue
return acc;
}, {});
};
// Fix on Foundry native formula replacement for DH
const nativeReplaceFormulaData = Roll.replaceFormulaData;
Roll.replaceFormulaData = function (formula, data, { missing, warn = false } = {}) {
const terms = [
{ term: 'prof', default: 1 },
{ term: 'cast', default: 1 }
];
formula = terms.reduce((a, c) => a.replaceAll(`@${c.term}`, data[c.term] ?? c.default), formula);
return nativeReplaceFormulaData(formula, data, { missing, warn });
};