diff --git a/daggerheart.mjs b/daggerheart.mjs
index 06f1aaf6..d53cfb5c 100644
--- a/daggerheart.mjs
+++ b/daggerheart.mjs
@@ -2,6 +2,7 @@ import { SYSTEM } from './module/config/system.mjs';
import * as applications from './module/applications/_module.mjs';
import * as data from './module/data/_module.mjs';
import * as models from './module/data/_module.mjs';
+import * as canvas from './module/canvas/_module.mjs';
import * as documents from './module/documents/_module.mjs';
import * as dice from './module/dice/_module.mjs';
import * as fields from './module/data/fields/_module.mjs';
@@ -20,6 +21,7 @@ import {
import { placeables } from './module/canvas/_module.mjs';
import './node_modules/@yaireo/tagify/dist/tagify.css';
import TemplateManager from './module/documents/templateManager.mjs';
+import TokenManager from './module/documents/tokenManager.mjs';
CONFIG.DH = SYSTEM;
CONFIG.TextEditor.enrichers.push(...enricherConfig);
@@ -53,6 +55,8 @@ CONFIG.ChatMessage.template = 'systems/daggerheart/templates/ui/chat/chat-messag
CONFIG.Canvas.rulerClass = placeables.DhRuler;
CONFIG.Canvas.layers.templates.layerClass = placeables.DhTemplateLayer;
+CONFIG.Canvas.layers.tokens.layerClass = canvas.DhTokenLayer;
+
CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate;
CONFIG.Scene.documentClass = documents.DhScene;
@@ -64,6 +68,7 @@ CONFIG.Token.rulerClass = placeables.DhTokenRuler;
CONFIG.Token.hudClass = applications.hud.DHTokenHUD;
CONFIG.ui.combat = applications.ui.DhCombatTracker;
+CONFIG.ui.nav = applications.ui.DhSceneNavigation;
CONFIG.ui.chat = applications.ui.DhChatLog;
CONFIG.ui.effectsDisplay = applications.ui.DhEffectsDisplay;
CONFIG.ui.hotbar = applications.ui.DhHotbar;
@@ -75,6 +80,7 @@ CONFIG.ui.countdowns = applications.ui.DhCountdowns;
CONFIG.ux.ContextMenu = applications.ux.DHContextMenu;
CONFIG.ux.TooltipManager = documents.DhTooltipManager;
CONFIG.ux.TemplateManager = new TemplateManager();
+CONFIG.ux.TokenManager = new TokenManager();
Hooks.once('init', () => {
game.system.api = {
@@ -86,6 +92,8 @@ Hooks.once('init', () => {
fields
};
+ game.system.registeredTriggers = new RegisteredTriggers();
+
const { DocumentSheetConfig } = foundry.applications.apps;
DocumentSheetConfig.unregisterSheet(TokenDocument, 'core', foundry.applications.sheets.TokenConfig);
DocumentSheetConfig.registerSheet(TokenDocument, SYSTEM.id, applications.sheetConfigs.DhTokenConfig, {
@@ -323,11 +331,11 @@ Hooks.on('chatMessage', (_, message) => {
const fateTypeFromRollCommand = getFateType(rollCommand?.type);
- if (fateTypeFromRollCommand == "BAD") {
+ if (fateTypeFromRollCommand == 'BAD') {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing'));
return false;
}
-
+
const fateType = fateTypeFromRollCommand;
const target = getCommandTarget({ allowNull: true });
@@ -341,7 +349,6 @@ Hooks.on('chatMessage', (_, message) => {
});
return false;
}
-
});
const updateActorsRangeDependentEffects = async token => {
@@ -413,3 +420,50 @@ Hooks.on('refreshToken', (_, options) => {
Hooks.on('renderCompendiumDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html));
Hooks.on('renderDocumentDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html));
+
+class RegisteredTriggers extends Map {
+ constructor() {
+ super();
+ }
+
+ async registerTriggers(trigger, actor, triggeringActorType, uuid, commands) {
+ const existingTrigger = this.get(trigger);
+ if (!existingTrigger) this.set(trigger, new Map());
+
+ this.get(trigger).set(uuid, { actor, triggeringActorType, commands });
+ }
+
+ async runTrigger(trigger, currentActor, ...args) {
+ const updates = [];
+ const triggerSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).triggers;
+ if (!triggerSettings.enabled) return updates;
+
+ const dualityTrigger = this.get(trigger);
+ if (dualityTrigger) {
+ for (let { actor, triggeringActorType, commands } of dualityTrigger.values()) {
+ const triggerData = CONFIG.DH.TRIGGER.triggers[trigger];
+ if (triggerData.usesActor && triggeringActorType !== 'any') {
+ if (triggeringActorType === 'self' && currentActor?.uuid !== actor) continue;
+ else if (triggeringActorType === 'other' && currentActor?.uuid === actor) continue;
+ }
+
+ for (let command of commands) {
+ try {
+ const result = await command(...args);
+ if (result?.updates?.length) updates.push(...result.updates);
+ } catch (_) {
+ const triggerName = game.i18n.localize(triggerData.label);
+ ui.notifications.error(
+ game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerError', {
+ trigger: triggerName,
+ actor: currentActor?.name
+ })
+ );
+ }
+ }
+ }
+ }
+
+ return updates;
+ }
+}
diff --git a/lang/en.json b/lang/en.json
index 4a7f44d0..b8f811ac 100755
--- a/lang/en.json
+++ b/lang/en.json
@@ -69,7 +69,11 @@
},
"summon": {
"name": "Summon",
- "tooltip": "Create tokens in the scene."
+ "tooltip": "Create tokens in the scene.",
+ "error": "You do not have permission to summon tokens or there is no active scene.",
+ "invalidDrop": "You can only drop Actor entities to summon.",
+ "chatMessageTitle": "Test2",
+ "chatMessageHeaderTitle": "Summoning"
}
},
"Config": {
@@ -90,7 +94,9 @@
"customFormula": "Custom Formula",
"formula": "Formula"
},
- "displayInChat": "Display in chat"
+ "displayInChat": "Display in chat",
+ "deleteTriggerTitle": "Delete Trigger",
+ "deleteTriggerContent": "Are you sure you want to delete the {trigger} trigger?"
},
"RollField": {
"diceRolling": {
@@ -120,6 +126,9 @@
},
"cost": {
"stepTooltip": "+{step} per step"
+ },
+ "summon": {
+ "dropSummonsHere": "Drop Summons Here"
}
}
},
@@ -194,6 +203,8 @@
"unequip": "Unequip",
"useItem": "Use Item"
},
+ "defaultHopeDice": "Default Hope Dice",
+ "defaultFearDice": "Default Fear Dice",
"disadvantageSources": {
"label": "Disadvantage Sources",
"hint": "Add single words or short text as reminders and hints of what a character has disadvantage on."
@@ -1144,7 +1155,8 @@
"any": "Any",
"friendly": "Friendly",
"hostile": "Hostile",
- "self": "Self"
+ "self": "Self",
+ "other": "Other"
},
"TemplateTypes": {
"circle": "Circle",
@@ -1218,6 +1230,29 @@
}
}
},
+ "Triggers": {
+ "postDamageReduction": {
+ "label": "After Damage Reduction"
+ },
+ "preDamageReduction": {
+ "label": "Before Damage Reduction"
+ },
+ "dualityRoll": {
+ "label": "Duality Roll"
+ },
+ "fearRoll": {
+ "label": "Fear Roll"
+ },
+ "triggerTexts": {
+ "strangePatternsContentTitle": "Matched {nr} times.",
+ "strangePatternsContentSubTitle": "Increase hope and stress to a total of {nr}.",
+ "ferocityContent": "Spend 2 Hope to gain {bonus} bonus Evasion until after the next attack against you?",
+ "ferocityEffectDescription": "Your evasion is increased by {bonus}. This bonus lasts until after the next attack made against you."
+ },
+ "triggerType": "Trigger Type",
+ "triggeringActorType": "Triggering Actor Type",
+ "triggerError": "{trigger} trigger failed for {actor}. It's probably configured wrong."
+ },
"WeaponFeature": {
"barrier": {
"name": "Barrier",
@@ -2055,10 +2090,10 @@
"partyMembers": "Party Members",
"projects": "Projects",
"types": "Types",
- "itemFeatures": "Item Features",
"questions": "Questions",
"configuration": "Configuration",
- "base": "Base"
+ "base": "Base",
+ "triggers": "Triggers"
},
"Tiers": {
"singular": "Tier",
@@ -2183,6 +2218,10 @@
"stress": "Stress",
"subclasses": "Subclasses",
"success": "Success",
+ "summon": {
+ "single": "Summon",
+ "plural": "Summons"
+ },
"take": "Take",
"Target": {
"single": "Target",
@@ -2298,6 +2337,9 @@
"DomainCard": {
"type": "Type",
"recallCost": "Recall Cost",
+ "vaultActive": "Active In Vault",
+ "loadoutIgnore": "Ignores Loadout Limits",
+ "domainTouched": "Domain Touched",
"foundationTitle": "Foundation",
"specializationTitle": "Specialization",
"masteryTitle": "Mastery"
@@ -2436,6 +2478,12 @@
"hint": "Automatically apply effects. Targets must be selected before the action is made and Reaction Roll Automation must be different than Never. Bypass users permissions."
}
},
+ "triggers": {
+ "enabled": {
+ "label": "Enabled",
+ "hint": "Advanced automation such as triggering a popup for a wizard's Strange Patterns"
+ }
+ },
"summaryMessages": {
"label": "Summary Messages"
}
@@ -2445,6 +2493,9 @@
},
"roll": {
"title": "Actions"
+ },
+ "trigger": {
+ "title": "Triggers"
}
},
"Homebrew": {
@@ -2572,7 +2623,9 @@
}
},
"disabledText": "Daggerheart Measurements are disabled in System Settings - Variant Rules",
- "rangeMeasurement": "Range Measurement"
+ "rangeMeasurement": "Range Measurement",
+ "sceneEnvironments": "Scene Environments",
+ "dragEnvironmentHere": "Drag environments here"
}
},
"UI": {
@@ -2795,7 +2848,8 @@
"noActorOwnership": "You do not have permissions for this character",
"documentIsMissing": "The {documentType} is missing from the world.",
"tokenActorMissing": "{name} is missing an Actor",
- "tokenActorsMissing": "[{names}] missing Actors"
+ "tokenActorsMissing": "[{names}] missing Actors",
+ "domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used"
},
"Sidebar": {
"actorDirectory": {
@@ -2840,7 +2894,8 @@
"deleteItem": "Delete Item",
"immune": "Immune",
"middleClick": "[Middle Click] Keep tooltip view",
- "tokenSize": "The token size used on the canvas"
+ "tokenSize": "The token size used on the canvas",
+ "previewTokenHelp": "Left-click to place, right-click to cancel"
}
}
}
diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs
index 34ca02cd..d8306923 100644
--- a/module/applications/dialogs/d20RollDialog.mjs
+++ b/module/applications/dialogs/d20RollDialog.mjs
@@ -10,6 +10,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.config = config;
this.config.experiences = [];
this.reactionOverride = config.actionType === 'reaction';
+ this.selectedEffects = this.config.bonusEffects;
if (config.source?.action) {
this.item = config.data.parent.items.get(config.source.item) ?? config.data.parent;
@@ -35,6 +36,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
selectExperience: this.selectExperience,
toggleReaction: this.toggleReaction,
toggleTagTeamRoll: this.toggleTagTeamRoll,
+ toggleSelectedEffect: this.toggleSelectedEffect,
submitRoll: this.submitRoll
},
form: {
@@ -76,6 +78,9 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
icon
}));
+ context.hasSelectedEffects = Boolean(this.selectedEffects && Object.keys(this.selectedEffects).length);
+ context.selectedEffects = this.selectedEffects;
+
this.config.costs ??= [];
if (this.config.costs?.length) {
const updatedCosts = game.system.api.fields.ActionFields.CostField.calcCosts.call(
@@ -208,6 +213,11 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.render();
}
+ static toggleSelectedEffect(_event, button) {
+ this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected;
+ this.render();
+ }
+
static async submitRoll() {
await this.close({ submitted: true });
}
diff --git a/module/applications/dialogs/damageDialog.mjs b/module/applications/dialogs/damageDialog.mjs
index fbc584e4..b24570cc 100644
--- a/module/applications/dialogs/damageDialog.mjs
+++ b/module/applications/dialogs/damageDialog.mjs
@@ -6,6 +6,7 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
this.roll = roll;
this.config = config;
+ this.selectedEffects = this.config.bonusEffects;
}
static DEFAULT_OPTIONS = {
@@ -20,6 +21,7 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
icon: 'fa-solid fa-dice'
},
actions: {
+ toggleSelectedEffect: this.toggleSelectedEffect,
submitRoll: this.submitRoll
},
form: {
@@ -57,6 +59,9 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
icon
}));
context.modifiers = this.config.modifiers;
+ context.hasSelectedEffects = Boolean(Object.keys(this.selectedEffects).length);
+ context.selectedEffects = this.selectedEffects;
+
return context;
}
@@ -69,6 +74,11 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
this.render();
}
+ static toggleSelectedEffect(_event, button) {
+ this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected;
+ this.render();
+ }
+
static async submitRoll() {
await this.close({ submitted: true });
}
diff --git a/module/applications/dialogs/downtime.mjs b/module/applications/dialogs/downtime.mjs
index f03524f0..9a9a9ddb 100644
--- a/module/applications/dialogs/downtime.mjs
+++ b/module/applications/dialogs/downtime.mjs
@@ -93,27 +93,29 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
}
getRefreshables() {
- const actionItems = this.actor.items.filter(x => this.actor.system.isItemAvailable(x)).reduce((acc, x) => {
- if (x.system.actions) {
- const recoverable = x.system.actions.reduce((acc, action) => {
- if (refreshIsAllowed([this.shortrest ? 'shortRest' : 'longRest'], action.uses.recovery)) {
- acc.push({
- title: x.name,
- name: action.name,
- uuid: action.uuid
- });
+ const actionItems = this.actor.items
+ .filter(x => this.actor.system.isItemAvailable(x))
+ .reduce((acc, x) => {
+ if (x.system.actions) {
+ const recoverable = x.system.actions.reduce((acc, action) => {
+ if (refreshIsAllowed([this.shortrest ? 'shortRest' : 'longRest'], action.uses.recovery)) {
+ acc.push({
+ title: x.name,
+ name: action.name,
+ uuid: action.uuid
+ });
+ }
+
+ return acc;
+ }, []);
+
+ if (recoverable) {
+ acc.push(...recoverable);
}
-
- return acc;
- }, []);
-
- if (recoverable) {
- acc.push(...recoverable);
}
- }
- return acc;
- }, []);
+ return acc;
+ }, []);
const resourceItems = this.actor.items.reduce((acc, x) => {
if (
x.system.resource &&
@@ -189,7 +191,8 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
}));
});
});
- const characters = game.actors.filter(x => x.type === 'character')
+ const characters = game.actors
+ .filter(x => x.type === 'character')
.filter(x => x.testUserPermission(game.user, 'LIMITED'))
.filter(x => x.uuid !== this.actor.uuid);
diff --git a/module/applications/scene/sceneConfigSettings.mjs b/module/applications/scene/sceneConfigSettings.mjs
index be8f7b71..1b93aa8c 100644
--- a/module/applications/scene/sceneConfigSettings.mjs
+++ b/module/applications/scene/sceneConfigSettings.mjs
@@ -1,16 +1,28 @@
+import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
+
export default class DhSceneConfigSettings extends foundry.applications.sheets.SceneConfig {
- // static DEFAULT_OPTIONS = {
- // ...super.DEFAULT_OPTIONS,
- // form: {
- // handler: this.updateData,
- // closeOnSubmit: true
- // }
- // };
+ constructor(options) {
+ super(options);
+
+ Hooks.on(socketEvent.Refresh, ({ refreshType }) => {
+ if (refreshType === RefreshType.Scene) {
+ this.daggerheartFlag = new game.system.api.data.scenes.DHScene(this.document.flags.daggerheart);
+ this.render();
+ }
+ });
+ }
+
+ static DEFAULT_OPTIONS = {
+ ...super.DEFAULT_OPTIONS,
+ actions: {
+ ...super.DEFAULT_OPTIONS.actions,
+ removeSceneEnvironment: DhSceneConfigSettings.#removeSceneEnvironment
+ }
+ };
static buildParts() {
const { footer, tabs, ...parts } = super.PARTS;
const tmpParts = {
- // tabs,
tabs: { template: 'systems/daggerheart/templates/scene/tabs.hbs' },
...parts,
dh: { template: 'systems/daggerheart/templates/scene/dh-config.hbs' },
@@ -28,27 +40,45 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
static TABS = DhSceneConfigSettings.buildTabs();
+ async _preRender(context, options) {
+ await super._preFirstRender(context, options);
+ this.daggerheartFlag = new game.system.api.data.scenes.DHScene(this.document.flags.daggerheart);
+ }
+
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
+
switch (partId) {
case 'dh':
htmlElement.querySelector('#rangeMeasurementSetting')?.addEventListener('change', async event => {
- const flagData = foundry.utils.mergeObject(this.document.flags.daggerheart, {
- rangeMeasurement: { setting: event.target.value }
- });
- this.document.flags.daggerheart = flagData;
+ this.daggerheartFlag.updateSource({ rangeMeasurement: { setting: event.target.value } });
this.render();
});
+
+ const dragArea = htmlElement.querySelector('.scene-environments');
+ if (dragArea) dragArea.ondrop = this._onDrop.bind(this);
+
break;
}
}
+ async _onDrop(event) {
+ const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
+ const item = await foundry.utils.fromUuid(data.uuid);
+ if (item instanceof game.system.api.documents.DhpActor && item.type === 'environment') {
+ await this.daggerheartFlag.updateSource({
+ sceneEnvironments: [...this.daggerheartFlag.sceneEnvironments, data.uuid]
+ });
+ this.render();
+ }
+ }
+
/** @inheritDoc */
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
switch (partId) {
case 'dh':
- context.data = new game.system.api.data.scenes.DHScene(canvas.scene.flags.daggerheart);
+ context.data = this.daggerheartFlag;
context.variantRules = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules);
break;
}
@@ -56,8 +86,24 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
return context;
}
- // static async updateData(event, _, formData) {
- // const data = foundry.utils.expandObject(formData.object);
- // this.close(data);
- // }
+ static async #removeSceneEnvironment(_event, button) {
+ await this.daggerheartFlag.updateSource({
+ sceneEnvironments: this.daggerheartFlag.sceneEnvironments.filter(
+ (_, index) => index !== Number.parseInt(button.dataset.index)
+ )
+ });
+ this.render();
+ }
+
+ /** @override */
+ async _processSubmitData(event, form, submitData, options) {
+ submitData.flags.daggerheart = this.daggerheartFlag.toObject();
+ for (const key of Object.keys(this.document._source.flags.daggerheart?.sceneEnvironments ?? {})) {
+ if (!submitData.flags.daggerheart.sceneEnvironments[key]) {
+ submitData.flags.daggerheart.sceneEnvironments[`-=${key}`] = null;
+ }
+ }
+
+ super._processSubmitData(event, form, submitData, options);
+ }
}
diff --git a/module/applications/sheets-configs/action-base-config.mjs b/module/applications/sheets-configs/action-base-config.mjs
index 96790a5b..7051ad2b 100644
--- a/module/applications/sheets-configs/action-base-config.mjs
+++ b/module/applications/sheets-configs/action-base-config.mjs
@@ -7,6 +7,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
this.action = action;
this.openSection = null;
+ this.openTrigger = this.action.triggers.length > 0 ? 0 : null;
}
get title() {
@@ -15,7 +16,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
static DEFAULT_OPTIONS = {
tag: 'form',
- classes: ['daggerheart', 'dh-style', 'dialog', 'max-800'],
+ classes: ['daggerheart', 'dh-style', 'action-config', 'dialog', 'max-800'],
window: {
icon: 'fa-solid fa-wrench',
resizable: false
@@ -29,13 +30,18 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
removeElement: this.removeElement,
editEffect: this.editEffect,
addDamage: this.addDamage,
- removeDamage: this.removeDamage
+ removeDamage: this.removeDamage,
+ editDoc: this.editDoc,
+ addTrigger: this.addTrigger,
+ removeTrigger: this.removeTrigger,
+ expandTrigger: this.expandTrigger
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
- }
+ },
+ dragDrop: [{ dragSelector: null, dropSelector: '#summon-drop-zone', handlers: ['_onDrop'] }]
};
static PARTS = {
@@ -55,6 +61,10 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
effect: {
id: 'effect',
template: 'systems/daggerheart/templates/sheets-settings/action-settings/effect.hbs'
+ },
+ trigger: {
+ id: 'trigger',
+ template: 'systems/daggerheart/templates/sheets-settings/action-settings/trigger.hbs'
}
};
@@ -82,10 +92,18 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
id: 'effect',
icon: null,
label: 'DAGGERHEART.GENERAL.Tabs.effects'
+ },
+ trigger: {
+ active: false,
+ cssClass: '',
+ group: 'primary',
+ id: 'trigger',
+ icon: null,
+ label: 'DAGGERHEART.GENERAL.Tabs.triggers'
}
};
- static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects'];
+ static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects', 'summon'];
_getTabs(tabs) {
for (const v of Object.values(tabs)) {
@@ -96,9 +114,24 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
return tabs;
}
+ _attachPartListeners(partId, htmlElement, options) {
+ super._attachPartListeners(partId, htmlElement, options);
+
+ htmlElement.querySelectorAll('.summon-count-wrapper input').forEach(element => {
+ element.addEventListener('change', this.updateSummonCount.bind(this));
+ });
+ }
+
async _prepareContext(_options) {
const context = await super._prepareContext(_options, 'action');
context.source = this.action.toObject(true);
+
+ context.summons = [];
+ for (const summon of context.source.summon ?? []) {
+ const actor = await foundry.utils.fromUuid(summon.actorUUID);
+ context.summons.push({ actor, count: summon.count });
+ }
+
context.openSection = this.openSection;
context.tabs = this._getTabs(this.constructor.TABS);
context.config = CONFIG.DH;
@@ -111,6 +144,16 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
context.baseSaveDifficulty = this.action.actor?.baseSaveDifficulty;
context.baseAttackBonus = this.action.actor?.system.attack?.roll.bonus;
context.hasRoll = this.action.hasRoll;
+ context.triggers = context.source.triggers.map((trigger, index) => {
+ const { hint, returns, usesActor } = CONFIG.DH.TRIGGER.triggers[trigger.trigger];
+ return {
+ ...trigger,
+ hint,
+ returns,
+ usesActor,
+ revealed: this.openTrigger === index
+ };
+ });
const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers;
context.tierOptions = [
@@ -181,8 +224,9 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
}
static async updateForm(event, _, formData) {
- const submitData = this._prepareSubmitData(event, formData),
- data = foundry.utils.mergeObject(this.action.toObject(), submitData);
+ const submitData = this._prepareSubmitData(event, formData);
+
+ const data = foundry.utils.mergeObject(this.action.toObject(), submitData);
this.action = await this.action.update(data);
this.sheetUpdate?.(this.action);
@@ -201,12 +245,26 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
static removeElement(event, button) {
event.stopPropagation();
const data = this.action.toObject(),
- key = event.target.closest('[data-key]').dataset.key,
- index = button.dataset.index;
+ key = event.target.closest('[data-key]').dataset.key;
+
+ // Prefer explicit index, otherwise find by uuid
+ let index = button?.dataset.index;
+ if (index === undefined || index === null || index === '') {
+ const uuid = button?.dataset.uuid ?? button?.dataset.itemUuid;
+ index = data[key].findIndex(e => (e?.actorUUID ?? e?.uuid) === uuid);
+ if (index === -1) return;
+ } else index = Number(index);
+
data[key].splice(index, 1);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
+ static async editDoc(_event, target) {
+ const element = target.closest('[data-item-uuid]');
+ const doc = (await foundry.utils.fromUuid(element.dataset.itemUuid)) ?? null;
+ if (doc) return doc.sheet.render({ force: true });
+ }
+
static addDamage(_event) {
if (!this.action.damage.parts) return;
const data = this.action.toObject(),
@@ -224,6 +282,69 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
+ static addTrigger() {
+ const data = this.action.toObject();
+ data.triggers.push({
+ trigger: CONFIG.DH.TRIGGER.triggers.dualityRoll.id,
+ triggeringActor: CONFIG.DH.TRIGGER.triggerActorTargetType.any.id
+ });
+ this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
+ }
+
+ static async removeTrigger(_event, button) {
+ const trigger = CONFIG.DH.TRIGGER.triggers[this.action.triggers[button.dataset.index].trigger];
+ const confirmed = await foundry.applications.api.DialogV2.confirm({
+ window: {
+ title: game.i18n.localize('DAGGERHEART.ACTIONS.Config.deleteTriggerTitle')
+ },
+ content: game.i18n.format('DAGGERHEART.ACTIONS.Config.deleteTriggerContent', {
+ trigger: game.i18n.localize(trigger.label)
+ })
+ });
+
+ if (!confirmed) return;
+
+ const data = this.action.toObject();
+ data.triggers = data.triggers.filter((_, index) => index !== Number.parseInt(button.dataset.index));
+ this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
+ }
+
+ static async expandTrigger(_event, button) {
+ const index = Number.parseInt(button.dataset.index);
+ const toggle = (element, codeMirror) => {
+ codeMirror.classList.toggle('revealed');
+ const button = element.querySelector('a > i');
+ button.classList.toggle('fa-angle-up');
+ button.classList.toggle('fa-angle-down');
+ };
+
+ const fieldset = button.closest('fieldset');
+ const codeMirror = fieldset.querySelector('.code-mirror-wrapper');
+ toggle(fieldset, codeMirror);
+
+ if (this.openTrigger !== null && this.openTrigger !== index) {
+ const previouslyExpanded = fieldset
+ .closest(`section`)
+ .querySelector(`fieldset[data-index="${this.openTrigger}"]`);
+ const codeMirror = previouslyExpanded.querySelector('.code-mirror-wrapper');
+ toggle(previouslyExpanded, codeMirror);
+ this.openTrigger = index;
+ } else if (this.openTrigger === index) {
+ this.openTrigger = null;
+ } else {
+ this.openTrigger = index;
+ }
+ }
+
+ updateSummonCount(event) {
+ event.stopPropagation();
+ const wrapper = event.target.closest('.summon-count-wrapper');
+ const index = wrapper.dataset.index;
+ const data = this.action.toObject();
+ data.summon[index].count = event.target.value;
+ 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) {}
@@ -233,4 +354,29 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
this.tabGroups.primary = 'base';
await super.close(options);
}
+
+ async _onDrop(event) {
+ const data = foundry.applications.ux.TextEditor.getDragEventData(event);
+ const item = await foundry.utils.fromUuid(data.uuid);
+ if (!(item instanceof game.system.api.documents.DhpActor)) {
+ ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.summon.invalidDrop'));
+ return;
+ }
+
+ const actionData = this.action.toObject();
+ let countvalue = 1;
+ for (const entry of actionData.summon) {
+ if (entry.actorUUID === data.uuid) {
+ entry.count += 1;
+ countvalue = entry.count;
+ await this.constructor.updateForm.bind(this)(null, null, {
+ object: foundry.utils.flattenObject(actionData)
+ });
+ return;
+ }
+ }
+
+ actionData.summon.push({ actorUUID: data.uuid, count: countvalue });
+ await this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(actionData) });
+ }
}
diff --git a/module/applications/sheets/actors/adversary.mjs b/module/applications/sheets/actors/adversary.mjs
index 98282d9f..d8a3df29 100644
--- a/module/applications/sheets/actors/adversary.mjs
+++ b/module/applications/sheets/actors/adversary.mjs
@@ -31,7 +31,7 @@ export default class AdversarySheet extends DHBaseActorSheet {
dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
dropSelector: null
}
- ],
+ ]
};
static PARTS = {
@@ -185,7 +185,6 @@ export default class AdversarySheet extends DHBaseActorSheet {
super._onDragStart(event);
}
-
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs
index 852757a6..79cbe304 100644
--- a/module/applications/sheets/actors/character.mjs
+++ b/module/applications/sheets/actors/character.mjs
@@ -33,7 +33,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
advanceResourceDie: CharacterSheet.#advanceResourceDie,
cancelBeastform: CharacterSheet.#cancelBeastform,
useDowntime: this.useDowntime,
- viewParty: CharacterSheet.#viewParty,
+ viewParty: CharacterSheet.#viewParty
},
window: {
resizable: true,
@@ -338,15 +338,20 @@ export default class CharacterSheet extends DHBaseActorSheet {
}
const type = 'effect';
const cls = game.system.api.models.actions.actionsTypes[type];
- const action = new cls({
- ...cls.getSourceConfig(doc.system),
- type: type,
- chatDisplay: false,
- cost: [{
- key: 'stress',
- value: doc.system.recallCost
- }]
- }, { parent: doc.system });
+ const action = new cls(
+ {
+ ...cls.getSourceConfig(doc.system),
+ type: type,
+ chatDisplay: false,
+ cost: [
+ {
+ key: 'stress',
+ value: doc.system.recallCost
+ }
+ ]
+ },
+ { parent: doc.system }
+ );
const config = await action.use(event);
if (config) {
return doc.update({ 'system.inVault': false });
@@ -707,8 +712,10 @@ export default class CharacterSheet extends DHBaseActorSheet {
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: abilityLabel
}),
+ effects: Array.from(await this.document.allApplicableEffects()),
roll: {
- trait: button.dataset.attribute
+ trait: button.dataset.attribute,
+ type: 'trait'
},
hasRoll: true,
actionType: 'action',
@@ -718,6 +725,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
})
};
const result = await this.document.diceRoll(config);
+ if (!result) return;
/* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */
const costResources = result.costs
@@ -822,7 +830,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
static async #toggleVault(_event, button) {
const doc = await getDocFromElement(button);
const { available } = this.document.system.loadoutSlot;
- if (doc.system.inVault && !available) {
+ if (doc.system.inVault && !available && !doc.system.loadoutIgnore) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.loadoutMaxReached'));
}
@@ -900,32 +908,32 @@ export default class CharacterSheet extends DHBaseActorSheet {
return;
}
- const buttons = parties.map((p) => {
- const button = document.createElement("button");
- button.type = "button";
- button.classList.add("plain");
- const img = document.createElement("img");
+ const buttons = parties.map(p => {
+ const button = document.createElement('button');
+ button.type = 'button';
+ button.classList.add('plain');
+ const img = document.createElement('img');
img.src = p.img;
button.append(img);
- const name = document.createElement("span");
+ const name = document.createElement('span');
name.textContent = p.name;
button.append(name);
- button.addEventListener("click", () => {
+ button.addEventListener('click', () => {
p.sheet?.render({ force: true });
game.tooltip.dismissLockedTooltips();
});
return button;
});
- const html = document.createElement("div");
- html.classList.add("party-list");
+ const html = document.createElement('div');
+ html.classList.add('party-list');
html.append(...buttons);
-
+
game.tooltip.dismissLockedTooltips();
game.tooltip.activate(target, {
html,
- locked: true,
- })
+ locked: true
+ });
}
/**
diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs
index d25a1a4e..903caa2a 100644
--- a/module/applications/sheets/api/application-mixin.mjs
+++ b/module/applications/sheets/api/application-mixin.mjs
@@ -211,7 +211,7 @@ export default function DHApplicationMixin(Base) {
const step = event.key === 'ArrowUp' ? 1 : event.key === 'ArrowDown' ? -1 : 0;
if (step !== 0) {
handleUpdate(step);
- deltaInput.dispatchEvent(new Event("change", { bubbles: true }));
+ deltaInput.dispatchEvent(new Event('change', { bubbles: true }));
}
});
@@ -222,7 +222,7 @@ export default function DHApplicationMixin(Base) {
if (deltaInput === document.activeElement) {
event.preventDefault();
handleUpdate(Math.sign(-1 * event.deltaY));
- deltaInput.dispatchEvent(new Event("change", { bubbles: true }));
+ deltaInput.dispatchEvent(new Event('change', { bubbles: true }));
}
},
{ passive: false }
@@ -236,7 +236,7 @@ export default function DHApplicationMixin(Base) {
// Handle contenteditable
for (const input of htmlElement.querySelectorAll('[contenteditable][data-property]')) {
const property = input.dataset.property;
- input.addEventListener("blur", () => {
+ input.addEventListener('blur', () => {
const selection = document.getSelection();
if (input.contains(selection.anchorNode)) {
selection.empty();
@@ -244,12 +244,12 @@ export default function DHApplicationMixin(Base) {
this.document.update({ [property]: input.textContent });
});
- input.addEventListener("keydown", event => {
- if (event.key === "Enter") input.blur();
+ input.addEventListener('keydown', event => {
+ if (event.key === 'Enter') input.blur();
});
// Chrome sometimes add
, which aren't a problem for the value but are for the placeholder
- input.addEventListener("input", () => input.querySelectorAll("br").forEach((i) => i.remove()));
+ input.addEventListener('input', () => input.querySelectorAll('br').forEach(i => i.remove()));
}
}
@@ -585,7 +585,9 @@ export default function DHApplicationMixin(Base) {
if (!doc || !descriptionElement) continue;
// localize the description (idk if it's still necessary)
- const description = game.i18n.localize(doc.system?.description ?? doc.description);
+ const description = doc.system?.getEnrichedDescription
+ ? await doc.system.getEnrichedDescription()
+ : game.i18n.localize(doc.system?.description ?? doc.description);
// Enrich the description and attach it;
const isAction = doc.documentName === 'Action';
@@ -736,7 +738,7 @@ export default function DHApplicationMixin(Base) {
};
if (inVault) data['system.inVault'] = true;
if (disabled) data.disabled = true;
- if (type === "domainCard" && parent?.system.domains?.length) {
+ if (type === 'domainCard' && parent?.system.domains?.length) {
data.system.domain = parent.system.domains[0];
}
diff --git a/module/applications/sheets/api/base-item.mjs b/module/applications/sheets/api/base-item.mjs
index 42ed9426..e3568b23 100644
--- a/module/applications/sheets/api/base-item.mjs
+++ b/module/applications/sheets/api/base-item.mjs
@@ -76,16 +76,10 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
/**@inheritdoc */
async _preparePartContext(partId, context, options) {
await super._preparePartContext(partId, context, options);
- const { TextEditor } = foundry.applications.ux;
switch (partId) {
case 'description':
- const value = foundry.utils.getProperty(this.document, 'system.description') ?? '';
- context.enrichedDescription = await TextEditor.enrichHTML(value, {
- relativeTo: this.item,
- rollData: this.item.getRollData(),
- secrets: this.item.isOwner
- });
+ context.enrichedDescription = await this.document.system.getEnrichedDescription();
break;
case 'effects':
await this._prepareEffectsContext(context, options);
diff --git a/module/applications/ui/_module.mjs b/module/applications/ui/_module.mjs
index d5f31906..8c5c020e 100644
--- a/module/applications/ui/_module.mjs
+++ b/module/applications/ui/_module.mjs
@@ -5,4 +5,5 @@ export { default as DhCombatTracker } from './combatTracker.mjs';
export { default as DhEffectsDisplay } from './effectsDisplay.mjs';
export { default as DhFearTracker } from './fearTracker.mjs';
export { default as DhHotbar } from './hotbar.mjs';
+export { default as DhSceneNavigation } from './sceneNavigation.mjs';
export { ItemBrowser } from './itemBrowser.mjs';
diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs
index cc42df2f..20dfea8d 100644
--- a/module/applications/ui/chatLog.mjs
+++ b/module/applications/ui/chatLog.mjs
@@ -135,7 +135,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
async actionUseButton(event, message) {
const { moveIndex, actionIndex, movePath } = event.currentTarget.dataset;
const targetUuid = event.currentTarget.closest('.action-use-button-parent').querySelector('select')?.value;
- const parent = await foundry.utils.fromUuid(targetUuid || message.system.actor)
+ const parent = await foundry.utils.fromUuid(targetUuid || message.system.actor);
const actionType = message.system.moves[moveIndex].actions[actionIndex];
const cls = game.system.api.models.actions.actionsTypes[actionType.type];
diff --git a/module/applications/ui/sceneNavigation.mjs b/module/applications/ui/sceneNavigation.mjs
new file mode 100644
index 00000000..ac16ac99
--- /dev/null
+++ b/module/applications/ui/sceneNavigation.mjs
@@ -0,0 +1,89 @@
+import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
+
+export default class DhSceneNavigation extends foundry.applications.ui.SceneNavigation {
+ /** @inheritdoc */
+ static DEFAULT_OPTIONS = {
+ ...super.DEFAULT_OPTIONS,
+ classes: ['faded-ui', 'flexcol', 'scene-navigation'],
+ actions: {
+ openSceneEnvironment: DhSceneNavigation.#openSceneEnvironment
+ }
+ };
+
+ /** @inheritdoc */
+ static PARTS = {
+ scenes: {
+ root: true,
+ template: 'systems/daggerheart/templates/ui/sceneNavigation/scene-navigation.hbs'
+ }
+ };
+
+ /** @inheritdoc */
+ async _prepareContext(options) {
+ const context = await super._prepareContext(options);
+
+ const extendScenes = scenes =>
+ scenes.map(x => {
+ const scene = game.scenes.get(x.id);
+ if (!scene.flags.daggerheart) return x;
+
+ const daggerheartInfo = new game.system.api.data.scenes.DHScene(scene.flags.daggerheart);
+ const environments = daggerheartInfo.sceneEnvironments.filter(
+ x => x && x.testUserPermission(game.user, 'LIMITED')
+ );
+ const hasEnvironments = environments.length > 0;
+ return {
+ ...x,
+ hasEnvironments,
+ environmentImage: hasEnvironments ? environments[0].img : null,
+ environments: environments
+ };
+ });
+ context.scenes.active = extendScenes(context.scenes.active);
+ context.scenes.inactive = extendScenes(context.scenes.inactive);
+
+ return context;
+ }
+
+ static async #openSceneEnvironment(event, button) {
+ const scene = game.scenes.get(button.dataset.sceneId);
+ const sceneEnvironments = new game.system.api.data.scenes.DHScene(
+ scene.flags.daggerheart
+ ).sceneEnvironments.filter(x => x.testUserPermission(game.user, 'LIMITED'));
+
+ if (sceneEnvironments.length === 1 || event.shiftKey) {
+ sceneEnvironments[0].sheet.render(true);
+ } else {
+ new foundry.applications.ux.ContextMenu.implementation(
+ button,
+ '.scene-environment',
+ sceneEnvironments.map(environment => ({
+ name: environment.name,
+ callback: () => {
+ if (scene.flags.daggerheart.sceneEnvironments[0] !== environment.uuid) {
+ const newEnvironments = scene.flags.daggerheart.sceneEnvironments;
+ const newFirst = newEnvironments.splice(
+ newEnvironments.findIndex(x => x === environment.uuid)
+ )[0];
+ newEnvironments.unshift(newFirst);
+ emitAsGM(
+ GMUpdateEvent.UpdateDocument,
+ scene.update.bind(scene),
+ { 'flags.daggerheart.sceneEnvironments': newEnvironments },
+ scene.uuid
+ );
+ }
+
+ environment.sheet.render({ force: true });
+ }
+ })),
+ {
+ jQuery: false,
+ fixed: true
+ }
+ );
+
+ CONFIG.ux.ContextMenu.triggerContextMenu(event, '.scene-environment');
+ }
+ }
+}
diff --git a/module/applications/ux/contextMenu.mjs b/module/applications/ux/contextMenu.mjs
index 09454848..081e6ba0 100644
--- a/module/applications/ux/contextMenu.mjs
+++ b/module/applications/ux/contextMenu.mjs
@@ -96,11 +96,11 @@ export default class DHContextMenu extends foundry.applications.ux.ContextMenu {
* Trigger a context menu event in response to a normal click on a additional options button.
* @param {PointerEvent} event
*/
- static triggerContextMenu(event) {
+ static triggerContextMenu(event, altSelector) {
event.preventDefault();
event.stopPropagation();
const { clientX, clientY } = event;
- const selector = '[data-item-uuid]';
+ const selector = altSelector ?? '[data-item-uuid]';
const target = event.target.closest(selector) ?? event.currentTarget.closest(selector);
target?.dispatchEvent(
new PointerEvent('contextmenu', {
diff --git a/module/canvas/_module.mjs b/module/canvas/_module.mjs
index 6b8885f4..c211b549 100644
--- a/module/canvas/_module.mjs
+++ b/module/canvas/_module.mjs
@@ -1 +1,2 @@
export * as placeables from './placeables/_module.mjs';
+export { default as DhTokenLayer } from './tokens.mjs';
diff --git a/module/canvas/placeables/token.mjs b/module/canvas/placeables/token.mjs
index e8b85938..d8acb73a 100644
--- a/module/canvas/placeables/token.mjs
+++ b/module/canvas/placeables/token.mjs
@@ -1,4 +1,12 @@
export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
+ /** @inheritdoc */
+ async _draw(options) {
+ await super._draw(options);
+
+ if (this.document.flags.daggerheart?.createPlacement)
+ this.previewHelp ||= this.addChild(this.#drawPreviewHelp());
+ }
+
/** @inheritDoc */
async _drawEffects() {
this.effects.renderable = false;
@@ -34,7 +42,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
this.renderFlags.set({ refreshEffects: true });
}
- /**
+ /**
* Returns the distance from this token to another token object.
* This value is corrected to handle alternate token sizes and other grid types
* according to the diagonal rules.
@@ -47,11 +55,11 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
const destinationPoint = target.center;
// Compute for gridless. This version returns circular edge to edge + grid distance,
- // so that tokens that are touching return 5.
+ // so that tokens that are touching return 5.
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
const boundsCorrection = canvas.grid.distance / canvas.grid.size;
- const originRadius = this.bounds.width * boundsCorrection / 2;
- const targetRadius = target.bounds.width * boundsCorrection / 2;
+ const originRadius = (this.bounds.width * boundsCorrection) / 2;
+ const targetRadius = (target.bounds.width * boundsCorrection) / 2;
const distance = canvas.grid.measurePath([originPoint, destinationPoint]).distance;
return distance - originRadius - targetRadius + canvas.grid.distance;
}
@@ -61,11 +69,11 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
const targetEdge = this.#getEdgeBoundary(target.bounds, originPoint, destinationPoint);
const adjustedOriginPoint = canvas.grid.getTopLeftPoint({
x: originEdge.x + Math.sign(originPoint.x - originEdge.x),
- y: originEdge.y + Math.sign(originPoint.y - originEdge.y)
+ y: originEdge.y + Math.sign(originPoint.y - originEdge.y)
});
const adjustDestinationPoint = canvas.grid.getTopLeftPoint({
x: targetEdge.x + Math.sign(destinationPoint.x - targetEdge.x),
- y: targetEdge.y + Math.sign(destinationPoint.y - targetEdge.y)
+ y: targetEdge.y + Math.sign(destinationPoint.y - targetEdge.y)
});
return canvas.grid.measurePath([adjustedOriginPoint, adjustDestinationPoint]).distance;
}
@@ -94,7 +102,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
/** Tests if the token is at least adjacent with another, with some leeway for diagonals */
isAdjacentWith(token) {
- return this.distanceTo(token) <= (canvas.grid.distance * 1.5);
+ return this.distanceTo(token) <= canvas.grid.distance * 1.5;
}
/** @inheritDoc */
@@ -132,4 +140,25 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
bar.position.set(0, posY);
return true;
}
+
+ /**
+ * Draw a helptext for previews as a text object
+ * @returns {PreciseText} The Text object for the preview helper
+ */
+ #drawPreviewHelp() {
+ const { uiScale } = canvas.dimensions;
+
+ const textStyle = CONFIG.canvasTextStyle.clone();
+ textStyle.fontSize = 18;
+ textStyle.wordWrapWidth = this.w * 2.5;
+ textStyle.fontStyle = 'italic';
+
+ const helpText = new PreciseText(
+ `(${game.i18n.localize('DAGGERHEART.UI.Tooltip.previewTokenHelp')})`,
+ textStyle
+ );
+ helpText.anchor.set(helpText.width / 900, 1);
+ helpText.scale.set(uiScale, uiScale);
+ return helpText;
+ }
}
diff --git a/module/canvas/tokens.mjs b/module/canvas/tokens.mjs
new file mode 100644
index 00000000..9813cd48
--- /dev/null
+++ b/module/canvas/tokens.mjs
@@ -0,0 +1,16 @@
+export default class DhTokenLayer extends foundry.canvas.layers.TokenLayer {
+ async _createPreview(createData, options) {
+ if (options.actor) {
+ const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes;
+ if (options.actor?.system.metadata.usesSize) {
+ const tokenSize = tokenSizes[options.actor.system.size];
+ if (tokenSize && options.actor.system.size !== CONFIG.DH.ACTOR.tokenSize.custom.id) {
+ createData.width = tokenSize;
+ createData.height = tokenSize;
+ }
+ }
+ }
+
+ return super._createPreview(createData, options);
+ }
+}
diff --git a/module/config/_module.mjs b/module/config/_module.mjs
index ef26b958..560f3fec 100644
--- a/module/config/_module.mjs
+++ b/module/config/_module.mjs
@@ -10,3 +10,4 @@ export * as itemConfig from './itemConfig.mjs';
export * as settingsConfig from './settingsConfig.mjs';
export * as systemConfig from './system.mjs';
export * as itemBrowserConfig from './itemBrowserConfig.mjs';
+export * as triggerConfig from './triggerConfig.mjs';
diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs
index 3f49f7aa..37894644 100644
--- a/module/config/generalConfig.mjs
+++ b/module/config/generalConfig.mjs
@@ -496,6 +496,8 @@ export const diceTypes = {
d20: 'd20'
};
+export const dieFaces = [4, 6, 8, 10, 12, 20];
+
export const multiplierTypes = {
prof: 'Proficiency',
cast: 'Spellcast',
diff --git a/module/config/hooksConfig.mjs b/module/config/hooksConfig.mjs
index d316c4d4..9140ea0a 100644
--- a/module/config/hooksConfig.mjs
+++ b/module/config/hooksConfig.mjs
@@ -1,5 +1,3 @@
-const hooksConfig = {
+export const hooksConfig = {
effectDisplayToggle: 'DHEffectDisplayToggle'
};
-
-export default hooksConfig;
diff --git a/module/config/system.mjs b/module/config/system.mjs
index ac15b1d9..47a41e8d 100644
--- a/module/config/system.mjs
+++ b/module/config/system.mjs
@@ -7,7 +7,8 @@ import * as SETTINGS from './settingsConfig.mjs';
import * as EFFECTS from './effectConfig.mjs';
import * as ACTIONS from './actionConfig.mjs';
import * as FLAGS from './flagsConfig.mjs';
-import HOOKS from './hooksConfig.mjs';
+import * as HOOKS from './hooksConfig.mjs';
+import * as TRIGGER from './triggerConfig.mjs';
import * as ITEMBROWSER from './itemBrowserConfig.mjs';
export const SYSTEM_ID = 'daggerheart';
@@ -24,5 +25,6 @@ export const SYSTEM = {
ACTIONS,
FLAGS,
HOOKS,
+ TRIGGER,
ITEMBROWSER
};
diff --git a/module/config/triggerConfig.mjs b/module/config/triggerConfig.mjs
new file mode 100644
index 00000000..35609bd1
--- /dev/null
+++ b/module/config/triggerConfig.mjs
@@ -0,0 +1,42 @@
+/* hints and returns are intentionally not translated. They are programatical terms and best understood in english */
+export const triggers = {
+ dualityRoll: {
+ id: 'dualityRoll',
+ usesActor: true,
+ args: ['roll', 'actor'],
+ label: 'DAGGERHEART.CONFIG.Triggers.dualityRoll.label',
+ hint: 'this: Action, roll: DhRoll, actor: DhActor',
+ returns: '{ updates: [{ key, value, total }] }'
+ },
+ fearRoll: {
+ id: 'fearRoll',
+ usesActor: true,
+ args: ['roll', 'actor'],
+ label: 'DAGGERHEART.CONFIG.Triggers.fearRoll.label',
+ hint: 'this: Action, roll: DhRoll, actor: DhActor',
+ returns: '{ updates: [{ key, value, total }] }'
+ },
+ postDamageReduction: {
+ id: 'postDamageReduction',
+ usesActor: true,
+ args: ['damageUpdates', 'actor'],
+ label: 'DAGGERHEART.CONFIG.Triggers.postDamageReduction.label',
+ hint: 'damageUpdates: ResourceUpdates, actor: DhActor',
+ returns: '{ updates: [{ originActor: this.actor, updates: [{ key, value, total }] }] }'
+ }
+};
+
+export const triggerActorTargetType = {
+ any: {
+ id: 'any',
+ label: 'DAGGERHEART.CONFIG.TargetTypes.any'
+ },
+ self: {
+ id: 'self',
+ label: 'DAGGERHEART.CONFIG.TargetTypes.self'
+ },
+ other: {
+ id: 'other',
+ label: 'DAGGERHEART.CONFIG.TargetTypes.other'
+ }
+};
diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs
index 18a09904..dac4cf68 100644
--- a/module/data/action/baseAction.mjs
+++ b/module/data/action/baseAction.mjs
@@ -2,6 +2,7 @@ import DhpActor from '../../documents/actor.mjs';
import D20RollDialog from '../../applications/dialogs/d20RollDialog.mjs';
import { ActionMixin } from '../fields/actionField.mjs';
import { originItemField } from '../chat-message/actorRoll.mjs';
+import TriggerField from '../fields/triggerField.mjs';
const fields = foundry.data.fields;
@@ -34,7 +35,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
nullable: false,
required: true
}),
- targetUuid: new fields.StringField({ initial: undefined })
+ targetUuid: new fields.StringField({ initial: undefined }),
+ triggers: new fields.ArrayField(new TriggerField())
};
this.extraSchemas.forEach(s => {
@@ -164,7 +166,6 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
*/
getRollData(data = {}) {
const actorData = this.actor ? this.actor.getRollData(false) : {};
-
actorData.result = data.roll?.total ?? 1;
actorData.scale = data.costs?.length // Right now only return the first scalable cost.
? (data.costs.find(c => c.scalable)?.total ?? 1)
@@ -197,6 +198,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
let config = this.prepareConfig(event);
if (!config) return;
+ await this.addEffects(config);
+
if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return;
// Display configuration window if necessary
@@ -263,6 +266,16 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return config;
}
+ /** */
+ async addEffects(config) {
+ let effects = [];
+ if (this.actor) {
+ effects = Array.from(await this.actor.allApplicableEffects());
+ }
+
+ config.effects = effects;
+ }
+
/**
* Method used to know if a configuration dialog must be shown or not when there is no roll.
* @param {*} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
@@ -343,6 +356,10 @@ export class ResourceUpdateMap extends Map {
}
addResources(resources) {
+ if (!resources?.length) return;
+ const invalidResources = resources.some(resource => !resource.key);
+ if (invalidResources) return;
+
for (const resource of resources) {
if (!resource.key) continue;
diff --git a/module/data/action/summonAction.mjs b/module/data/action/summonAction.mjs
index b06f1d38..1505ce2d 100644
--- a/module/data/action/summonAction.mjs
+++ b/module/data/action/summonAction.mjs
@@ -1,19 +1,5 @@
import DHBaseAction from './baseAction.mjs';
export default class DHSummonAction extends DHBaseAction {
- static defineSchema() {
- const fields = foundry.data.fields;
- return {
- ...super.defineSchema(),
- documentUUID: new fields.DocumentUUIDField({ type: 'Actor' })
- };
- }
-
- async trigger(event, ...args) {
- if (!this.canSummon || !canvas.scene) return;
- }
-
- get canSummon() {
- return game.user.can('TOKEN_CREATE');
- }
+ static extraSchemas = [...super.extraSchemas, 'summon'];
}
diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs
index b729d590..a305c6ce 100644
--- a/module/data/actor/character.mjs
+++ b/module/data/actor/character.mjs
@@ -275,6 +275,24 @@ export default class DhCharacter extends BaseDataActor {
})
})
}),
+ dualityRoll: new fields.SchemaField({
+ defaultHopeDice: new fields.NumberField({
+ nullable: false,
+ required: true,
+ integer: true,
+ choices: CONFIG.DH.GENERAL.dieFaces,
+ initial: 12,
+ label: 'DAGGERHEART.ACTORS.Character.defaultHopeDice'
+ }),
+ defaultFearDice: new fields.NumberField({
+ nullable: false,
+ required: true,
+ integer: true,
+ choices: CONFIG.DH.GENERAL.dieFaces,
+ initial: 12,
+ label: 'DAGGERHEART.ACTORS.Character.defaultFearDice'
+ })
+ }),
runeWard: new fields.BooleanField({ initial: false }),
burden: new fields.SchemaField({
ignore: new fields.BooleanField()
diff --git a/module/data/actor/environment.mjs b/module/data/actor/environment.mjs
index 4ed3819e..0aaf8eb0 100644
--- a/module/data/actor/environment.mjs
+++ b/module/data/actor/environment.mjs
@@ -1,8 +1,11 @@
import BaseDataActor from './base.mjs';
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
import DHEnvironmentSettings from '../../applications/sheets-configs/environment-settings.mjs';
+import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
export default class DhEnvironment extends BaseDataActor {
+ scenes = new Set();
+
/**@override */
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Environment'];
@@ -53,6 +56,31 @@ export default class DhEnvironment extends BaseDataActor {
}
isItemValid(source) {
- return source.type === "feature";
+ return source.type === 'feature';
+ }
+
+ _onUpdate(changes, options, userId) {
+ super._onUpdate(changes, options, userId);
+ for (const scene of this.scenes) {
+ scene.render();
+ }
+ }
+
+ _onDelete(options, userId) {
+ super._onDelete(options, userId);
+ for (const scene of this.scenes) {
+ if (game.user.isActiveGM) {
+ const newSceneEnvironments = scene.flags.daggerheart.sceneEnvironments.filter(
+ x => x !== this.parent.uuid
+ );
+ scene.update({ 'flags.daggerheart.sceneEnvironments': newSceneEnvironments }).then(() => {
+ Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Scene });
+ game.socket.emit(`system.${CONFIG.DH.id}`, {
+ action: socketEvent.Refresh,
+ data: { refreshType: RefreshType.TagTeamRoll }
+ });
+ });
+ }
+ }
}
}
diff --git a/module/data/fields/_module.mjs b/module/data/fields/_module.mjs
index 8d36b76d..2a8ba454 100644
--- a/module/data/fields/_module.mjs
+++ b/module/data/fields/_module.mjs
@@ -2,5 +2,6 @@ export { ActionCollection } from './actionField.mjs';
export { default as FormulaField } from './formulaField.mjs';
export { default as ForeignDocumentUUIDField } from './foreignDocumentUUIDField.mjs';
export { default as ForeignDocumentUUIDArrayField } from './foreignDocumentUUIDArrayField.mjs';
+export { default as TriggerField } from './triggerField.mjs';
export { default as MappingField } from './mappingField.mjs';
export * as ActionFields from './action/_module.mjs';
diff --git a/module/data/fields/action/_module.mjs b/module/data/fields/action/_module.mjs
index ef69394a..0bdffca2 100644
--- a/module/data/fields/action/_module.mjs
+++ b/module/data/fields/action/_module.mjs
@@ -9,3 +9,4 @@ export { default as BeastformField } from './beastformField.mjs';
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';
diff --git a/module/data/fields/action/summonField.mjs b/module/data/fields/action/summonField.mjs
new file mode 100644
index 00000000..dce6414c
--- /dev/null
+++ b/module/data/fields/action/summonField.mjs
@@ -0,0 +1,89 @@
+import FormulaField from '../formulaField.mjs';
+
+const fields = foundry.data.fields;
+
+export default class DHSummonField extends fields.ArrayField {
+ /**
+ * Action Workflow order
+ */
+ static order = 120;
+
+ constructor(options = {}, context = {}) {
+ const summonFields = new fields.SchemaField({
+ actorUUID: new fields.DocumentUUIDField({
+ type: 'Actor',
+ required: true
+ }),
+ count: new FormulaField({
+ required: true,
+ default: '1'
+ })
+ });
+ super(summonFields, options, context);
+ }
+
+ static async execute() {
+ if (!canvas.scene) {
+ ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.summon.error'));
+ return;
+ }
+
+ if (this.summon.length === 0) {
+ ui.notifications.warn('No actors configured for this Summon action.');
+ return;
+ }
+
+ const rolls = [];
+ const summonData = [];
+ for (const summon of this.summon) {
+ let count = summon.count;
+ const roll = new Roll(summon.count);
+ if (!roll.isDeterministic) {
+ await roll.evaluate();
+ if (game.modules.get('dice-so-nice')?.active) rolls.push(roll);
+ count = roll.total;
+ }
+
+ const actor = DHSummonField.getWorldActor(await foundry.utils.fromUuid(summon.actorUUID));
+ /* Extending summon data in memory so it's available in actionField.toChat. Think it's harmless, but ugly. Could maybe find a better way. */
+ summon.rolledCount = count;
+ summon.actor = actor.toObject();
+
+ summonData.push({ actor, count: count });
+ }
+
+ if (rolls.length) await Promise.all(rolls.map(roll => game.dice3d.showForRoll(roll, game.user, true)));
+
+ this.actor.sheet?.minimize();
+ DHSummonField.handleSummon(summonData, this.actor);
+ }
+
+ /* Check for any available instances of the actor present in the world if we're missing artwork in the compendium */
+ static getWorldActor(baseActor) {
+ const dataType = game.system.api.data.actors[`Dh${baseActor.type.capitalize()}`];
+ if (baseActor.inCompendium && dataType && baseActor.img === dataType.DEFAULT_ICON) {
+ const worldActorCopy = game.actors.find(x => x.name === baseActor.name);
+ return worldActorCopy ?? baseActor;
+ }
+
+ return baseActor;
+ }
+
+ static async handleSummon(summonData, actionActor, summonIndex = 0) {
+ const summon = summonData[summonIndex];
+ const result = await CONFIG.ux.TokenManager.createPreviewAsync(summon.actor, {
+ name: `${summon.actor.prototypeToken.name}${summon.count > 1 ? ` (${summon.count}x)` : ''}`
+ });
+
+ if (!result) return actionActor.sheet?.maximize();
+ summon.actor = result.actor;
+
+ summon.count--;
+ if (summon.count <= 0) {
+ summonIndex++;
+ if (summonIndex === summonData.length) return actionActor.sheet?.maximize();
+ }
+
+ DHSummonField.handleSummon(summonData, actionActor, summonIndex);
+ }
+}
diff --git a/module/data/fields/actionField.mjs b/module/data/fields/actionField.mjs
index d0d04721..0d71ab86 100644
--- a/module/data/fields/actionField.mjs
+++ b/module/data/fields/actionField.mjs
@@ -267,7 +267,8 @@ export function ActionMixin(Base) {
action: {
name: this.name,
img: this.baseAction ? this.parent.parent.img : this.img,
- tags: this.tags ? this.tags : ['Spell', 'Arcana', 'Lv 10']
+ tags: this.tags ? this.tags : ['Spell', 'Arcana', 'Lv 10'],
+ summon: this.summon
},
itemOrigin: this.item,
description: this.description || (this.item instanceof Item ? this.item.system.description : '')
diff --git a/module/data/fields/triggerField.mjs b/module/data/fields/triggerField.mjs
new file mode 100644
index 00000000..8378ea19
--- /dev/null
+++ b/module/data/fields/triggerField.mjs
@@ -0,0 +1,24 @@
+export default class TriggerField extends foundry.data.fields.SchemaField {
+ constructor(context) {
+ super(
+ {
+ trigger: new foundry.data.fields.StringField({
+ nullable: false,
+ blank: false,
+ initial: CONFIG.DH.TRIGGER.triggers.dualityRoll.id,
+ choices: CONFIG.DH.TRIGGER.triggers,
+ label: 'DAGGERHEART.CONFIG.Triggers.triggerType'
+ }),
+ triggeringActorType: new foundry.data.fields.StringField({
+ nullable: false,
+ blank: false,
+ initial: CONFIG.DH.TRIGGER.triggerActorTargetType.any.id,
+ choices: CONFIG.DH.TRIGGER.triggerActorTargetType,
+ label: 'DAGGERHEART.CONFIG.Triggers.triggeringActorType'
+ }),
+ command: new foundry.data.fields.JavaScriptField({ async: true })
+ },
+ context
+ );
+ }
+}
diff --git a/module/data/item/armor.mjs b/module/data/item/armor.mjs
index e35fae46..3d4a62fa 100644
--- a/module/data/item/armor.mjs
+++ b/module/data/item/armor.mjs
@@ -54,6 +54,21 @@ export default class DHArmor extends AttachableItem {
);
}
+ /**@inheritdoc */
+ async getDescriptionData() {
+ const baseDescription = this.description;
+ const allFeatures = CONFIG.DH.ITEM.allArmorFeatures();
+ const features = this.armorFeatures.map(x => allFeatures[x.value]);
+ if (!features.length) return { prefix: null, value: baseDescription, suffix: null };
+
+ const prefix = await foundry.applications.handlebars.renderTemplate(
+ 'systems/daggerheart/templates/sheets/items/armor/description.hbs',
+ { features }
+ );
+
+ return { prefix, value: baseDescription, suffix: null };
+ }
+
/**@inheritdoc */
async _preUpdate(changes, options, user) {
const allowed = await super._preUpdate(changes, options, user);
diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs
index 11be0a52..415fc8d4 100644
--- a/module/data/item/base.mjs
+++ b/module/data/item/base.mjs
@@ -8,7 +8,7 @@
* @property {boolean} isInventoryItem- Indicates whether items of this type is a Inventory Item
*/
-import { addLinkedItemsDiff, createScrollText, getScrollTextData, updateLinkedItemApps } from '../../helpers/utils.mjs';
+import { addLinkedItemsDiff, getScrollTextData, updateLinkedItemApps } from '../../helpers/utils.mjs';
import { ActionsField } from '../fields/actionField.mjs';
import FormulaField from '../fields/formulaField.mjs';
@@ -124,6 +124,33 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
return [source, page ? `pg ${page}.` : null].filter(x => x).join('. ');
}
+ /**
+ * Augments the description for the item with type specific info to display. Implemented in applicable item subtypes.
+ * @param {object} [options] - Options that modify the styling of the rendered template. { headerStyle: undefined|'none'|'large' }
+ * @returns {string}
+ */
+ async getDescriptionData(_options) {
+ return { prefix: null, value: this.description, suffix: null };
+ }
+
+ /**
+ * Gets the enriched and augmented description for the item.
+ * @param {object} [options] - Options that modify the styling of the rendered template. { headerStyle: undefined|'none'|'large' }
+ * @returns {string}
+ */
+ async getEnrichedDescription() {
+ if (!this.metadata.hasDescription) return '';
+
+ const { prefix, value, suffix } = await this.getDescriptionData();
+ const fullDescription = [prefix, value, suffix].filter(p => !!p).join('\n
Spend a Fear to summon a @UUID[Compendium.daggerheart.adversaries.Actor.YhJrP7rTBiRdX5Fp]{Zombie Legion}, which appears at Close range and immediately takes the spotlight.
", "resource": null, "actions": { - "gZg3AkzCYUTExjE6": { - "type": "effect", - "_id": "gZg3AkzCYUTExjE6", + "qSuWxC8xQOhnbBx9": { + "type": "summon", + "_id": "qSuWxC8xQOhnbBx9", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", - "cost": [ - { - "scalable": false, - "key": "fear", - "value": 1, - "step": null - } - ], + "cost": [], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "any", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.YhJrP7rTBiRdX5Fp", + "count": "1" + } + ], "name": "Spend Fear", - "img": "icons/magic/death/undead-zombie-grave-green.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Demon_of_Despair_kE4dfhqmIQpNd44e.json b/src/packs/adversaries/adversary_Demon_of_Despair_kE4dfhqmIQpNd44e.json index b1804074..188b2687 100644 --- a/src/packs/adversaries/adversary_Demon_of_Despair_kE4dfhqmIQpNd44e.json +++ b/src/packs/adversaries/adversary_Demon_of_Despair_kE4dfhqmIQpNd44e.json @@ -312,7 +312,14 @@ "range": "melee" } }, - "changes": [], + "changes": [ + { + "key": "system.rules.dualityRoll.defaultHopeDice", + "mode": 5, + "value": "d8", + "priority": null + } + ], "disabled": false, "duration": { "startTime": null, @@ -323,7 +330,7 @@ "startRound": null, "startTurn": null }, - "description": "All targets aff ected replace their Hope Die with a d8 until they roll a success with Hope or their next rest.
", + "description": "All targets affected replace their Hope Die with a d8 until they roll a success with Hope or their next rest.
", "tint": "#ffffff", "statuses": [], "sort": 0, diff --git a/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json b/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json index 9e838d6d..2341ee8a 100644 --- a/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json +++ b/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json @@ -256,34 +256,45 @@ "description": "Spend a Fear to boil the blood of all PCs within Far range. They use a d20 as their Fear Die until the end of the scene.
@Template[type:emanation|range:f]
", "resource": null, "actions": { - "V142qYppCGJn8OiN": { + "jKvzbQT0vp66DDOH": { "type": "effect", - "_id": "V142qYppCGJn8OiN", + "_id": "jKvzbQT0vp66DDOH", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "fear", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, "max": "", - "recovery": null + "recovery": null, + "consumeOnSuccess": false }, - "effects": [], + "effects": [ + { + "_id": "gFeHLGgeRoDdd3VG", + "onSave": false + } + ], "target": { - "type": "self", + "type": "hostile", "amount": null }, "name": "Spend Fear", - "img": "icons/skills/melee/maneuver-greatsword-yellow.webp", - "range": "" + "range": "far" } }, "originItemType": null, @@ -292,7 +303,51 @@ }, "_id": "a33PW8UkziliowlR", "img": "icons/skills/melee/maneuver-greatsword-yellow.webp", - "effects": [], + "effects": [ + { + "name": "Battle Lust", + "img": "icons/skills/melee/maneuver-greatsword-yellow.webp", + "origin": "Compendium.daggerheart.adversaries.Actor.5lphJAgzoqZI3VoG.Item.a33PW8UkziliowlR", + "transfer": false, + "_id": "gFeHLGgeRoDdd3VG", + "type": "base", + "system": { + "rangeDependence": { + "enabled": false, + "type": "withinRange", + "target": "hostile", + "range": "melee" + } + }, + "changes": [ + { + "key": "system.rules.dualityRoll.defaultFearDice", + "mode": 5, + "value": "d20", + "priority": null + } + ], + "disabled": false, + "duration": { + "startTime": null, + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null + }, + "description": "You use a d20 as your Fear Die until the end of the scene.
", + "tint": "#ffffff", + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null + }, + "_key": "!actors.items.effects!5lphJAgzoqZI3VoG.a33PW8UkziliowlR.gFeHLGgeRoDdd3VG" + } + ], "folder": null, "sort": 0, "ownership": { @@ -457,11 +512,12 @@ "img": "icons/creatures/unholy/demon-fire-horned-clawed.webp", "range": "" }, - "7G6uWlFEeOLsJIWY": { - "type": "effect", - "_id": "7G6uWlFEeOLsJIWY", + "FlE6i0tbKEguF9wz": { + "type": "summon", + "_id": "FlE6i0tbKEguF9wz", "systemPath": "actions", - "description": "Summon [[/r 1d4]]@UUID[Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb]{Minor Demons}, who appear at Close range.
", + "baseAction": false, + "description": "", "chatDisplay": true, "originItem": { "type": "itemCollection" @@ -474,13 +530,13 @@ "recovery": null, "consumeOnSuccess": false }, - "effects": [], - "target": { - "type": "any", - "amount": null - }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb", + "count": "1d4" + } + ], "name": "Summon", - "img": "icons/creatures/unholy/demon-fire-horned-clawed.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Dryad_wR7cFKrHvRzbzhBT.json b/src/packs/adversaries/adversary_Dryad_wR7cFKrHvRzbzhBT.json index f0a5d81c..ca9ce647 100644 --- a/src/packs/adversaries/adversary_Dryad_wR7cFKrHvRzbzhBT.json +++ b/src/packs/adversaries/adversary_Dryad_wR7cFKrHvRzbzhBT.json @@ -363,33 +363,31 @@ "description": "Spend a Fear to grow three @UUID[Compendium.daggerheart.adversaries.Actor.o63nS0k3wHu6EgKP]{Treant Sapling Minions}, who appear at Close range and immediately take the spotlight.
", "resource": null, "actions": { - "84Q2b0zIY9c7Yhho": { - "type": "effect", - "_id": "84Q2b0zIY9c7Yhho", + "R84DdS0OIx2cUt1w": { + "type": "summon", + "_id": "R84DdS0OIx2cUt1w", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", - "cost": [ - { - "scalable": false, - "key": "fear", - "value": 1, - "step": null - } - ], + "cost": [], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "self", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.o63nS0k3wHu6EgKP", + "count": "3" + } + ], "name": "Spend Fear", - "img": "icons/magic/unholy/orb-hands-pink.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Green_Ooze_SHXedd9zZPVfUgUa.json b/src/packs/adversaries/adversary_Green_Ooze_SHXedd9zZPVfUgUa.json index c7446a11..b03b5495 100644 --- a/src/packs/adversaries/adversary_Green_Ooze_SHXedd9zZPVfUgUa.json +++ b/src/packs/adversaries/adversary_Green_Ooze_SHXedd9zZPVfUgUa.json @@ -510,34 +510,41 @@ "description": "When the @Lookup[@name] has 3 or more HP marked, you can spend a Fear to split them into two @UUID[Compendium.daggerheart.adversaries.Actor.aLkLFuVoKz2NLoBK]{Tiny Green Oozes} (with no marked HP or Stress). Immediately spotlight both of them.
", "resource": null, "actions": { - "s5mLw6DRGd76MLcC": { - "type": "effect", - "_id": "s5mLw6DRGd76MLcC", + "J8U7dw3cDSsEirr5": { + "type": "summon", + "_id": "J8U7dw3cDSsEirr5", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "fear", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "self", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.aLkLFuVoKz2NLoBK", + "count": "2" + } + ], "name": "Spend Fear", - "img": "icons/creatures/slimes/slime-movement-pseudopods-green.webp", - "range": "" + "range": "self" } }, "originItemType": null, diff --git a/src/packs/adversaries/adversary_Head_Vampire_i2UNbRvgyoSs07M6.json b/src/packs/adversaries/adversary_Head_Vampire_i2UNbRvgyoSs07M6.json index 9e948594..d5891359 100644 --- a/src/packs/adversaries/adversary_Head_Vampire_i2UNbRvgyoSs07M6.json +++ b/src/packs/adversaries/adversary_Head_Vampire_i2UNbRvgyoSs07M6.json @@ -474,33 +474,31 @@ "description": "Spend 2 Fear to summon [[/r 1d4]] @UUID[Compendium.daggerheart.adversaries.Actor.WWyUp6Mxl1S3KYUG]{Vampires}, who appear at Far range and immediately take the spotlight.
", "resource": null, "actions": { - "5Q6RMUTiauKw0tDj": { - "type": "effect", - "_id": "5Q6RMUTiauKw0tDj", + "jGFOnU6PNdWU6iF4": { + "type": "summon", + "_id": "jGFOnU6PNdWU6iF4", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", - "cost": [ - { - "scalable": false, - "key": "fear", - "value": 2, - "step": null - } - ], + "cost": [], "uses": { "value": null, "max": "", - "recovery": null + "recovery": null, + "consumeOnSuccess": false }, - "effects": [], - "target": { - "type": "any", - "amount": null - }, - "name": "Summon Vampires", - "img": "icons/creatures/mammals/bat-giant-tattered-purple.webp", + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.WWyUp6Mxl1S3KYUG", + "count": "1d4" + } + ], + "name": "Spend Fear", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json b/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json index 6f64f883..3bb8ae96 100644 --- a/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json +++ b/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json @@ -479,33 +479,31 @@ "description": "When the @Lookup[@name] has 4 or more HP marked, you can spend a Fear to split them into two @UUID[Compendium.daggerheart.adversaries.Actor.SHXedd9zZPVfUgUa]{Green Oozes}(with no marked HP or Stress). Immediately spotlight both of them.
", "resource": null, "actions": { - "iQsYAqpUFvJslRDr": { - "type": "effect", - "_id": "iQsYAqpUFvJslRDr", + "aeRdkiRsDNagTKhp": { + "type": "summon", + "_id": "aeRdkiRsDNagTKhp", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", - "cost": [ - { - "scalable": false, - "key": "fear", - "value": 1, - "step": null - } - ], + "cost": [], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "any", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.SHXedd9zZPVfUgUa", + "count": "2" + } + ], "name": "Spend Fear", - "img": "icons/creatures/slimes/slime-movement-pseudopods-green.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Jagged_Knife_Lieutenant_aTljstqteGoLpCBq.json b/src/packs/adversaries/adversary_Jagged_Knife_Lieutenant_aTljstqteGoLpCBq.json index 165bb160..c139d76f 100644 --- a/src/packs/adversaries/adversary_Jagged_Knife_Lieutenant_aTljstqteGoLpCBq.json +++ b/src/packs/adversaries/adversary_Jagged_Knife_Lieutenant_aTljstqteGoLpCBq.json @@ -287,7 +287,35 @@ "system": { "description": "Summon three @Compendium[daggerheart.adversaries.Actor.C0OMQqV7pN6t7ouR], who appear at Far range.
", "resource": null, - "actions": {}, + "actions": { + "MCTBsw9lusUdubj0": { + "type": "summon", + "_id": "MCTBsw9lusUdubj0", + "systemPath": "actions", + "baseAction": false, + "description": "", + "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, + "actionType": "action", + "cost": [], + "uses": { + "value": null, + "max": "", + "recovery": null, + "consumeOnSuccess": false + }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.C0OMQqV7pN6t7ouR", + "count": "3" + } + ], + "name": "Summon", + "range": "" + } + }, "originItemType": null, "subType": null, "originId": null, diff --git a/src/packs/adversaries/adversary_Petty_Noble_wycLpvebWdUqRhpP.json b/src/packs/adversaries/adversary_Petty_Noble_wycLpvebWdUqRhpP.json index 4ac7e746..db284f40 100644 --- a/src/packs/adversaries/adversary_Petty_Noble_wycLpvebWdUqRhpP.json +++ b/src/packs/adversaries/adversary_Petty_Noble_wycLpvebWdUqRhpP.json @@ -258,57 +258,40 @@ "description": "Once per scene, mark a Stress to summon 1d4 @UUID[Compendium.daggerheart.adversaries.Actor.B4LZcGuBAHzyVdzy]{Bladed Guards}, who appear at Far range to enforce the @Lookup[@name]’s will.
", "resource": null, "actions": { - "cUKwhq1imsTVru8D": { - "type": "attack", - "_id": "cUKwhq1imsTVru8D", + "tioTtYfIGFIXRITN": { + "type": "summon", + "_id": "tioTtYfIGFIXRITN", "systemPath": "actions", - "description": "Once per scene, mark a Stress to summon 1d4 @UUID[Compendium.daggerheart.adversaries.Actor.B4LZcGuBAHzyVdzy]{Bladed Guards}, who appear at Far range to enforce the Noble’s will.
", + "baseAction": false, + "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "stress", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, - "max": "", - "recovery": null - }, - "damage": { - "parts": [], - "includeBase": false - }, - "target": { - "type": "any", - "amount": null - }, - "effects": [], - "roll": { - "type": "diceSet", - "trait": null, - "difficulty": null, - "bonus": null, - "advState": "neutral", - "diceRolling": { - "multiplier": "prof", - "flatMultiplier": 1, - "dice": "d4", - "compare": null, - "treshold": null - }, - "useDefault": false - }, - "save": { - "trait": null, - "difficulty": null, - "damageMod": "none" + "max": "1", + "recovery": "scene", + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.B4LZcGuBAHzyVdzy", + "count": "1d4" + } + ], "name": "Summon Guards", - "img": "icons/environment/people/infantry-armored.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Pirate_Captain_OROJbjsqagVh7ECV.json b/src/packs/adversaries/adversary_Pirate_Captain_OROJbjsqagVh7ECV.json index 409d7698..5b00ec60 100644 --- a/src/packs/adversaries/adversary_Pirate_Captain_OROJbjsqagVh7ECV.json +++ b/src/packs/adversaries/adversary_Pirate_Captain_OROJbjsqagVh7ECV.json @@ -313,36 +313,43 @@ "_id": "WGEGO0DSOs5cF0EL", "img": "icons/environment/people/charge.webp", "system": { - "description": "Once per scene, mark a Stress to summon a Pirate Raiders Horde, which appears at Far range.
", + "description": "Once per scene, mark a Stress to summon a @UUID[Compendium.daggerheart.adversaries.Actor.5YgEajn0wa4i85kC]{Pirate Raider Horde}, which appears at Far range.
", "resource": null, "actions": { - "NlgIp0KrmZoS27Xy": { - "type": "effect", - "_id": "NlgIp0KrmZoS27Xy", + "nuYk5WeLLpIKa69q": { + "type": "summon", + "_id": "nuYk5WeLLpIKa69q", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "stress", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "any", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.5YgEajn0wa4i85kC", + "count": "1" + } + ], "name": "Mark Stress", - "img": "icons/environment/people/charge.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Red_Ooze_9rVlbJVrDNn1x7PS.json b/src/packs/adversaries/adversary_Red_Ooze_9rVlbJVrDNn1x7PS.json index 320b71af..2c10ae3f 100644 --- a/src/packs/adversaries/adversary_Red_Ooze_9rVlbJVrDNn1x7PS.json +++ b/src/packs/adversaries/adversary_Red_Ooze_9rVlbJVrDNn1x7PS.json @@ -454,33 +454,40 @@ "description": "When the @Lookup[@name] has 3 or more HP marked, you can spend a Fear to split them into two @UUID[Compendium.daggerheart.adversaries.Actor.1fkLQXVtmILqfJ44]{Tiny Red Oozes} (with no marked HP or Stress). Immediately spotlight both of them.
", "resource": null, "actions": { - "dw6Juw8mriH7sg0e": { - "type": "effect", - "_id": "dw6Juw8mriH7sg0e", + "BMEr77hDxaQyYBna": { + "type": "summon", + "_id": "BMEr77hDxaQyYBna", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "fear", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "any", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.1fkLQXVtmILqfJ44", + "count": "2" + } + ], "name": "Spend Fear", - "img": "icons/creatures/slimes/slime-movement-splashing-red.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Secret_Keeper_sLAccjvCWfeedbpI.json b/src/packs/adversaries/adversary_Secret_Keeper_sLAccjvCWfeedbpI.json index 0c8757c5..d17c3f86 100644 --- a/src/packs/adversaries/adversary_Secret_Keeper_sLAccjvCWfeedbpI.json +++ b/src/packs/adversaries/adversary_Secret_Keeper_sLAccjvCWfeedbpI.json @@ -416,28 +416,6 @@ "description": "Countdown (6). When the @Lookup[@name] is in the spotlight for the first time, activate the countdown. When they mark HP, tick down this countdown by the number of HP marked. When it triggers, summon a @UUID[Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb]{Minor Demon} who appears at Close range.
", "resource": null, "actions": { - "0rixG6jLRynAYNqA": { - "type": "effect", - "_id": "0rixG6jLRynAYNqA", - "systemPath": "actions", - "description": "Summon a @UUID[Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb]{Minor Demon} who appears at Close range.
", - "chatDisplay": true, - "actionType": "action", - "cost": [], - "uses": { - "value": null, - "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "any", - "amount": null - }, - "name": "Summon", - "img": "icons/magic/unholy/silhouette-light-fire-blue.webp", - "range": "close" - }, "ZVXHY2fpomoKV7jG": { "type": "countdown", "_id": "ZVXHY2fpomoKV7jG", @@ -474,6 +452,33 @@ "name": "Start Countdown", "img": "icons/magic/unholy/silhouette-light-fire-blue.webp", "range": "" + }, + "YReYG6DrWp4QGSij": { + "type": "summon", + "_id": "YReYG6DrWp4QGSij", + "systemPath": "actions", + "baseAction": false, + "description": "", + "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, + "actionType": "action", + "cost": [], + "uses": { + "value": null, + "max": "", + "recovery": null, + "consumeOnSuccess": false + }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb", + "count": "1" + } + ], + "name": "Summon", + "range": "" } }, "originItemType": null, @@ -502,33 +507,31 @@ "description": "Once per scene, when the @Lookup[@name] marks 2 or more HP, you can mark a Stress to summon a @UUID[Compendium.daggerheart.adversaries.Actor.NoRZ1PqB8N5wcIw0]{Demonic Hound Pack}, which appears at Close range and is immediately spotlighted.
", "resource": null, "actions": { - "JBuQUJhif2A7IlJd": { - "type": "effect", - "_id": "JBuQUJhif2A7IlJd", + "tfmY6HYkkY27NBaF": { + "type": "summon", + "_id": "tfmY6HYkkY27NBaF", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", - "cost": [ - { - "scalable": false, - "key": "stress", - "value": 1, - "step": null - } - ], + "cost": [], "uses": { "value": null, - "max": "1", - "recovery": "scene" - }, - "effects": [], - "target": { - "type": "self", - "amount": null + "max": "", + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.NoRZ1PqB8N5wcIw0", + "count": "1" + } + ], "name": "Mark Stress", - "img": "icons/creatures/unholy/demon-fire-horned-clawed.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json b/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json index a6e5ca17..0f1ba28f 100644 --- a/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json +++ b/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json @@ -340,7 +340,35 @@ "system": { "description": "When an attack from the @Lookup[@name] causes a target to mark HP and there are three or more @Lookup[@name] Minions within Close range, you can combine the Minions into a @UUID[Compendium.daggerheart.adversaries.Actor.PKSXFuaIHUCoH63A]{Tangle Bramble Swarm Horde}. The Horde’s HP is equal to the number of Minions combined.
", "resource": null, - "actions": {}, + "actions": { + "g1OQ5xlMHFWsoktd": { + "type": "summon", + "_id": "g1OQ5xlMHFWsoktd", + "systemPath": "actions", + "baseAction": false, + "description": "", + "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, + "actionType": "action", + "cost": [], + "uses": { + "value": null, + "max": "", + "recovery": null, + "consumeOnSuccess": false + }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.PKSXFuaIHUCoH63A", + "count": "1" + } + ], + "name": "Summon", + "range": "" + } + }, "originItemType": null, "subType": null, "originId": null, diff --git a/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json b/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json index bd364e6f..95f42c06 100644 --- a/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json +++ b/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json @@ -80,7 +80,14 @@ }, "name": "Clear Stress", "img": "icons/magic/symbols/rune-sigil-black-pink.webp", - "range": "" + "range": "", + "triggers": [ + { + "trigger": "dualityRoll", + "triggeringActorType": "self", + "command": "/* Ignore if it's a TagTeam roll */\nconst tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);\nif (tagTeam.members[actor.id]) return;\n\n/* Check if there's a Strange Pattern match */\nconst dice = [roll.dFear.total, roll.dHope.total];\nconst resource = this.parent.resource?.diceStates ? Object.values(this.parent.resource.diceStates).map(x => x.value)[0] : null;\nconst nrMatches = dice.filter(x => x === resource).length;\n\nif (!nrMatches) return;\n\n/* Create a dialog to choose Hope or Stress - or to cancel*/\nconst content = `\nWhen you cause an adversary to mark 1 or more Hit Points, you can spend 2 Hope to increase your Evasion by the number of Hit Points they marked. This bonus lasts until after the next attack made against you.
", "chatDisplay": true, "actionType": "action", - "cost": [], + "cost": [ + { + "scalable": false, + "key": "hope", + "value": 2, + "itemId": null, + "step": null, + "consumeOnSuccess": false + } + ], "uses": { "value": null, "max": "", @@ -30,8 +39,15 @@ "amount": null }, "name": "Spend Hope", - "img": "icons/skills/melee/maneuver-daggers-paired-orange.webp", - "range": "" + "img": "icons/skills/melee/maneuver-sword-katana-yellow.webp", + "range": "", + "triggers": [ + { + "trigger": "postDamageReduction", + "triggeringActorType": "other", + "command": "/* Check if sufficient hope */\nif (this.actor.system.resources.hope.value < 2) return;\n\n/* Check if hit point damage was dealt */\nconst hpDamage = damageUpdates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);\nif (hpDamage.value < 0) return;\n\n/* Dialog to give player choice */\nconst confirmed = await foundry.applications.api.DialogV2.confirm({\n window: { title: this.item?.name ?? '' },\n content: game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.ferocityContent', { bonus: hpDamage.value }),\n});\n\nif (!confirmed) return;\n\n/* Create the effect */\nthis.actor.createEmbeddedDocuments('ActiveEffect', [{\n name: this.item.name,\n img: 'icons/skills/melee/maneuver-sword-katana-yellow.webp',\n description: game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.ferocityEffectDescription', { bonus: hpDamage.value }),\n changes: [{ key: 'system.evasion', mode: 2, value: hpDamage.value }]\n}]);\n\n/* Update hope */\nreturn { updates: [{ \n originActor: this.actor, \n updates: [{\n key: CONFIG.DH.GENERAL.healingTypes.hope.id,\n value: -2,\n total: 2\n }] \n}]}" + } + ] } }, "attribution": { diff --git a/src/packs/domains/domainCard_Get_Back_Up_BFWN2cObMdlk9uVz.json b/src/packs/domains/domainCard_Get_Back_Up_BFWN2cObMdlk9uVz.json index ab74e805..571a3fb4 100644 --- a/src/packs/domains/domainCard_Get_Back_Up_BFWN2cObMdlk9uVz.json +++ b/src/packs/domains/domainCard_Get_Back_Up_BFWN2cObMdlk9uVz.json @@ -18,7 +18,7 @@ }, "flags": {}, "_id": "BFWN2cObMdlk9uVz", - "sort": 3400000, + "sort": 3500000, "effects": [ { "name": "Get Back Up", diff --git a/src/packs/domains/domainCard_Grace_Touched_KAuNb51AwhD8KEXk.json b/src/packs/domains/domainCard_Grace_Touched_KAuNb51AwhD8KEXk.json index b87ea24d..346a81f2 100644 --- a/src/packs/domains/domainCard_Grace_Touched_KAuNb51AwhD8KEXk.json +++ b/src/packs/domains/domainCard_Grace_Touched_KAuNb51AwhD8KEXk.json @@ -96,7 +96,8 @@ "source": "Daggerheart SRD", "page": 127, "artist": "" - } + }, + "domainTouched": 4 }, "flags": {}, "_id": "KAuNb51AwhD8KEXk", diff --git a/src/packs/domains/domainCard_Midnight_Touched_uSyGKVxOJcnp28po.json b/src/packs/domains/domainCard_Midnight_Touched_uSyGKVxOJcnp28po.json index 3370c30e..10c42418 100644 --- a/src/packs/domains/domainCard_Midnight_Touched_uSyGKVxOJcnp28po.json +++ b/src/packs/domains/domainCard_Midnight_Touched_uSyGKVxOJcnp28po.json @@ -111,7 +111,8 @@ "source": "Daggerheart SRD", "page": 129, "artist": "" - } + }, + "domainTouched": 4 }, "flags": {}, "_id": "uSyGKVxOJcnp28po", diff --git a/src/packs/domains/domainCard_Notorious_IqxzvvjZiYbgx21A.json b/src/packs/domains/domainCard_Notorious_IqxzvvjZiYbgx21A.json index dfb581e7..2e5f5ffd 100644 --- a/src/packs/domains/domainCard_Notorious_IqxzvvjZiYbgx21A.json +++ b/src/packs/domains/domainCard_Notorious_IqxzvvjZiYbgx21A.json @@ -44,7 +44,8 @@ "source": "Daggerheart SRD", "page": 127, "artist": "" - } + }, + "loadoutIgnore": true }, "flags": {}, "_id": "IqxzvvjZiYbgx21A", diff --git a/src/packs/domains/domainCard_Sage_Touched_VOSFaQHZbmhMyXwi.json b/src/packs/domains/domainCard_Sage_Touched_VOSFaQHZbmhMyXwi.json index dc9ac3d3..432ff638 100644 --- a/src/packs/domains/domainCard_Sage_Touched_VOSFaQHZbmhMyXwi.json +++ b/src/packs/domains/domainCard_Sage_Touched_VOSFaQHZbmhMyXwi.json @@ -94,7 +94,8 @@ "source": "Daggerheart SRD", "page": 131, "artist": "" - } + }, + "domainTouched": 4 }, "flags": {}, "_id": "VOSFaQHZbmhMyXwi", diff --git a/src/packs/domains/domainCard_Salvation_Beam_4uAFGp3LxiC07woC.json b/src/packs/domains/domainCard_Salvation_Beam_4uAFGp3LxiC07woC.json index d637f611..c7aeb02f 100644 --- a/src/packs/domains/domainCard_Salvation_Beam_4uAFGp3LxiC07woC.json +++ b/src/packs/domains/domainCard_Salvation_Beam_4uAFGp3LxiC07woC.json @@ -95,7 +95,7 @@ }, "flags": {}, "_id": "4uAFGp3LxiC07woC", - "sort": 3400000, + "sort": 3500000, "effects": [], "ownership": { "default": 0 diff --git a/src/packs/domains/domainCard_Splendor_Touched_JT5dM3gVL6chDBYU.json b/src/packs/domains/domainCard_Splendor_Touched_JT5dM3gVL6chDBYU.json index 45d0dc96..6b530289 100644 --- a/src/packs/domains/domainCard_Splendor_Touched_JT5dM3gVL6chDBYU.json +++ b/src/packs/domains/domainCard_Splendor_Touched_JT5dM3gVL6chDBYU.json @@ -13,7 +13,8 @@ "source": "Daggerheart SRD", "page": 133, "artist": "" - } + }, + "domainTouched": 4 }, "flags": {}, "_id": "JT5dM3gVL6chDBYU", diff --git a/src/packs/domains/domainCard_Valor_Touched_k1AtYd3lSchIymBr.json b/src/packs/domains/domainCard_Valor_Touched_k1AtYd3lSchIymBr.json index 99546d6f..20fe18ea 100644 --- a/src/packs/domains/domainCard_Valor_Touched_k1AtYd3lSchIymBr.json +++ b/src/packs/domains/domainCard_Valor_Touched_k1AtYd3lSchIymBr.json @@ -82,7 +82,8 @@ "source": "Daggerheart SRD", "page": 134, "artist": "" - } + }, + "domainTouched": 4 }, "flags": {}, "_id": "k1AtYd3lSchIymBr", diff --git a/src/packs/domains/domainCard_Vitality_sWUlSPOJEaXyQLCj.json b/src/packs/domains/domainCard_Vitality_sWUlSPOJEaXyQLCj.json index 729aa251..ec47c9f9 100644 --- a/src/packs/domains/domainCard_Vitality_sWUlSPOJEaXyQLCj.json +++ b/src/packs/domains/domainCard_Vitality_sWUlSPOJEaXyQLCj.json @@ -51,7 +51,8 @@ "source": "Daggerheart SRD", "page": 121, "artist": "" - } + }, + "vaultActive": true }, "flags": {}, "_id": "sWUlSPOJEaXyQLCj", diff --git a/src/packs/domains/folders_Level_10_7pKKYgRQAKlQAksV.json b/src/packs/domains/folders_Level_10_7pKKYgRQAKlQAksV.json index 613aa993..126323da 100644 --- a/src/packs/domains/folders_Level_10_7pKKYgRQAKlQAksV.json +++ b/src/packs/domains/folders_Level_10_7pKKYgRQAKlQAksV.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "7pKKYgRQAKlQAksV", "description": "", - "sort": 1000000, + "sort": 950000, "flags": {}, "_key": "!folders!7pKKYgRQAKlQAksV" } diff --git a/src/packs/domains/folders_Level_1_9Xc6KzNyjDtTGZkp.json b/src/packs/domains/folders_Level_1_9Xc6KzNyjDtTGZkp.json index 095ff6fb..2d9c78f9 100644 --- a/src/packs/domains/folders_Level_1_9Xc6KzNyjDtTGZkp.json +++ b/src/packs/domains/folders_Level_1_9Xc6KzNyjDtTGZkp.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "9Xc6KzNyjDtTGZkp", "description": "", - "sort": 100000, + "sort": 700000, "flags": {}, "_key": "!folders!9Xc6KzNyjDtTGZkp" } diff --git a/src/packs/domains/folders_Level_2_o7t2fsAmRxKLoHrO.json b/src/packs/domains/folders_Level_2_o7t2fsAmRxKLoHrO.json index b242e121..68cc5f04 100644 --- a/src/packs/domains/folders_Level_2_o7t2fsAmRxKLoHrO.json +++ b/src/packs/domains/folders_Level_2_o7t2fsAmRxKLoHrO.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "o7t2fsAmRxKLoHrO", "description": "", - "sort": 200000, + "sort": 800000, "flags": {}, "_key": "!folders!o7t2fsAmRxKLoHrO" } diff --git a/src/packs/domains/folders_Level_3_wWL9mV6i2EGX5xHS.json b/src/packs/domains/folders_Level_3_wWL9mV6i2EGX5xHS.json index 3a4b0055..e04c6f09 100644 --- a/src/packs/domains/folders_Level_3_wWL9mV6i2EGX5xHS.json +++ b/src/packs/domains/folders_Level_3_wWL9mV6i2EGX5xHS.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "wWL9mV6i2EGX5xHS", "description": "", - "sort": 300000, + "sort": 850000, "flags": {}, "_key": "!folders!wWL9mV6i2EGX5xHS" } diff --git a/src/packs/domains/folders_Level_4_yalAnCU3SndrYImF.json b/src/packs/domains/folders_Level_4_yalAnCU3SndrYImF.json index ab0ba963..2b70a495 100644 --- a/src/packs/domains/folders_Level_4_yalAnCU3SndrYImF.json +++ b/src/packs/domains/folders_Level_4_yalAnCU3SndrYImF.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "yalAnCU3SndrYImF", "description": "", - "sort": 400000, + "sort": 900000, "flags": {}, "_key": "!folders!yalAnCU3SndrYImF" } diff --git a/src/packs/domains/folders_Level_5_Emnx4o1DWGTVKoAg.json b/src/packs/domains/folders_Level_5_Emnx4o1DWGTVKoAg.json index 0a821a2d..5bde56f3 100644 --- a/src/packs/domains/folders_Level_5_Emnx4o1DWGTVKoAg.json +++ b/src/packs/domains/folders_Level_5_Emnx4o1DWGTVKoAg.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "Emnx4o1DWGTVKoAg", "description": "", - "sort": 500000, + "sort": 901563, "flags": {}, "_key": "!folders!Emnx4o1DWGTVKoAg" } diff --git a/src/packs/domains/folders_Level_6_EiP5dLozOFZKIeWN.json b/src/packs/domains/folders_Level_6_EiP5dLozOFZKIeWN.json index 5a58c052..e20ae6b5 100644 --- a/src/packs/domains/folders_Level_6_EiP5dLozOFZKIeWN.json +++ b/src/packs/domains/folders_Level_6_EiP5dLozOFZKIeWN.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "EiP5dLozOFZKIeWN", "description": "", - "sort": 600000, + "sort": 903125, "flags": {}, "_key": "!folders!EiP5dLozOFZKIeWN" } diff --git a/src/packs/domains/folders_Level_7_HAGbPLHwm0UozDeG.json b/src/packs/domains/folders_Level_7_HAGbPLHwm0UozDeG.json index 233e3756..e53c0e2a 100644 --- a/src/packs/domains/folders_Level_7_HAGbPLHwm0UozDeG.json +++ b/src/packs/domains/folders_Level_7_HAGbPLHwm0UozDeG.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "HAGbPLHwm0UozDeG", "description": "", - "sort": 700000, + "sort": 906250, "flags": {}, "_key": "!folders!HAGbPLHwm0UozDeG" } diff --git a/src/packs/domains/folders_Level_8_me7ywrVh38j6T8Sm.json b/src/packs/domains/folders_Level_8_me7ywrVh38j6T8Sm.json index 2b125f0d..9a0ad8d9 100644 --- a/src/packs/domains/folders_Level_8_me7ywrVh38j6T8Sm.json +++ b/src/packs/domains/folders_Level_8_me7ywrVh38j6T8Sm.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "me7ywrVh38j6T8Sm", "description": "", - "sort": 800000, + "sort": 912500, "flags": {}, "_key": "!folders!me7ywrVh38j6T8Sm" } diff --git a/src/packs/domains/folders_Level_9_QYdeGsmVYIF34kZR.json b/src/packs/domains/folders_Level_9_QYdeGsmVYIF34kZR.json index c7984fb9..3547b169 100644 --- a/src/packs/domains/folders_Level_9_QYdeGsmVYIF34kZR.json +++ b/src/packs/domains/folders_Level_9_QYdeGsmVYIF34kZR.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "QYdeGsmVYIF34kZR", "description": "", - "sort": 900000, + "sort": 925000, "flags": {}, "_key": "!folders!QYdeGsmVYIF34kZR" } diff --git a/src/packs/environments/environment_Burning_Heart_of_the_Woods_oY69NN4rYxoRE4hl.json b/src/packs/environments/environment_Burning_Heart_of_the_Woods_oY69NN4rYxoRE4hl.json index dc42fb07..ea4f1951 100644 --- a/src/packs/environments/environment_Burning_Heart_of_the_Woods_oY69NN4rYxoRE4hl.json +++ b/src/packs/environments/environment_Burning_Heart_of_the_Woods_oY69NN4rYxoRE4hl.json @@ -314,7 +314,7 @@ "name": "Charcoal Constructs", "type": "feature", "system": { - "description": "Warped animals wreathed in indigo f l ame trample through a point of your choice. All targets within Close range of that point must make an Agility Reaction Roll. Targets who fail take 3d12+3 physical damage. Targets who succeed take half damage instead.
@Template[type:emanation|range:c]
Are these real animals consumed by the fl ame or merely constructs of the corrupting magic?
Warped animals wreathed in indigo flame trample through a point of your choice. All targets within Close range of that point must make an Agility Reaction Roll. Targets who fail take 3d12+3 physical damage. Targets who succeed take half damage instead.
@Template[type:emanation|range:c]
Are these real animals consumed by the fl ame or merely constructs of the corrupting magic?
Spend a Fear to summon an @UUID[Compendium.daggerheart.adversaries.Actor.A0SeeDzwjvqOsyof]{Outer Realms Abomination}, an@UUID[Compendium.daggerheart.adversaries.Actor.ms6nuOl3NFkhPj1k]{Outer Realms Corrupter}, and [[/r 2d6]] @UUID[Compendium.daggerheart.adversaries.Actor.moJhHgKqTKPS2WYS]{Outer Realms Thrall}, who appear at Close range of a chosen PC in defiance of logic and causality. Immediately spotlight one of these adversaries, and you can spend an additional Fear to automatically succeed on that adversary’s standard attack.
What halfconsumed remnants of the shattered world do these monstrosities cast aside in pursuit of living flesh? What jagged refl ections of former personhood do you catch between moments of unquestioning malice?
Spend a Fear to summon an @UUID[Compendium.daggerheart.adversaries.Actor.A0SeeDzwjvqOsyof]{Outer Realms Abomination}, an@UUID[Compendium.daggerheart.adversaries.Actor.ms6nuOl3NFkhPj1k]{Outer Realms Corrupter}, and [[/r 2d6]] @UUID[Compendium.daggerheart.adversaries.Actor.moJhHgKqTKPS2WYS]{Outer Realms Thrall}, who appear at Close range of a chosen PC in defiance of logic and causality. Immediately spotlight one of these adversaries, and you can spend an additional Fear to automatically succeed on that adversary’s standard attack.
What halfconsumed remnants of the shattered world do these monstrosities cast aside in pursuit of living flesh? What jagged refl ections of former personhood do you catch between moments of unquestioning malice?
Cultists dedicated this place to the Fallen Gods, and their foul influence seeps into it. Reduce the PCs’ Hope Die to a d10 while in this environment. The desecration can be removed with a Progress Countdown (6).
How do the PCs fist notice that something is wrong about this place? What fears resurface while hope is kept at bay?
Your Hope Die is reduced to a d10 while in the Desecrated Grounds.
", + "tint": "#ffffff", + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null + }, + "_key": "!actors.items.effects!QAXXiOKBDmCTauHD.iiHjguQG2aBn9g8i.8yNIw8Y7rfMdOqWC" + } + ], "folder": null, "sort": 0, "ownership": { @@ -343,11 +419,12 @@ "img": "icons/magic/unholy/barrier-fire-pink.webp", "range": "" }, - "suFEnfpOfeVRvnJF": { - "type": "effect", - "_id": "suFEnfpOfeVRvnJF", + "HG7tbEdlYl3yLQnR": { + "type": "summon", + "_id": "HG7tbEdlYl3yLQnR", "systemPath": "actions", - "description": "Summon a @UUID[Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb]{Minor Demon} within Very Close range of the ritual’s leader.
", + "baseAction": false, + "description": "", "chatDisplay": true, "originItem": { "type": "itemCollection" @@ -360,13 +437,13 @@ "recovery": null, "consumeOnSuccess": false }, - "effects": [], - "target": { - "type": "any", - "amount": null - }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb", + "count": "1" + } + ], "name": "Summon Demon", - "img": "icons/magic/unholy/barrier-fire-pink.webp", "range": "" } }, diff --git a/src/packs/environments/environment_Divine_Usurpation_4DLYez7VbMCFDAuZ.json b/src/packs/environments/environment_Divine_Usurpation_4DLYez7VbMCFDAuZ.json index aacf87e9..d8e9cded 100644 --- a/src/packs/environments/environment_Divine_Usurpation_4DLYez7VbMCFDAuZ.json +++ b/src/packs/environments/environment_Divine_Usurpation_4DLYez7VbMCFDAuZ.json @@ -248,33 +248,31 @@ "description": "Spend 2 Fear to summon [[/r 1d4+2]] @UUID[Compendium.daggerheart.adversaries.Actor.OsLG2BjaEdTZUJU9]{Fallen Shock Troop} that appear within Close range of the Usurper to assist their divine siege. Immediately spotlight the Shock Troops to use a “Group Attack” action.
Which High Fallen do these troops serve? Which god’s fl esh do they wish to feast upon?
Spend 2 Fear to summon [[/r 1d4+2]] @UUID[Compendium.daggerheart.adversaries.Actor.OsLG2BjaEdTZUJU9]{Fallen Shock Troop} that appear within Close range of the Usurper to assist their divine siege. Immediately spotlight the Shock Troops to use a “Group Attack” action.
Which High Fallen do these troops serve? Which god’s fl esh do they wish to feast upon?
When the PCs enter the raptors’ hunting grounds, two @UUID[Compendium.daggerheart.adversaries.Actor.OMQ0v6PE8s1mSU0K]{Giant Eagles} appear at Very Far range of a chosen PC, identifying the PCs as likely prey.
How long has it been since the eagles last found prey? Do they have eggs in their nest or unfl edged young?
Spend a Fear to summon a @UUID[Compendium.daggerheart.adversaries.Actor.8KWVLWXFhlY2kYx0]{Glass Snake} within Close range of a chosen PC. The Snake appears in or near the river and immediately takes the spotlight to use their “Spinning Serpent” action.
What treasures does the beast have in their burrow? What travelers have already fallen victim to this predator?
Spend a Fear to summon a @UUID[Compendium.daggerheart.adversaries.Actor.8KWVLWXFhlY2kYx0]{Glass Snake} within Close range of a chosen PC. The Snake appears in or near the river and immediately takes the spotlight to use their “Spinning Serpent” action.
What treasures does the beast have in their burrow? What travelers have already fallen victim to this predator?