Merge branch 'v14-Dev' into v14/effect-stacking

This commit is contained in:
WBHarry 2026-03-14 00:12:43 +01:00
commit efc0c53dde
611 changed files with 4609 additions and 2941 deletions

View file

@ -233,6 +233,11 @@ Hooks.once('init', () => {
return handlebarsRegistration();
});
Hooks.on('i18nInit', () => {
// Setup homebrew resources
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).refreshConfig();
});
Hooks.on('setup', () => {
CONFIG.statusEffects = [
...CONFIG.statusEffects.filter(x => !['dead', 'unconscious'].includes(x.id)),

View file

@ -76,6 +76,15 @@
"invalidDrop": "You can only drop Actor entities to summon.",
"chatMessageTitle": "Test2",
"chatMessageHeaderTitle": "Summoning"
},
"transform": {
"name": "Transform",
"tooltip": "Transform one actor into another",
"noTransformActor": "There is no assigned actor to transform into",
"transformActorMissing": "The assigned actor to transform into does not exist. It was probably deleted or moved in/out of a compendium",
"canvasError": "There is no active scene.",
"prototypeError": "You can only use a transform action from a Token",
"actorLinkError": "You cannot transform a token with Actor Link set to true"
}
},
"Config": {
@ -131,6 +140,12 @@
},
"summon": {
"dropSummonsHere": "Drop Summons Here"
},
"transform": {
"dropTransformHere": "Drop Transform Here",
"actorIsMissing": "The linked actor is missing. You should delete this link.",
"clearHitPoints": "Clear Hitpoints",
"clearStress": "Clear Stress"
}
}
},
@ -1055,6 +1070,10 @@
"fear": "Fear",
"spotlight": "Spotlight"
},
"DaggerheartDiceAnimationEvents": {
"critical": { "name": "Critical" },
"higher": { "name": "Highest Roll" }
},
"DamageType": {
"physical": {
"name": "Physical",
@ -2278,6 +2297,7 @@
"identify": "Identity",
"imagePath": "Image Path",
"inactiveEffects": "Inactive Effects",
"initial": "Initial",
"inventory": "Inventory",
"itemResource": "Item Resource",
"itemQuantity": "Item Quantity",
@ -2305,6 +2325,7 @@
"plurial": "Players"
},
"portrait": "Portrait",
"preview": "Preview",
"proficiency": "Proficiency",
"quantity": "Quantity",
"range": "Range",
@ -2645,6 +2666,14 @@
"title": "Triggers"
}
},
"Metagaming": {
"FIELDS": {
"hideObserverPermissionInChat": {
"label": "Hide Chat Info From Players",
"hint": "Information such as hit/miss on attack rolls against adversaries will be hidden"
}
}
},
"Homebrew": {
"newDowntimeMove": "Downtime Move",
"downtimeMove": "Downtime Move",
@ -2659,6 +2688,8 @@
"resetMovesText": "Are you sure you want to reset?",
"deleteItemTitle": "Delete Homebrew Item",
"deleteItemText": "Are you sure you want to delete the item?",
"deleteResourceTitle": "Delete Homebrew Resource",
"deleteResourceText": "Are you sure you want to delete the resource?",
"FIELDS": {
"maxFear": { "label": "Max Fear" },
"maxHope": { "label": "Max Hope" },
@ -2667,6 +2698,13 @@
"label": "Max Cards in Loadout",
"hint": "Set to blank or 0 for unlimited maximum"
},
"resources": {
"resources": {
"value": { "label": "Icon" },
"isIcon": { "label": "Font Awesome Icon" },
"noColorFilter": { "label": "Disable Color Filter" }
}
},
"maxDomains": { "label": "Max Class Domains", "hint": "Max domains you can set on a class" }
},
"currency": {
@ -2695,6 +2733,13 @@
"adversaryType": {
"title": "Custom Adversary Types",
"newType": "Adversary Type"
},
"resources": {
"typeTitle": "{type} Resources",
"filledIcon": "Filled Icon",
"emptyIcon": "Empty Icon",
"resourceIdentifier": "Resource Identifier",
"setResourceIdentifier": "Set Resource Identifier"
}
},
"Menu": {
@ -2704,6 +2749,11 @@
"label": "Configure Automation",
"hint": "Various settings automating resource management and more"
},
"metagaming": {
"name": "Metagaming Settings",
"label": "Configure Metagaming",
"hint": "Various settings controlling the flow of information to players"
},
"homebrew": {
"name": "Homebrew Settings",
"label": "Configure Homebrew",
@ -2729,7 +2779,12 @@
"colorset": "Theme",
"material": "Material",
"system": "Dice Preset",
"font": "Font"
"font": "Font",
"critical": "Duality Critical Animation",
"diceAppearance": "Dice Appearance",
"animations": "Animations",
"defaultAnimations": "Set Animations As Player Defaults",
"previewAnimation": "Preview Animation"
}
},
"variantRules": {

View file

@ -67,7 +67,7 @@ export default class DhCompanionLevelUp extends BaseLevelUp {
break;
case 'summary':
const levelKeys = Object.keys(this.levelup.levels);
const actorDamageDice = this.actor.system.attack.damage.parts[0].value.dice;
const actorDamageDice = this.actor.system.attack.damage.parts.hitPoints.value.dice;
const actorRange = this.actor.system.attack.range;
let achievementExperiences = [];

View file

@ -1,4 +1,5 @@
export { default as DhAppearanceSettings } from './appearanceSettings.mjs';
export { default as DhAutomationSettings } from './automationSettings.mjs';
export { default as DhHomebrewSettings } from './homebrewSettings.mjs';
export { default as DhMetagamingSettings } from './metagamingSettings.mjs';
export { default as DhVariantRuleSettings } from './variantRuleSettings.mjs';

View file

@ -12,7 +12,7 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
static DEFAULT_OPTIONS = {
tag: 'form',
id: 'daggerheart-appearance-settings',
classes: ['daggerheart', 'dialog', 'dh-style', 'setting'],
classes: ['daggerheart', 'dialog', 'dh-style', 'setting', 'appearance-settings'],
position: { width: '600', height: 'auto' },
window: {
title: 'DAGGERHEART.SETTINGS.Menu.title',
@ -70,6 +70,14 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
}
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement
.querySelector('.default-animations-input')
?.addEventListener('change', this.toggleSFXOverride.bind(this));
}
/** @inheritdoc */
_configureRenderParts(options) {
const parts = super._configureRenderParts(options);
@ -83,15 +91,20 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
/**@inheritdoc */
async _prepareContext(options) {
const context = await super._prepareContext(options);
if (options.isFirstRender)
if (options.isFirstRender) {
this.setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
this.globalOverrides = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.GlobalOverrides);
}
context.setting = this.setting;
context.globalOverrides = this.globalOverrides;
context.fields = this.setting.schema.fields;
context.tabs = this._prepareTabs('general');
context.dsnTabs = this._prepareTabs('diceSoNice');
context.isGM = game.user.isGM;
return context;
}
@ -120,6 +133,9 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
* @protected
*/
async prepareDiceSoNiceContext(context) {
context.animationEvents = CONFIG.DH.GENERAL.daggerheartDiceAnimationEvents;
context.previewAnimation = this.previewAnimation;
context.diceSoNiceTextures = Object.entries(game.dice3d.exports.TEXTURELIST).reduce(
(acc, [k, v]) => ({
...acc,
@ -146,6 +162,13 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
);
context.diceSoNiceFonts = game.dice3d.exports.Utils.prepareFontList();
const getAnimationsOptions = key => {
const fields = context.fields.diceSoNice.fields[key].fields.sfx.fields;
return {
higher: fields.higher.fields.class.choices
};
};
foundry.utils.mergeObject(
context.dsnTabs,
['hope', 'fear', 'advantage', 'disadvantage'].reduce(
@ -153,7 +176,8 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
...acc,
[key]: {
values: this.setting.diceSoNice[key],
fields: this.setting.schema.getField(`diceSoNice.${key}`).fields
fields: this.setting.schema.getField(`diceSoNice.${key}`).fields,
animations: ['hope', 'fear'].includes(key) ? getAnimationsOptions(key) : {}
}
}),
{}
@ -169,13 +193,20 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
* @param {foundry.applications.ux.FormDataExtended} formData
* @returns {Promise<void>}
*/
static async #onSubmit(event, form, formData) {
static async #onSubmit(_event, _form, formData) {
const data = this.setting.schema.clean(foundry.utils.expandObject(formData.object));
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance, data);
}
/* -------------------------------------------- */
async toggleSFXOverride(event) {
await this.globalOverrides.diceSoNiceSFXUpdate(this.setting, event.target.checked);
this.globalOverrides = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.GlobalOverrides);
this.render();
}
/**
* Submit the configuration form.
* @this {DHAppearanceSettings}
@ -183,13 +214,25 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
*/
static async #onPreview(_, target) {
const formData = new foundry.applications.ux.FormDataExtended(target.closest('form'));
const { diceSoNice } = foundry.utils.expandObject(formData.object);
const { diceSoNice, ...rest } = foundry.utils.expandObject(formData.object);
const { key } = target.dataset;
const faces = ['advantage', 'disadvantage'].includes(key) ? 'd6' : 'd12';
const preset = await getDiceSoNicePreset(diceSoNice[key], faces);
const diceSoNiceRoll = await new foundry.dice.Roll(`1${faces}`).evaluate();
diceSoNiceRoll.dice[0].options.appearance = preset.appearance;
diceSoNiceRoll.dice[0].options.modelFile = preset.modelFile;
const previewAnimation = rest[`${key}PreviewAnimation`];
const events = CONFIG.DH.GENERAL.daggerheartDiceAnimationEvents;
if (previewAnimation) {
if (previewAnimation === events.critical.id && diceSoNice.sfx.critical.class) {
diceSoNiceRoll.dice[0].options.sfx = { specialEffect: diceSoNice.sfx.critical.class };
}
if (previewAnimation === events.higher.id && diceSoNice[key].sfx.higher) {
diceSoNiceRoll.dice[0].options.sfx = { specialEffect: diceSoNice[key].sfx.higher.class };
}
}
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, false);
}

View file

@ -31,8 +31,8 @@ export default class DhAutomationSettings extends HandlebarsApplicationMixin(App
};
static PARTS = {
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
header: { template: 'systems/daggerheart/templates/settings/automation-settings/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
general: { template: 'systems/daggerheart/templates/settings/automation-settings/general.hbs' },
rules: { template: 'systems/daggerheart/templates/settings/automation-settings/deathMoves.hbs' },
roll: { template: 'systems/daggerheart/templates/settings/automation-settings/roll.hbs' },

View file

@ -1,4 +1,5 @@
import { DhHomebrew } from '../../data/settings/_module.mjs';
import { Resource } from '../../data/settings/Homebrew.mjs';
import { slugify } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -44,6 +45,9 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
addAdversaryType: this.addAdversaryType,
deleteAdversaryType: this.deleteAdversaryType,
selectAdversaryType: this.selectAdversaryType,
addResource: this.addResource,
removeResource: this.removeResource,
resetResourceImage: this.resetResourceImage,
save: this.save,
resetTokenSizes: this.resetTokenSizes,
reset: this.reset
@ -56,6 +60,10 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
settings: { template: 'systems/daggerheart/templates/settings/homebrew-settings/settings.hbs' },
domains: { template: 'systems/daggerheart/templates/settings/homebrew-settings/domains.hbs' },
types: { template: 'systems/daggerheart/templates/settings/homebrew-settings/types.hbs' },
resources: {
template: 'systems/daggerheart/templates/settings/homebrew-settings/resources.hbs',
scrollable: ['.resource-types-container']
},
itemTypes: { template: 'systems/daggerheart/templates/settings/homebrew-settings/itemFeatures.hbs' },
downtime: { template: 'systems/daggerheart/templates/settings/homebrew-settings/downtime.hbs' },
footer: { template: 'systems/daggerheart/templates/settings/homebrew-settings/footer.hbs' }
@ -64,7 +72,14 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
/** @inheritdoc */
static TABS = {
main: {
tabs: [{ id: 'settings' }, { id: 'domains' }, { id: 'types' }, { id: 'itemFeatures' }, { id: 'downtime' }],
tabs: [
{ id: 'settings' },
{ id: 'domains' },
{ id: 'types' },
{ id: 'resources' },
{ id: 'itemFeatures' },
{ id: 'downtime' }
],
initial: 'settings',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}
@ -77,9 +92,17 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
this.render();
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
for (const element of htmlElement.querySelectorAll('.path-field input'))
element.addEventListener('change', this.toggleResourceIsIcon.bind(this));
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.settingFields = this.settings;
context.schemaFields = context.settingFields.schema.fields;
return context;
}
@ -103,6 +126,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
? { id: this.selected.adversaryType, ...this.settings.adversaryTypes[this.selected.adversaryType] }
: null;
break;
case 'resources':
break;
case 'downtime':
context.restOptions = {
shortRest: CONFIG.DH.GENERAL.defaultRestOptions.shortRest(),
@ -124,6 +149,33 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
this.render();
}
async toggleResourceIsIcon(event) {
const element = event.target.closest('.resource-icon-container');
const { actorType, resourceKey, imageKey } = element.dataset;
const current = this.settings.resources[actorType].resources[resourceKey].images[imageKey];
await this.settings.updateSource({
[`resources.${actorType}.resources.${resourceKey}.images.${imageKey}`]: {
isIcon: !current.isIcon,
value: ''
}
});
this.render();
}
static async resetResourceImage(_event, button) {
const element = button.closest('.resource-icon-container');
const { actorType, resourceKey, imageKey } = element.dataset;
await this.settings.updateSource({
[`resources.${actorType}.resources.${resourceKey}.images.${imageKey}`]:
Resource.getDefaultImageData(imageKey)
});
this.render();
}
static async changeCurrencyIcon(_, target) {
const type = target.dataset.currency;
const currentIcon = this.settings.currency[type].icon;
@ -466,6 +518,58 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
this.render();
}
static async addResource(_, target) {
const { actorType } = target.dataset;
const content = new foundry.data.fields.StringField({
label: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.resources.resourceIdentifier'),
required: true
}).toFormGroup({}, { name: 'identifier', localize: true }).outerHTML;
async function callback(_, button) {
const identifier = button.form.elements.identifier.value;
if (!identifier) return;
const sluggedIdentifier = slugify(identifier);
await this.settings.updateSource({
[`resources.${actorType}.resources.${sluggedIdentifier}`]: Resource.getDefaultResourceData(identifier)
});
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
this.render();
}
await foundry.applications.api.DialogV2.prompt({
content: content,
rejectClose: false,
modal: true,
ok: { callback: callback.bind(this) },
window: {
title: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.resources.setResourceIdentifier')
},
position: { width: 400 }
});
}
static async removeResource(_, target) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize(`DAGGERHEART.SETTINGS.Homebrew.deleteResourceTitle`)
},
content: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.deleteResourceText')
});
if (!confirmed) return;
const { actorType, resourceKey } = target.dataset;
await this.settings.updateSource({
[`resources.${actorType}.resources.${resourceKey}`]: _del
});
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
this.render();
}
static async save() {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
this.close();

View file

@ -0,0 +1,62 @@
import { DhMetagaming } from '../../data/settings/_module.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhMetagamingSettings extends HandlebarsApplicationMixin(ApplicationV2) {
constructor() {
super({});
this.settings = new DhMetagaming(
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming).toObject()
);
}
get title() {
return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.title');
}
static DEFAULT_OPTIONS = {
tag: 'form',
id: 'daggerheart-metagaming-settings',
classes: ['daggerheart', 'dh-style', 'dialog', 'setting'],
position: { width: '600', height: 'auto' },
window: {
icon: 'fa-solid fa-eye-low-vision'
},
actions: {
reset: this.reset,
save: this.save
},
form: { handler: this.updateData, submitOnChange: true }
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/settings/metagaming-settings/header.hbs' },
general: { template: 'systems/daggerheart/templates/settings/metagaming-settings/general.hbs' },
footer: { template: 'systems/daggerheart/templates/settings/metagaming-settings/footer.hbs' }
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.settingFields = this.settings;
return context;
}
static async updateData(_event, _element, formData) {
const updatedSettings = foundry.utils.expandObject(formData.object);
await this.settings.updateSource(updatedSettings);
this.render();
}
static async reset() {
this.settings = new DhMetagaming();
this.render();
}
static async save() {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming, this.settings.toObject());
this.close();
}
}

View file

@ -1,3 +1,4 @@
import { getUnusedDamageTypes } from '../../helpers/utils.mjs';
import DaggerheartSheet from '../sheets/daggerheart-sheet.mjs';
const { ApplicationV2 } = foundry.applications.api;
@ -28,6 +29,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
removeEffect: this.removeEffect,
addElement: this.addElement,
removeElement: this.removeElement,
removeTransformActor: this.removeTransformActor,
editEffect: this.editEffect,
addDamage: this.addDamage,
removeDamage: this.removeDamage,
@ -41,7 +43,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
submitOnChange: true,
closeOnSubmit: false
},
dragDrop: [{ dragSelector: null, dropSelector: '#summon-drop-zone', handlers: ['_onDrop'] }]
dragDrop: [{ dragSelector: null, dropSelector: '[data-is-drop-zone]', handlers: ['_onDrop'] }]
};
static PARTS = {
@ -103,7 +105,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
}
};
static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects', 'summon'];
static CLEAN_ARRAYS = ['cost', 'effects', 'summon'];
_getTabs(tabs) {
for (const v of Object.values(tabs)) {
@ -120,6 +122,10 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
htmlElement.querySelectorAll('.summon-count-wrapper input').forEach(element => {
element.addEventListener('change', this.updateSummonCount.bind(this));
});
htmlElement.querySelectorAll('.transform-resource input').forEach(element => {
element.addEventListener('change', this.updateTransformResource.bind(this));
});
}
async _prepareContext(_options) {
@ -133,6 +139,18 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
context.summons.push({ actor, count: summon.count });
}
if (context.source.transform) {
const actor = await foundry.utils.fromUuid(context.source.transform.actorUUID);
context.transform = {
...context.source.transform,
actor:
actor ??
(context.source.transform.actorUUID && !actor
? { error: game.i18n.localize('DAGGERHEART.ACTIONS.Settings.transform.actorIsMissing') }
: null)
};
}
context.openSection = this.openSection;
context.tabs = this._getTabs(this.constructor.TABS);
context.config = CONFIG.DH;
@ -155,6 +173,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
revealed: this.openTrigger === index
};
});
context.allDamageTypesUsed = !getUnusedDamageTypes(this.action.damage.parts).length;
const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers;
context.tierOptions = [
@ -266,20 +285,69 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
if (doc) return doc.sheet.render({ force: true });
}
static async removeTransformActor() {
const data = this.action.toObject();
data.transform.actorUUID = null;
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static addDamage(_event) {
if (!this.action.damage.parts) return;
const data = this.action.toObject(),
part = {};
const choices = getUnusedDamageTypes(this.action.damage.parts);
const content = new foundry.data.fields.StringField({
label: game.i18n.localize('Damage Type'),
choices,
required: true
}).toFormGroup(
{},
{
name: 'type',
localize: true,
nameAttr: 'value',
labelAttr: 'label'
}
).outerHTML;
const callback = (_, button) => {
const data = this.action.toObject();
const type = choices[button.form.elements.type.value].value;
const part = { applyTo: type };
if (this.action.actor?.isNPC) part.value = { multiplier: 'flat' };
data.damage.parts.push(part);
data.damage.parts[type] = part;
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
};
const typeDialog = new foundry.applications.api.DialogV2({
buttons: [
foundry.utils.mergeObject(
{
action: 'ok',
label: 'Confirm',
icon: 'fas fa-check',
default: true
},
{ callback: callback }
)
],
content: content,
rejectClose: false,
modal: false,
window: {
title: game.i18n.localize('Add Damage')
},
position: { width: 300 }
});
typeDialog.render(true);
}
static removeDamage(_event, button) {
if (!this.action.damage.parts) return;
const data = this.action.toObject(),
index = button.dataset.index;
data.damage.parts.splice(index, 1);
const data = this.action.toObject();
const key = button.dataset.key;
delete data.damage.parts[key];
data.damage.parts[`${key}`] = _del;
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
@ -346,6 +414,14 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
updateTransformResource(event) {
event.stopPropagation();
const data = this.action.toObject();
data.transform.resourceRefresh[event.target.dataset.resource] = event.target.checked;
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
/** Specific implementation in extending classes **/
static async addEffect(_event) {}
static removeEffect(_event, _button) {}
@ -364,6 +440,18 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
return;
}
const dropZone = event.target.closest('[data-is-drop-zone]');
if (!dropZone) return;
switch (dropZone.id) {
case 'summon-drop-zone':
return this.onSummonDrop(data);
case 'transform-drop-zone':
return this.onTransformDrop(data);
}
}
async onSummonDrop(data) {
const actionData = this.action.toObject();
let countvalue = 1;
for (const entry of actionData.summon) {
@ -380,4 +468,10 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
actionData.summon.push({ actorUUID: data.uuid, count: countvalue });
await this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(actionData) });
}
async onTransformDrop(data) {
const actionData = this.action.toObject();
actionData.transform.actorUUID = data.uuid;
await this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(actionData) });
}
}

View file

@ -33,6 +33,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
handleResourceDice: CharacterSheet.#handleResourceDice,
advanceResourceDie: CharacterSheet.#advanceResourceDie,
cancelBeastform: CharacterSheet.#cancelBeastform,
toggleResourceManagement: CharacterSheet.#toggleResourceManagement,
useDowntime: this.useDowntime,
viewParty: CharacterSheet.#viewParty
},
@ -226,6 +227,9 @@ export default class CharacterSheet extends DHBaseActorSheet {
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
switch (partId) {
case 'header':
await this._prepareHeaderContext(context, options);
break;
case 'loadout':
await this._prepareLoadoutContext(context, options);
break;
@ -240,6 +244,12 @@ export default class CharacterSheet extends DHBaseActorSheet {
return context;
}
async _prepareHeaderContext(context, _options) {
context.hasExtraResources = Object.keys(CONFIG.DH.RESOURCE.character.all).some(
key => !CONFIG.DH.RESOURCE.character.base[key]
);
}
/**
* Prepare render context for the Loadout part.
* @param {ApplicationRenderContext} context
@ -942,6 +952,78 @@ export default class CharacterSheet extends DHBaseActorSheet {
});
}
static async #toggleResourceManagement(event, button) {
event.stopPropagation();
const existingTooltip = document.body.querySelector('.locked-tooltip .resource-management-container');
if (existingTooltip) {
game.tooltip.dismissLockedTooltips();
return;
}
const extraResources = Object.values(CONFIG.DH.RESOURCE.character.all).reduce((acc, resource) => {
if (CONFIG.DH.RESOURCE.character.base[resource.id]) return acc;
const resourceData = this.document.system.resources[resource.id];
acc[resource.id] = {
id: resource.id,
label: game.i18n.localize(resource.label),
value: resourceData.value,
max: resourceData.max,
fullIcon: resource.images?.full ?? { value: 'fa-solid fa-circle', isIcon: true },
emptyIcon: resource.images?.empty ?? { value: 'fa-regular fa-circle', isIcon: true }
};
return acc;
}, {});
const html = document.createElement('div');
html.innerHTML = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/resourceManagement.hbs`,
{
resources: extraResources
}
);
const target = button.closest('.resource-section');
game.tooltip.dismissLockedTooltips();
game.tooltip.activate(target, {
html,
locked: true,
cssClass: 'bordered-tooltip',
direction: 'DOWN',
noOffset: true
});
const resourceManager = target.querySelector('.resource-manager');
resourceManager.classList.toggle('inverted');
Hooks.once(CONFIG.DH.HOOKS.hooksConfig.lockedTooltipDismissed, () => {
resourceManager.classList.toggle('inverted');
});
for (const element of html.querySelectorAll('.resource-value'))
element.addEventListener('click', this.onUpdateResource.bind(this));
}
async onUpdateResource(event) {
const target = event.target.closest('.resource-value');
const { resource, value: textValue } = target.dataset;
const inputValue = Number.parseInt(textValue);
const decreasing = inputValue <= this.document.system.resources[resource].value;
const value = decreasing ? inputValue - 1 : inputValue;
await this.document.update({ [`system.resources.${resource}.value`]: value }, { render: false });
/* Update resource symbols */
const section = target.closest('.resource-section');
for (const element of section.querySelectorAll('.resource-value')) {
const showFull = Number.parseInt(element.dataset.value) <= value;
element.querySelector('.full').classList.toggle('hidden', !showFull);
element.querySelector('.empty').classList.toggle('hidden', showFull);
}
}
/**
* Open the downtime application.
* @type {ApplicationClickAction}

View file

@ -44,8 +44,32 @@ export default class DHBaseActorSettings extends DHApplicationMixin(DocumentShee
const context = await super._prepareContext(options);
context.isNPC = this.actor.isNPC;
if (context.systemFields.attack)
if (context.systemFields.attack) {
context.systemFields.attack.fields = this.actor.system.attack.schema.fields;
}
// Create fake fields for actor configurable max resource value.
const resourceConfig = CONFIG.DH.RESOURCE[this.actor.type]?.all;
if (resourceConfig) {
const relevant = ['hitPoints', 'stress'].filter(r => r in resourceConfig);
context.resources = relevant.map(key => {
const data = this.actor._source.system.resources[key];
const config = resourceConfig[key];
return {
label: config.label,
name: `system.resources.${key}.max`,
value: data.max ?? config.max,
tooltip: key === 'hitPoints' ? game.i18n.localize('DAGGERHEART.UI.Tooltip.maxHPClassBound') : null,
field: new foundry.data.fields.NumberField({
initial: config.max,
integer: true,
label: game.i18n.format('DAGGERHEART.GENERAL.maxWithThing', {
thing: game.i18n.localize(config.label)
})
})
};
});
}
return context;
}

View file

@ -74,6 +74,15 @@ export default function DHApplicationMixin(Base) {
class DHSheetV2 extends HandlebarsApplicationMixin(Base) {
#nonHeaderAttribution = ['environment', 'ancestry', 'community', 'domainCard'];
/**
* @param {DHSheetV2Configuration} [options={}]
*/
constructor(options = {}) {
super(options);
this._setupDragDrop();
}
/**
* The default options for the sheet.
* @type {DHSheetV2Configuration}
@ -165,7 +174,9 @@ export default function DHApplicationMixin(Base) {
/**@inheritdoc */
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
// this._dragDrop.forEach(d => d.bind(htmlElement));
/* Core dragDrop from ActorDocument is always only 1. Possible we could refactor our own */
if (Array.isArray(this._dragDrop)) this._dragDrop.forEach(d => d.bind(htmlElement));
// Handle delta inputs
for (const deltaInput of htmlElement.querySelectorAll('input[data-allow-delta]')) {
@ -338,6 +349,26 @@ export default function DHApplicationMixin(Base) {
/* Drag and Drop */
/* -------------------------------------------- */
/**
* Creates drag-drop handlers from the configured options.
* @returns {foundry.applications.ux.DragDrop[]}
* @private
*/
_setupDragDrop() {
if (this._dragDrop) {
this._dragDrop.callbacks.dragStart = this._onDragStart;
this._dragDrop.callback.drop = this._onDrop;
} else {
this._dragDrop = this.options.dragDrop.map(d => {
d.callbacks = {
dragstart: this._onDragStart.bind(this),
drop: this._onDrop.bind(this)
};
return new foundry.applications.ux.DragDrop.implementation(d);
});
}
}
/**
* Handle dragStart event.
* @param {DragEvent} event
@ -472,7 +503,10 @@ export default function DHApplicationMixin(Base) {
icon: 'fa-solid fa-explosion',
condition: target => {
const doc = getDocFromElementSync(target);
return doc?.system?.attack?.damage.parts.length || doc?.damage?.parts.length;
return (
!foundry.utils.isEmpty(doc?.system?.attack?.damage.parts) ||
!foundry.utils.isEmpty(doc?.damage?.parts)
);
},
callback: async (target, event) => {
const doc = await getDocFromElement(target),
@ -664,6 +698,9 @@ export default function DHApplicationMixin(Base) {
case 'weapon':
presets.folder = 'equipments.folders.weapons';
break;
case 'feature':
presets.folder = 'features';
break;
case 'domainCard':
presets.folder = 'domains';
presets.filter = {

View file

@ -251,6 +251,12 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
/* If any noticeable slowdown occurs, consider replacing with enriching description on clicking to expand descriptions */
for (const item of this.items) {
if (['weapon', 'armor'].includes(item.type)) {
item.system.enrichedTags = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/global/partials/item-tags.hbs',
item.system
);
}
item.system.enrichedDescription =
(await item.system.getEnrichedDescription?.()) ??
(await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.description));

View file

@ -43,6 +43,53 @@ export default class DhRegionLayer extends foundry.canvas.layers.RegionLayer {
const hole = ui.controls.controls[this.options.name].tools.hole?.active ?? false;
if (game.activeTool === 'inFront') return { type: 'cone', x: 0, y: 0, radius: 0, angle: 180, hole };
return super._createDragShapeData(event);
const shape = super._createDragShapeData(event);
const token = shape?.type === 'emanation' && shape.base?.type === 'token' ? this.#findTokenInBounds(event.interactionData.origin) : null;
if (token) {
shape.base.width = token.width;
shape.base.height = token.height;
event.interactionData.origin = token.getCenterPoint();
}
return shape;
}
async placeRegion(data, options = {}) {
const preConfirm = ({ _event, document, _create, _options }) => {
const shape = document.shapes[0];
const isEmanation = shape.type === 'emanation';
if (isEmanation) {
const token = this.#findTokenInBounds(shape.base.origin);
if (!token) return options.preConfirm?.() ?? true;
const shapeData = shape.toObject();
document.updateSource({
shapes: [
{
...shapeData,
base: {
...shapeData.base,
height: token.height,
width: token.width,
x: token.x,
y: token.y
}
}
]
});
}
return options?.preConfirm?.() ?? true;
};
super.placeRegion(data, { ...options, preConfirm });
}
/** Searches for token at origin point, returning null if there are no tokens or multiple overlapping tokens */
#findTokenInBounds(origin) {
const { x, y } = origin;
const gridSize = canvas.grid.size;
const inBounds = canvas.scene.tokens.filter(t => {
return x.between(t.x, t.x + t.width * gridSize) && y.between(t.y, t.y + t.height * gridSize);
});
return inBounds.length === 1 ? inBounds[0] : null;
}
}

View file

@ -1,116 +0,0 @@
export default class DhTemplateLayer extends foundry.canvas.layers.TemplateLayer {
static prepareSceneControls() {
const sc = foundry.applications.ui.SceneControls;
return {
name: 'templates',
order: 2,
title: 'CONTROLS.GroupMeasure',
icon: 'fa-solid fa-ruler-combined',
visible: game.user.can('REGION_CREATE'),
onChange: (event, active) => {
if (active) canvas.templates.activate();
},
onToolChange: () => canvas.templates.setAllRenderFlags({ refreshState: true }),
tools: {
circle: {
name: 'circle',
order: 1,
title: 'CONTROLS.MeasureCircle',
icon: 'fa-regular fa-circle',
toolclip: {
src: 'toolclips/tools/measure-circle.webm',
heading: 'CONTROLS.MeasureCircle',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete'])
}
},
cone: {
name: 'cone',
order: 2,
title: 'CONTROLS.MeasureCone',
icon: 'fa-solid fa-angle-left',
toolclip: {
src: 'toolclips/tools/measure-cone.webm',
heading: 'CONTROLS.MeasureCone',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
}
},
inFront: {
name: 'inFront',
order: 3,
title: 'CONTROLS.inFront',
icon: 'fa-solid fa-eye',
toolclip: {
src: 'toolclips/tools/measure-cone.webm',
heading: 'CONTROLS.inFront',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
}
},
rect: {
name: 'rect',
order: 4,
title: 'CONTROLS.MeasureRect',
icon: 'fa-regular fa-square',
toolclip: {
src: 'toolclips/tools/measure-rect.webm',
heading: 'CONTROLS.MeasureRect',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
}
},
ray: {
name: 'ray',
order: 5,
title: 'CONTROLS.MeasureRay',
icon: 'fa-solid fa-up-down',
toolclip: {
src: 'toolclips/tools/measure-ray.webm',
heading: 'CONTROLS.MeasureRay',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
}
},
clear: {
name: 'clear',
order: 6,
title: 'CONTROLS.MeasureClear',
icon: 'fa-solid fa-trash',
visible: game.user.isGM,
onChange: () => canvas.templates.deleteAll(),
button: true
}
},
activeTool: 'circle'
};
}
_onDragLeftStart(event) {
const interaction = event.interactionData;
// Snap the origin to the grid
if (!event.shiftKey) interaction.origin = this.getSnappedPoint(interaction.origin);
// Create a pending MeasuredTemplateDocument
const tool = game.activeTool === 'inFront' ? 'cone' : game.activeTool;
const previewData = {
user: game.user.id,
t: tool,
x: interaction.origin.x,
y: interaction.origin.y,
sort: Math.max(this.getMaxSort() + 1, 0),
distance: 1,
direction: 0,
fillColor: game.user.color || '#FF0000',
hidden: event.altKey
};
const defaults = CONFIG.MeasuredTemplate.defaults;
if (game.activeTool === 'cone') previewData.angle = defaults.angle;
else if (game.activeTool === 'inFront') previewData.angle = 180;
else if (game.activeTool === 'ray') previewData.width = defaults.width * canvas.dimensions.distance;
const cls = foundry.utils.getDocumentClass('MeasuredTemplate');
const doc = new cls(previewData, { parent: canvas.scene });
// Create a preview MeasuredTemplate object
const template = new this.constructor.placeableClass(doc);
doc._object = template;
interaction.preview = this.preview.addChild(template);
template.draw();
}
}

View file

@ -11,3 +11,4 @@ export * as settingsConfig from './settingsConfig.mjs';
export * as systemConfig from './system.mjs';
export * as itemBrowserConfig from './itemBrowserConfig.mjs';
export * as triggerConfig from './triggerConfig.mjs';
export * as resourceConfig from './resourceConfig.mjs';

View file

@ -35,6 +35,12 @@ export const actionTypes = {
icon: 'fa-ghost',
tooltip: 'DAGGERHEART.ACTIONS.TYPES.summon.tooltip'
},
transform: {
id: 'transform',
name: 'DAGGERHEART.ACTIONS.TYPES.transform.name',
icon: 'fa-dragon',
tooltip: 'DAGGERHEART.ACTIONS.TYPES.transform.tooltip'
},
effect: {
id: 'effect',
name: 'DAGGERHEART.ACTIONS.TYPES.effect.name',

View file

@ -55,24 +55,6 @@ export const abilities = {
}
};
export const scrollingTextResource = {
hitPoints: {
label: 'DAGGERHEART.GENERAL.HitPoints.plural',
reversed: true
},
stress: {
label: 'DAGGERHEART.GENERAL.stress',
reversed: true
},
hope: {
label: 'DAGGERHEART.GENERAL.hope'
},
armor: {
label: 'DAGGERHEART.GENERAL.armor',
reversed: true
}
};
export const featureProperties = {
agility: {
name: 'DAGGERHEART.CONFIG.Traits.agility.name',

View file

@ -245,8 +245,8 @@ export const defaultRestOptions = {
type: 'friendly'
},
damage: {
parts: [
{
parts: {
hitPoints: {
applyTo: healingTypes.hitPoints.id,
value: {
custom: {
@ -255,7 +255,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
}
},
@ -279,8 +279,8 @@ export const defaultRestOptions = {
type: 'self'
},
damage: {
parts: [
{
parts: {
stress: {
applyTo: healingTypes.stress.id,
value: {
custom: {
@ -289,7 +289,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
}
},
@ -314,8 +314,8 @@ export const defaultRestOptions = {
type: 'friendly'
},
damage: {
parts: [
{
parts: {
armor: {
applyTo: healingTypes.armor.id,
value: {
custom: {
@ -324,7 +324,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
}
},
@ -348,8 +348,8 @@ export const defaultRestOptions = {
type: 'self'
},
damage: {
parts: [
{
parts: {
hope: {
applyTo: healingTypes.hope.id,
value: {
custom: {
@ -358,7 +358,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
},
prepareWithFriends: {
@ -372,8 +372,8 @@ export const defaultRestOptions = {
type: 'self'
},
damage: {
parts: [
{
parts: {
hope: {
applyTo: healingTypes.hope.id,
value: {
custom: {
@ -382,7 +382,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
}
},
@ -409,8 +409,8 @@ export const defaultRestOptions = {
type: 'friendly'
},
damage: {
parts: [
{
parts: {
hitPoints: {
applyTo: healingTypes.hitPoints.id,
value: {
custom: {
@ -419,7 +419,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
}
},
@ -443,8 +443,8 @@ export const defaultRestOptions = {
type: 'self'
},
damage: {
parts: [
{
parts: {
stress: {
applyTo: healingTypes.stress.id,
value: {
custom: {
@ -453,7 +453,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
}
},
@ -478,8 +478,8 @@ export const defaultRestOptions = {
type: 'friendly'
},
damage: {
parts: [
{
parts: {
armor: {
applyTo: healingTypes.armor.id,
value: {
custom: {
@ -488,7 +488,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
}
},
@ -512,8 +512,8 @@ export const defaultRestOptions = {
type: 'self'
},
damage: {
parts: [
{
parts: {
hope: {
applyTo: healingTypes.hope.id,
value: {
custom: {
@ -522,7 +522,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
},
prepareWithFriends: {
@ -536,8 +536,8 @@ export const defaultRestOptions = {
type: 'self'
},
damage: {
parts: [
{
parts: {
hope: {
applyTo: healingTypes.hope.id,
value: {
custom: {
@ -546,7 +546,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
}
},
@ -634,7 +634,95 @@ export const diceSetNumbers = {
flat: 'Flat'
};
export const getDiceSoNicePreset = async (type, faces) => {
export const diceSoNiceSFXClasses = {
PlayAnimationBright: {
id: 'PlayAnimationBright',
label: 'DICESONICE.PlayAnimationBright'
},
PlayAnimationDark: {
id: 'PlayAnimationDark',
label: 'DICESONICE.PlayAnimationDark'
},
PlayAnimationOutline: {
id: 'PlayAnimationOutline',
label: 'DICESONICE.PlayAnimationOutline'
},
PlayAnimationImpact: {
id: 'PlayAnimationImpact',
label: 'DICESONICE.PlayAnimationImpact'
},
// PlayConfettiStrength1: {
// id: 'PlayConfettiStrength1',
// label: 'DICESONICE.PlayConfettiStrength1'
// },
// PlayConfettiStrength2: {
// id: 'PlayConfettiStrength2',
// label: 'DICESONICE.PlayConfettiStrength2'
// },
// PlayConfettiStrength3: {
// id: 'PlayConfettiStrength3',
// label: 'DICESONICE.PlayConfettiStrength3'
// },
PlayAnimationThormund: {
id: 'PlayAnimationThormund',
label: 'DICESONICE.PlayAnimationThormund'
},
PlayAnimationParticleSpiral: {
id: 'PlayAnimationParticleSpiral',
label: 'DICESONICE.PlayAnimationParticleSpiral'
},
PlayAnimationParticleSparkles: {
id: 'PlayAnimationParticleSparkles',
label: 'DICESONICE.PlayAnimationParticleSparkles'
},
PlayAnimationParticleVortex: {
id: 'PlayAnimationParticleVortex',
label: 'DICESONICE.PlayAnimationParticleVortex'
},
PlaySoundEpicWin: {
id: 'PlaySoundEpicWin',
label: 'DICESONICE.PlaySoundEpicWin'
},
PlaySoundEpicFail: {
id: 'PlaySoundEpicFail',
label: 'DICESONICE.PlaySoundEpicFail'
}
// "PlaySoundCustom",
// "PlayMacro"
};
export const daggerheartDiceAnimationEvents = {
critical: {
id: 'critical',
label: 'DAGGERHEART.CONFIG.DaggerheartDiceAnimationEvents.critical.name'
},
higher: {
id: 'higher',
label: 'DAGGERHEART.CONFIG.DaggerheartDiceAnimationEvents.higher.name'
}
};
const getDiceSoNiceSFX = sfxOptions => {
const diceSoNice = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance).diceSoNiceData;
const criticalAnimationData = diceSoNice.sfx.critical;
if (sfxOptions.critical && criticalAnimationData.class) {
return {
specialEffect: criticalAnimationData.class,
options: {}
};
}
if (sfxOptions.higher && sfxOptions.data.higher) {
return {
specialEffect: sfxOptions.data.higher.class,
options: {}
};
}
return {};
};
export const getDiceSoNicePreset = async (type, faces, sfxOptions = {}) => {
const system = game.dice3d.DiceFactory.systems.get(type.system).dice.get(faces);
if (!system) {
ui.notifications.error(
@ -657,16 +745,33 @@ export const getDiceSoNicePreset = async (type, faces) => {
appearance: {
...system.appearance,
...type
}
},
sfx: getDiceSoNiceSFX(sfxOptions)
};
};
export const getDiceSoNicePresets = async (hopeFaces, fearFaces, advantageFaces = 'd6', disadvantageFaces = 'd6') => {
const { diceSoNice } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
export const getDiceSoNicePresets = async (
result,
hopeFaces,
fearFaces,
advantageFaces = 'd6',
disadvantageFaces = 'd6'
) => {
const diceSoNice = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance).diceSoNiceData;
const { isCritical, withHope, withFear } = result;
return {
hope: await getDiceSoNicePreset(diceSoNice.hope, hopeFaces),
fear: await getDiceSoNicePreset(diceSoNice.fear, fearFaces),
hope: await getDiceSoNicePreset(diceSoNice.hope, hopeFaces, {
critical: isCritical,
higher: withHope,
data: diceSoNice.hope.sfx
}),
fear: await getDiceSoNicePreset(diceSoNice.fear, fearFaces, {
critical: isCritical,
higher: withFear,
data: diceSoNice.fear.sfx
}),
advantage: await getDiceSoNicePreset(diceSoNice.advantage, advantageFaces),
disadvantage: await getDiceSoNicePreset(diceSoNice.disadvantage, disadvantageFaces)
};

View file

@ -1,3 +1,4 @@
export const hooksConfig = {
effectDisplayToggle: 'DHEffectDisplayToggle'
effectDisplayToggle: 'DHEffectDisplayToggle',
lockedTooltipDismissed: 'DHLockedTooltipDismissed'
};

View file

@ -14,8 +14,8 @@ export const armorFeatures = {
type: 'hostile'
},
damage: {
parts: [
{
parts: {
stress: {
applyTo: 'stress',
value: {
custom: {
@ -24,7 +24,7 @@ export const armorFeatures = {
}
}
}
]
}
}
}
]
@ -732,8 +732,8 @@ export const weaponFeatures = {
type: 'hostile'
},
damage: {
parts: [
{
parts: {
stress: {
applyTo: 'stress',
value: {
custom: {
@ -742,7 +742,7 @@ export const weaponFeatures = {
}
}
}
]
}
}
}
],
@ -914,8 +914,8 @@ export const weaponFeatures = {
type: 'self'
},
damage: {
parts: [
{
parts: {
hitPoints: {
applyTo: 'hitPoints',
value: {
custom: {
@ -924,7 +924,7 @@ export const weaponFeatures = {
}
}
}
]
}
}
}
]

View file

@ -0,0 +1,88 @@
/**
* Full custom typing:
* id
* initial
* max
* reverse
* label
* images {
* full { value, isIcon, noColorFilter }
* empty { value, isIcon noColorFilter }
* }
*/
const characterBaseResources = Object.freeze({
hitPoints: {
id: 'hitPoints',
initial: 0,
max: 0,
reverse: true,
label: 'DAGGERHEART.GENERAL.HitPoints.plural',
maxLabel: 'DAGGERHEART.ACTORS.Character.maxHPBonus'
},
stress: {
id: 'stress',
initial: 0,
max: 6,
reverse: true,
label: 'DAGGERHEART.GENERAL.stress'
},
hope: {
id: 'hope',
initial: 2,
reverse: false,
label: 'DAGGERHEART.GENERAL.hope'
}
});
const adversaryBaseResources = Object.freeze({
hitPoints: {
id: 'hitPoints',
initial: 0,
max: 0,
reverse: true,
label: 'DAGGERHEART.GENERAL.HitPoints.plural',
maxLabel: 'DAGGERHEART.ACTORS.Character.maxHPBonus'
},
stress: {
id: 'stress',
initial: 0,
max: 0,
reverse: true,
label: 'DAGGERHEART.GENERAL.stress'
}
});
const companionBaseResources = Object.freeze({
stress: {
id: 'stress',
initial: 0,
max: 0,
reverse: true,
label: 'DAGGERHEART.GENERAL.stress'
},
hope: {
id: 'hope',
initial: 0,
reverse: false,
label: 'DAGGERHEART.GENERAL.hope'
}
});
export const character = {
base: characterBaseResources,
custom: {}, // module stuff goes here
all: { ...characterBaseResources }
};
export const adversary = {
base: adversaryBaseResources,
custom: {}, // module stuff goes here
all: { ...adversaryBaseResources }
};
export const companion = {
base: companionBaseResources,
custom: {}, // module stuff goes here
all: { ...companionBaseResources }
};

View file

@ -3,6 +3,10 @@ export const menu = {
Name: 'GameSettingsAutomation',
Icon: 'fa-solid fa-robot'
},
Metagaming: {
Name: 'GameSettingsMetagaming',
Icon: 'fa-solid fa-eye-low-vision'
},
Homebrew: {
Name: 'GameSettingsHomebrew',
Icon: 'fa-solid fa-flask-vial'
@ -19,8 +23,10 @@ export const menu = {
export const gameSettings = {
Automation: 'Automation',
Metagaming: 'Metagaming',
Homebrew: 'Homebrew',
appearance: 'Appearance',
GlobalOverrides: 'GlobalOverrides',
variantRules: 'VariantRules',
Resources: {
Fear: 'ResourcesFear'

View file

@ -2,6 +2,7 @@ import * as GENERAL from './generalConfig.mjs';
import * as DOMAIN from './domainConfig.mjs';
import * as ENCOUNTER from './encounterConfig.mjs';
import * as ACTOR from './actorConfig.mjs';
import * as RESOURCE from './resourceConfig.mjs';
import * as ITEM from './itemConfig.mjs';
import * as SETTINGS from './settingsConfig.mjs';
import * as EFFECTS from './effectConfig.mjs';
@ -19,6 +20,7 @@ export const SYSTEM = {
GENERAL,
DOMAIN,
ACTOR,
RESOURCE,
ITEM,
SETTINGS,
EFFECTS,

View file

@ -7,6 +7,7 @@ import EffectAction from './effectAction.mjs';
import HealingAction from './healingAction.mjs';
import MacroAction from './macroAction.mjs';
import SummonAction from './summonAction.mjs';
import TransformAction from './transformAction.mjs';
export const actionsTypes = {
base: BaseAction,
@ -17,5 +18,6 @@ export const actionsTypes = {
summon: SummonAction,
effect: EffectAction,
macro: MacroAction,
beastform: BeastformAction
beastform: BeastformAction,
transform: TransformAction
};

View file

@ -26,23 +26,23 @@ export default class DHAttackAction extends DHDamageAction {
return {
value: {
multiplier: 'prof',
dice: this.item?.system?.attack.damage.parts[0].value.dice,
bonus: this.item?.system?.attack.damage.parts[0].value.bonus ?? 0
dice: this.item?.system?.attack.damage.parts.hitPoints.value.dice,
bonus: this.item?.system?.attack.damage.parts.hitPoints.value.bonus ?? 0
},
type: this.item?.system?.attack.damage.parts[0].type,
type: this.item?.system?.attack.damage.parts.hitPoints.type,
base: true
};
}
get damageFormula() {
const hitPointsPart = this.damage.parts.find(x => x.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
const hitPointsPart = this.damage.parts.hitPoints;
if (!hitPointsPart) return '0';
return hitPointsPart.value.getFormula();
}
get altDamageFormula() {
const hitPointsPart = this.damage.parts.find(x => x.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
const hitPointsPart = this.damage.parts.hitPoints;
if (!hitPointsPart) return '0';
return hitPointsPart.valueAlt.getFormula();

View file

@ -197,7 +197,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
async executeWorkflow(config) {
for (const [key, part] of this.workflow) {
if (Hooks.call(`${CONFIG.DH.id}.pre${key.capitalize()}Action`, this, config) === false) return;
if ((await part.execute(config)) === false) return;
if ((await part.execute(config)) === false) return false;
if (Hooks.call(`${CONFIG.DH.id}.post${key.capitalize()}Action`, this, config) === false) return;
}
}
@ -224,7 +224,9 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
}
// Execute the Action Worflow in order based of schema fields
await this.executeWorkflow(config);
const result = await this.executeWorkflow(config);
if (result === false) return;
await config.resourceUpdates.updateResources();
if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return;
@ -352,11 +354,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
}
get hasDamage() {
return this.damage?.parts?.length && this.type !== 'healing';
return !foundry.utils.isEmpty(this.damage?.parts) && this.type !== 'healing';
}
get hasHealing() {
return this.damage?.parts?.length && this.type === 'healing';
return !foundry.utils.isEmpty(this.damage?.parts) && this.type === 'healing';
}
get hasSave() {
@ -376,6 +378,15 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return tags;
}
static migrateData(source) {
if (source.damage?.parts && Array.isArray(source.damage.parts)) {
source.damage.parts = source.damage.parts.reduce((acc, part) => {
acc[part.applyTo] = part;
return acc;
}, {});
}
}
}
export class ResourceUpdateMap extends Map {

View file

@ -0,0 +1,5 @@
import DHBaseAction from './baseAction.mjs';
export default class DHTransformAction extends DHBaseAction {
static extraSchemas = [...super.extraSchemas, 'transform'];
}

View file

@ -2,7 +2,7 @@ import DHAdversarySettings from '../../applications/sheets-configs/adversary-set
import { ActionField } from '../fields/actionField.mjs';
import { commonActorRules } from './base.mjs';
import DhCreature from './creature.mjs';
import { resourceField, bonusField } from '../fields/actorField.mjs';
import { bonusField } from '../fields/actorField.mjs';
import { calculateExpectedValue, parseTermsFromSimpleFormula } from '../../helpers/utils.mjs';
import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs';
@ -65,10 +65,6 @@ export default class DhpAdversary extends DhCreature {
label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold'
})
}),
resources: new fields.SchemaField({
hitPoints: resourceField(0, 0, 'DAGGERHEART.GENERAL.HitPoints.plural', true),
stress: resourceField(0, 0, 'DAGGERHEART.GENERAL.stress', true)
}),
rules: new fields.SchemaField({
...commonActorRules()
}),
@ -89,14 +85,14 @@ export default class DhpAdversary extends DhCreature {
type: 'attack'
},
damage: {
parts: [
{
parts: {
hitPoints: {
type: ['physical'],
value: {
multiplier: 'flat'
}
}
]
}
}
}
}),
@ -191,6 +187,7 @@ export default class DhpAdversary extends DhCreature {
}
prepareDerivedData() {
super.prepareDerivedData();
this.attack.roll.isStandardAttack = true;
}
@ -268,12 +265,12 @@ export default class DhpAdversary extends DhCreature {
}
// Update damage in item actions
for (const action of Object.values(item.system.actions)) {
if (!action.damage) continue;
// Parse damage, and convert all formula matches in the descriptions to the new damage
for (const action of Object.values(item.system.actions)) {
try {
const result = this.#adjustActionDamage(action, { ...damageMeta, type: 'action' });
if (!result) continue;
for (const { previousFormula, formula } of Object.values(result)) {
const oldFormulaRegexp = new RegExp(
previousFormula.replace(' ', '').replace('+', '(?:\\s)?\\+(?:\\s)?')
@ -375,16 +372,14 @@ export default class DhpAdversary extends DhCreature {
/**
* Updates damage to reflect a specific value.
* @throws if damage structure is invalid for conversion
* @returns the converted formula and value as a simplified term
* @returns the converted formula and value as a simplified term, or null if it doesn't deal HP damage
*/
#adjustActionDamage(action, damageMeta) {
// The current algorithm only returns a value if there is a single damage part
const hpDamageParts = action.damage.parts.filter(d => d.applyTo === 'hitPoints');
if (hpDamageParts.length !== 1) throw new Error('incorrect number of hp parts');
if (!action.damage?.parts.hitPoints) return null;
const result = {};
for (const property of ['value', 'valueAlt']) {
const data = hpDamageParts[0][property];
const data = action.damage.parts.hitPoints[property];
const previousFormula = data.custom.enabled
? data.custom.formula
: [data.flatMultiplier ? `${data.flatMultiplier}${data.dice}` : 0, data.bonus ?? 0]

View file

@ -211,7 +211,7 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
const textData = Object.keys(changes.system.resources).reduce((acc, key) => {
const resource = changes.system.resources[key];
if (resource.value !== undefined && resource.value !== this.resources[key].value) {
acc.push(getScrollTextData(this.resources, resource, key));
acc.push(getScrollTextData(this.parent, resource, key));
}
return acc;

View file

@ -3,7 +3,7 @@ import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import DhLevelData from '../levelData.mjs';
import { commonActorRules } from './base.mjs';
import DhCreature from './creature.mjs';
import { attributeField, resourceField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs';
import { attributeField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs';
import { ActionField } from '../fields/actionField.mjs';
import DHCharacterSettings from '../../applications/sheets-configs/character-settings.mjs';
@ -27,28 +27,6 @@ export default class DhCharacter extends DhCreature {
return {
...super.defineSchema(),
resources: new fields.SchemaField({
hitPoints: resourceField(
0,
0,
'DAGGERHEART.GENERAL.HitPoints.plural',
true,
'DAGGERHEART.ACTORS.Character.maxHPBonus'
),
stress: resourceField(6, 0, 'DAGGERHEART.GENERAL.stress', true),
hope: new fields.SchemaField(
{
value: new fields.NumberField({
initial: 2,
min: 0,
integer: true,
label: 'DAGGERHEART.GENERAL.hope'
}),
isReversed: new fields.BooleanField({ initial: false })
},
{ label: 'DAGGERHEART.GENERAL.hope' }
)
}),
traits: new fields.SchemaField({
agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'),
strength: attributeField('DAGGERHEART.CONFIG.Traits.strength.name'),
@ -118,8 +96,8 @@ export default class DhCharacter extends DhCreature {
trait: 'strength'
},
damage: {
parts: [
{
parts: {
hitPoints: {
type: ['physical'],
value: {
custom: {
@ -128,7 +106,7 @@ export default class DhCharacter extends DhCreature {
}
}
}
]
}
}
}
}),
@ -609,6 +587,7 @@ export default class DhCharacter extends DhCreature {
}
prepareBaseData() {
super.prepareBaseData();
this.evasion += this.class.value?.system?.evasion ?? 0;
const currentLevel = this.levelData.level.current;
@ -680,6 +659,7 @@ export default class DhCharacter extends DhCreature {
}
prepareDerivedData() {
super.prepareDerivedData();
let baseHope = this.resources.hope.value;
if (this.companion) {
for (let levelKey in this.companion.system.levelData.levelups) {
@ -699,12 +679,13 @@ export default class DhCharacter extends DhCreature {
this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait;
this.resources.armor = {
label: 'DAGGERHEART.GENERAL.armor',
value: this.armor?.system?.marks?.value ?? 0,
max: this.armorScore,
isReversed: true
};
this.attack.damage.parts[0].value.custom.formula = `@prof${this.basicAttackDamageDice}${this.rules.attack.damage.bonus ? ` + ${this.rules.attack.damage.bonus}` : ''}`;
this.attack.damage.parts.hitPoints.value.custom.formula = `@prof${this.basicAttackDamageDice}${this.rules.attack.damage.bonus ? ` + ${this.rules.attack.damage.bonus}` : ''}`;
}
getRollData() {
@ -740,7 +721,8 @@ export default class DhCharacter extends DhCreature {
const newHopeMax = this.system.resources.hope.max + diff;
const newHopeValue = Math.min(newHopeMax, this.system.resources.hope.value);
if (newHopeValue != this.system.resources.hope.value) {
if (!changes.system.resources) changes.system.resources = { hope: { value: 0 } };
if (!changes.system.resources.hope) changes.system.resources.hope = { value: 0 };
changes.system.resources.hope = {
...changes.system.resources.hope,
value: changes.system.resources.hope.value + newHopeValue

View file

@ -4,7 +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';
import { bonusField } from '../fields/actorField.mjs';
export default class DhCompanion extends DhCreature {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Companion'];
@ -26,10 +26,6 @@ export default class DhCompanion extends DhCreature {
return {
...super.defineSchema(),
partner: new ForeignDocumentUUIDField({ type: 'Actor' }),
resources: new fields.SchemaField({
stress: resourceField(3, 0, 'DAGGERHEART.GENERAL.stress', true),
hope: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.hope' })
}),
evasion: new fields.NumberField({
required: true,
min: 1,
@ -85,15 +81,15 @@ export default class DhCompanion extends DhCreature {
bonus: 0
},
damage: {
parts: [
{
parts: {
hitPoints: {
type: ['physical'],
value: {
dice: 'd6',
multiplier: 'prof'
}
}
]
}
}
}
}),
@ -127,6 +123,7 @@ export default class DhCompanion extends DhCreature {
}
prepareBaseData() {
super.prepareBaseData();
this.attack.roll.bonus = this.partner?.system?.spellcastModifier ?? 0;
for (let levelKey in this.levelData.levelups) {
@ -138,7 +135,9 @@ export default class DhCompanion extends DhCreature {
break;
case 'vicious':
if (selection.data[0] === 'damage') {
this.attack.damage.parts[0].value.dice = adjustDice(this.attack.damage.parts[0].value.dice);
this.attack.damage.parts.hitPoints.value.dice = adjustDice(
this.attack.damage.parts.hitPoints.value.dice
);
} else {
this.attack.range = adjustRange(this.attack.range).id;
}
@ -161,6 +160,7 @@ export default class DhCompanion extends DhCreature {
}
prepareDerivedData() {
super.prepareDerivedData();
/* Partner Related Setup */
if (this.partner) {
this.levelData.level.changed = this.partner.system.levelData.level.current;

View file

@ -1,3 +1,4 @@
import { ResourcesField } from '../fields/actorField.mjs';
import BaseDataActor from './base.mjs';
export default class DhCreature extends BaseDataActor {
@ -7,6 +8,7 @@ export default class DhCreature extends BaseDataActor {
return {
...super.defineSchema(),
resources: new ResourcesField(this.metadata.type),
advantageSources: new fields.ArrayField(new fields.StringField(), {
label: 'DAGGERHEART.ACTORS.Character.advantageSources.label',
hint: 'DAGGERHEART.ACTORS.Character.advantageSources.hint'

View file

@ -37,7 +37,7 @@ export default class DhEnvironment extends BaseDataActor {
potentialAdversaries: new fields.TypedObjectField(
new fields.SchemaField({
label: new fields.StringField(),
adversaries: new ForeignDocumentUUIDArrayField({ type: 'Actor' }, { required: false, initial: [] })
adversaries: new ForeignDocumentUUIDArrayField({ type: 'Actor' })
})
),
notes: new fields.HTMLField()

View file

@ -1,4 +1,5 @@
export { ActionCollection } from './actionField.mjs';
export { default as IterableTypedObjectField } from './iterableTypedObjectField.mjs';
export { default as FormulaField } from './formulaField.mjs';
export { default as ForeignDocumentUUIDField } from './foreignDocumentUUIDField.mjs';
export { default as ForeignDocumentUUIDArrayField } from './foreignDocumentUUIDArrayField.mjs';

View file

@ -10,3 +10,4 @@ export { default as DamageField } from './damageField.mjs';
export { default as RollField } from './rollField.mjs';
export { default as MacroField } from './macroField.mjs';
export { default as SummonField } from './summonField.mjs';
export { default as TransformField } from './transformField.mjs';

View file

@ -1,5 +1,6 @@
import FormulaField from '../formulaField.mjs';
import { setsEqual } from '../../../helpers/utils.mjs';
import IterableTypedObjectField from '../iterableTypedObjectField.mjs';
const fields = foundry.data.fields;
@ -12,7 +13,7 @@ export default class DamageField extends fields.SchemaField {
/** @inheritDoc */
constructor(options, context = {}) {
const damageFields = {
parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)),
parts: new IterableTypedObjectField(DHDamageData),
includeBase: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.ACTIONS.Settings.includeBase.label'

View file

@ -0,0 +1,103 @@
const fields = foundry.data.fields;
export default class DHSummonField extends fields.SchemaField {
/**
* Action Workflow order
*/
static order = 130;
constructor(options = {}, context = {}) {
const transformFields = {
actorUUID: new fields.DocumentUUIDField({
type: 'Actor',
required: true
}),
resourceRefresh: new fields.SchemaField({
hitPoints: new fields.BooleanField({ initial: true }),
stress: new fields.BooleanField({ initial: true })
})
};
super(transformFields, options, context);
}
static async execute() {
if (!this.transform.actorUUID) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.transform.noTransformActor'));
return false;
}
const baseActor = await foundry.utils.fromUuid(this.transform.actorUUID);
if (!baseActor) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.transform.transformActorMissing'));
return false;
}
if (!canvas.scene) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.transform.canvasError'));
return false;
}
if (this.actor.prototypeToken.actorLink) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.transform.actorLinkError'));
return false;
}
if (!this.actor.token) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.transform.prototypeError'));
return false;
}
const actor = await DHSummonField.getWorldActor(baseActor);
const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes;
const tokenSize = actor?.system.metadata.usesSize ? tokenSizes[actor.system.size] : actor.prototypeToken.width;
await this.actor.token.update(
{ ...actor.prototypeToken.toJSON(), actorId: actor.id, width: tokenSize, height: tokenSize },
{ diff: false, recursive: false, noHook: true }
);
if (this.actor.token.combatant) {
this.actor.token.combatant.update({ actorId: actor.id, img: actor.prototypeToken.texture.src });
}
const marks = { hitPoints: 0, stress: 0 };
if (!this.transform.resourceRefresh.hitPoints) {
marks.hitPoints = Math.min(
this.actor.system.resources.hitPoints.value,
this.actor.token.actor.system.resources.hitPoints.max - 1
);
}
if (!this.transform.resourceRefresh.stress) {
marks.stress = Math.min(
this.actor.system.resources.stress.value,
this.actor.token.actor.system.resources.stress.max - 1
);
}
if (marks.hitPoints || marks.stress) {
this.actor.token.actor.update({
'system.resources': {
hitPoints: { value: marks.hitPoints },
stress: { value: marks.stress }
}
});
}
const prevPosition = { ...this.actor.sheet.position };
this.actor.sheet.close();
this.actor.token.actor.sheet.render({ force: true, position: prevPosition });
}
/* Check for any available instances of the actor present in the world, or create a world actor based on compendium */
static async getWorldActor(baseActor) {
if (!baseActor.inCompendium) return baseActor;
const dataType = game.system.api.data.actors[`Dh${baseActor.type.capitalize()}`];
if (dataType && baseActor.img === dataType.DEFAULT_ICON) {
const worldActorCopy = game.actors.find(x => x.name === baseActor.name);
if (worldActorCopy) return worldActorCopy;
}
const worldActor = await game.system.api.documents.DhpActor.create(baseActor.toObject());
return worldActor;
}
}

View file

@ -87,10 +87,10 @@ export class ActionField extends foundry.data.fields.ObjectField {
/* -------------------------------------------- */
/** @override */
_cleanType(value, options) {
_cleanType(value, options, _state) {
if (!(typeof value === 'object')) value = {};
const cls = this.getModel(value);
if (cls) return cls.cleanData(value, options);
if (cls) return cls.cleanData(value, options, _state);
return value;
}

View file

@ -6,22 +6,6 @@ const attributeField = label =>
tierMarked: new fields.BooleanField({ initial: false })
});
const resourceField = (max = 0, initial = 0, label, reverse = false, maxLabel) =>
new fields.SchemaField(
{
value: new fields.NumberField({ initial: initial, min: 0, integer: true, label }),
max: new fields.NumberField({
initial: max,
integer: true,
label:
maxLabel ??
game.i18n.format('DAGGERHEART.GENERAL.maxWithThing', { thing: game.i18n.localize(label) })
}),
isReversed: new fields.BooleanField({ initial: reverse })
},
{ label }
);
const stressDamageReductionRule = localizationPath =>
new fields.SchemaField({
cost: new fields.NumberField({
@ -37,4 +21,67 @@ const bonusField = label =>
dice: new fields.ArrayField(new fields.StringField(), { label: `${game.i18n.localize(label)} Dice` })
});
export { attributeField, resourceField, stressDamageReductionRule, bonusField };
/**
* Field used for actor resources. It is a resource that validates dynamically based on the config.
* Because "max" may be defined during runtime, we don't attempt to clamp the maximum value.
*/
class ResourcesField extends fields.TypedObjectField {
constructor(actorType) {
super(
new fields.SchemaField({
value: new fields.NumberField({ min: 0, initial: 0, integer: true }),
// Some resources allow changing max. A null max means its the default
max: new fields.NumberField({ initial: null, integer: true, nullable: true })
})
);
this.actorType = actorType;
}
getInitialValue() {
const resources = CONFIG.DH.RESOURCE[this.actorType].all;
return Object.values(resources).reduce((result, resource) => {
result[resource.id] = {
value: resource.initial,
max: null
};
return result;
}, {});
}
_validateKey(key) {
return key in CONFIG.DH.RESOURCE[this.actorType].all;
}
_cleanType(value, options, _state) {
value = super._cleanType(value, options, _state);
// If not partial, ensure all data exists
if (!options.partial) {
value = foundry.utils.mergeObject(this.getInitialValue(), value);
}
return value;
}
/** Initializes the original source data, returning prepared data */
initialize(...args) {
const data = super.initialize(...args);
const resources = CONFIG.DH.RESOURCE[this.actorType].all;
for (const [key, value] of Object.entries(data)) {
// TypedObjectField only calls _validateKey when persisting, so we also call it here
if (!this._validateKey(key)) {
delete value[key];
continue;
}
// Add basic prepared data.
const resource = resources[key];
value.label = resource.label;
value.isReversed = resources[key].reverse;
value.max = typeof resource.max === 'number' ? (value.max ?? resource.max) : null;
}
return data;
}
}
export { attributeField, ResourcesField, stressDamageReductionRule, bonusField };

View file

@ -0,0 +1,32 @@
export default class IterableTypedObjectField extends foundry.data.fields.TypedObjectField {
constructor(model, options = { collectionClass: foundry.utils.Collection }, context = {}) {
super(new foundry.data.fields.EmbeddedDataField(model), options, context);
this.#elementClass = model;
}
#elementClass;
/** Initializes an object with an iterator. This modifies the prototype instead of */
initialize(values) {
const object = Object.create(IterableObjectPrototype);
for (const [key, value] of Object.entries(values)) {
object[key] = new this.#elementClass(value);
}
return object;
}
}
/**
* The prototype of an iterable object.
* This allows the functionality of a class but also allows foundry.utils.getType() to return "Object" instead of "Unknown".
*/
const IterableObjectPrototype = {
[Symbol.iterator]: function*() {
for (const value of Object.values(this)) {
yield value;
}
},
map: function (func) {
return Array.from(this, func);
}
};

View file

@ -224,7 +224,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
const armorChanged =
changed.system?.marks?.value !== undefined && changed.system.marks.value !== this.marks.value;
if (armorChanged && autoSettings.resourceScrollTexts && this.parent.parent?.type === 'character') {
const armorData = getScrollTextData(this.parent.parent.system.resources, changed.system.marks, 'armor');
const armorData = getScrollTextData(this.parent.parent, changed.system.marks, 'armor');
options.scrollingTextData = [armorData];
}

View file

@ -63,15 +63,15 @@ export default class DHWeapon extends AttachableItem {
type: 'attack'
},
damage: {
parts: [
{
parts: {
hitPoints: {
type: ['physical'],
value: {
multiplier: 'prof',
dice: 'd8'
}
}
]
}
}
}
}),
@ -112,24 +112,14 @@ export default class DHWeapon extends AttachableItem {
async getDescriptionData() {
const baseDescription = this.description;
const tier = game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${this.tier}`);
const trait = game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.attack.roll.trait].label);
const range = game.i18n.localize(`DAGGERHEART.CONFIG.Range.${this.attack.range}.name`);
const damage = Roll.replaceFormulaData(this.attack.damageFormula, this.parent.parent ?? this.parent);
const burden = game.i18n.localize(CONFIG.DH.GENERAL.burden[this.burden].label);
const allFeatures = CONFIG.DH.ITEM.allWeaponFeatures();
const features = this.weaponFeatures.map(x => allFeatures[x.value]).filter(x => x);
const prefix = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/items/weapon/description.hbs',
{
features,
tier,
trait,
range,
damage,
burden
item: this,
features
}
);

View file

@ -1,6 +1,16 @@
export default class DhAppearance extends foundry.abstract.DataModel {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.SETTINGS.Appearance'];
static sfxSchema = () =>
new foundry.data.fields.SchemaField({
class: new foundry.data.fields.StringField({
nullable: true,
initial: null,
blank: true,
choices: CONFIG.DH.GENERAL.diceSoNiceSFXClasses
})
});
static defineSchema() {
const { StringField, ColorField, BooleanField, SchemaField } = foundry.data.fields;
@ -15,7 +25,10 @@ export default class DhAppearance extends foundry.abstract.DataModel {
colorset: new StringField({ initial: 'inspired', required: true, blank: false }),
material: new StringField({ initial: 'metal', required: true, blank: false }),
system: new StringField({ initial: 'standard', required: true, blank: false }),
font: new StringField({ initial: 'auto', required: true, blank: false })
font: new StringField({ initial: 'auto', required: true, blank: false }),
sfx: new SchemaField({
higher: DhAppearance.sfxSchema()
})
});
return {
@ -30,7 +43,10 @@ export default class DhAppearance extends foundry.abstract.DataModel {
hope: diceStyle({ fg: '#ffffff', bg: '#ffe760', outline: '#000000', edge: '#ffffff' }),
fear: diceStyle({ fg: '#000000', bg: '#0032b1', outline: '#ffffff', edge: '#000000' }),
advantage: diceStyle({ fg: '#ffffff', bg: '#008000', outline: '#000000', edge: '#ffffff' }),
disadvantage: diceStyle({ fg: '#000000', bg: '#b30000', outline: '#ffffff', edge: '#000000' })
disadvantage: diceStyle({ fg: '#000000', bg: '#b30000', outline: '#ffffff', edge: '#000000' }),
sfx: new SchemaField({
critical: DhAppearance.sfxSchema()
})
}),
extendCharacterDescriptions: new BooleanField(),
extendAdversaryDescriptions: new BooleanField(),
@ -65,4 +81,48 @@ export default class DhAppearance extends foundry.abstract.DataModel {
showGenericStatusEffects: new BooleanField({ initial: true })
};
}
get diceSoNiceData() {
const globalOverrides = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.GlobalOverrides);
const getSFX = (baseClientData, overrideKey) => {
if (!globalOverrides.diceSoNice.sfx.overrideEnabled) return baseClientData;
const overrideData = globalOverrides.diceSoNice.sfx[overrideKey];
const clientData = foundry.utils.deepClone(baseClientData);
return Object.keys(clientData).reduce((acc, key) => {
const data = clientData[key];
acc[key] = Object.keys(data).reduce((acc, dataKey) => {
const value = data[dataKey];
acc[dataKey] = value ? value : overrideData[key][dataKey];
return acc;
}, {});
return acc;
}, {});
};
return {
...this.diceSoNice,
sfx: getSFX(this.diceSoNice.sfx, 'global'),
hope: {
...this.diceSoNice.hope,
sfx: getSFX(this.diceSoNice.hope.sfx, 'hope')
},
fear: {
...this.diceSoNice.fear,
sfx: getSFX(this.diceSoNice.fear.sfx, 'fear')
}
};
}
/** Invoked by the setting when data changes */
handleChange() {
if (this.displayFear) {
if (ui.resources) {
if (this.displayFear === 'hide') ui.resources.close({ allowed: true });
else ui.resources.render({ force: true });
}
}
const globalOverrides = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.GlobalOverrides);
globalOverrides.diceSoNiceSFXUpdate(this);
}
}

View file

@ -0,0 +1,55 @@
import DhAppearance from './Appearance.mjs';
/**
* A setting to handle cases where we want to allow the GM to set a global default for client settings.
*/
export default class DhGlobalOverrides extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
diceSoNice: new fields.SchemaField({
sfx: new fields.SchemaField({
overrideEnabled: new fields.BooleanField(),
global: new fields.SchemaField({
critical: DhAppearance.sfxSchema()
}),
hope: new fields.SchemaField({
higher: DhAppearance.sfxSchema()
}),
fear: new fields.SchemaField({
higher: DhAppearance.sfxSchema()
})
})
})
};
}
async diceSoNiceSFXUpdate(appearanceSettings, enabled) {
if (!game.user.isGM) return;
const newEnabled = enabled !== undefined ? enabled : this.diceSoNice.sfx.overrideEnabled;
if (newEnabled) {
const newOverrides = foundry.utils.mergeObject(this.toObject(), {
diceSoNice: {
sfx: {
overrideEnabled: true,
global: appearanceSettings.diceSoNice.sfx,
hope: appearanceSettings.diceSoNice.hope.sfx,
fear: appearanceSettings.diceSoNice.fear.sfx
}
}
});
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.GlobalOverrides, newOverrides);
} else {
const newOverrides = {
...this.toObject(),
diceSoNice: {
sfx: {
overrideEnabled: false
}
}
};
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.GlobalOverrides, newOverrides);
}
}
}

View file

@ -145,6 +145,16 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
description: new fields.StringField()
})
),
resources: new fields.TypedObjectField(
new fields.SchemaField({
resources: new fields.TypedObjectField(new fields.EmbeddedDataField(Resource))
}),
{
initial: {
character: { resources: {} }
}
}
),
itemFeatures: new fields.SchemaField({
weaponFeatures: new fields.TypedObjectField(
new fields.SchemaField({
@ -185,4 +195,117 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
}
return source;
}
/** Invoked by the setting when data changes */
handleChange() {
if (this.maxFear) {
if (ui.resources) ui.resources.render({ force: true });
}
this.refreshConfig();
this.#resetActors();
}
/** Update config values based on homebrew data. Make sure the references don't change */
refreshConfig() {
for (const [actorType, actorData] of Object.entries(this.resources)) {
const config = CONFIG.DH.RESOURCE[actorType];
for (const key of Object.keys(config.all)) {
delete config.all[key];
}
Object.assign(config.all, {
...Object.entries(actorData.resources).reduce((result, [key, value]) => {
result[key] = value.toObject();
result[key].id = key;
return result;
}, {}),
...config.custom,
...config.base
});
}
}
/**
* Triggers a reset and non-forced re-render on all given actors (if given)
* or all world actors and actors in all scenes to show immediate results for a changed setting.
*/
#resetActors() {
const actors = new Set(
[
game.actors.contents,
game.scenes.contents.flatMap(s => s.tokens.contents).flatMap(t => t.actor ?? [])
].flat()
);
for (const actor of actors) {
for (const app of Object.values(actor.apps)) {
for (const element of app.element?.querySelectorAll('prose-mirror.active')) {
element.open = false; // This triggers a save
}
}
actor.reset();
actor.render();
}
}
}
export class Resource extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
initial: new fields.NumberField({
required: true,
integer: true,
initial: 0,
min: 0,
label: 'DAGGERHEART.GENERAL.initial'
}),
max: new fields.NumberField({
nullable: true,
initial: null,
min: 0,
label: 'DAGGERHEART.GENERAL.max'
}),
label: new fields.StringField({ label: 'DAGGERHEART.GENERAL.label' }),
images: new fields.SchemaField({
full: imageIconField('fa solid fa-circle'),
empty: imageIconField('fa-regular fa-circle')
})
};
}
static getDefaultResourceData = label => {
const images = Resource.schema.fields.images.getInitialValue();
return {
initial: 0,
max: 0,
label: label ?? '',
images
};
};
static getDefaultImageData = imageKey => {
return Resource.schema.fields.images.fields[imageKey].getInitialValue();
};
}
const imageIconField = defaultValue =>
new foundry.data.fields.SchemaField(
{
value: new foundry.data.fields.StringField({
initial: defaultValue,
label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.resources.resources.value.label'
}),
isIcon: new foundry.data.fields.BooleanField({
required: true,
initial: true,
label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.resources.resources.isIcon.label'
}),
noColorFilter: new foundry.data.fields.BooleanField({
required: true,
initial: false,
label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.resources.resources.noColorFilter.label'
})
},
{ required: true }
);

View file

@ -0,0 +1,12 @@
export default class DhMetagaming extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
hideObserverPermissionInChat: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hideObserverPermissionInChat.label',
hint: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hideObserverPermissionInChat.hint'
})
};
}
}

View file

@ -1,4 +1,6 @@
export { default as DhAppearance } from './Appearance.mjs';
export { default as DhAutomation } from './Automation.mjs';
export { default as DhHomebrew } from './Homebrew.mjs';
export { default as DhMetagaming } from './Metagaming.mjs';
export { default as DhVariantRules } from './VariantRules.mjs';
export { default as DhGlobalOverrides } from './GlobalOverrides.mjs';

View file

@ -140,8 +140,14 @@ export default class DHRoll extends Roll {
/** @inheritDoc */
async render({ flavor, template = this.constructor.CHAT_TEMPLATE, isPrivate = false, ...options } = {}) {
if (!this._evaluated) return;
const metagamingSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming);
const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options });
return foundry.applications.handlebars.renderTemplate(template, chatData);
return foundry.applications.handlebars.renderTemplate(template, {
...chatData,
parent: chatData.parent,
metagamingSettings
});
}
/** @inheritDoc */

View file

@ -378,6 +378,8 @@ export default class DualityRoll extends D20Roll {
let parsedRoll = game.system.api.dice.DualityRoll.fromData({ ...rollString, evaluated: false });
const term = parsedRoll.terms[target.dataset.dieIndex];
await term.reroll(`/r1=${term.total}`);
const result = await parsedRoll.evaluate();
if (game.modules.get('dice-so-nice')?.active) {
const diceSoNiceRoll = {
_evaluated: true,
@ -391,7 +393,7 @@ export default class DualityRoll extends D20Roll {
options: { appearance: {} }
};
const diceSoNicePresets = await getDiceSoNicePresets(`d${term._faces}`, `d${term._faces}`);
const diceSoNicePresets = await getDiceSoNicePresets(result, `d${term._faces}`, `d${term._faces}`);
const type = target.dataset.type;
if (diceSoNicePresets[type]) {
diceSoNiceRoll.dice[0].options = diceSoNicePresets[type];
@ -400,8 +402,6 @@ export default class DualityRoll extends D20Roll {
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
}
await parsedRoll.evaluate();
const newRoll = game.system.api.dice.DualityRoll.postEvaluate(parsedRoll, {
targets: message.system.targets,
roll: {

View file

@ -68,8 +68,11 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
document = fromUuidSync(uuid);
if (!document) return;
e.setAttribute('data-view-perm', document.testUserPermission(game.user, 'OBSERVER'));
e.setAttribute('data-use-perm', document.testUserPermission(game.user, 'OWNER'));
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming);
if (settings.hideObserverPermissionInChat)
e.setAttribute('data-view-perm', document.testUserPermission(game.user, 'OBSERVER'));
});
if (this.isContentVisible) {

View file

@ -3,6 +3,7 @@ import { AdversaryBPPerEncounter, BaseBPPerEncounter } from '../config/encounter
export default class DhTooltipManager extends foundry.helpers.interaction.TooltipManager {
#wide = false;
#bordered = false;
#active = false;
async activate(element, options = {}) {
const { TextEditor } = foundry.applications.ux;
@ -168,7 +169,100 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
}
}
super.activate(element, { ...options, html: html });
this.baseActivate(element, { ...options, html: html });
}
/* Need to pass more options to _setAnchor, so have to copy whole foundry method >_< */
async baseActivate(element, options) {
let { text, direction, cssClass, locked = false, html, content } = options;
if (content && !html) {
foundry.utils.logCompatibilityWarning(
'The content option has been deprecated in favor of the html option',
{ since: 13, until: 15, once: true }
);
html = content;
}
if (text && html) throw new Error('Cannot provide both text and html options to TooltipManager#activate.');
// Deactivate currently active element
this.deactivate();
// Check if the element still exists in the DOM.
if (!document.body.contains(element)) return;
// Mark the new element as active
this.#active = true;
this.element = element;
element.setAttribute('aria-describedby', 'tooltip');
html ||= element.dataset.tooltipHtml;
if (html) {
if (typeof html === 'string') this.tooltip.innerHTML = foundry.utils.cleanHTML(html);
else {
this.tooltip.innerHTML = ''; // Clear existing HTML
this.tooltip.appendChild(html);
}
} else {
text ||= element.dataset.tooltipText;
if (text) this.tooltip.textContent = text;
else {
text = element.dataset.tooltip;
// Localized message should be safe
if (game.i18n.has(text)) this.tooltip.innerHTML = game.i18n.localize(text);
else this.tooltip.innerHTML = foundry.utils.cleanHTML(text);
}
}
// Activate display of the tooltip
this.tooltip.removeAttribute('class');
this.tooltip.classList.add('active', 'themed', 'theme-dark');
this.tooltip.showPopover();
cssClass ??= element.closest('[data-tooltip-class]')?.dataset.tooltipClass;
if (cssClass) this.tooltip.classList.add(...cssClass.split(' '));
// Set tooltip position
direction ??= element.closest('[data-tooltip-direction]')?.dataset.tooltipDirection;
if (!direction) direction = this._determineDirection();
this._setAnchor(direction, options);
if (locked || element.dataset.hasOwnProperty('locked')) this.lockTooltip();
}
_setAnchor(direction, options) {
const directions = this.constructor.TOOLTIP_DIRECTIONS;
const pad = this.constructor.TOOLTIP_MARGIN_PX;
const pos = this.element.getBoundingClientRect();
const { innerHeight, innerWidth } = this.tooltip.ownerDocument.defaultView;
const tooltipPadding = 16;
const horizontalOffset = options.noOffset ? tooltipPadding : this.tooltip.offsetWidth / 2 - pos.width / 2;
const verticalOffset = options.noOffset ? tooltipPadding : this.tooltip.offsetHeight / 2 - pos.height / 2;
const style = {};
switch (direction) {
case directions.DOWN:
style.textAlign = 'center';
style.left = pos.left - horizontalOffset;
style.top = pos.bottom + pad;
break;
case directions.LEFT:
style.textAlign = 'left';
style.right = innerWidth - pos.left + pad;
style.top = pos.top - verticalOffset;
break;
case directions.RIGHT:
style.textAlign = 'right';
style.left = pos.right + pad;
style.top = pos.top - verticalOffset;
break;
case directions.UP:
style.textAlign = 'center';
style.left = pos.left - horizontalOffset;
style.bottom = innerHeight - pos.top + pad;
break;
case directions.CENTER:
style.textAlign = 'center';
style.left = pos.left - horizontalOffset;
style.top = pos.top - verticalOffset;
break;
}
return this._setStyle(style);
}
_determineItemTooltipDirection(element, prefered = this.constructor.TOOLTIP_DIRECTIONS.LEFT) {
@ -270,6 +364,12 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
return clone;
}
/**@inheritdoc */
dismissLockedTooltips() {
super.dismissLockedTooltips();
Hooks.callAll(CONFIG.DH.HOOKS.hooksConfig.lockedTooltipDismissed);
}
/** Get HTML for Battlepoints tooltip */
async getBattlepointHTML(combatId) {
const combat = game.combats.get(combatId);

View file

@ -49,9 +49,7 @@ export default class RegisterHandlebarsHelpers {
}
static damageSymbols(damageParts) {
const symbols = [...new Set(damageParts.reduce((a, c) => a.concat([...c.type]), []))].map(
p => CONFIG.DH.GENERAL.damageTypes[p].icon
);
const symbols = [...new Set(damageParts.map(x => x.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

@ -60,7 +60,13 @@ export const getCommandTarget = (options = {}) => {
export const setDiceSoNiceForDualityRoll = async (rollResult, advantageState, hopeFaces, fearFaces, advantageFaces) => {
if (!game.modules.get('dice-so-nice')?.active) return;
const diceSoNicePresets = await getDiceSoNicePresets(hopeFaces, fearFaces, advantageFaces, advantageFaces);
const diceSoNicePresets = await getDiceSoNicePresets(
rollResult,
hopeFaces,
fearFaces,
advantageFaces,
advantageFaces
);
rollResult.dice[0].options = diceSoNicePresets.hope;
rollResult.dice[1].options = diceSoNicePresets.fear;
if (rollResult.dice[2] && advantageState) {
@ -378,17 +384,18 @@ export const arraysEqual = (a, b) =>
export const setsEqual = (a, b) => a.size === b.size && [...a].every(value => b.has(value));
export function getScrollTextData(resources, resource, key) {
const { reversed, label } = CONFIG.DH.ACTOR.scrollingTextResource[key];
export function getScrollTextData(actor, resource, key) {
const { BOTTOM, TOP } = CONST.TEXT_ANCHOR_POINTS;
const resources = actor.system.resources;
const increased = resources[key].value < resource.value;
const value = -1 * (resources[key].value - resource.value);
const { label, isReversed } = resources[key];
const text = `${game.i18n.localize(label)} ${value.signedString()}`;
const stroke = increased ? (reversed ? 0xffffff : 0x000000) : reversed ? 0x000000 : 0xffffff;
const fill = increased ? (reversed ? 0x0032b1 : 0xffe760) : reversed ? 0xffe760 : 0x0032b1;
const direction = increased ? (reversed ? BOTTOM : TOP) : reversed ? TOP : BOTTOM;
const stroke = increased ? (isReversed ? 0xffffff : 0x000000) : isReversed ? 0x000000 : 0xffffff;
const fill = increased ? (isReversed ? 0x0032b1 : 0xffe760) : isReversed ? 0xffe760 : 0x0032b1;
const direction = increased ? (isReversed ? BOTTOM : TOP) : isReversed ? TOP : BOTTOM;
return { text, stroke, fill, direction };
}
@ -722,3 +729,16 @@ export async function RefreshFeatures(
return refreshedActors;
}
export function getUnusedDamageTypes(parts) {
const usedKeys = Object.keys(parts);
return Object.keys(CONFIG.DH.GENERAL.healingTypes).reduce((acc, key) => {
if (!usedKeys.includes(key))
acc.push({
value: key,
label: game.i18n.localize(CONFIG.DH.GENERAL.healingTypes[key].label)
});
return acc;
}, []);
}

View file

@ -17,9 +17,10 @@ export const preloadHandlebarsTemplates = async function () {
'systems/daggerheart/templates/sheets/global/partials/resource-section/dice-value.hbs',
'systems/daggerheart/templates/sheets/global/partials/resource-section/die.hbs',
'systems/daggerheart/templates/sheets/global/partials/resource-bar.hbs',
'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs',
'systems/daggerheart/templates/sheets/global/partials/item-tags.hbs',
'systems/daggerheart/templates/components/card-preview.hbs',
'systems/daggerheart/templates/levelup/parts/selectable-card-preview.hbs',
'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs',
'systems/daggerheart/templates/ui/combatTracker/combatTrackerSection.hbs',
'systems/daggerheart/templates/actionTypes/damage.hbs',
'systems/daggerheart/templates/actionTypes/resource.hbs',
@ -33,6 +34,7 @@ export const preloadHandlebarsTemplates = async function () {
'systems/daggerheart/templates/actionTypes/beastform.hbs',
'systems/daggerheart/templates/actionTypes/countdown.hbs',
'systems/daggerheart/templates/actionTypes/summon.hbs',
'systems/daggerheart/templates/actionTypes/transform.hbs',
'systems/daggerheart/templates/settings/components/settings-item-line.hbs',
'systems/daggerheart/templates/ui/tooltip/parts/tooltipChips.hbs',
'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs',
@ -44,6 +46,7 @@ export const preloadHandlebarsTemplates = async function () {
'systems/daggerheart/templates/ui/chat/parts/target-part.hbs',
'systems/daggerheart/templates/ui/chat/parts/button-part.hbs',
'systems/daggerheart/templates/ui/itemBrowser/itemContainer.hbs',
'systems/daggerheart/templates/scene/dh-config.hbs'
'systems/daggerheart/templates/scene/dh-config.hbs',
'systems/daggerheart/templates/settings/appearance-settings/diceSoNiceTab.hbs'
]);
};

View file

@ -1,10 +1,18 @@
import { defaultLevelTiers, DhLevelTiers } from '../data/levelTier.mjs';
import DhCountdowns from '../data/countdowns.mjs';
import { DhAppearance, DhAutomation, DhHomebrew, DhVariantRules } from '../data/settings/_module.mjs';
import {
DhAppearance,
DhAutomation,
DhGlobalOverrides,
DhHomebrew,
DhMetagaming,
DhVariantRules
} from '../data/settings/_module.mjs';
import {
DhAppearanceSettings,
DhAutomationSettings,
DhHomebrewSettings,
DhMetagamingSettings,
DhVariantRuleSettings
} from '../applications/settings/_module.mjs';
import { CompendiumBrowserSettings, DhTagTeamRoll } from '../data/_module.mjs';
@ -38,18 +46,25 @@ const registerMenuSettings = () => {
type: DhAutomation
});
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming, {
scope: 'world',
config: false,
type: DhMetagaming
});
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, {
scope: 'world',
config: false,
type: DhHomebrew,
onChange: value => {
if (value.maxFear) {
if (ui.resources) ui.resources.render({ force: true });
value.handleChange();
}
});
// Some homebrew settings may change sheets in various ways, so trigger a re-render
resetActors();
}
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.GlobalOverrides, {
scope: 'world',
config: false,
type: DhGlobalOverrides
});
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance, {
@ -57,12 +72,7 @@ const registerMenuSettings = () => {
config: false,
type: DhAppearance,
onChange: value => {
if (value.displayFear) {
if (ui.resources) {
if (value.displayFear === 'hide') ui.resources.close({ allowed: true });
else ui.resources.render({ force: true });
}
}
value.handleChange();
}
});
};
@ -76,6 +86,16 @@ const registerMenus = () => {
type: DhAutomationSettings,
restricted: true
});
game.settings.registerMenu(CONFIG.DH.id, CONFIG.DH.SETTINGS.menu.Metagaming.Name, {
name: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.metagaming.name'),
label: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.metagaming.label'),
hint: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.metagaming.hint'),
icon: CONFIG.DH.SETTINGS.menu.Metagaming.Icon,
type: DhMetagamingSettings,
restricted: true
});
game.settings.registerMenu(CONFIG.DH.id, CONFIG.DH.SETTINGS.menu.Homebrew.Name, {
name: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.homebrew.name'),
label: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.homebrew.label'),
@ -144,30 +164,8 @@ const registerNonConfigSettings = () => {
});
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings, {
scope: 'client',
scope: 'world',
config: false,
type: CompendiumBrowserSettings
});
};
/**
* Triggers a reset and non-forced re-render on all given actors (if given)
* or all world actors and actors in all scenes to show immediate results for a changed setting.
*/
function resetActors(actors) {
actors ??= [
game.actors.contents,
game.scenes.contents.flatMap(s => s.tokens.contents).flatMap(t => t.actor ?? [])
].flat();
actors = new Set(actors);
for (const actor of actors) {
for (const app of Object.values(actor.apps)) {
for (const element of app.element?.querySelectorAll('prose-mirror.active')) {
element.open = false; // This triggers a save
}
}
actor.reset();
actor.render();
}
}

View file

@ -91,8 +91,8 @@
"useDefault": false
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -118,7 +118,7 @@
},
"base": false
}
],
},
"includeBase": false
},
"_id": "TCKVaVweyJzhEArX",
@ -343,7 +343,7 @@
"recovery": null
},
"damage": {
"parts": [],
"parts": {},
"includeBase": false
},
"target": {
@ -471,8 +471,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": true,
@ -499,7 +499,7 @@
}
}
},
{
"armor": {
"value": {
"custom": {
"enabled": true,
@ -524,7 +524,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {
@ -598,8 +598,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": true,
@ -626,7 +626,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -652,8 +652,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": true,
@ -680,7 +680,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {

View file

@ -75,8 +75,8 @@
},
"range": "veryClose",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -102,7 +102,7 @@
},
"base": false
}
]
}
},
"type": "attack",
"chatDisplay": false
@ -400,8 +400,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -427,7 +427,7 @@
}
}
}
],
},
"includeBase": false,
"direct": true
},
@ -508,7 +508,7 @@
"recovery": null
},
"damage": {
"parts": [],
"parts": {},
"includeBase": false
},
"target": {
@ -581,8 +581,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -608,7 +608,7 @@
}
}
},
{
"hope": {
"value": {
"custom": {
"enabled": true,
@ -633,7 +633,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {

View file

@ -72,8 +72,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": true,
@ -100,7 +100,7 @@
},
"base": false
}
]
}
},
"img": "icons/weapons/daggers/dagger-bone-black.webp",
"type": "attack",

View file

@ -85,8 +85,8 @@
},
"range": "far",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -112,7 +112,7 @@
},
"base": false
}
]
}
},
"img": "icons/magic/unholy/beam-ringed-impact-purple.webp",
"type": "attack",
@ -256,7 +256,7 @@
"recovery": null
},
"damage": {
"parts": [],
"parts": {},
"includeBase": false
},
"target": {
@ -336,8 +336,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -363,7 +363,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -414,8 +414,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"fear": {
"value": {
"custom": {
"enabled": true,
@ -440,7 +440,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {
@ -619,7 +619,7 @@
"recovery": null
},
"damage": {
"parts": [],
"parts": {},
"includeBase": false
},
"target": {
@ -692,8 +692,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -719,7 +719,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {

View file

@ -80,8 +80,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -107,7 +107,7 @@
},
"base": false
}
]
}
},
"img": "icons/weapons/bows/longbow-recurve-leather-brown.webp",
"type": "attack",
@ -246,8 +246,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -273,7 +273,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {

View file

@ -68,8 +68,8 @@
"description": "<p>A group of trained archers bearing massive bows.</p>",
"attack": {
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -95,7 +95,7 @@
"resultBased": false,
"base": false
}
]
}
},
"name": "Longbow",
"img": "icons/weapons/bows/longbow-recurve-leather-brown.webp",
@ -270,8 +270,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -295,7 +295,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -368,8 +368,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -393,7 +393,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {

View file

@ -81,8 +81,8 @@
},
"range": "close",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -108,7 +108,7 @@
},
"base": false
}
]
}
},
"type": "attack",
"chatDisplay": false

View file

@ -80,8 +80,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -107,7 +107,7 @@
},
"base": false
}
]
}
},
"range": "melee",
"type": "attack",
@ -309,7 +309,7 @@
"recovery": null
},
"damage": {
"parts": [],
"parts": {},
"includeBase": false
},
"target": {
@ -382,8 +382,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -409,7 +409,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -482,8 +482,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -509,7 +509,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -582,8 +582,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -609,7 +609,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -737,8 +737,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"stress": {
"value": {
"custom": {
"enabled": true,
@ -763,7 +763,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {
@ -836,7 +836,7 @@
"recovery": null
},
"damage": {
"parts": [],
"parts": {},
"includeBase": false
},
"target": {
@ -964,8 +964,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -991,7 +991,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -1071,8 +1071,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"fear": {
"value": {
"custom": {
"enabled": true,
@ -1097,7 +1097,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {
@ -1165,8 +1165,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -1192,7 +1192,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {

View file

@ -84,8 +84,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -111,7 +111,7 @@
},
"base": false
}
]
}
},
"img": "icons/creatures/claws/claw-straight-brown.webp",
"type": "attack",
@ -284,8 +284,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -311,7 +311,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -441,8 +441,8 @@
"consumeOnSuccess": false
},
"damage": {
"parts": [
{
"parts": {
"fear": {
"value": {
"custom": {
"enabled": true,
@ -468,7 +468,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {

View file

@ -80,8 +80,8 @@
"name": "Longsword",
"img": "icons/weapons/swords/sword-guard.webp",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -107,7 +107,7 @@
},
"base": false
}
]
}
},
"type": "attack",
"range": "melee",
@ -246,7 +246,7 @@
"recovery": null
},
"damage": {
"parts": [],
"parts": {},
"includeBase": false
},
"target": {
@ -319,7 +319,7 @@
"recovery": null
},
"damage": {
"parts": [],
"parts": {},
"includeBase": false
},
"target": {

View file

@ -83,8 +83,8 @@
},
"range": "veryClose",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -110,7 +110,7 @@
},
"base": false
}
]
}
},
"img": "icons/skills/melee/unarmed-punch-fist-yellow-red.webp",
"type": "attack",
@ -280,8 +280,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -307,7 +307,7 @@
}
}
}
],
},
"includeBase": false,
"direct": true
},
@ -389,8 +389,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"stress": {
"value": {
"custom": {
"enabled": true,
@ -415,7 +415,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {

View file

@ -79,8 +79,8 @@
},
"range": "veryClose",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -106,7 +106,7 @@
},
"base": false
}
],
},
"direct": true
},
"name": "Club",
@ -336,8 +336,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false,
@ -365,7 +365,7 @@
}
}
}
],
},
"includeBase": false,
"direct": true
},
@ -412,8 +412,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"fear": {
"value": {
"custom": {
"enabled": true,
@ -438,7 +438,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {
@ -507,8 +507,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -534,7 +534,7 @@
}
}
}
],
},
"includeBase": false,
"direct": true
},

View file

@ -74,8 +74,8 @@
},
"range": "close",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -101,7 +101,7 @@
},
"base": false
}
]
}
},
"img": "icons/magic/light/beam-rays-magenta.webp",
"type": "attack",
@ -383,8 +383,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -410,7 +410,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -483,8 +483,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"stress": {
"value": {
"custom": {
"enabled": false
@ -508,7 +508,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {

View file

@ -67,8 +67,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": true,
@ -95,7 +95,7 @@
},
"base": false
}
]
}
},
"type": "attack",
"chatDisplay": false

View file

@ -72,8 +72,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -99,7 +99,7 @@
},
"base": false
}
]
}
},
"name": "Fist Slam",
"img": "icons/skills/melee/unarmed-punch-fist-yellow-red.webp",
@ -332,8 +332,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -359,7 +359,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -534,8 +534,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -561,7 +561,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {

View file

@ -84,8 +84,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -111,7 +111,7 @@
},
"base": false
}
]
}
},
"img": "icons/weapons/daggers/dagger-straight-cracked.webp",
"type": "attack",
@ -256,8 +256,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"stress": {
"value": {
"custom": {
"enabled": true,
@ -282,7 +282,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {

View file

@ -79,8 +79,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -106,7 +106,7 @@
},
"base": false
}
]
}
},
"img": "icons/weapons/daggers/dagger-twin-green.webp",
"type": "attack",
@ -253,8 +253,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"stress": {
"value": {
"custom": {
"enabled": true,
@ -279,7 +279,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {

View file

@ -84,8 +84,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -111,7 +111,7 @@
},
"base": false
}
]
}
},
"range": "far",
"img": "icons/weapons/staves/staff-ornate-purple.webp",
@ -256,8 +256,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -283,7 +283,7 @@
}
}
},
{
"stress": {
"value": {
"custom": {
"enabled": true,
@ -308,7 +308,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -614,8 +614,8 @@
"recovery": "scene"
},
"damage": {
"parts": [
{
"parts": {
"stress": {
"value": {
"custom": {
"enabled": true,
@ -640,7 +640,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {

View file

@ -74,8 +74,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -101,7 +101,7 @@
},
"base": false
}
]
}
},
"range": "melee",
"type": "attack",
@ -300,8 +300,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"stress": {
"value": {
"custom": {
"enabled": true,
@ -326,7 +326,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {

View file

@ -66,8 +66,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": true,
@ -94,7 +94,7 @@
},
"base": false
}
]
}
},
"type": "attack",
"range": "melee",

View file

@ -79,8 +79,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -106,7 +106,7 @@
},
"base": false
}
]
}
},
"img": "icons/magic/nature/root-vines-grow-brown.webp",
"type": "attack",
@ -245,8 +245,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"stress": {
"value": {
"custom": {
"enabled": true,
@ -271,7 +271,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -325,8 +325,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -350,7 +350,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {

View file

@ -80,8 +80,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -107,7 +107,7 @@
},
"base": false
}
]
}
},
"type": "attack",
"range": "melee",

View file

@ -80,8 +80,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -107,7 +107,7 @@
},
"base": false
}
]
}
},
"type": "attack",
"range": "far",
@ -435,8 +435,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hope": {
"value": {
"custom": {
"enabled": true,
@ -461,7 +461,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -515,8 +515,8 @@
"consumeOnSuccess": false
},
"damage": {
"parts": [
{
"parts": {
"fear": {
"value": {
"custom": {
"enabled": true,
@ -542,7 +542,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {

View file

@ -81,8 +81,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -108,7 +108,7 @@
},
"base": false
}
]
}
},
"type": "attack",
"chatDisplay": false
@ -251,8 +251,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hope": {
"value": {
"custom": {
"enabled": true,
@ -277,7 +277,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -329,8 +329,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"stress": {
"value": {
"custom": {
"enabled": true,
@ -355,7 +355,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {
@ -414,8 +414,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -441,7 +441,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -550,8 +550,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"stress": {
"value": {
"custom": {
"enabled": true,
@ -576,7 +576,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {

View file

@ -80,8 +80,8 @@
},
"range": "far",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -107,7 +107,7 @@
},
"base": false
}
],
},
"direct": true
},
"img": "icons/magic/symbols/rune-sigil-rough-white-teal.webp",
@ -352,7 +352,7 @@
"recovery": null
},
"damage": {
"parts": [],
"parts": {},
"includeBase": false
},
"target": {

View file

@ -81,8 +81,8 @@
},
"img": "icons/skills/melee/unarmed-punch-fist-yellow-red.webp",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -108,7 +108,7 @@
},
"base": false
}
],
},
"direct": true
},
"type": "attack",
@ -398,8 +398,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -425,7 +425,7 @@
}
}
}
],
},
"includeBase": false,
"direct": true
},

View file

@ -74,8 +74,8 @@
"motivesAndTactics": "Cause fear, consume fl esh, please masters",
"attack": {
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -101,7 +101,7 @@
"resultBased": false,
"base": false
}
]
}
},
"name": "Claws and Fangs",
"img": "icons/creatures/abilities/mouth-teeth-rows-red.webp",
@ -269,8 +269,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hope": {
"value": {
"custom": {
"enabled": true,
@ -295,7 +295,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {
@ -349,8 +349,8 @@
"consumeOnSuccess": false
},
"damage": {
"parts": [
{
"parts": {
"fear": {
"value": {
"custom": {
"enabled": true,
@ -376,7 +376,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {

View file

@ -78,8 +78,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -105,7 +105,7 @@
},
"base": false
}
]
}
},
"img": "icons/creatures/claws/claw-hooked-curved.webp",
"type": "attack",
@ -312,8 +312,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"stress": {
"value": {
"custom": {
"enabled": false
@ -337,7 +337,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {
@ -397,8 +397,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -424,7 +424,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {

View file

@ -79,8 +79,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -106,7 +106,7 @@
},
"base": false
}
]
}
},
"img": "icons/creatures/claws/claw-straight-brown.webp",
"type": "attack",
@ -247,8 +247,8 @@
"consumeOnSuccess": false
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -272,7 +272,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -352,8 +352,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -377,7 +377,7 @@
}
}
}
],
},
"includeBase": false,
"direct": true
},

View file

@ -81,8 +81,8 @@
},
"range": "far",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -108,7 +108,7 @@
},
"base": false
}
]
}
},
"type": "attack",
"chatDisplay": false
@ -251,7 +251,7 @@
"recovery": null
},
"damage": {
"parts": [],
"parts": {},
"includeBase": false
},
"target": {
@ -297,8 +297,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -324,7 +324,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -438,8 +438,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": true,
@ -464,7 +464,7 @@
},
"type": []
},
{
"stress": {
"value": {
"custom": {
"enabled": true,
@ -489,7 +489,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {

View file

@ -68,8 +68,8 @@
"motivesAndTactics": "Avoid larger predators, shock prey, tear apart",
"attack": {
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -95,7 +95,7 @@
"resultBased": false,
"base": false
}
]
}
},
"name": "Shocking Bite",
"img": "icons/creatures/abilities/mouth-teeth-sharp.webp",
@ -270,8 +270,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -297,7 +297,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {

View file

@ -67,8 +67,8 @@
},
"range": "close",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": true,
@ -95,7 +95,7 @@
},
"base": false
}
]
}
},
"type": "attack",
"chatDisplay": false

View file

@ -98,8 +98,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -125,7 +125,7 @@
},
"base": false
}
],
},
"includeBase": false
},
"target": {
@ -276,8 +276,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -303,7 +303,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {

View file

@ -79,8 +79,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -106,7 +106,7 @@
},
"base": false
}
]
}
},
"img": "icons/creatures/claws/claw-hooked-barbed.webp",
"range": "melee",

View file

@ -67,8 +67,8 @@
"img": "icons/weapons/axes/axe-battle-skull-black.webp",
"range": "veryClose",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": true,
@ -95,7 +95,7 @@
},
"base": false
}
]
}
},
"type": "attack",
"chatDisplay": false
@ -256,8 +256,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hope": {
"value": {
"custom": {
"enabled": true,
@ -282,7 +282,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {

View file

@ -80,8 +80,8 @@
},
"range": "far",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -107,7 +107,7 @@
},
"base": false
}
]
}
},
"img": "icons/weapons/staves/staff-animal-skull-bull.webp",
"type": "attack",
@ -251,8 +251,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -278,7 +278,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -482,8 +482,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"stress": {
"value": {
"custom": {
"enabled": true,
@ -508,7 +508,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {

View file

@ -67,8 +67,8 @@
"useDefault": false
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -94,7 +94,7 @@
},
"base": false
}
],
},
"includeBase": false
},
"description": "",
@ -337,7 +337,7 @@
"recovery": null
},
"damage": {
"parts": [],
"parts": {},
"includeBase": false
},
"target": {
@ -416,8 +416,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -443,7 +443,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -516,8 +516,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -543,7 +543,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -653,8 +653,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hope": {
"value": {
"custom": {
"enabled": true,
@ -679,7 +679,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -717,7 +717,35 @@
"system": {
"description": "<p>When the @Lookup[@name] marks their last HP, replace them with the @UUID[Compendium.daggerheart.adversaries.Actor.RXkZTwBRi4dJ3JE5]{Fallen Warlord: Undefeated Champion} and immediately spotlight them.</p>",
"resource": null,
"actions": {},
"actions": {
"gP426WmWbtrZEWCD": {
"type": "transform",
"_id": "gP426WmWbtrZEWCD",
"systemPath": "actions",
"baseAction": false,
"description": "",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action",
"triggers": [],
"cost": [],
"uses": {
"value": null,
"max": null,
"recovery": null,
"consumeOnSuccess": false
},
"transform": {
"actorUUID": "Compendium.daggerheart.adversaries.Actor.RXkZTwBRi4dJ3JE5",
"resourceRefresh": {
"hitPoints": true,
"stress": true
}
}
}
},
"originItemType": null,
"originId": null,
"featureForm": "reaction"

View file

@ -67,8 +67,8 @@
"useDefault": false
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -94,7 +94,7 @@
},
"base": false
}
],
},
"includeBase": false
},
"description": "",
@ -338,7 +338,7 @@
"recovery": null
},
"damage": {
"parts": [],
"parts": {},
"includeBase": false
},
"target": {
@ -410,8 +410,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -437,7 +437,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -697,8 +697,8 @@
"consumeOnSuccess": false
},
"damage": {
"parts": [
{
"parts": {
"fear": {
"value": {
"custom": {
"enabled": true,
@ -724,7 +724,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {
@ -792,8 +792,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hope": {
"value": {
"custom": {
"enabled": true,
@ -818,7 +818,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {

View file

@ -81,8 +81,8 @@
},
"range": "far",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -108,7 +108,7 @@
},
"base": false
}
]
}
},
"type": "attack",
"chatDisplay": false
@ -269,8 +269,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -294,7 +294,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {

View file

@ -81,8 +81,8 @@
},
"range": "veryClose",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -108,7 +108,7 @@
},
"base": false
}
]
}
},
"type": "attack",
"chatDisplay": false
@ -251,8 +251,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -278,7 +278,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -351,8 +351,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -378,7 +378,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -452,8 +452,8 @@
"consumeOnSuccess": false
},
"damage": {
"parts": [
{
"parts": {
"fear": {
"value": {
"custom": {
"enabled": true,
@ -479,7 +479,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {

View file

@ -98,8 +98,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -125,7 +125,7 @@
},
"base": false
}
],
},
"includeBase": false
},
"target": {
@ -345,8 +345,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -372,7 +372,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -499,8 +499,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -526,7 +526,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -643,8 +643,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -670,7 +670,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {

View file

@ -74,8 +74,8 @@
"motivesAndTactics": "Fly away, harass, steal blood",
"attack": {
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -101,7 +101,7 @@
"resultBased": false,
"base": false
}
]
}
},
"name": "Proboscis",
"img": "icons/skills/wounds/blood-cells-vessel-red.webp",

View file

@ -72,8 +72,8 @@
"name": "Claws",
"img": "icons/creatures/claws/claw-straight-brown.webp",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": true,
@ -100,7 +100,7 @@
},
"base": false
}
]
}
},
"type": "attack",
"range": "melee",
@ -272,7 +272,7 @@
"recovery": null
},
"damage": {
"parts": [],
"parts": {},
"includeBase": false
},
"target": {

View file

@ -62,8 +62,8 @@
"name": "Warhammer",
"img": "icons/weapons/hammers/hammer-double-stone-worn.webp",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": true,
@ -90,7 +90,7 @@
},
"base": false
}
]
}
},
"range": "veryClose",
"roll": {

Some files were not shown because too many files have changed in this diff Show more