Merge branch 'main' into bug/103-enrich-htmlfield-content-before-its-used-in-applications

This commit is contained in:
Joaquin Pereyra 2025-07-18 15:24:25 -03:00
commit f56c2482cb
551 changed files with 2831 additions and 14203 deletions

View file

@ -506,9 +506,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
name: this.setup.ancestryName ?? this.setup.primaryAncestry.name,
system: {
...this.setup.primaryAncestry.system,
features: [primaryAncestryFeature.uuid, secondaryAncestryFeature.uuid],
primaryFeature: primaryAncestryFeature.uuid,
secondaryFeature: secondaryAncestryFeature.uuid
features: [primaryAncestryFeature.uuid, secondaryAncestryFeature.uuid]
}
};

View file

@ -64,6 +64,13 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.rollConfig = this.config;
context.hasRoll = !!this.config.roll;
context.canRoll = true;
context.selectedRollMode = this.config.selectedRollMode;
context.rollModes = Object.entries(CONFIG.Dice.rollModes).map(([action, { label, icon }]) => ({
action,
label,
icon
}));
if (this.config.costs?.length) {
const updatedCosts = this.action.calcCosts(this.config.costs);
context.costs = updatedCosts.map(x => ({
@ -82,6 +89,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
if (this.roll) {
context.roll = this.roll;
context.rollType = this.roll?.constructor.name;
context.rallyDie = this.roll.rallyChoices;
context.experiences = Object.keys(this.config.data.experiences).map(id => ({
id,
...this.config.data.experiences[id]
@ -99,6 +107,8 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
static updateRollConfiguration(event, _, formData) {
const { ...rest } = foundry.utils.expandObject(formData.object);
this.config.selectedRollMode = rest.selectedRollMode;
if (this.config.costs) {
this.config.costs = foundry.utils.mergeObject(this.config.costs, rest.costs);
}
@ -122,11 +132,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
}
static selectExperience(_, button) {
/* if (this.config.experiences.find(x => x === button.dataset.key)) {
this.config.experiences = this.config.experiences.filter(x => x !== button.dataset.key);
} else {
this.config.experiences = [...this.config.experiences, button.dataset.key];
} */
this.config.experiences =
this.config.experiences.indexOf(button.dataset.key) > -1
? this.config.experiences.filter(x => x !== button.dataset.key)

View file

@ -48,12 +48,22 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
: game.i18n.localize('DAGGERHEART.EFFECTS.ApplyLocations.damageRoll.name');
context.extraFormula = this.config.extraFormula;
context.formula = this.roll.constructFormula(this.config);
context.directDamage = this.config.directDamage;
context.selectedRollMode = this.config.selectedRollMode;
context.rollModes = Object.entries(CONFIG.Dice.rollModes).map(([action, { label, icon }]) => ({
action,
label,
icon
}));
return context;
}
static updateRollConfiguration(event, _, formData) {
static updateRollConfiguration(_event, _, formData) {
const { ...rest } = foundry.utils.expandObject(formData.object);
this.config.extraFormula = rest.extraFormula;
this.config.selectedRollMode = rest.selectedRollMode;
this.render();
}

View file

@ -7,8 +7,22 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
this.actor = actor;
this.shortrest = shortrest;
const options = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).restMoves;
this.moveData = shortrest ? options.shortRest : options.longRest;
this.moveData = foundry.utils.deepClone(
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).restMoves
);
this.nrChoices = {
shortRest: {
max:
(shortrest ? this.moveData.shortRest.nrChoices : 0) +
actor.system.bonuses.rest[`${shortrest ? 'short' : 'long'}Rest`].shortMoves
},
longRest: {
max:
(!shortrest ? this.moveData.longRest.nrChoices : 0) +
actor.system.bonuses.rest[`${shortrest ? 'short' : 'long'}Rest`].longMoves
}
};
this.nrChoices.total = { max: this.nrChoices.shortRest.max + this.nrChoices.longRest.max };
}
get title() {
@ -17,8 +31,8 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'views', 'downtime'],
position: { width: 680, height: 'auto' },
classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'downtime'],
position: { width: 'auto', height: 'auto' },
actions: {
selectMove: this.selectMove,
takeDowntime: this.takeDowntime
@ -29,7 +43,7 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
static PARTS = {
application: {
id: 'downtime',
template: 'systems/daggerheart/templates/dialogs/downtime.hbs'
template: 'systems/daggerheart/templates/dialogs/downtime/downtime.hbs'
}
};
@ -37,46 +51,83 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
super._attachPartListeners(partId, htmlElement, options);
htmlElement
.querySelectorAll('.activity-image')
.querySelectorAll('.activity-container')
.forEach(element => element.addEventListener('contextmenu', this.deselectMove.bind(this)));
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.title = game.i18n.localize(
`DAGGERHEART.APPLICATIONS.Downtime.${this.shortrest ? 'shortRest' : 'longRest'}.title`
);
context.selectedActivity = this.selectedActivity;
context.moveData = this.moveData;
context.nrCurrentChoices = Object.values(this.moveData.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0);
context.disabledDowntime = context.nrCurrentChoices < context.moveData.nrChoices;
context.nrCurrentChoices = Object.values(this.moveData).reduce((acc, category) => {
acc += Object.values(category.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0);
return acc;
}, 0);
context.nrChoices = {
...this.nrChoices,
shortRest: {
...this.nrChoices.shortRest,
current: Object.values(this.moveData.shortRest.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0)
},
longRest: {
...this.nrChoices.longRest,
current: Object.values(this.moveData.longRest.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0)
}
};
context.nrChoices.total = {
...this.nrChoices.total,
current: context.nrChoices.shortRest.current + context.nrChoices.longRest.current
};
context.shortRestMoves = this.nrChoices.shortRest.max > 0 ? this.moveData.shortRest : null;
context.longRestMoves = this.nrChoices.longRest.max > 0 ? this.moveData.longRest : null;
context.disabledDowntime = context.nrChoices.total.current < context.nrChoices.total.max;
return context;
}
static selectMove(_, button) {
const nrSelected = Object.values(this.moveData.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0);
if (nrSelected === this.moveData.nrChoices) {
static selectMove(_, target) {
const nrSelected = Object.values(this.moveData[target.dataset.category].moves).reduce(
(acc, x) => acc + (x.selected ?? 0),
0
);
if (nrSelected === this.nrChoices[target.dataset.category].max) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.noMoreMoves'));
return;
}
const move = button.dataset.move;
this.moveData.moves[move].selected = this.moveData.moves[move].selected
? this.moveData.moves[move].selected + 1
const move = target.dataset.move;
this.moveData[target.dataset.category].moves[move].selected = this.moveData[target.dataset.category].moves[move]
.selected
? this.moveData[target.dataset.category].moves[move].selected + 1
: 1;
this.render();
}
deselectMove(event) {
const move = event.currentTarget.dataset.move;
this.moveData.moves[move].selected = this.moveData.moves[move].selected
? this.moveData.moves[move].selected - 1
const button = event.target.closest('.activity-container');
const move = button.dataset.move;
this.moveData[button.dataset.category].moves[move].selected = this.moveData[button.dataset.category].moves[move]
.selected
? this.moveData[button.dataset.category].moves[move].selected - 1
: 0;
this.render();
}
static async takeDowntime() {
const moves = Object.values(this.moveData.moves).filter(x => x.selected);
const moves = Object.values(this.moveData).flatMap(category => {
return Object.values(category.moves)
.filter(x => x.selected)
.flatMap(move => [...Array(move.selected).keys()].map(_ => move));
});
const cls = getDocumentClass('ChatMessage');
const msg = new cls({

View file

@ -67,7 +67,7 @@ export default class ResourceDiceDialog extends HandlebarsApplicationMixin(Appli
static async rerollDice() {
const max = itemAbleRollParse(this.item.system.resource.max, this.actor, this.item);
const diceFormula = `${max}d${this.item.system.resource.dieFaces}`;
const diceFormula = `${max}${this.item.system.resource.dieFaces}`;
const roll = await new Roll(diceFormula).evaluate();
if (game.modules.get('dice-so-nice')?.active) await game.dice3d.showForRoll(roll, game.user, true);
this.rollValues = roll.terms[0].results.map(x => ({ value: x.result, used: false }));

View file

@ -1 +1 @@
export { default as DHTokenHUD } from './tokenHud.mjs';
export { default as DHTokenHUD } from './tokenHUD.mjs';

View file

@ -1,4 +1,4 @@
export default class DHTokenHUD extends TokenHUD {
export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
static DEFAULT_OPTIONS = {
classes: ['daggerheart']
};

View file

@ -4,13 +4,14 @@ import DHActionConfig from '../../sheets-configs/action-config.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhSettingsActionView extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(resolve, reject, title, name, img, description, actions) {
constructor(resolve, reject, title, name, icon, img, description, actions) {
super({});
this.resolve = resolve;
this.reject = reject;
this.viewTitle = title;
this.name = name;
this.icon = icon;
this.img = img;
this.description = description;
this.actions = actions;
@ -23,7 +24,7 @@ export default class DhSettingsActionView extends HandlebarsApplicationMixin(App
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'setting', 'dh-style'],
position: { width: '400', height: 'auto' },
position: { width: 440, height: 'auto' },
actions: {
editImage: this.onEditImage,
addItem: this.addItem,
@ -46,6 +47,7 @@ export default class DhSettingsActionView extends HandlebarsApplicationMixin(App
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.name = this.name;
context.icon = this.icon;
context.img = this.img;
context.description = this.description;
context.enrichedDescription = await foundry.applications.ux.TextEditor.enrichHTML(context.description);
@ -55,8 +57,9 @@ export default class DhSettingsActionView extends HandlebarsApplicationMixin(App
}
static async updateData(event, element, formData) {
const { name, img, description } = foundry.utils.expandObject(formData.object);
const { name, icon, description } = foundry.utils.expandObject(formData.object);
this.name = name;
this.icon = icon;
this.description = description;
this.render();
@ -65,6 +68,7 @@ export default class DhSettingsActionView extends HandlebarsApplicationMixin(App
static async saveForm(event) {
this.resolve({
name: this.name,
icon: this.icon,
img: this.img,
description: this.description,
actions: this.actions

View file

@ -76,6 +76,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
reject,
game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.downtimeMoves'),
move.name,
move.icon,
move.img,
move.description,
move.actions
@ -87,6 +88,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
await this.settings.updateSource({
[`restMoves.${type}.moves.${id}`]: {
name: data.name,
icon: data.icon,
img: data.img,
description: data.description
}

View file

@ -3,3 +3,5 @@ export { default as AdversarySettings } from './adversary-settings.mjs';
export { default as CompanionSettings } from './companion-settings.mjs';
export { default as EnvironmentSettings } from './environment-settings.mjs';
export { default as ActiveEffectConfig } from './activeEffectConfig.mjs';
export { default as DhTokenConfig } from './token-config.mjs';
export { default as DhPrototypeTokenConfig } from './prototype-token-config.mjs';

View file

@ -1,4 +1,24 @@
import autocomplete from 'autocompleter';
export default class DhActiveEffectConfig extends foundry.applications.sheets.ActiveEffectConfig {
constructor(options) {
super(options);
const ignoredActorKeys = ['config', 'DhEnvironment'];
this.changeChoices = Object.keys(game.system.api.models.actors).reduce((acc, key) => {
if (!ignoredActorKeys.includes(key)) {
const model = game.system.api.models.actors[key];
const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model);
const group = game.i18n.localize(model.metadata.label);
const choices = CONFIG.Token.documentClass
.getTrackedAttributeChoices(attributes, model)
.map(x => ({ ...x, group: group }));
acc.push(...choices);
}
return acc;
}, []);
}
static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'sheet', 'dh-style']
};
@ -27,36 +47,62 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
}
};
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
const changeChoices = this.changeChoices;
htmlElement.querySelectorAll('.effect-change-input').forEach(element => {
autocomplete({
input: element,
fetch: function (text, update) {
if (!text) {
update(changeChoices);
} else {
text = text.toLowerCase();
var suggestions = changeChoices.filter(n => n.label.toLowerCase().includes(text));
update(suggestions);
}
},
render: function (item, search) {
const label = game.i18n.localize(item.label);
const matchIndex = label.toLowerCase().indexOf(search);
const beforeText = label.slice(0, matchIndex);
const matchText = label.slice(matchIndex, matchIndex + search.length);
const after = label.slice(matchIndex + search.length, label.length);
const element = document.createElement('li');
element.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
if (item.hint) {
element.dataset.tooltip = game.i18n.localize(item.hint);
}
return element;
},
renderGroup: function (label) {
const itemElement = document.createElement('div');
itemElement.textContent = game.i18n.localize(label);
return itemElement;
},
onSelect: function (item) {
element.value = `system.${item.value}`;
},
click: e => e.fetch(),
customize: function (_input, _inputRect, container) {
container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ;
},
minLength: 0
});
});
}
async _preparePartContext(partId, context) {
const partContext = await super._preparePartContext(partId, context);
switch (partId) {
case 'changes':
const fieldPaths = [];
const validFieldPath = fieldPath => this.validFieldPath(fieldPath, this.#unapplicablePaths);
context.document.parent.system.schema.apply(function () {
if (!(this instanceof foundry.data.fields.SchemaField)) {
if (validFieldPath(this.fieldPath)) {
fieldPaths.push(this.fieldPath);
}
}
});
context.fieldPaths = fieldPaths;
break;
}
return partContext;
}
#unapplicablePaths = ['story', 'pronouns', 'description'];
validFieldPath(fieldPath, unapplicablePaths) {
const splitPath = fieldPath.split('.');
if (splitPath.length > 1 && unapplicablePaths.includes(splitPath[1])) return false;
/* The current value of a resource should not be modified */
if (new RegExp(/resources.*\.value/).exec(fieldPath)) return false;
return true;
}
}

View file

@ -0,0 +1,20 @@
export default class DhPrototypeTokenConfig extends foundry.applications.sheets.PrototypeTokenConfig {
/** @inheritDoc */
async _prepareResourcesTab() {
const token = this.token;
const usesTrackableAttributes = !foundry.utils.isEmpty(CONFIG.Actor.trackableAttributes);
const attributeSource =
this.actor?.system instanceof foundry.abstract.DataModel && usesTrackableAttributes
? this.actor?.type
: this.actor?.system;
const TokenDocument = foundry.utils.getDocumentClass('Token');
const attributes = TokenDocument.getTrackedAttributes(attributeSource);
return {
barAttributes: TokenDocument.getTrackedAttributeChoices(attributes, attributeSource),
bar1: token.getBarAttribute?.('bar1'),
bar2: token.getBarAttribute?.('bar2'),
turnMarkerModes: DhPrototypeTokenConfig.TURN_MARKER_MODES,
turnMarkerAnimations: CONFIG.Combat.settings.turnMarkerAnimations
};
}
}

View file

@ -0,0 +1,20 @@
export default class DhTokenConfig extends foundry.applications.sheets.TokenConfig {
/** @inheritDoc */
async _prepareResourcesTab() {
const token = this.token;
const usesTrackableAttributes = !foundry.utils.isEmpty(CONFIG.Actor.trackableAttributes);
const attributeSource =
this.actor?.system instanceof foundry.abstract.DataModel && usesTrackableAttributes
? this.actor?.type
: this.actor?.system;
const TokenDocument = foundry.utils.getDocumentClass('Token');
const attributes = TokenDocument.getTrackedAttributes(attributeSource);
return {
barAttributes: TokenDocument.getTrackedAttributeChoices(attributes, attributeSource),
bar1: token.getBarAttribute?.('bar1'),
bar2: token.getBarAttribute?.('bar2'),
turnMarkerModes: DhTokenConfig.TURN_MARKER_MODES,
turnMarkerAnimations: CONFIG.Combat.settings.turnMarkerAnimations
};
}
}

View file

@ -25,6 +25,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
toggleEquipItem: CharacterSheet.#toggleEquipItem,
toggleResourceDice: CharacterSheet.#toggleResourceDice,
handleResourceDice: CharacterSheet.#handleResourceDice,
useDowntime: this.useDowntime
},
window: {
resizable: true
@ -113,6 +114,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
htmlElement.querySelectorAll('.inventory-item-quantity').forEach(element => {
element.addEventListener('change', this.updateItemQuantity.bind(this));
});
// Add listener for armor marks input
htmlElement.querySelectorAll('.armor-marks-input').forEach(element => {
element.addEventListener('change', this.updateArmorMarks.bind(this));
});
}
/** @inheritDoc */
@ -519,6 +525,16 @@ export default class CharacterSheet extends DHBaseActorSheet {
await item.update({ 'system.quantity': event.currentTarget.value });
}
async updateArmorMarks(event) {
const armor = this.document.system.armor;
if (!armor) return;
const maxMarks = this.document.system.armorScore;
const value = Math.min(Math.max(Number(event.currentTarget.value), 0), maxMarks);
await armor.update({ 'system.marks.value': value });
this.render();
}
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
@ -669,7 +685,12 @@ export default class CharacterSheet extends DHBaseActorSheet {
return acc;
}, {})
});
//this.render();
}
static useDowntime(_, button) {
new game.system.api.applications.dialogs.Downtime(this.document, button.dataset.type === 'shortRest').render(
true
);
}
async _onDragStart(event) {

View file

@ -272,13 +272,15 @@ export default function DHApplicationMixin(Base) {
const getAction = (target) => {
const { actionId } = target.closest('[data-action-id]').dataset;
const { actions, attack } = this.document.system;
return attack.id === actionId ? attack : actions?.find(a => a.id === actionId);
return attack?.id === actionId ? attack : actions?.find(a => a.id === actionId);
};
const options = [
{
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem',
icon: 'fa-solid fa-burst',
condition: this.document instanceof foundry.documents.Actor ||
(this.document instanceof foundry.documents.Item && this.document.parent),
callback: (target, event) => getAction(target).use(event),
},
{
@ -297,7 +299,7 @@ export default function DHApplicationMixin(Base) {
condition: (target) => {
const { actionId } = target.closest('[data-action-id]').dataset;
const { attack } = this.document.system;
return attack.id !== actionId
return attack?.id !== actionId
},
callback: async (target) => {
const action = getAction(target)
@ -313,7 +315,7 @@ export default function DHApplicationMixin(Base) {
if (!confirmed) return;
return this.document.update({
'system.actions': this.document.system.actions.do.filter((a) => a.id !== action.id)
'system.actions': this.document.system.actions.filter((a) => a.id !== action.id)
});
}
}
@ -353,7 +355,11 @@ export default function DHApplicationMixin(Base) {
if (deletable) options.push({
name: 'CONTROLS.CommonDelete',
icon: 'fa-solid fa-trash',
callback: target => getDocFromElement(target).deleteDialog(),
callback: (target, event) => {
const doc = getDocFromElement(target);
if (event.shiftKey) return doc.delete();
else return doc.deleteDialog();
}
})
return options.map(option => ({
@ -402,7 +408,7 @@ export default function DHApplicationMixin(Base) {
const isAction = !!actionId;
descriptionElement.innerHTML = await foundry.applications.ux.TextEditor.implementation.enrichHTML(description, {
relativeTo: isAction ? doc.parent : doc,
rollData: doc.getRollData(),
rollData: doc.getRollData?.(),
secrets: isAction ? doc.parent.isOwner : doc.isOwner
});
}
@ -432,7 +438,7 @@ export default function DHApplicationMixin(Base) {
type: game.i18n.localize('DAGGERHEART.GENERAL.Action.single')
}),
}
});
}) ?? {};
if (!actionType) return;
const cls = game.system.api.models.actions.actionsTypes[actionType]
const action = new cls({
@ -482,7 +488,7 @@ export default function DHApplicationMixin(Base) {
// TODO: REDO this
const { actionId } = target.closest('[data-action-id]').dataset;
const { actions, attack } = this.document.system;
const action = attack.id === actionId ? attack : actions?.find(a => a.id === actionId);
const action = attack?.id === actionId ? attack : actions?.find(a => a.id === actionId);
new DHActionConfig(action).render({ force: true })
}
@ -490,27 +496,32 @@ export default function DHApplicationMixin(Base) {
* Delete an embedded document.
* @type {ApplicationClickAction}
*/
static async #deleteDoc(_event, target) {
static async #deleteDoc(event, target) {
const doc = getDocFromElement(target);
// TODO: REDO this
if (doc) return await doc.deleteDialog()
if (doc) {
if (event.shiftKey) return doc.delete()
else return await doc.deleteDialog()
}
// TODO: REDO this
const { actionId } = target.closest('[data-action-id]').dataset;
const { actions, attack } = this.document.system;
if (attack.id === actionId) return;
if (attack?.id === actionId) return;
const action = actions.find(a => a.id === actionId);
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize(`DAGGERHEART.GENERAL.Action.single`),
name: action.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: action.name })
});
if (!confirmed) return;
if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize(`DAGGERHEART.GENERAL.Action.single`),
name: action.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: action.name })
});
if (!confirmed) return;
}
return await this.document.update({
'system.actions': actions.filter((a) => a.id !== action.id)
@ -528,7 +539,7 @@ export default function DHApplicationMixin(Base) {
if (!doc) {
const { actionId } = target.closest('[data-action-id]').dataset;
const { actions, attack } = this.document.system;
doc = attack.id === actionId ? attack : actions?.find(a => a.id === actionId);
doc = attack?.id === actionId ? attack : actions?.find(a => a.id === actionId);
}
return doc.toChat(this.document.id);
}
@ -544,6 +555,7 @@ export default function DHApplicationMixin(Base) {
const { actionId } = target.closest('[data-action-id]').dataset;
const { actions, attack } = this.document.system;
doc = attack?.id === actionId ? attack : actions?.find(a => a.id === actionId);
if(this.document instanceof foundry.documents.Item && !this.document.parent) return;
}
await doc.use(event);

View file

@ -34,7 +34,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
}
}
],
dragDrop: []
dragDrop: [{ dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null }]
};
/* -------------------------------------------- */
@ -139,4 +139,27 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
)
});
}
/* -------------------------------------------- */
/* Application Drag/Drop */
/* -------------------------------------------- */
/**
* On dragStart on the item.
* @param {DragEvent} event - The drag event
*/
async _onDragStart(event) {
const attackItem = event.currentTarget.closest('.inventory-item[data-type="attack"]');
if (attackItem) {
const attackData = {
type: 'Attack',
actorUuid: this.document.uuid,
img: this.document.system.attack.img,
fromInternal: true
};
event.dataTransfer.setData('text/plain', JSON.stringify(attackData));
event.dataTransfer.setDragImage(attackItem.querySelector('img'), 60, 0);
}
}
}

View file

@ -22,14 +22,14 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
actions: {
removeAction: DHBaseItemSheet.#removeAction,
addFeature: DHBaseItemSheet.#addFeature,
editFeature: DHBaseItemSheet.#editFeature,
removeFeature: DHBaseItemSheet.#removeFeature,
deleteFeature: DHBaseItemSheet.#deleteFeature,
addResource: DHBaseItemSheet.#addResource,
removeResource: DHBaseItemSheet.#removeResource
},
dragDrop: [
{ dragSelector: null, dropSelector: '.tab.features .drop-section' },
{ dragSelector: '.feature-item', dropSelector: null }
{ dragSelector: '.feature-item', dropSelector: null },
{ dragSelector: '.action-item', dropSelector: null }
],
contextMenus: [
{
@ -153,16 +153,19 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
const actionIndex = button.closest('[data-index]').dataset.index;
const action = this.document.system.actions[actionIndex];
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize(`DAGGERHEART.GENERAL.Action.single`),
name: action.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: action.name })
});
if (!confirmed) return;
if(!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize(`DAGGERHEART.GENERAL.Action.single`),
name: action.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: action.name })
});
if (!confirmed) return;
}
await this.document.update({
'system.actions': this.document.system.actions.filter((_, index) => index !== Number.parseInt(actionIndex))
@ -173,58 +176,31 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
* Add a new feature to the item, prompting the user for its type.
* @type {ApplicationClickAction}
*/
static async #addFeature(_event, _button) {
static async #addFeature(_, target) {
const { type } = target.dataset;
const cls = foundry.documents.Item.implementation;
const feature = await cls.create({
type: 'feature',
name: cls.defaultName({ type: 'feature' }),
"system.subType": CONFIG.DH.ITEM.featureSubTypes[type]
});
await this.document.update({
'system.features': [...this.document.system.features, feature]
'system.features': [...this.document.system.features, feature].map(f => f.uuid)
});
}
/**
* Edit an existing feature on the item
* @type {ApplicationClickAction}
*/
static async #editFeature(_event, button) {
const target = button.closest('.feature-item');
const feature = this.document.system.features.find(x => x?.id === target.id);
if (!feature) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing'));
return;
}
feature.sheet.render(true);
}
/**
* Remove a feature from the item.
* @type {ApplicationClickAction}
*/
static async #removeFeature(event, button) {
event.stopPropagation();
const target = button.closest('.feature-item');
const feature = this.document.system.features.find(x => x && x.id === target.id);
if (feature) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize(`TYPES.Item.feature`),
name: feature.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: feature.name })
});
if (!confirmed) return;
}
static async #deleteFeature(_, target) {
const feature = getDocFromElement(target);
if (!feature) return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing'));
await feature.update({ 'system.subType': null });
await this.document.update({
'system.features': this.document.system.features
.filter(feature => feature && feature.id !== target.id)
.map(x => x.uuid)
.filter(uuid => uuid !== feature.uuid)
});
}
@ -269,6 +245,23 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
const featureData = { type: 'Item', data: { ...feature.toObject(), _id: null }, fromInternal: true };
event.dataTransfer.setData('text/plain', JSON.stringify(featureData));
event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0);
} else {
const actionItem = event.currentTarget.closest('.action-item');
if (actionItem) {
const action = this.document.system.actions[actionItem.dataset.index];
if (!action) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.actionIsMissing'));
return;
}
const actionData = {
type: 'Action',
data: { ...action.toObject(), id: action.id, itemUuid: this.document.uuid },
fromInternal: true
};
event.dataTransfer.setData('text/plain', JSON.stringify(actionData));
event.dataTransfer.setDragImage(actionItem.querySelector('img'), 60, 0);
}
}
}

View file

@ -5,7 +5,6 @@ export default class AncestrySheet extends DHHeritageSheet {
static DEFAULT_OPTIONS = {
classes: ['ancestry'],
actions: {
addFeature: AncestrySheet.#addFeature,
editFeature: AncestrySheet.#editFeature,
removeFeature: AncestrySheet.#removeFeature
},
@ -23,23 +22,6 @@ export default class AncestrySheet extends DHHeritageSheet {
/* Application Clicks Actions */
/* -------------------------------------------- */
/**
* Add a new feature to the item, prompting the user for its type.
* @type {ApplicationClickAction}
*/
static async #addFeature(_event, button) {
const feature = await game.items.documentClass.create({
type: 'feature',
name: game.i18n.format('DOCUMENT.New', { type: game.i18n.localize('TYPES.Item.feature') }),
system: {
subType: button.dataset.type
}
});
await this.document.update({
'system.features': [...this.document.system.features.map(x => x.uuid), feature.uuid]
});
}
/**
* Edit an existing feature on the item
* @type {ApplicationClickAction}
@ -63,22 +45,8 @@ export default class AncestrySheet extends DHHeritageSheet {
event.stopPropagation();
const target = button.closest('.feature-item');
const feature = this.document.system[`${target.dataset.type}Feature`];
const featureExists = feature && Object.keys(feature).length > 0;
if (featureExists) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize(`TYPES.Item.feature`),
name: feature.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: feature.name })
});
if (!confirmed) return;
}
if (featureExists && target.dataset.type === 'primary') await feature.update({ 'system.primary': null });
if (feature) await feature.update({ 'system.subType': null });
await this.document.update({
'system.features': this.document.system.features.filter(x => x && x.uuid !== feature.uuid).map(x => x.uuid)
});
@ -94,15 +62,18 @@ export default class AncestrySheet extends DHHeritageSheet {
*/
async _onDrop(event) {
event.stopPropagation();
event.preventDefault();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const item = await fromUuid(data.uuid);
if (item?.type === 'feature') {
const subType = event.target.closest('.primary-feature') ? 'primary' : 'secondary';
await item.update({ 'system.subType': subType });
if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes[subType]) {
const error = subType === 'primary' ? 'featureNotPrimary' : 'featureNotSecondary';
ui.notifications.warn(game.i18n.localize(`DAGGERHEART.UI.Notifications.${error}`));
return;
}
await item.update({ 'system.subType': subType });
await this.document.update({
'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
});

View file

@ -10,8 +10,6 @@ export default class ClassSheet extends DHBaseItemSheet {
actions: {
removeItemFromCollection: ClassSheet.#removeItemFromCollection,
removeSuggestedItem: ClassSheet.#removeSuggestedItem,
addFeature: ClassSheet.#addFeature,
deleteFeature: ClassSheet.#deleteFeature
},
tagifyConfigs: [
{
@ -80,6 +78,7 @@ export default class ClassSheet extends DHBaseItemSheet {
/* -------------------------------------------- */
async _onDrop(event) {
event.stopPropagation();
const data = TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid);
const target = event.target.closest('fieldset.drop-section');
@ -89,12 +88,24 @@ export default class ClassSheet extends DHBaseItemSheet {
});
} else if (item.type === 'feature') {
if (target.classList.contains('hope-feature')) {
if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.hope) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotHope'));
return;
}
await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.hope });
await this.document.update({
'system.hopeFeatures': [...this.document.system.hopeFeatures.map(x => x.uuid), item.uuid]
'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
});
} else if (target.classList.contains('class-feature')) {
if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.class) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotClass'));
return;
}
await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.class });
await this.document.update({
'system.classFeatures': [...this.document.system.classFeatures.map(x => x.uuid), item.uuid]
'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
});
}
} else if (item.type === 'weapon') {
@ -168,28 +179,4 @@ export default class ClassSheet extends DHBaseItemSheet {
const { target } = element.dataset;
await this.document.update({ [`system.characterGuide.${target}`]: null });
}
static async #addFeature(_, target) {
const { actionPath } = target.dataset;
const cls = foundry.documents.Item.implementation;
const feature = await cls.create({
type: 'feature',
name: cls.defaultName({ type: 'feature' }),
});
await this.document.update({
[`system.${actionPath}`]: [
...this.document.system[actionPath],
feature.uuid
]
});
}
static async #deleteFeature(_, button) {
const { actionPath, itemUuid } = button.dataset;
await this.document.update({
[`system.${actionPath}`]: this.document.system[actionPath].filter(f => f.uuid !== itemUuid)
});
}
}

View file

@ -10,7 +10,7 @@ export default class CommunitySheet extends DHHeritageSheet {
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/items/community/header.hbs' },
...super.PARTS,
feature: {
features: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-features.hbs',
scrollable: ['.feature']
}

View file

@ -6,10 +6,7 @@ export default class SubclassSheet extends DHBaseItemSheet {
classes: ['subclass'],
position: { width: 600 },
window: { resizable: false },
actions: {
addFeature: this.addFeature,
deleteFeature: this.deleteFeature
}
actions: {}
};
/**@override */
@ -40,24 +37,6 @@ export default class SubclassSheet extends DHBaseItemSheet {
}
};
static async addFeature(_, target) {
const cls = foundry.documents.Item.implementation;
const feature = await cls.create({
type: 'feature',
name: cls.defaultName({ type: 'feature' }),
});
await this.document.update({
[`system.${target.dataset.type}`]: feature
});
}
static async deleteFeature(_, button) {
await this.document.update({
[`system.${button.dataset.actionPath}`]: null
});
}
async _onDragStart(event) {
const featureItem = event.currentTarget.closest('.drop-section');
@ -75,18 +54,45 @@ export default class SubclassSheet extends DHBaseItemSheet {
}
async _onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (data.fromInternal) return;
const item = await fromUuid(data.uuid);
if (item?.type === 'feature') {
const dropSection = event.target.closest('.drop-section');
if (this.document.system[dropSection.dataset.type]) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.notifications.featureIsFull'));
return;
}
const target = event.target.closest('fieldset.drop-section');
if (item.type === 'feature') {
if (target.dataset.type === 'foundation') {
if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.foundation) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotFoundation'));
return;
}
await this.document.update({ [`system.${dropSection.dataset.type}`]: item.uuid });
await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.foundation });
await this.document.update({
'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
});
} else if (target.dataset.type === 'specialization') {
if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.specialization) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotSpecialization'));
return;
}
await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.specialization });
await this.document.update({
'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
});
} else if (target.dataset.type === 'mastery') {
if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.mastery) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotMastery'));
return;
}
await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.mastery });
await this.document.update({
'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
});
}
}
}
}

View file

@ -2,3 +2,4 @@ export { default as DhChatLog } from './chatLog.mjs';
export { default as DhCombatTracker } from './combatTracker.mjs';
export * as DhCountdowns from './countdowns.mjs';
export { default as DhFearTracker } from './fearTracker.mjs';
export { default as DhHotbar } from './hotbar.mjs';

View file

@ -14,8 +14,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
}
addChatListeners = async (app, html, data) => {
super.addChatListeners(app, html, data);
html.querySelectorAll('.duality-action-damage').forEach(element =>
element.addEventListener('click', event => this.onRollDamage(event, data.message))
);
@ -90,7 +88,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
onRollDamage = async (event, message) => {
event.stopPropagation();
const actor = await this.getActor(message.system.source.actor);
if (!actor || !game.user.isGM) return true;
if (game.user.character?.id !== actor.id && !game.user.isGM) return true;
if (message.system.source.item && message.system.source.action) {
const action = this.getAction(actor, message.system.source.item, message.system.source.action);
if (!action || !action?.rollDamage) return;

View file

@ -66,6 +66,11 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
}
async setCombatantSpotlight(combatantId) {
const update = {
system: {
'spotlight.requesting': false
}
};
const combatant = this.viewed.combatants.get(combatantId);
const toggleTurn = this.viewed.combatants.contents
@ -73,10 +78,18 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
.map(x => x.id)
.indexOf(combatantId);
if (this.viewed.turn !== toggleTurn) Hooks.callAll(CONFIG.DH.HOOKS.spotlight, {});
if (this.viewed.turn !== toggleTurn) {
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
await updateCountdowns(CONFIG.DH.GENERAL.countdownTypes.spotlight.id);
const autoPoints = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).actionPoints;
if (autoPoints) {
update.system.actionTokens = Math.max(combatant.system.actionTokens - 1, 0);
}
}
await this.viewed.update({ turn: this.viewed.turn === toggleTurn ? null : toggleTurn });
await combatant.update({ 'system.spotlight.requesting': false });
await combatant.update(update);
}
static async requestSpotlight(_, target) {

View file

@ -1,4 +1,3 @@
import { countdownTypes } from '../../config/generalConfig.mjs';
import { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import constructHTMLButton from '../../helpers/utils.mjs';
import OwnershipSelection from '../dialogs/ownershipSelection.mjs';
@ -328,43 +327,29 @@ export class EncounterCountdowns extends Countdowns {
};
}
export const registerCountdownApplicationHooks = () => {
const updateCountdowns = async shouldProgress => {
if (game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).countdowns) {
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
for (let countdownCategoryKey in countdownSetting) {
const countdownCategory = countdownSetting[countdownCategoryKey];
for (let countdownKey in countdownCategory.countdowns) {
const countdown = countdownCategory.countdowns[countdownKey];
if (shouldProgress(countdown)) {
await countdownSetting.updateSource({
[`${countdownCategoryKey}.countdowns.${countdownKey}.progress.current`]:
countdown.progress.current - 1
});
await game.settings.set(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Countdowns,
countdownSetting
);
foundry.applications.instances.get(`${countdownCategoryKey}-countdowns`)?.render();
}
export async function updateCountdowns(progressType) {
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const update = Object.keys(countdownSetting).reduce((update, typeKey) => {
return foundry.utils.mergeObject(
update,
Object.keys(countdownSetting[typeKey].countdowns).reduce((acc, countdownKey) => {
const countdown = countdownSetting[typeKey].countdowns[countdownKey];
if (countdown.progress.current > 0 && countdown.progress.type.value === progressType) {
acc[`${typeKey}.countdowns.${countdownKey}.progress.current`] = countdown.progress.current - 1;
}
}
}
};
Hooks.on(CONFIG.DH.HOOKS.characterAttack, async () => {
updateCountdowns(countdown => {
return (
countdown.progress.type.value === countdownTypes.characterAttack.id && countdown.progress.current > 0
);
});
});
return acc;
}, {})
);
}, {});
Hooks.on(CONFIG.DH.HOOKS.spotlight, async () => {
updateCountdowns(countdown => {
return countdown.progress.type.value === countdownTypes.spotlight.id && countdown.progress.current > 0;
});
await countdownSetting.updateSource(update);
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, countdownSetting);
const data = { refreshType: RefreshType.Countdown };
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data
});
};
Hooks.callAll(socketEvent.Refresh, data);
}

View file

@ -0,0 +1,129 @@
export default class DhHotbar extends foundry.applications.ui.Hotbar {
constructor(options) {
super(options);
this.setupHooks();
}
static async useItem(uuid) {
const item = await fromUuid(uuid);
if (!item) {
return ui.notifications.warn('WARNING.ObjectDoesNotExist', {
format: {
name: game.i18n.localize('Document'),
identifier: uuid
}
});
}
await item.use({});
}
static async useAction(itemUuid, actionId) {
const item = await foundry.utils.fromUuid(itemUuid);
if (!item) {
return ui.notifications.warn('WARNING.ObjectDoesNotExist', {
format: {
name: game.i18n.localize('Document'),
identifier: itemUuid
}
});
}
const action = item.system.actions.find(x => x.id === actionId);
if (!action) {
return ui.notifications.warn('DAGGERHEART.UI.Notifications.actionIsMissing');
}
await action.use({});
}
static async useAttack(actorUuid) {
const actor = await foundry.utils.fromUuid(actorUuid);
if (!actor) {
return ui.notifications.warn('WARNING.ObjectDoesNotExist', {
format: {
name: game.i18n.localize('Document'),
identifier: actorUuid
}
});
}
const attack = actor.system.attack;
if (!attack) {
return ui.notifications.warn('DAGGERHEART.UI.Notifications.attackIsMissing');
}
await attack.use({});
}
setupHooks() {
Hooks.on('hotbarDrop', (bar, data, slot) => {
if (data.type === 'Item') {
const item = foundry.utils.fromUuidSync(data.uuid);
if (item.uuid.startsWith('Compendium') || !item.isOwned || !item.isOwner) return true;
switch (item.type) {
case 'ancestry':
case 'community':
case 'class':
case 'subclass':
return true;
default:
this.createItemMacro(item, slot);
return false;
}
} else if (data.type === 'Action') {
const item = foundry.utils.fromUuidSync(data.data.itemUuid);
if (item.uuid.startsWith('Compendium')) return true;
if (!item.isOwned || !item.isOwner) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.unownedActionMacro'));
return false;
}
this.createActionMacro(data, slot);
return false;
} else if (data.type === 'Attack') {
const actor = foundry.utils.fromUuidSync(data.actorUuid);
if (actor.uuid.startsWith('Compendium')) return true;
if (!actor.isOwner) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.unownedAttackMacro'));
return false;
}
this.createAttackMacro(data, slot);
return false;
}
});
}
async createItemMacro(data, slot) {
const macro = await Macro.implementation.create({
name: `${game.i18n.localize('Display')} ${name}`,
type: CONST.MACRO_TYPES.SCRIPT,
img: data.img,
command: `await game.system.api.applications.ui.DhHotbar.useItem("${data.uuid}");`
});
await game.user.assignHotbarMacro(macro, slot);
}
async createActionMacro(data, slot) {
const macro = await Macro.implementation.create({
name: `${game.i18n.localize('Display')} ${name}`,
type: CONST.MACRO_TYPES.SCRIPT,
img: data.data.img,
command: `await game.system.api.applications.ui.DhHotbar.useAction("${data.data.itemUuid}", "${data.data.id}");`
});
await game.user.assignHotbarMacro(macro, slot);
}
async createAttackMacro(data, slot) {
const macro = await Macro.implementation.create({
name: `${game.i18n.localize('Display')} ${name}`,
type: CONST.MACRO_TYPES.SCRIPT,
img: data.img,
command: `await game.system.api.applications.ui.DhHotbar.useAttack("${data.actorUuid}");`
});
await game.user.assignHotbarMacro(macro, slot);
}
}

View file

@ -1,4 +1,5 @@
export { default as DhMeasuredTemplate } from './measuredTemplate.mjs';
export { default as DhRuler } from './ruler.mjs';
export { default as DhTemplateLayer } from './templateLayer.mjs';
export { default as DhTokenPlaceable } from './token.mjs';
export { default as DhTokenRuler } from './tokenRuler.mjs';

View file

@ -0,0 +1,36 @@
export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
/** @inheritDoc */
async _drawEffects() {
this.effects.renderable = false;
// Clear Effects Container
this.effects.removeChildren().forEach(c => c.destroy());
this.effects.bg = this.effects.addChild(new PIXI.Graphics());
this.effects.bg.zIndex = -1;
this.effects.overlay = null;
// Categorize effects
const activeEffects = this.actor ? Array.from(this.actor.effects).filter(x => !x.disabled) : [];
const overlayEffect = activeEffects.findLast(e => e.img && e.getFlag('core', 'overlay'));
// Draw effects
const promises = [];
for (const [i, effect] of activeEffects.entries()) {
if (!effect.img) continue;
const promise =
effect === overlayEffect
? this._drawOverlay(effect.img, effect.tint)
: this._drawEffect(effect.img, effect.tint);
promises.push(
promise.then(e => {
if (e) e.zIndex = i;
})
);
}
await Promise.allSettled(promises);
this.effects.sortChildren();
this.effects.renderable = true;
this.renderFlags.set({ refreshEffects: true });
}
}

View file

@ -4,7 +4,6 @@ export * as domainConfig from './domainConfig.mjs';
export * as effectConfig from './effectConfig.mjs';
export * as flagsConfig from './flagsConfig.mjs';
export * as generalConfig from './generalConfig.mjs';
export * as hooksConfig from './hooksConfig.mjs';
export * as itemConfig from './itemConfig.mjs';
export * as settingsConfig from './settingsConfig.mjs';
export * as systemConfig from './system.mjs';

View file

@ -411,7 +411,7 @@ export const levelupData = {
};
export const subclassFeatureLabels = {
1: 'DAGGERHEART.ITEMS.DomainCard.foundation',
1: 'DAGGERHEART.ITEMS.DomainCard.foundationTitle',
2: 'DAGGERHEART.ITEMS.DomainCard.specializationTitle',
3: 'DAGGERHEART.ITEMS.DomainCard.masteryTitle'
};

View file

@ -3,55 +3,55 @@ export const domains = {
id: 'arcana',
label: 'DAGGERHEART.GENERAL.Domain.arcana.label',
src: 'systems/daggerheart/assets/icons/domains/arcana.svg',
description: 'DAGGERHEART.GENERAL.Domain.Arcana'
description: 'DAGGERHEART.GENERAL.Domain.arcana.description'
},
blade: {
id: 'blade',
label: 'DAGGERHEART.GENERAL.Domain.blade.label',
src: 'systems/daggerheart/assets/icons/domains/blade.svg',
description: 'DAGGERHEART.GENERAL.Domain.Blade'
description: 'DAGGERHEART.GENERAL.Domain.blade.description'
},
bone: {
id: 'bone',
label: 'DAGGERHEART.GENERAL.Domain.bone.label',
src: 'systems/daggerheart/assets/icons/domains/bone.svg',
description: 'DAGGERHEART.GENERAL.Domain.Bone'
description: 'DAGGERHEART.GENERAL.Domain.bone.description'
},
codex: {
id: 'codex',
label: 'DAGGERHEART.GENERAL.Domain.codex.label',
src: 'systems/daggerheart/assets/icons/domains/codex.svg',
description: 'DAGGERHEART.GENERAL.Domain.Codex'
description: 'DAGGERHEART.GENERAL.Domain.codex.description'
},
grace: {
id: 'grace',
label: 'DAGGERHEART.GENERAL.Domain.grace.label',
src: 'systems/daggerheart/assets/icons/domains/grace.svg',
description: 'DAGGERHEART.GENERAL.Domain.Grace'
description: 'DAGGERHEART.GENERAL.Domain.grace.description'
},
midnight: {
id: 'midnight',
label: 'DAGGERHEART.GENERAL.Domain.midnight.label',
src: 'systems/daggerheart/assets/icons/domains/midnight.svg',
description: 'DAGGERHEART.GENERAL.Domain.Midnight'
description: 'DAGGERHEART.GENERAL.Domain.midnight.description'
},
sage: {
id: 'sage',
label: 'DAGGERHEART.GENERAL.Domain.sage.label',
src: 'systems/daggerheart/assets/icons/domains/sage.svg',
description: 'DAGGERHEART.GENERAL.Domain.Sage'
description: 'DAGGERHEART.GENERAL.Domain.sage.description'
},
splendor: {
id: 'splendor',
label: 'DAGGERHEART.GENERAL.Domain.splendor.label',
src: 'systems/daggerheart/assets/icons/domains/splendor.svg',
description: 'DAGGERHEART.GENERAL.Domain.Splendor'
description: 'DAGGERHEART.GENERAL.Domain.splendor.description'
},
valor: {
id: 'valor',
label: 'DAGGERHEART.GENERAL.Domain.valor.label',
src: 'systems/daggerheart/assets/icons/domains/valor.svg',
description: 'DAGGERHEART.GENERAL.Domain.Valor'
description: 'DAGGERHEART.GENERAL.Domain.valor.description'
}
};

View file

@ -110,6 +110,18 @@ export const conditions = {
name: 'DAGGERHEART.CONFIG.Condition.restrained.name',
icon: 'icons/magic/control/debuff-chains-shackle-movement-red.webp',
description: 'DAGGERHEART.CONFIG.Condition.restrained.description'
},
unconcious: {
id: 'unconcious',
name: 'DAGGERHEART.CONFIG.Condition.unconcious.name',
icon: 'icons/magic/control/sleep-bubble-purple.webp',
description: 'DAGGERHEART.CONFIG.Condition.unconcious.description'
},
dead: {
id: 'dead',
name: 'DAGGERHEART.CONFIG.Condition.dead.name',
icon: 'icons/magic/death/grave-tombstone-glow-teal.webp',
description: 'DAGGERHEART.CONFIG.Condition.dead.description'
}
};
@ -118,6 +130,7 @@ export const defaultRestOptions = {
tendToWounds: {
id: 'tendToWounds',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.tendToWounds.name'),
icon: 'fa-solid fa-bandage',
img: 'icons/magic/life/cross-worn-green.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.tendToWounds.description'),
actions: [
@ -141,6 +154,7 @@ export const defaultRestOptions = {
clearStress: {
id: 'clearStress',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.clearStress.name'),
icon: 'fa-regular fa-face-surprise',
img: 'icons/magic/perception/eye-ringed-green.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.clearStress.description'),
actions: [
@ -164,6 +178,7 @@ export const defaultRestOptions = {
repairArmor: {
id: 'repairArmor',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.repairArmor.name'),
icon: 'fa-solid fa-hammer',
img: 'icons/skills/trades/smithing-anvil-silver-red.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.repairArmor.description'),
actions: []
@ -171,6 +186,7 @@ export const defaultRestOptions = {
prepare: {
id: 'prepare',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepare.name'),
icon: 'fa-solid fa-dumbbell',
img: 'icons/skills/trades/academics-merchant-scribe.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepare.description'),
actions: []
@ -180,6 +196,7 @@ export const defaultRestOptions = {
tendToWounds: {
id: 'tendToWounds',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.tendToWounds.name'),
icon: 'fa-solid fa-bandage',
img: 'icons/magic/life/cross-worn-green.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.tendToWounds.description'),
actions: []
@ -187,6 +204,7 @@ export const defaultRestOptions = {
clearStress: {
id: 'clearStress',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.clearStress.name'),
icon: 'fa-regular fa-face-surprise',
img: 'icons/magic/perception/eye-ringed-green.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.clearStress.description'),
actions: []
@ -194,6 +212,7 @@ export const defaultRestOptions = {
repairArmor: {
id: 'repairArmor',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.repairArmor.name'),
icon: 'fa-solid fa-hammer',
img: 'icons/skills/trades/smithing-anvil-silver-red.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.repairArmor.description'),
actions: []
@ -201,6 +220,7 @@ export const defaultRestOptions = {
prepare: {
id: 'prepare',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepare.name'),
icon: 'fa-solid fa-dumbbell',
img: 'icons/skills/trades/academics-merchant-scribe.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepare.description'),
actions: []
@ -208,19 +228,12 @@ export const defaultRestOptions = {
workOnAProject: {
id: 'workOnAProject',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.workOnAProject.name'),
icon: 'fa-solid fa-diagram-project',
img: 'icons/skills/social/thumbsup-approval-like.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.workOnAProject.description'),
actions: []
}
}),
custom: {
id: 'customActivity',
name: '',
img: 'icons/skills/trades/academics-investigation-puzzles.webp',
description: '',
namePlaceholder: 'DAGGERHEART.APPLICATIONS.Downtime.custom.namePlaceholder',
placeholder: 'DAGGERHEART.APPLICATIONS.Downtime.custom.placeholder'
}
})
};
export const deathMoves = {
@ -376,29 +389,29 @@ export const abilityCosts = {
export const countdownTypes = {
spotlight: {
id: 'spotlight',
label: 'DAGGERHEART.CONFIG.CountdownTypes.Spotlight'
label: 'DAGGERHEART.CONFIG.CountdownType.spotlight'
},
characterAttack: {
id: 'characterAttack',
label: 'DAGGERHEART.CONFIG.CountdownTypes.CharacterAttack'
label: 'DAGGERHEART.CONFIG.CountdownType.characterAttack'
},
custom: {
id: 'custom',
label: 'DAGGERHEART.CONFIG.CountdownTypes.Custom'
label: 'DAGGERHEART.CONFIG.CountdownType.custom'
}
};
export const rollTypes = {
weapon: {
id: 'weapon',
label: 'DAGGERHEART.CONFIG.RollTypes.weapon.name'
attack: {
id: 'attack',
label: 'DAGGERHEART.CONFIG.RollTypes.attack.name'
},
spellcast: {
id: 'spellcast',
label: 'DAGGERHEART.CONFIG.RollTypes.spellcast.name'
},
ability: {
id: 'ability',
label: 'DAGGERHEART.CONFIG.RollTypes.ability.name'
trait: {
id: 'trait',
label: 'DAGGERHEART.CONFIG.RollTypes.trait.name'
},
diceSet: {
id: 'diceSet',

View file

@ -1,4 +0,0 @@
export const hooks = {
characterAttack: 'characterAttackHook',
spotlight: 'spotlightHook'
};

View file

@ -439,7 +439,7 @@ export const weaponFeatures = {
{
key: 'system.bonuses.damage.primaryWeapon.bonus',
mode: 2,
value: '@system.levelData.levels.current'
value: '@system.levelData.level.current'
}
]
}
@ -1261,15 +1261,6 @@ export const weaponFeatures = {
timebending: {
label: 'DAGGERHEART.CONFIG.WeaponFeature.timebending.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.timebending.description'
},
versatile: {
label: 'DAGGERHEART.CONFIG.WeaponFeature.versatile.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.versatile.description'
// versatile: {
// characterTrait: '',
// range: '',
// damage: ''
// }
}
};
@ -1322,7 +1313,12 @@ export const featureTypes = {
export const featureSubTypes = {
primary: 'primary',
secondary: 'secondary'
secondary: 'secondary',
hope: 'hope',
class: 'class',
foundation: 'foundation',
specialization: 'specialization',
mastery: 'mastery'
};
export const actionTypes = {

View file

@ -3,7 +3,6 @@ import * as DOMAIN from './domainConfig.mjs';
import * as ACTOR from './actorConfig.mjs';
import * as ITEM from './itemConfig.mjs';
import * as SETTINGS from './settingsConfig.mjs';
import { hooks as HOOKS } from './hooksConfig.mjs';
import * as EFFECTS from './effectConfig.mjs';
import * as ACTIONS from './actionConfig.mjs';
import * as FLAGS from './flagsConfig.mjs';
@ -17,7 +16,6 @@ export const SYSTEM = {
ACTOR,
ITEM,
SETTINGS,
HOOKS,
EFFECTS,
ACTIONS,
FLAGS

View file

@ -5,7 +5,7 @@ export default class DHAttackAction extends DHDamageAction {
static extraSchemas = [...super.extraSchemas, ...['roll', 'save']];
static getRollType(parent) {
return parent.type === 'weapon' ? 'weapon' : 'spellcast';
return parent.type === 'weapon' ? 'attack' : 'spellcast';
}
get chatTemplate() {
@ -21,7 +21,7 @@ export default class DHAttackAction extends DHDamageAction {
}
if (this.roll.useDefault) {
this.roll.trait = this.item.system.attack.roll.trait;
this.roll.type = 'weapon';
this.roll.type = 'attack';
}
}
}
@ -37,4 +37,17 @@ export default class DHAttackAction extends DHDamageAction {
base: true
};
}
async use(event, ...args) {
const result = await super.use(event, args);
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
await updateCountdowns(CONFIG.DH.GENERAL.countdownTypes.characterAttack.id);
return result;
}
// get modifiers() {
// return [];
// }
}

View file

@ -150,7 +150,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
}
static getRollType(parent) {
return 'ability';
return 'trait';
}
static getSourceConfig(parent) {
@ -268,7 +268,8 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
hasDamage: !!this.damage?.parts?.length,
hasHealing: !!this.healing,
hasEffect: !!this.effects?.length,
hasSave: this.hasSave
hasSave: this.hasSave,
selectedRollMode: game.settings.get('core', 'rollMode')
};
}
@ -308,7 +309,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
prepareRoll() {
const roll = {
modifiers: [],
modifiers: this.modifiers,
trait: this.roll?.trait,
label: 'Attack',
type: this.actionType,
@ -362,6 +363,13 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
get hasRoll() {
return !!this.roll?.type || !!this.roll?.bonus;
}
get modifiers() {
if (!this.actor) return [];
const modifiers = [];
/** Placeholder for specific bonuses **/
return modifiers;
}
/* ROLL */
/* SAVE */

View file

@ -6,10 +6,20 @@ export default class DHDamageAction extends DHBaseAction {
getFormulaValue(part, data) {
let formulaValue = part.value;
if (this.hasRoll && part.resultBased && data.system.roll.result.duality === -1) return part.valueAlt;
const isAdversary = this.actor.type === 'adversary';
if (isAdversary && this.actor.system.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id) {
const hasHordeDamage = this.actor.effects.find(
x => x.name === game.i18n.localize('DAGGERHEART.CONFIG.AdversaryType.horde.label')
);
if (hasHordeDamage) return part.valueAlt;
}
return formulaValue;
}
async rollDamage(event, data) {
const systemData = data.system ?? data;
let formula = this.damage.parts.map(p => this.getFormulaValue(p, data).getFormula(this.actor)).join(' + '),
damageTypes = [...new Set(this.damage.parts.reduce((a, c) => a.concat([...c.type]), []))];
@ -19,15 +29,16 @@ export default class DHDamageAction extends DHBaseAction {
let roll = { formula: formula, total: formula },
bonusDamage = [];
if (isNaN(formula)) formula = Roll.replaceFormulaData(formula, this.getRollData(data.system ?? data));
if (isNaN(formula)) formula = Roll.replaceFormulaData(formula, this.getRollData(systemData));
const config = {
title: game.i18n.format('DAGGERHEART.UI.Chat.damageRoll.title', { damage: this.name }),
roll: { formula },
targets: data.system?.targets.filter(t => t.hit) ?? data.targets,
targets: systemData.targets.filter(t => t.hit) ?? data.targets,
hasSave: this.hasSave,
isCritical: data.system?.roll?.isCritical ?? false,
source: data.system?.source,
isCritical: systemData.roll?.isCritical ?? false,
source: systemData.source,
data: this.getRollData(),
damageTypes,
event
};
@ -35,8 +46,14 @@ export default class DHDamageAction extends DHBaseAction {
if (data.system) {
config.source.message = data._id;
config.directDamage = false;
} else {
config.directDamage = true;
}
roll = CONFIG.Dice.daggerheart.DamageRoll.build(config);
}
// get modifiers() {
// return [];
// }
}

View file

@ -39,4 +39,8 @@ export default class DHHealingAction extends DHBaseAction {
get chatTemplate() {
return 'systems/daggerheart/templates/ui/chat/healing-roll.hbs';
}
get modifiers() {
return [];
}
}

View file

@ -1,13 +1,7 @@
import DHAdversarySettings from '../../applications/sheets-configs/adversary-settings.mjs';
import ActionField from '../fields/actionField.mjs';
import BaseDataActor from './base.mjs';
const resourceField = () =>
new foundry.data.fields.SchemaField({
value: new foundry.data.fields.NumberField({ initial: 0, integer: true }),
max: new foundry.data.fields.NumberField({ initial: 0, integer: true }),
isReversed: new foundry.data.fields.BooleanField({ initial: true })
});
import { resourceField, bonusField } from '../fields/actorField.mjs';
export default class DhpAdversary extends BaseDataActor {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Adversary'];
@ -37,14 +31,29 @@ export default class DhpAdversary extends BaseDataActor {
motivesAndTactics: new fields.StringField(),
notes: new fields.HTMLField(),
difficulty: new fields.NumberField({ required: true, initial: 1, integer: true }),
hordeHp: new fields.NumberField({ required: true, initial: 1, integer: true }),
hordeHp: new fields.NumberField({
required: true,
initial: 1,
integer: true,
label: 'DAGGERHEART.GENERAL.hordeHp'
}),
damageThresholds: new fields.SchemaField({
major: new fields.NumberField({ required: true, initial: 0, integer: true }),
severe: new fields.NumberField({ required: true, initial: 0, integer: true })
major: new fields.NumberField({
required: true,
initial: 0,
integer: true,
label: 'DAGGERHEART.GENERAL.DamageThresholds.majorThreshold'
}),
severe: new fields.NumberField({
required: true,
initial: 0,
integer: true,
label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold'
})
}),
resources: new fields.SchemaField({
hitPoints: resourceField(),
stress: resourceField()
hitPoints: resourceField(0, 'DAGGERHEART.GENERAL.hitPoints.plural', true),
stress: resourceField(0, 'DAGGERHEART.GENERAL.stress', true)
}),
attack: new ActionField({
initial: {
@ -59,7 +68,7 @@ export default class DhpAdversary extends BaseDataActor {
amount: 1
},
roll: {
type: 'weapon'
type: 'attack'
},
damage: {
parts: [
@ -80,9 +89,14 @@ export default class DhpAdversary extends BaseDataActor {
})
),
bonuses: new fields.SchemaField({
difficulty: new fields.SchemaField({
all: new fields.NumberField({ integer: true, initial: 0 }),
reaction: new fields.NumberField({ integer: true, initial: 0 })
roll: new fields.SchemaField({
attack: bonusField('DAGGERHEART.GENERAL.Roll.attack'),
action: bonusField('DAGGERHEART.GENERAL.Roll.action'),
reaction: bonusField('DAGGERHEART.GENERAL.Roll.reaction')
}),
damage: new fields.SchemaField({
physical: bonusField('DAGGERHEART.GENERAL.Damage.physicalDamage'),
magical: bonusField('DAGGERHEART.GENERAL.Damage.magicalDamage')
})
})
};
@ -95,4 +109,37 @@ export default class DhpAdversary extends BaseDataActor {
get features() {
return this.parent.items.filter(x => x.type === 'feature');
}
async _preUpdate(changes, options, user) {
const allowed = await super._preUpdate(changes, options, user);
if (allowed === false) return false;
if (this.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id) {
if (changes.system?.resources?.hitPoints?.value) {
const halfHP = Math.ceil(this.resources.hitPoints.max / 2);
const newHitPoints = changes.system.resources.hitPoints.value;
const previouslyAboveHalf = this.resources.hitPoints.value < halfHP;
const loweredBelowHalf = previouslyAboveHalf && newHitPoints >= halfHP;
const raisedAboveHalf = !previouslyAboveHalf && newHitPoints < halfHP;
if (loweredBelowHalf) {
await this.parent.createEmbeddedDocuments('ActiveEffect', [
{
name: game.i18n.localize('DAGGERHEART.CONFIG.AdversaryType.horde.label'),
img: 'icons/magic/movement/chevrons-down-yellow.webp',
disabled: !game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation)
.hordeDamage
}
]);
} else if (raisedAboveHalf) {
const hordeEffects = this.parent.effects.filter(
x => x.name === game.i18n.localize('DAGGERHEART.CONFIG.AdversaryType.horde.label')
);
await this.parent.deleteEmbeddedDocuments(
'ActiveEffect',
hordeEffects.map(x => x.id)
);
}
}
}
}
}

View file

@ -1,10 +1,10 @@
import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs';
const resistanceField = () =>
const resistanceField = reductionLabel =>
new foundry.data.fields.SchemaField({
resistance: new foundry.data.fields.BooleanField({ initial: false }),
immunity: new foundry.data.fields.BooleanField({ initial: false }),
reduction: new foundry.data.fields.NumberField({ integer: true, initial: 0 })
reduction: new foundry.data.fields.NumberField({ integer: true, initial: 0, label: reductionLabel })
});
/**
@ -40,8 +40,8 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
if (this.metadata.isNPC) schema.description = new fields.HTMLField({ required: true, nullable: true });
if (this.metadata.hasResistances)
schema.resistance = new fields.SchemaField({
physical: resistanceField(),
magical: resistanceField()
physical: resistanceField('DAGGERHEART.GENERAL.DamageResistance.physicalReduction'),
magical: resistanceField('DAGGERHEART.GENERAL.DamageResistance.magicalReduction')
});
return schema;
}

View file

@ -2,27 +2,11 @@ import { burden } from '../../config/generalConfig.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import DhLevelData from '../levelData.mjs';
import BaseDataActor from './base.mjs';
const attributeField = () =>
new foundry.data.fields.SchemaField({
value: new foundry.data.fields.NumberField({ initial: 0, integer: true }),
tierMarked: new foundry.data.fields.BooleanField({ initial: false })
});
const resourceField = (max, reverse = false) =>
new foundry.data.fields.SchemaField({
value: new foundry.data.fields.NumberField({ initial: 0, integer: true }),
max: new foundry.data.fields.NumberField({ initial: max, integer: true }),
isReversed: new foundry.data.fields.BooleanField({ initial: reverse })
});
const stressDamageReductionRule = () =>
new foundry.data.fields.SchemaField({
enabled: new foundry.data.fields.BooleanField({ required: true, initial: false }),
cost: new foundry.data.fields.NumberField({ integer: true })
});
import { attributeField, resourceField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs';
export default class DhCharacter extends BaseDataActor {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Character'];
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {
label: 'TYPES.Actor.character',
@ -37,24 +21,36 @@ export default class DhCharacter extends BaseDataActor {
return {
...super.defineSchema(),
resources: new fields.SchemaField({
hitPoints: resourceField(0, true),
stress: resourceField(6, true),
hope: resourceField(6)
hitPoints: resourceField(0, 'DAGGERHEART.GENERAL.hitPoints.plural', true),
stress: resourceField(6, 'DAGGERHEART.GENERAL.stress', true),
hope: resourceField(6, 'DAGGERHEART.GENERAL.hope')
}),
traits: new fields.SchemaField({
agility: attributeField(),
strength: attributeField(),
finesse: attributeField(),
instinct: attributeField(),
presence: attributeField(),
knowledge: attributeField()
agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'),
strength: attributeField('DAGGERHEART.CONFIG.Traits.strength.name'),
finesse: attributeField('DAGGERHEART.CONFIG.Traits.finesse.name'),
instinct: attributeField('DAGGERHEART.CONFIG.Traits.instinct.name'),
presence: attributeField('DAGGERHEART.CONFIG.Traits.presence.name'),
knowledge: attributeField('DAGGERHEART.CONFIG.Traits.knowledge.name')
}),
proficiency: new fields.NumberField({ initial: 1, integer: true }),
evasion: new fields.NumberField({ initial: 0, integer: true }),
armorScore: new fields.NumberField({ integer: true, initial: 0 }),
proficiency: new fields.NumberField({
initial: 1,
integer: true,
label: 'DAGGERHEART.GENERAL.proficiency'
}),
evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }),
armorScore: new fields.NumberField({ integer: true, initial: 0, label: 'DAGGERHEART.GENERAL.armorScore' }),
damageThresholds: new fields.SchemaField({
severe: new fields.NumberField({ integer: true, initial: 0 }),
major: new fields.NumberField({ integer: true, initial: 0 })
severe: new fields.NumberField({
integer: true,
initial: 0,
label: 'DAGGERHEART.GENERAL.DamageThresholds.majorThreshold'
}),
major: new fields.NumberField({
integer: true,
initial: 0,
label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold'
})
}),
experiences: new fields.TypedObjectField(
new fields.SchemaField({
@ -91,24 +87,82 @@ export default class DhCharacter extends BaseDataActor {
value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }),
subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true })
}),
advantageSources: new fields.ArrayField(new fields.StringField()),
disadvantageSources: new fields.ArrayField(new fields.StringField()),
levelData: new fields.EmbeddedDataField(DhLevelData),
bonuses: new fields.SchemaField({
roll: new fields.SchemaField({
attack: new fields.NumberField({ integer: true, initial: 0 }),
primaryWeapon: new fields.SchemaField({
attack: new fields.NumberField({ integer: true, initial: 0 })
}),
spellcast: new fields.NumberField({ integer: true, initial: 0 }),
action: new fields.NumberField({ integer: true, initial: 0 }),
hopeOrFear: new fields.NumberField({ integer: true, initial: 0 })
attack: bonusField('DAGGERHEART.GENERAL.Roll.attack'),
spellcast: bonusField('DAGGERHEART.GENERAL.Roll.spellcast'),
trait: bonusField('DAGGERHEART.GENERAL.Roll.trait'),
action: bonusField('DAGGERHEART.GENERAL.Roll.action'),
reaction: bonusField('DAGGERHEART.GENERAL.Roll.reaction'),
primaryWeapon: bonusField('DAGGERHEART.GENERAL.Roll.primaryWeaponAttack'),
secondaryWeapon: bonusField('DAGGERHEART.GENERAL.Roll.secondaryWeaponAttack')
}),
damage: new fields.SchemaField({
all: new fields.NumberField({ integer: true, initial: 0 }),
physical: new fields.NumberField({ integer: true, initial: 0 }),
magic: new fields.NumberField({ integer: true, initial: 0 }),
primaryWeapon: new fields.SchemaField({
bonus: new fields.NumberField({ integer: true }),
extraDice: new fields.NumberField({ integer: true })
physical: bonusField('DAGGERHEART.GENERAL.Damage.physicalDamage'),
magical: bonusField('DAGGERHEART.GENERAL.Damage.magicalDamage'),
primaryWeapon: bonusField('DAGGERHEART.GENERAL.Damage.primaryWeapon'),
secondaryWeapon: bonusField('DAGGERHEART.GENERAL.Damage.secondaryWeapon')
}),
healing: bonusField('DAGGERHEART.GENERAL.Healing.healingAmount'),
range: new fields.SchemaField({
weapon: new fields.NumberField({
integer: true,
initial: 0,
label: 'DAGGERHEART.GENERAL.Range.weapon'
}),
spell: new fields.NumberField({
integer: true,
initial: 0,
label: 'DAGGERHEART.GENERAL.Range.spell'
}),
other: new fields.NumberField({
integer: true,
initial: 0,
label: 'DAGGERHEART.GENERAL.Range.other'
})
}),
rally: new fields.ArrayField(new fields.StringField(), {
label: 'DAGGERHEART.CLASS.Feature.rallyDice'
}),
rest: new fields.SchemaField({
shortRest: new fields.SchemaField({
shortMoves: new fields.NumberField({
required: true,
integer: true,
min: 0,
initial: 0,
label: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.shortRestMoves.label',
hint: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.shortRestMoves.hint'
}),
longMoves: new fields.NumberField({
required: true,
integer: true,
min: 0,
initial: 0,
label: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.longRestMoves.label',
hint: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.longRestMoves.hint'
})
}),
longRest: new fields.SchemaField({
shortMoves: new fields.NumberField({
required: true,
integer: true,
min: 0,
initial: 0,
label: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.shortRestMoves.label',
hint: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.shortRestMoves.hint'
}),
longMoves: new fields.NumberField({
required: true,
integer: true,
min: 0,
initial: 0,
label: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.longRestMoves.label',
hint: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.longRestMoves.hint'
})
})
})
}),
@ -117,25 +171,34 @@ export default class DhCharacter extends BaseDataActor {
damageReduction: new fields.SchemaField({
maxArmorMarked: new fields.SchemaField({
value: new fields.NumberField({ required: true, integer: true, initial: 1 }),
bonus: new fields.NumberField({ required: true, integer: true, initial: 0 }),
stressExtra: new fields.NumberField({ required: true, integer: true, initial: 0 })
bonus: new fields.NumberField({
required: true,
integer: true,
initial: 0,
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.maxArmorMarkedBonus'
}),
stressExtra: new fields.NumberField({
required: true,
integer: true,
initial: 0,
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.maxArmorMarkedStress.label',
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.maxArmorMarkedStress.hint'
})
}),
stressDamageReduction: new fields.SchemaField({
severe: stressDamageReductionRule(),
major: stressDamageReductionRule(),
minor: stressDamageReductionRule()
severe: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.severe'),
major: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.major'),
minor: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.minor')
}),
increasePerArmorMark: new fields.NumberField({
integer: true,
initial: 1,
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.label',
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint'
}),
increasePerArmorMark: new fields.NumberField({ integer: true, initial: 1 }),
magical: new fields.BooleanField({ initial: false }),
physical: new fields.BooleanField({ initial: false })
}),
strangePatterns: new fields.NumberField({
integer: true,
min: 1,
max: 12,
nullable: true,
initial: null
}),
weapon: new fields.SchemaField({
/* Unimplemented
-> Should remove the lowest damage dice from weapon damage
@ -181,6 +244,11 @@ export default class DhCharacter extends BaseDataActor {
return !this.class.value || !this.class.subclass;
}
get spellcastModifier() {
const subClasses = this.parent.items.filter(x => x.type === 'subclass') ?? [];
return Math.max(subClasses?.map(sc => this.traits[sc.system.spellcastingTrait]?.value));
}
get spellcastingModifiers() {
return {
main: this.class.subclass?.system?.spellcastingTrait,
@ -219,23 +287,23 @@ export default class DhCharacter extends BaseDataActor {
features = [];
for (let item of this.parent.items) {
if (item.system.type === CONFIG.DH.ITEM.featureTypes.ancestry.id) {
if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.ancestry.id) {
ancestryFeatures.push(item);
} else if (item.system.type === CONFIG.DH.ITEM.featureTypes.community.id) {
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.community.id) {
communityFeatures.push(item);
} else if (item.system.type === CONFIG.DH.ITEM.featureTypes.class.id) {
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.class.id) {
classFeatures.push(item);
} else if (item.system.type === CONFIG.DH.ITEM.featureTypes.subclass.id) {
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.subclass.id) {
const subclassState = this.class.subclass.system.featureState;
const identifier = item.system.identifier;
const subType = item.system.subType;
if (
identifier === 'foundationFeature' ||
(identifier === 'specializationFeature' && subclassState >= 2) ||
(identifier === 'masterFeature' && subclassState >= 3)
subType === CONFIG.DH.ITEM.featureSubTypes.foundation ||
(subType === CONFIG.DH.ITEM.featureSubTypes.specialization && subclassState >= 2) ||
(subType === CONFIG.DH.ITEM.featureSubTypes.mastery && subclassState >= 3)
) {
subclassFeatures.push(item);
}
} else if (item.system.type === CONFIG.DH.ITEM.featureTypes.companion.id) {
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.companion.id) {
companionFeatures.push(item);
} else if (item.type === 'feature' && !item.system.type) {
features.push(item);
@ -323,6 +391,8 @@ export default class DhCharacter extends BaseDataActor {
}
prepareBaseData() {
this.evasion = this.class.value?.system?.evasion ?? 0;
const currentLevel = this.levelData.level.current;
const currentTier =
currentLevel === 1
@ -380,7 +450,8 @@ export default class DhCharacter extends BaseDataActor {
}
prepareDerivedData() {
this.resources.hope.value = Math.min(this.resources.hope.value, this.resources.hope.max);
const baseHope = this.resources.hope.value + (this.companion?.system?.resources?.hope ?? 0);
this.resources.hope.value = Math.min(baseHope, this.resources.hope.max);
}
getRollData() {

View file

@ -4,6 +4,7 @@ import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import ActionField from '../fields/actionField.mjs';
import { adjustDice, adjustRange } from '../../helpers/utils.mjs';
import DHCompanionSettings from '../../applications/sheets-configs/companion-settings.mjs';
import { resourceField, bonusField } from '../fields/actorField.mjs';
export default class DhCompanion extends BaseDataActor {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Companion'];
@ -23,14 +24,16 @@ export default class DhCompanion extends BaseDataActor {
...super.defineSchema(),
partner: new ForeignDocumentUUIDField({ type: 'Actor' }),
resources: new fields.SchemaField({
stress: new fields.SchemaField({
value: new fields.NumberField({ initial: 0, integer: true }),
max: new fields.NumberField({ initial: 3, integer: true }),
isReversed: new foundry.data.fields.BooleanField({ initial: true })
}),
hope: new fields.NumberField({ initial: 0, integer: true })
stress: resourceField(3, 'DAGGERHEART.GENERAL.stress', true),
hope: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.hope' })
}),
evasion: new fields.NumberField({
required: true,
min: 1,
initial: 10,
integer: true,
label: 'DAGGERHEART.GENERAL.evasion'
}),
evasion: new fields.NumberField({ required: true, min: 1, initial: 10, integer: true }),
experiences: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({}),
@ -56,9 +59,8 @@ export default class DhCompanion extends BaseDataActor {
amount: 1
},
roll: {
type: 'weapon',
bonus: 0,
trait: 'instinct'
type: 'attack',
bonus: 0
},
damage: {
parts: [
@ -74,13 +76,13 @@ export default class DhCompanion extends BaseDataActor {
}
}),
actions: new fields.ArrayField(new ActionField()),
levelData: new fields.EmbeddedDataField(DhLevelData)
};
}
get traits() {
return {
instinct: { value: this.attack.roll.bonus }
levelData: new fields.EmbeddedDataField(DhLevelData),
bonuses: new fields.SchemaField({
damage: new fields.SchemaField({
physical: bonusField('DAGGERHEART.GENERAL.Damage.physicalDamage'),
magical: bonusField('DAGGERHEART.GENERAL.Damage.magicalDamage')
})
})
};
}
@ -89,9 +91,7 @@ export default class DhCompanion extends BaseDataActor {
}
prepareBaseData() {
const partnerSpellcastingModifier = this.partner?.system?.spellcastingModifiers?.main;
const spellcastingModifier = this.partner?.system?.traits?.[partnerSpellcastingModifier]?.value;
this.attack.roll.bonus = spellcastingModifier ?? 0; // Needs to expand on which modifier it is that should be used because of multiclassing;
this.attack.roll.bonus = this.partner?.system?.spellcastModifier ?? 0;
for (let levelKey in this.levelData.levelups) {
const level = this.levelData.levelups[levelKey];
@ -124,12 +124,6 @@ export default class DhCompanion extends BaseDataActor {
}
}
prepareDerivedData() {
if (this.partner) {
this.partner.system.resources.hope.max += this.resources.hope;
}
}
async _preDelete() {
if (this.partner) {
await this.partner.update({ 'system.companion': null });

View file

@ -102,7 +102,7 @@ class DhCountdown extends foundry.abstract.DataModel {
value: new fields.StringField({
required: true,
choices: CONFIG.DH.GENERAL.countdownTypes,
initial: CONFIG.DH.GENERAL.countdownTypes.spotlight.id,
initial: CONFIG.DH.GENERAL.countdownTypes.custom.id,
label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.progress.type.value.label'
}),
label: new fields.StringField({
@ -132,7 +132,13 @@ class DhCountdown extends foundry.abstract.DataModel {
export const registerCountdownHooks = () => {
Hooks.on(socketEvent.Refresh, ({ refreshType, application }) => {
if (refreshType === RefreshType.Countdown) {
foundry.applications.instances.get(application)?.render();
if (application) {
foundry.applications.instances.get(application)?.render();
} else {
foundry.applications.instances.get('narrative-countdowns')?.render();
foundry.applications.instances.get('encounter-countdowns')?.render();
}
return false;
}
});

View file

@ -0,0 +1,35 @@
const fields = foundry.data.fields;
const attributeField = label =>
new fields.SchemaField({
value: new fields.NumberField({ initial: 0, integer: true, label }),
tierMarked: new fields.BooleanField({ initial: false })
});
const resourceField = (max = 0, label, reverse = false) =>
new fields.SchemaField({
value: new fields.NumberField({ initial: 0, integer: true, label }),
max: new fields.NumberField({ initial: max, integer: true }),
isReversed: new fields.BooleanField({ initial: reverse })
});
const stressDamageReductionRule = localizationPath =>
new fields.SchemaField({
enabled: new fields.BooleanField({ required: true, initial: false }),
cost: new fields.NumberField({
integer: true,
label: `${localizationPath}.label`,
hint: `${localizationPath}.hint`
})
});
const bonusField = label =>
new fields.SchemaField({
bonus: new fields.NumberField({ integer: true, initial: 0, label: `${game.i18n.localize(label)} Value` }),
dice: new fields.ArrayField(
new fields.StringField(),
{ label: `${game.i18n.localize(label)} Dice` }
)
});
export { attributeField, resourceField, stressDamageReductionRule, bonusField };

View file

@ -53,11 +53,14 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
}),
diceStates: new fields.TypedObjectField(
new fields.SchemaField({
value: new fields.NumberField({ integer: true, nullable: true, initial: null }),
value: new fields.NumberField({ integer: true, initial: 1, min: 1 }),
used: new fields.BooleanField({ initial: false })
})
),
dieFaces: new fields.StringField({ initial: '4' })
dieFaces: new fields.StringField({
choices: CONFIG.DH.GENERAL.diceTypes,
initial: CONFIG.DH.GENERAL.diceTypes.d4
})
},
{ nullable: true, initial: null }
);

View file

@ -69,6 +69,7 @@ export default class DHBeastform extends BaseDataItem {
const beastformEffect = this.parent.effects.find(x => x.type === 'beastform');
await beastformEffect.updateSource({
changes: [...beastformEffect.changes, { key: 'system.advantageSources', mode: 2, value: this.advantageOn }],
system: {
characterTokenData: {
tokenImg: this.parent.parent.prototypeToken.texture.src,

View file

@ -24,11 +24,10 @@ export default class DHClass extends BaseDataItem {
integer: true,
min: 1,
initial: 5,
label: 'DAGGERHEART.GENERAL.hitPoints'
label: 'DAGGERHEART.GENERAL.hitPoints.plural'
}),
evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }),
hopeFeatures: new ForeignDocumentUUIDArrayField({ type: 'Item' }),
classFeatures: new ForeignDocumentUUIDArrayField({ type: 'Item' }),
features: new ForeignDocumentUUIDArrayField({ type: 'Item' }),
subclasses: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
inventory: new fields.SchemaField({
take: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
@ -52,12 +51,18 @@ export default class DHClass extends BaseDataItem {
};
}
get hopeFeature() {
return this.hopeFeatures.length > 0 ? this.hopeFeatures[0] : null;
get hopeFeatures() {
return (
this.features.filter(x => x?.system?.subType === CONFIG.DH.ITEM.featureSubTypes.hope) ??
(this.features.filter(x => !x).length > 0 ? {} : null)
);
}
get features() {
return [...this.hopeFeatures.filter(x => x), ...this.classFeatures.filter(x => x)];
get classFeatures() {
return (
this.features.filter(x => x?.system?.subType === CONFIG.DH.ITEM.featureSubTypes.class) ??
(this.features.filter(x => !x).length > 0 ? {} : null)
);
}
async _preCreate(data, options, user) {

View file

@ -29,7 +29,6 @@ export default class DHDomainCard extends BaseDataItem {
required: true,
initial: CONFIG.DH.DOMAIN.cardTypes.ability.id
}),
foundation: new fields.BooleanField({ initial: false }),
inVault: new fields.BooleanField({ initial: false }),
actions: new fields.ArrayField(new ActionField())
};

View file

@ -1,4 +1,4 @@
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
import BaseDataItem from './base.mjs';
export default class DHSubclass extends BaseDataItem {
@ -22,20 +22,22 @@ export default class DHSubclass extends BaseDataItem {
nullable: true,
initial: null
}),
foundationFeature: new ForeignDocumentUUIDField({ type: 'Item' }),
specializationFeature: new ForeignDocumentUUIDField({ type: 'Item' }),
masteryFeature: new ForeignDocumentUUIDField({ type: 'Item' }),
features: new ForeignDocumentUUIDArrayField({ type: 'Item' }),
featureState: new fields.NumberField({ required: true, initial: 1, min: 1 }),
isMulticlass: new fields.BooleanField({ initial: false })
};
}
get features() {
return [
{ ...this.foundationFeature?.toObject(), identifier: 'foundationFeature' },
{ ...this.specializationFeature?.toObject(), identifier: 'specializationFeature' },
{ ...this.masteryFeature?.toObject(), identifier: 'masteryFeature' }
];
get foundationFeatures() {
return this.features.filter(x => x.system.subType === CONFIG.DH.ITEM.featureSubTypes.foundation);
}
get specializationFeatures() {
return this.features.filter(x => x.system.subType === CONFIG.DH.ITEM.featureSubTypes.specialization);
}
get masteryFeatures() {
return this.features.filter(x => x.system.subType === CONFIG.DH.ITEM.featureSubTypes.mastery);
}
async _preCreate(data, options, user) {

View file

@ -50,7 +50,7 @@ export default class DHWeapon extends AttachableItem {
},
roll: {
trait: 'agility',
type: 'weapon'
type: 'attack'
},
damage: {
parts: [

View file

@ -1,23 +1,28 @@
export default class DhAutomation extends foundry.abstract.DataModel {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.SETTINGS.Automation']; // Doesn't work for some reason
static defineSchema() {
const fields = foundry.data.fields;
return {
hope: new fields.BooleanField({
required: true,
initial: false,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hope.label'
}), // Label need to be updated into something like "Duality Roll Auto Gain" + a hint
hopeFear: new fields.SchemaField({
gm: new fields.BooleanField({
required: true,
initial: false,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hopeFear.gm.label'
}),
players: new fields.BooleanField({
required: true,
initial: false,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hopeFear.players.label'
})
}),
actionPoints: new fields.BooleanField({
required: true,
initial: false,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.actionPoints.label'
}),
countdowns: new fields.BooleanField({
requireD: true,
initial: false,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.countdowns.label'
hordeDamage: new fields.BooleanField({
required: true,
initial: true,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hordeDamage.label'
})
};
}

View file

@ -54,6 +54,7 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
moves: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({ required: true }),
icon: new fields.StringField({ required: true }),
img: new fields.FilePathField({
initial: 'icons/magic/life/cross-worn-green.webp',
categories: ['IMAGE'],
@ -70,6 +71,7 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
moves: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({ required: true }),
icon: new fields.StringField({ required: true }),
img: new fields.FilePathField({
initial: 'icons/magic/life/cross-worn-green.webp',
categories: ['IMAGE'],

View file

@ -39,11 +39,13 @@ export default class D20Roll extends DHRoll {
}
get hasAdvantage() {
return this.options.roll.advantage === this.constructor.ADV_MODE.ADVANTAGE;
const adv = this.options.roll.advantage.type ?? this.options.roll.advantage;
return adv === this.constructor.ADV_MODE.ADVANTAGE;
}
get hasDisadvantage() {
return this.options.roll.advantage === this.constructor.ADV_MODE.DISADVANTAGE;
const adv = this.options.roll.advantage.type ?? this.options.roll.advantage;
return adv === this.constructor.ADV_MODE.DISADVANTAGE;
}
static applyKeybindings(config) {
@ -74,7 +76,6 @@ export default class D20Roll extends DHRoll {
}
constructFormula(config) {
// this.terms = [];
this.createBaseDice();
this.configureModifiers();
this.resetFormula();
@ -91,7 +92,10 @@ export default class D20Roll extends DHRoll {
configureModifiers() {
this.applyAdvantage();
this.applyBaseBonus();
this.baseTerms = foundry.utils.deepClone(this.dice);
this.options.roll.modifiers = this.applyBaseBonus();
this.options.experiences?.forEach(m => {
if (this.options.data.experiences?.[m])
@ -101,12 +105,7 @@ export default class D20Roll extends DHRoll {
});
});
this.options.roll.modifiers?.forEach(m => {
this.terms.push(...this.formatModifier(m.value));
});
this.baseTerms = foundry.utils.deepClone(this.terms);
this.addModifiers();
if (this.options.extraFormula) {
this.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
@ -125,13 +124,20 @@ export default class D20Roll extends DHRoll {
}
applyBaseBonus() {
this.options.roll.modifiers = [];
if (!this.options.roll.bonus) return;
this.options.roll.modifiers.push({
label: 'Bonus to Hit',
value: this.options.roll.bonus
// value: Roll.replaceFormulaData('@attackBonus', this.data)
});
const modifiers = [];
if (this.options.roll.bonus)
modifiers.push({
label: 'Bonus to Hit',
value: this.options.roll.bonus
});
modifiers.push(...this.getBonus(`roll.${this.options.type}`, `${this.options.type?.capitalize()} Bonus`));
modifiers.push(
...this.getBonus(`roll.${this.options.roll.type}`, `${this.options.roll.type?.capitalize()} Bonus`)
);
return modifiers;
}
static async buildEvaluate(roll, config = {}, message = {}) {

View file

@ -24,8 +24,26 @@ export default class DamageRoll extends DHRoll {
}
}
applyBaseBonus() {
const modifiers = [],
type = this.options.messageType ?? 'damage';
modifiers.push(...this.getBonus(`${type}`, `${type.capitalize()} Bonus`));
this.options.damageTypes?.forEach(t => {
modifiers.push(...this.getBonus(`${type}.${t}`, `${t.capitalize()} ${type.capitalize()} Bonus`));
});
const weapons = ['primaryWeapon', 'secondaryWeapon'];
weapons.forEach(w => {
if (this.options.source.item && this.options.source.item === this.data[w]?.id)
modifiers.push(...this.getBonus(`${type}.${w}`, 'Weapon Bonus'));
});
return modifiers;
}
constructFormula(config) {
super.constructFormula(config);
if (config.isCritical) {
const tmpRoll = new Roll(this._formula)._evaluateSync({ maximize: true }),
criticalBonus = tmpRoll.total - this.constructor.calculateTotalModifiers(tmpRoll);

View file

@ -4,6 +4,7 @@ export default class DHRoll extends Roll {
baseTerms = [];
constructor(formula, data, options) {
super(formula, data, options);
if (!this.data || !Object.keys(this.data).length) this.data = options.data;
}
static messageType = 'adversaryRoll';
@ -86,7 +87,7 @@ export default class DHRoll extends Roll {
system: config,
rolls: [roll]
};
return await cls.create(msg);
return await cls.create(msg, { rollMode: config.selectedRollMode });
}
static applyKeybindings(config) {
@ -99,11 +100,44 @@ export default class DHRoll extends Roll {
}
formatModifier(modifier) {
const numTerm = modifier < 0 ? '-' : '+';
return [
new foundry.dice.terms.OperatorTerm({ operator: numTerm }),
new foundry.dice.terms.NumericTerm({ number: Math.abs(modifier) })
];
if (Array.isArray(modifier)) {
return [
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
...this.constructor.parse(modifier.join(' + '), this.options.data)
];
} else {
const numTerm = modifier < 0 ? '-' : '+';
return [
new foundry.dice.terms.OperatorTerm({ operator: numTerm }),
new foundry.dice.terms.NumericTerm({ number: Math.abs(modifier) })
];
}
}
applyBaseBonus() {
return [];
}
addModifiers() {
this.options.roll.modifiers?.forEach(m => {
this.terms.push(...this.formatModifier(m.value));
});
}
getBonus(path, label) {
const bonus = foundry.utils.getProperty(this.data.bonuses, path),
modifiers = [];
if (bonus?.bonus)
modifiers.push({
label: label,
value: bonus?.bonus
});
if (bonus?.dice?.length)
modifiers.push({
label: label,
value: bonus?.dice
});
return modifiers;
}
getFaces(faces) {
@ -113,6 +147,9 @@ export default class DHRoll extends Roll {
constructFormula(config) {
this.terms = Roll.parse(this.options.roll.formula, config.data);
this.options.roll.modifiers = this.applyBaseBonus();
this.addModifiers();
if (this.options.extraFormula) {
this.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
@ -138,9 +175,10 @@ export default class DHRoll extends Roll {
export const registerRollDiceHooks = () => {
Hooks.on(`${CONFIG.DH.id}.postRollDuality`, async (config, message) => {
const hopeFearAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).hopeFear;
if (
!config.source?.actor ||
!game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).hope ||
(game.user.isGM ? !hopeFearAutomation.gm : !hopeFearAutomation.players) ||
config.roll.type === 'reaction'
)
return;
@ -148,11 +186,16 @@ export const registerRollDiceHooks = () => {
const actor = await fromUuid(config.source.actor),
updates = [];
if (!actor) return;
if (config.roll.isCritical || config.roll.result.duality === 1) updates.push({ type: 'hope', value: 1 });
if (config.roll.isCritical) updates.push({ type: 'stress', value: -1 });
if (config.roll.result.duality === -1) updates.push({ type: 'fear', value: 1 });
if (config.roll.isCritical || config.roll.result.duality === 1) updates.push({ key: 'hope', value: 1 });
if (config.roll.isCritical) updates.push({ key: 'stress', value: -1 });
if (config.roll.result.duality === -1) updates.push({ key: 'fear', value: 1 });
if (updates.length) actor.modifyResource(updates);
if (updates.length) {
const target = actor.system.partner ?? actor;
if (!['dead', 'unconcious'].some(x => actor.statuses.has(x))) {
target.modifyResource(updates);
}
}
if (!config.roll.hasOwnProperty('success') && !config.targets?.length) return;

View file

@ -4,9 +4,12 @@ import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
export default class DualityRoll extends D20Roll {
_advantageFaces = 6;
_advantageNumber = 1;
_rallyIndex;
constructor(formula, data = {}, options = {}) {
super(formula, data, options);
this.rallyChoices = this.setRallyChoices();
}
static messageType = 'dualityRoll';
@ -51,6 +54,35 @@ export default class DualityRoll extends D20Roll {
this._advantageFaces = this.getFaces(faces);
}
get advantageNumber() {
return this._advantageNumber;
}
set advantageNumber(value) {
this._advantageNumber = Number(value);
}
setRallyChoices() {
return this.data?.parent?.effects.reduce((a,c) => {
const change = c.changes.find(ch => ch.key === 'system.bonuses.rally');
if(change) a.push({ value: c.id, label: change.value });
return a;
}, []);
}
get dRally() {
if(!this.rallyFaces) return null;
if(this.hasDisadvantage || this.hasAdvantage)
return this.dice[3];
else
return this.dice[2];
}
get rallyFaces() {
const rallyChoice = this.rallyChoices?.find(r => r.value === this._rallyIndex)?.label;
return rallyChoice ? this.getFaces(rallyChoice) : null;
}
get isCritical() {
if (!this.dHope._evaluated || !this.dFear._evaluated) return;
return this.dHope.total === this.dFear.total;
@ -66,10 +98,6 @@ export default class DualityRoll extends D20Roll {
return this.dHope.total < this.dFear.total;
}
get hasBarRally() {
return null;
}
get totalLabel() {
const label = this.withHope
? 'DAGGERHEART.GENERAL.hope'
@ -98,37 +126,43 @@ export default class DualityRoll extends D20Roll {
}
applyAdvantage() {
const dieFaces = this.advantageFaces,
bardRallyFaces = this.hasBarRally,
advDie = new foundry.dice.terms.Die({ faces: dieFaces });
if (this.hasAdvantage || this.hasDisadvantage || bardRallyFaces)
this.terms.push(new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }));
if (bardRallyFaces) {
const rallyDie = new foundry.dice.terms.Die({ faces: bardRallyFaces });
if (this.hasAdvantage) {
this.terms.push(
new foundry.dice.terms.PoolTerm({
terms: [advDie.formula, rallyDie.formula],
modifiers: ['kh']
})
);
} else if (this.hasDisadvantage) {
this.terms.push(advDie, new foundry.dice.terms.OperatorTerm({ operator: '+' }), rallyDie);
}
} else if (this.hasAdvantage || this.hasDisadvantage) this.terms.push(advDie);
if (this.hasAdvantage || this.hasDisadvantage) {
const dieFaces = this.advantageFaces,
advDie = new foundry.dice.terms.Die({ faces: dieFaces, number: this.advantageNumber });
if(this.advantageNumber > 1) advDie.modifiers = ['kh'];
this.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }),
advDie
);
}
if(this.rallyFaces)
this.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }),
new foundry.dice.terms.Die({ faces: this.rallyFaces })
);
}
applyBaseBonus() {
this.options.roll.modifiers = [];
if (!this.options.roll.trait) return;
this.options.roll.modifiers.push({
label: `DAGGERHEART.CONFIG.Traits.${this.options.roll.trait}.name`,
value: Roll.replaceFormulaData(`@traits.${this.options.roll.trait}.value`, this.data)
const modifiers = super.applyBaseBonus();
if (this.options.roll.trait && this.data.traits[this.options.roll.trait])
modifiers.unshift({
label: `DAGGERHEART.CONFIG.Traits.${this.options.roll.trait}.name`,
value: this.data.traits[this.options.roll.trait].value
});
const weapons = ['primaryWeapon', 'secondaryWeapon'];
weapons.forEach(w => {
if (this.options.source.item && this.options.source.item === this.data[w]?.id)
modifiers.push(...this.getBonus(`roll.${w}`, 'Weapon Bonus'));
});
return modifiers;
}
static postEvaluate(roll, config = {}) {
super.postEvaluate(roll, config);
config.roll.hope = {
dice: roll.dHope.denomination,
value: roll.dHope.total
@ -137,12 +171,19 @@ export default class DualityRoll extends D20Roll {
dice: roll.dFear.denomination,
value: roll.dFear.total
};
config.roll.rally = {
dice: roll.dRally?.denomination,
value: roll.dRally?.total
};
config.roll.result = {
duality: roll.withHope ? 1 : roll.withFear ? -1 : 0,
total: roll.dHope.total + roll.dFear.total,
label: roll.totalLabel
};
if(roll._rallyIndex && roll.data?.parent)
roll.data.parent.deleteEmbeddedDocuments('ActiveEffect', [roll._rallyIndex]);
setDiceSoNiceForDualityRoll(roll, config.roll.advantage.type);
}
}

View file

@ -3,4 +3,5 @@ export { default as DHItem } from './item.mjs';
export { default as DhpCombat } from './combat.mjs';
export { default as DhActiveEffect } from './activeEffect.mjs';
export { default as DhChatMessage } from './chatMessage.mjs';
export { default as DhToken } from './token.mjs';
export { default as DhTooltipManager } from './tooltipManager.mjs';

View file

@ -55,10 +55,24 @@ export default class DhActiveEffect extends ActiveEffect {
}
static applyField(model, change, field) {
change.value = itemAbleRollParse(change.value, model, change.effect.parent);
change.value = this.effectSafeEval(itemAbleRollParse(change.value, model, change.effect.parent));
super.applyField(model, change, field);
}
/* Altered Foundry safeEval to allow non-numeric returns */
static effectSafeEval(expression) {
let result;
try {
// eslint-disable-next-line no-new-func
const evl = new Function('sandbox', `with (sandbox) { return ${expression}}`);
result = evl(Roll.MATH_PROXY);
} catch (err) {
return expression;
}
return result;
}
async toChat(origin) {
const cls = getDocumentClass('ChatMessage');
const systemData = {

View file

@ -371,7 +371,7 @@ export default class DhpActor extends Actor {
getRollData() {
const rollData = super.getRollData();
rollData.prof = this.system.proficiency ?? 1;
rollData.cast = this.system.spellcast ?? 1;
rollData.cast = this.system.spellcastModifier ?? 1;
return rollData;
}

View file

@ -37,4 +37,15 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
e.setAttribute('data-use-perm', document.testUserPermission(game.user, 'OWNER'));
});
}
async _preCreate(data, options, user) {
options.speaker = ChatMessage.getSpeaker();
const rollActorOwner = data.rolls?.[0]?.data?.parent?.owner;
if (rollActorOwner) {
data.author = rollActorOwner ? rollActorOwner.id : data.author;
await this.updateSource({ author: rollActorOwner ?? user });
}
return super._preCreate(data, options, rollActorOwner ?? user);
}
}

View file

@ -0,0 +1,57 @@
export default class DHToken extends TokenDocument {
/**
* Inspect the Actor data model and identify the set of attributes which could be used for a Token Bar.
* @param {object} attributes The tracked attributes which can be chosen from
* @returns {object} A nested object of attribute choices to display
*/
static getTrackedAttributeChoices(attributes, model) {
attributes = attributes || this.getTrackedAttributes();
const barGroup = game.i18n.localize('TOKEN.BarAttributes');
const valueGroup = game.i18n.localize('TOKEN.BarValues');
const bars = attributes.bar.map(v => {
const a = v.join('.');
const modelLabel = model ? game.i18n.localize(model.schema.getField(`${a}.value`).label) : null;
return { group: barGroup, value: a, label: modelLabel ? modelLabel : a };
});
bars.sort((a, b) => a.label.compare(b.label));
const invalidAttributes = ['gold', 'levelData', 'actions', 'rules.damageReduction.maxArmorMarked.value'];
const values = attributes.value.reduce((acc, v) => {
const a = v.join('.');
if (invalidAttributes.some(x => a.startsWith(x))) return acc;
const field = model ? model.schema.getField(a) : null;
const modelLabel = field ? game.i18n.localize(field.label) : null;
const hint = field ? game.i18n.localize(field.hint) : null;
acc.push({ group: valueGroup, value: a, label: modelLabel ? modelLabel : a, hint: hint });
return acc;
}, []);
values.sort((a, b) => a.label.compare(b.label));
return bars.concat(values);
}
static _getTrackedAttributesFromSchema(schema, _path = []) {
const attributes = { bar: [], value: [] };
for (const [name, field] of Object.entries(schema.fields)) {
const p = _path.concat([name]);
if (field instanceof foundry.data.fields.NumberField) attributes.value.push(p);
if (field instanceof foundry.data.fields.ArrayField) attributes.value.push(p);
const isSchema = field instanceof foundry.data.fields.SchemaField;
const isModel = field instanceof foundry.data.fields.EmbeddedDataField;
if (isSchema || isModel) {
const schema = isModel ? field.model.schema : field;
const isBar = schema.has && schema.has('value') && schema.has('max');
if (isBar) attributes.bar.push(p);
else {
const inner = this.getTrackedAttributes(schema, p);
attributes.bar.push(...inner.bar);
attributes.value.push(...inner.value);
}
}
}
return attributes;
}
}

View file

@ -21,14 +21,79 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(element);
}
} else {
const shortRest = element.dataset.tooltip?.startsWith('#shortRest#');
const longRest = element.dataset.tooltip?.startsWith('#longRest#');
if (shortRest || longRest) {
const key = element.dataset.tooltip.slice(shortRest ? 11 : 10);
const downtimeOptions = shortRest
? CONFIG.DH.GENERAL.defaultRestOptions.shortRest()
: CONFIG.DH.GENERAL.defaultRestOptions.longRest();
const move = downtimeOptions[key];
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/downtime.hbs`,
{
move: move
}
);
this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(
element,
this.constructor.TOOLTIP_DIRECTIONS.UP
);
}
const isAdvantage = element.dataset.tooltip?.startsWith('#advantage#');
const isDisadvantage = element.dataset.tooltip?.startsWith('#disadvantage#');
if (isAdvantage || isDisadvantage) {
const actorUuid = element.dataset.tooltip.slice(isAdvantage ? 11 : 14);
const actor = await foundry.utils.fromUuid(actorUuid);
if (actor) {
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/advantage.hbs`,
{
sources: isAdvantage ? actor.system.advantageSources : actor.system.disadvantageSources
}
);
this.tooltip.innerHTML = html;
}
}
}
super.activate(element, { ...options, html: html });
}
_determineItemTooltipDirection(element) {
_determineItemTooltipDirection(element, prefered = this.constructor.TOOLTIP_DIRECTIONS.LEFT) {
const pos = element.getBoundingClientRect();
const dirs = this.constructor.TOOLTIP_DIRECTIONS;
return dirs[pos.x - this.tooltip.offsetWidth < 0 ? 'DOWN' : 'LEFT'];
switch (prefered) {
case this.constructor.TOOLTIP_DIRECTIONS.LEFT:
return dirs[
pos.x - this.tooltip.offsetWidth < 0
? this.constructor.TOOLTIP_DIRECTIONS.DOWN
: this.constructor.TOOLTIP_DIRECTIONS.LEFT
];
case this.constructor.TOOLTIP_DIRECTIONS.UP:
return dirs[
pos.y - this.tooltip.offsetHeight < 0
? this.constructor.TOOLTIP_DIRECTIONS.RIGHT
: this.constructor.TOOLTIP_DIRECTIONS.UP
];
case this.constructor.TOOLTIP_DIRECTIONS.RIGHT:
return dirs[
pos.x + this.tooltip.offsetWidth > document.body.clientWidth
? this.constructor.TOOLTIP_DIRECTIONS.DOWN
: this.constructor.TOOLTIP_DIRECTIONS.RIGHT
];
case this.constructor.TOOLTIP_DIRECTIONS.DOWN:
return dirs[
pos.y + this.tooltip.offsetHeight > document.body.clientHeight
? this.constructor.TOOLTIP_DIRECTIONS.LEFT
: this.constructor.TOOLTIP_DIRECTIONS.DOWN
];
}
}
}

View file

@ -39,8 +39,9 @@ export default class RegisterHandlebarsHelpers {
}
static damageSymbols(damageParts) {
const symbols = new Set();
damageParts.forEach(part => symbols.add(...CONFIG.DH.GENERAL.damageTypes[part.type].icon));
const symbols = [...new Set(damageParts.reduce((a, c) => a.concat([...c.type]), []))].map(
p => CONFIG.DH.GENERAL.damageTypes[p].icon
);
return new Handlebars.SafeString(Array.from(symbols).map(symbol => `<i class="fa-solid ${symbol}"></i>`));
}

View file

@ -318,7 +318,7 @@ export const itemAbleRollParse = (value, actor, item) => {
const isItemTarget = value.toLowerCase().startsWith('item.');
const slicedValue = isItemTarget ? value.slice(5) : value;
try {
return Roll.safeEval(Roll.replaceFormulaData(slicedValue, isItemTarget ? item : actor));
return Roll.replaceFormulaData(slicedValue, isItemTarget ? item : actor);
} catch (_) {
return '';
}

View file

@ -29,6 +29,7 @@ export const preloadHandlebarsTemplates = async function () {
'systems/daggerheart/templates/settings/components/settings-item-line.hbs',
'systems/daggerheart/templates/ui/chat/parts/damage-chat.hbs',
'systems/daggerheart/templates/ui/chat/parts/target-chat.hbs',
'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs'
'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs',
'systems/daggerheart/templates/dialogs/downtime/activities.hbs'
]);
};