Compare commits

...

83 commits
1.3.0 ... main

Author SHA1 Message Date
WBHarry
0b343c9f52
Fixed a lot of cases where we expected a combatant to have an attached actor (#1520) 2026-01-10 00:21:44 +01:00
Carlos Fernandez
e6973fabd0
Add view party button to character sheet (#1508) 2026-01-09 17:41:35 +01:00
WBHarry
4e18ed8270
Fixed so that chatMessages always get actor data available (#1519) 2026-01-09 17:35:00 +01:00
Nikhil Nagarajan
e7cf6594b6
[PR] Rolltables Compendium Added (#1516)
* Initial Setup

* Updated Consumables RollTable

* Placed the rolltable compendium in the SRD folder

* updated Journal with fixed links

* Re-added descriptions in rolltable

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>
2026-01-09 17:34:11 +01:00
WBHarry
bbe8fb953e
Fixed so that tagify tooltip descriptions cannot end up with raw HTML that breaks it (#1504) 2026-01-09 15:57:57 +01:00
Nikhil Nagarajan
6cebccd958
Template removed from Stardrop JSON (#1513) 2026-01-09 15:56:48 +01:00
Chris Ryan
248f7b41e7
Safety check for experiences (#1515)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2026-01-09 15:55:16 +01:00
WBHarry
c6bdc846ab
[Fix] SRD Effect Priorities (#1505)
* Increased priority on effects in srd domain cards that depend on character data

* Increased priority on effects in srd classes/subclasses that depend on character data

* Increased priority on effects in srd ancestries that depend on character data

* Increased priority on effects in srd beastforms that depend on character data

* Increased priority on effects in remaining SRD items that depend on character data

* Adversaries

* Raised system version
2026-01-09 15:50:56 +01:00
Carlos Fernandez
6deadea437
Fix detection of range dependencies (#1497) 2026-01-08 19:52:25 -05:00
WBHarry
9564edb244 . 2026-01-05 13:45:03 +01:00
WBHarry
bca7e0d3c9
[Fix] Beastforms Getting Stuck (#1495)
* Fixed beastforms getting stuck

* Raised version
2025-12-31 04:52:19 +01:00
Nick Salyzyn
3b7b6258a1
[PR] Adding the ability to target downtime actions. (#1475)
* Adding the ability to target downtime actions.

* No longer using an arbitrary 100 healing. Changing the action's parent
2025-12-29 21:55:13 +01:00
WBHarry
e8c541c002
Added damage reduction rules (#1491) 2025-12-29 14:00:40 +01:00
WBHarry
f6bd1430e3
Fixed drag/drop of features for environments (#1488) 2025-12-29 13:02:22 +01:00
WBHarry
c070c6cc2d
[Fix] Evolved Beastform Wildcard (#1486)
* Fixed so picking an evolved beastform will properly handle wildcard images

* .
2025-12-29 04:44:49 +01:00
WBHarry
d0e55aeb8d
Resource Generation Fix 2025-12-28 21:16:24 +01:00
Carlos Fernandez
f11b018bd7
Support drag/drop resorting of adversary features (#1469) 2025-12-28 19:07:12 +01:00
Nick Salyzyn
c83fe25a47
Using the same drag-drop approach in environments and adversaries as in the character sheet (#1481) 2025-12-28 18:50:26 +01:00
Nick Salyzyn
3405b53900
[PR] Adding recall functionality to the loadout tab (#1482)
* Adding a Recall button that doesn't yet show a popup

* Adding an action with a cost dialog for stress - if there is a stress cost
2025-12-28 18:31:48 +01:00
Nick Salyzyn
5f001a9f83
Adding scrollable to various tabs in the homebrew settings (#1483) 2025-12-28 17:26:39 +01:00
WBHarry
87dfebec2f Raised version 2025-12-27 18:17:05 +01:00
WBHarry
ab7ea03d84
[Fix] Actor Roll Data (#1477)
* .

* .
2025-12-27 18:15:56 +01:00
Nikhil Nagarajan
09aafd0999
Icon for Action Changed (#1478) 2025-12-27 17:02:24 +01:00
Nick Salyzyn
52b32a4d12
Adding myself as a contributor (#1476) 2025-12-27 10:38:29 +10:00
WBHarry
fa21baf8bf
Fixed rollData for actions and fallback for lookup enricher (#1472) 2025-12-25 13:12:25 +01:00
WBHarry
50a307b271
[Feature] TokenConfig Actor Size Edit (#1470)
* Added the select and handliing

* Fixed so tokenPreview works with tokenSize

* Correction for prototypeToken

* Extracted common logic to token-config-mixin.mjs

* Update templates/sheets-settings/token-config/appearance.hbs

Co-authored-by: Carlos Fernandez <CarlosFdez@users.noreply.github.com>

---------

Co-authored-by: Carlos Fernandez <CarlosFdez@users.noreply.github.com>
2025-12-25 02:12:36 +01:00
WBHarry
c63ba3b41d
[Fix] Action Source Handling (#1468)
* Fixed so that action context.source can be assigned to

* .

* .
2025-12-24 03:05:27 +01:00
WBHarry
2104549617
[PR][Feature] Adversary Compendium Name Lookup (#1458)
* Exchanged all name references for @Lookup. Removed duplicated descriptions on feature actions.

* Corrected action.description fallback
2025-12-24 01:15:50 +01:00
WBHarry
92b31b71a7
[PR][Fix] Cleaned Up Countdown Ownership View (#1459)
* Removed the disabled default ownership select in the ownership view. Added a fallback text incase there are no players added yet

* Corrected noPlayers condition
2025-12-24 01:09:51 +01:00
Murilo Brito
f8b003b304
[PR][Feature] Items Tooltips Styles (#1445)
* feat: add basic tooltip style and style domain card template

* feat: change weapon hbs tooltip and increase box-shadow blur

* feat: style armor hbs tooltip

* feat: style consumable hbs tooltip

* feat: style loot hbs tooltip

* feat: style feature hbs tooltip

* bugfix: prevent style conflicts between tooltips

* feat: style action hbs tooltip

* feat: style attack hbs tooltip

* feat: style effect hbs tooltip

* feat: increase tooltip width

* style beatform tooltip, fix unnarmed attack location, add outline border when users use midle click

* feat: add beige outline and box shadow to tooltips to enhance contrast

* bugfix: requested changes

* bugfix: fix typo

* bugfix: fix tooltip breaking interface position
2025-12-23 19:02:28 -05:00
WBHarry
0806c2d1ac
[Fix] Levelup New Experience Increase (#1461)
* Fixed so you can select a newly gained experience to increase

* .

* Exchanged forEach with for..of. The future is now
2025-12-24 01:00:24 +01:00
WBHarry
f184db1f93
[Fix] Sheet Sidebar Experience Layout (#1462)
* Fixed the layout

* Added small line-height to the experience name

* Centered
2025-12-24 00:52:57 +01:00
WBHarry
7e2b144bf4
Fixed so that the reaction rolls from chat messages work with diceSoNice (#1465) 2025-12-23 18:11:01 -05:00
Murilo Brito
1b7893324a
bugfix: fix chat styles breaking in stream mode (#1467) 2025-12-23 23:59:23 +01:00
Nick Salyzyn
0f5f866b22
[PR] Adding max attributes to the action list and missing localization for them (#1446)
* Adding max attributes and missing localization for them

* Making sure the adversary settings page has only 'max' in the fieldset
2025-12-23 14:17:50 +01:00
Nick Salyzyn
a168d8de65
[PR] Changing the drag targets in the inventory item v2 template (#1443)
* Adding styles to make the text div take up the full area of the inventory items

* Moving the draggable spots to internal divs and images

* For better visuals in dragDrop, always make the drag image the first image selected

* Fixing the dragDrop behavior on the sidebar, which still uses the classic layout

* Fixing other uses of dragDrop to handle the layout change

* Moving the draggable attribute to the parent img-portrait from img directly

* Switching to the less pretty version of the drag drop in case of currency problems

* Reverting how the dragSelector DEFAULT_OPTION is set and only modifying a few

* Removing extra space in styles/less/global/inventory-item.less

Co-authored-by: Carlos Fernandez <CarlosFdez@users.noreply.github.com>

* Fixing up the character sheet to once again allow selecting the text areas

---------

Co-authored-by: Carlos Fernandez <CarlosFdez@users.noreply.github.com>
2025-12-23 14:14:55 +01:00
WBHarry
51eadc499f Corrected system.json syntax 2025-12-22 17:06:43 +01:00
WBHarry
f0531d3587
[Fix] Sheet Labels (#1456)
* Added human readable sheet labels to all sheets

* Prefixed sheet labels with 'Default DH'

* 🪚
2025-12-22 11:03:49 -05:00
Nikhil Nagarajan
790a5b4938
Adding in more details in authors - Ikraik (#1457) 2025-12-22 16:59:22 +01:00
WBHarry
8178fa5738
[PR][Feature] Actor Sizes (#1433)
* Added support for adversary actor sizes

* .

* .

* Finished token implementation

* Fixed token-config

* Updated SRD adversaries

* .

* Added size to Beastform tokenData

* Fixed sizing for evolved beastforms

* Beastform compendium update

* .
2025-12-22 16:58:53 +01:00
Nick Salyzyn
7926c614e3
[PR] Updating the logic for rangeDependencies so that paired weapons work with multiple adversaries (#1434)
* Include item effects when applying rangeDependence

* Creating a new method to update range dependent effects that uses the players targets

* Using debouncing to fix an issue with selected and unselecting targets

* Using token destinations instead of their current location for calculation range
2025-12-22 14:45:27 +01:00
WBHarry
16f6fa98a6
Fixed so that players rolling reactions will update the message (#1455) 2025-12-22 14:34:43 +01:00
WBHarry
659f73116a
[Fix] 1453 - Async Resource Generation Errors (#1454)
* Fixed so that we do not run separate actor.modifyResource calls during actions and dice rolls

* .

* Simplified resourcemap
2025-12-22 07:56:49 -05:00
Carlos Fernandez
e8dd38fbfa
[PR] [Feature] Refresh actors when a homebrew setting is changed (#1382)
* Refresh actors when a homebrew setting is changed

* Newline at end of file

* Close open prosemirror documents during reset
2025-12-22 13:48:03 +01:00
Carlos Fernandez
99d0eab5bd
[PR] [Feature] Support drag dropping currencies to actor sheets (#1431)
* Support drag dropping currencies to actor sheets

* Adjust sizing and spacing

* Restore ItemTransferDialog subclass for module use

* Bigger is better
2025-12-21 17:37:00 +01:00
WBHarry
f786ee5f06 Fixed actionType constants 2025-12-21 15:39:05 +01:00
Nick Salyzyn
b8e08fccd1
[PR] Removing a potential reference error when creating a tag team roll (#1430) 2025-12-20 17:14:23 -05:00
Carlos Fernandez
fe80b4d0f8
[PR] [Feature] Refocus consumable quantity and simple resource fields on change (#1401)
* Refocus quantity and simple resource fields on change

* Swap to uuid
2025-12-20 23:08:24 +01:00
Nick Salyzyn
148c9c019a
[PR] Use the token name in the chat message targets window (#1441) 2025-12-20 17:04:36 -05:00
Nick Salyzyn
9cfa206adc
[PR] Checking for empty data in renderChatMessageHTML calls (#1452) 2025-12-20 16:56:22 -05:00
Nick Salyzyn
0508bf4188
[PR] Removing refreshables the user doesn't have during rest actions (#1449)
* Marking up the places that need changing for bug #1160

* Creating a shared method called isItemAvailable and using it in downtime

* Explicitely adding the false value rather than relying on undefined returns. Oops

* Removing spaces

* Removing a needless return line

* Adding missing semicolon
2025-12-20 22:00:16 +01:00
Carlos Fernandez
605a23ab58
Fix invalid form control is not focusable error on gold fields (#1451) 2025-12-20 21:25:51 +01:00
Nikhil Nagarajan
7d1e70f66f
[PR] [Feature] Feature form labels + SRD Update (Adversaries & Environments) (#1429)
* New labels. Time to print them somewhere.

* Action buttons have icons. Pretty Iconic

* Features tweaked to support only on limited actors

* Duplicate ActionTypes+references are removed.

* Adversary and Environment SRD entries updated.

* Updated name field workflow in character actor.

* Adversary name fields are improved as well.

* Revert "Updated name field workflow in character actor."

This reverts commit 66924c530f.

* Revert "Adversary name fields are improved as well."

This reverts commit f60e8cffda.

* Fixed prototype token in Abandoned Grove

* Label change

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>
2025-12-20 19:36:06 +01:00
Carlos Fernandez
474cf28a53
[PR][Feature] Support multiline adversary and character names (#1437)
* Support multiline adversary names

* Create fake placeholder

* Also support multiline character names
2025-12-19 16:57:15 -05:00
Nick Salyzyn
5f6d08d8c2
Fixing spelling error in Elundrian Chain Armor (#1442) 2025-12-19 12:53:44 -05:00
Nick Salyzyn
a8b15c8252
Fixing Summon Swarm to do damage threshold reduction (#1425) 2025-12-19 12:53:25 -05:00
WBHarry
27fe83d906
Fixed so effects are only applied to correct tokens (#1439) 2025-12-18 17:55:25 -05:00
Nick Salyzyn
0936b46926
[PR] Avoid getRollData adding side effects to the system data (#1436) 2025-12-18 17:00:02 -05:00
Nick Salyzyn
05dec9fcea
Moving chat display to be after processing (#1428) 2025-12-18 07:37:09 -05:00
Nick Salyzyn
e74ce7726a
Make wings of light self-target (#1426) 2025-12-17 18:42:02 +01:00
Carlos Fernandez
9b4249b100
[Feature] Support for configurable currency icons (#1422)
* Add support for configurable currency icons

* Remove unused plain style

* Changed so that icons don't have to have an icon

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>
2025-12-13 23:03:34 +01:00
WBHarry
f60792f714
Fixed iron will armor allowance (#1423) 2025-12-13 16:56:42 +01:00
Nikhil Nagarajan
d5b8431f88
Massive Damage incoming. Over 9000. (#1419) 2025-12-13 16:38:07 +01:00
Nick Salyzyn
315f1ef8e0
Adding direct damage to adversaries and environments (#1412) 2025-12-13 16:24:22 +01:00
WBHarry
6cb635901f
Made the effect-hud more foundry standard to work better for modules (#1414) 2025-12-13 16:10:44 +01:00
Nick Salyzyn
a8c120be8e
Allowing coundown automations to work for players on action rolls (#1416) 2025-12-13 16:09:53 +01:00
Nick Salyzyn
7a50d77952
Pulling out hope method and always updating the resources. (#1417)
Making sure stress decreases on critical.
2025-12-13 15:18:21 +01:00
Carlos Fernandez
64caff6fb2
Make the spotlight queue feature a toggle (default off) (#1418) 2025-12-13 15:05:44 +01:00
Murilo Brito
46a9aea029
[BUGFIX] Fix game system conflict css font vars with foundry (#1421)
* bugfix: fix game system conflict css font vars with foundry

* bugfix: fix typo
2025-12-13 14:58:41 +01:00
Josh Q.
360b903437
domain lookup fix for tooltipo (#1420) 2025-12-12 20:42:44 -08:00
Carlos Fernandez
f4dd9dc5c1
Minor tweaks to effect attributes (#1408)
* Minor tweaks to effect attributes

* Fix removing
2025-12-12 22:36:30 +10:00
Carlos Fernandez
00e9436fe0
Implement delta input for gold fields (#1400) 2025-12-11 21:39:07 +01:00
Nick Salyzyn
7f7536ee06
Adding stress cost to wings of light (#1413) 2025-12-11 21:32:43 +01:00
WBHarry
8eae1c0763
Loosened checks on statuses to make module compatible (#1407) 2025-12-09 22:43:36 +01:00
WBHarry
2b1535333a . 2025-12-08 21:01:29 +01:00
WBHarry
09141053c9 Raised version 2025-12-08 02:34:55 +01:00
WBHarry
5356f10b2a
[Fix] 1389 - Errata Update (#1391)
* Updated BookOfGrynn and SplinteringStrike for errata

* Corrected Improved Small Dagger
2025-12-08 02:32:36 +01:00
WBHarry
91d916a28d
Fixed so that the critdamage should be correct (#1393) 2025-12-08 02:32:25 +01:00
Carlos Fernandez
b307d65d18
Fix dropping features onto item sheets (#1394) 2025-12-08 02:32:05 +01:00
Carlos Fernandez
ccdd413933
[Fix] Allow disabling coins (#1381)
* Add ability to disable coins

* Omit currency section if all are disabled
2025-12-08 02:29:07 +01:00
WBHarry
f680ade1da
Corrected downtime prep from error state (#1390) 2025-12-08 01:53:58 +01:00
WBHarry
d6b1c7a36c
Fixed some missing translations (#1386) 2025-12-08 01:22:28 +01:00
Carlos Fernandez
28976bb4b8
[Fix] Updating item quantity in party inventory (#1378)
* Fix updating item quantity in party inventory

* Remove unnecessary second render
2025-12-07 13:39:40 +01:00
340 changed files with 8354 additions and 2259 deletions

View file

@ -17,7 +17,6 @@ import {
socketRegistration
} from './module/systemRegistration/_module.mjs';
import { placeables } from './module/canvas/_module.mjs';
import { registerRollDiceHooks } from './module/dice/dhRoll.mjs';
import './node_modules/@yaireo/tagify/dist/tagify.css';
import TemplateManager from './module/documents/templateManager.mjs';
@ -54,6 +53,8 @@ CONFIG.Canvas.rulerClass = placeables.DhRuler;
CONFIG.Canvas.layers.templates.layerClass = placeables.DhTemplateLayer;
CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate;
CONFIG.Scene.documentClass = documents.DhScene;
CONFIG.Token.documentClass = documents.DhToken;
CONFIG.Token.prototypeSheetClass = applications.sheetConfigs.DhPrototypeTokenConfig;
CONFIG.Token.objectClass = placeables.DhTokenPlaceable;
@ -89,34 +90,94 @@ Hooks.once('init', () => {
makeDefault: true
});
const sheetLabel = typePath => () =>
game.i18n.format('DAGGERHEART.GENERAL.typeSheet', {
type: game.i18n.localize(typePath)
});
const { Items, Actors } = foundry.documents.collections;
Items.unregisterSheet('core', foundry.applications.sheets.ItemSheetV2);
Items.registerSheet(SYSTEM.id, applications.sheets.items.Ancestry, { types: ['ancestry'], makeDefault: true });
Items.registerSheet(SYSTEM.id, applications.sheets.items.Community, { types: ['community'], makeDefault: true });
Items.registerSheet(SYSTEM.id, applications.sheets.items.Class, { types: ['class'], makeDefault: true });
Items.registerSheet(SYSTEM.id, applications.sheets.items.Subclass, { types: ['subclass'], makeDefault: true });
Items.registerSheet(SYSTEM.id, applications.sheets.items.Feature, { types: ['feature'], makeDefault: true });
Items.registerSheet(SYSTEM.id, applications.sheets.items.DomainCard, { types: ['domainCard'], makeDefault: true });
Items.registerSheet(SYSTEM.id, applications.sheets.items.Ancestry, {
types: ['ancestry'],
makeDefault: true,
label: sheetLabel('TYPES.Item.ancestry')
});
Items.registerSheet(SYSTEM.id, applications.sheets.items.Community, {
types: ['community'],
makeDefault: true,
label: sheetLabel('TYPES.Item.community')
});
Items.registerSheet(SYSTEM.id, applications.sheets.items.Class, {
types: ['class'],
makeDefault: true,
label: sheetLabel('TYPES.Item.class')
});
Items.registerSheet(SYSTEM.id, applications.sheets.items.Subclass, {
types: ['subclass'],
makeDefault: true,
label: sheetLabel('TYPES.Item.subclass')
});
Items.registerSheet(SYSTEM.id, applications.sheets.items.Feature, {
types: ['feature'],
makeDefault: true,
label: sheetLabel('TYPES.Item.feature')
});
Items.registerSheet(SYSTEM.id, applications.sheets.items.DomainCard, {
types: ['domainCard'],
makeDefault: true,
label: sheetLabel('TYPES.Item.domainCard')
});
Items.registerSheet(SYSTEM.id, applications.sheets.items.Loot, {
types: ['loot'],
makeDefault: true
makeDefault: true,
label: sheetLabel('TYPES.Item.loot')
});
Items.registerSheet(SYSTEM.id, applications.sheets.items.Consumable, {
types: ['consumable'],
makeDefault: true,
label: sheetLabel('TYPES.Item.consumable')
});
Items.registerSheet(SYSTEM.id, applications.sheets.items.Weapon, {
types: ['weapon'],
makeDefault: true,
label: sheetLabel('TYPES.Item.weapon')
});
Items.registerSheet(SYSTEM.id, applications.sheets.items.Armor, {
types: ['armor'],
makeDefault: true,
label: sheetLabel('TYPES.Item.armor')
});
Items.registerSheet(SYSTEM.id, applications.sheets.items.Beastform, {
types: ['beastform'],
makeDefault: true,
label: sheetLabel('TYPES.Item.beastform')
});
Items.registerSheet(SYSTEM.id, applications.sheets.items.Consumable, { types: ['consumable'], makeDefault: true });
Items.registerSheet(SYSTEM.id, applications.sheets.items.Weapon, { types: ['weapon'], makeDefault: true });
Items.registerSheet(SYSTEM.id, applications.sheets.items.Armor, { types: ['armor'], makeDefault: true });
Items.registerSheet(SYSTEM.id, applications.sheets.items.Beastform, { types: ['beastform'], makeDefault: true });
Actors.unregisterSheet('core', foundry.applications.sheets.ActorSheetV2);
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Character, { types: ['character'], makeDefault: true });
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Companion, { types: ['companion'], makeDefault: true });
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Adversary, { types: ['adversary'], makeDefault: true });
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Character, {
types: ['character'],
makeDefault: true,
label: sheetLabel('TYPES.Actor.character')
});
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Companion, {
types: ['companion'],
makeDefault: true,
label: sheetLabel('TYPES.Actor.companion')
});
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Adversary, {
types: ['adversary'],
makeDefault: true,
label: sheetLabel('TYPES.Actor.adversary')
});
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Environment, {
types: ['environment'],
makeDefault: true
makeDefault: true,
label: sheetLabel('TYPES.Actor.environment')
});
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Party, {
types: ['party'],
makeDefault: true
makeDefault: true,
label: sheetLabel('TYPES.Actor.party')
});
DocumentSheetConfig.unregisterSheet(
@ -129,7 +190,8 @@ Hooks.once('init', () => {
SYSTEM.id,
applications.sheetConfigs.ActiveEffectConfig,
{
makeDefault: true
makeDefault: true,
label: sheetLabel('DOCUMENT.ActiveEffect')
}
);
@ -138,9 +200,10 @@ Hooks.once('init', () => {
// Make Compendium Dialog resizable
foundry.applications.sidebar.apps.Compendium.DEFAULT_OPTIONS.window.resizable = true;
DocumentSheetConfig.unregisterSheet(foundry.documents.Scene, 'core', foundry.applications.sheets.SceneConfig);
DocumentSheetConfig.registerSheet(foundry.documents.Scene, SYSTEM.id, applications.scene.DhSceneConfigSettings, {
makeDefault: true,
label: 'Daggerheart'
label: sheetLabel('DOCUMENT.Scene')
});
settingsRegistration.registerDHSettings();
@ -177,7 +240,6 @@ Hooks.on('ready', async () => {
ui.compendiumBrowser = new applications.ui.ItemBrowser();
socketRegistration.registerSocketHooks();
registerRollDiceHooks();
socketRegistration.registerUserQueries();
if (!game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.welcomeMessage)) {
@ -193,9 +255,9 @@ Hooks.on('ready', async () => {
Hooks.once('dicesoniceready', () => {});
Hooks.on('renderChatMessageHTML', (_, element, message) => {
Hooks.on('renderChatMessageHTML', (document, element) => {
enricherRenderSetup(element);
const cssClass = message.message.flags?.daggerheart?.cssClass;
const cssClass = document.flags?.daggerheart?.cssClass;
if (cssClass) cssClass.split(' ').forEach(cls => element.classList.add(cls));
});
@ -248,51 +310,70 @@ Hooks.on('chatMessage', (_, message) => {
}
});
Hooks.on('moveToken', async (movedToken, data) => {
const updateActorsRangeDependentEffects = async token => {
const rangeMeasurement = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.variantRules
).rangeMeasurement;
for (let effect of token.actor?.allApplicableEffects() ?? []) {
if (!effect.system.rangeDependence?.enabled) continue;
const { target, range, type } = effect.system.rangeDependence;
// If there are no targets, assume false. Otherwise, start with the effect enabled.
let enabledEffect = game.user.targets.size !== 0;
// Expect all targets to meet the rangeDependence requirements
for (let userTarget of game.user.targets) {
const disposition = userTarget.document.disposition;
if ((target === 'friendly' && disposition !== 1) || (target === 'hostile' && disposition !== -1)) {
enabledEffect = false;
break;
}
// Get required distance and special case 5 feet to test adjacency
const required = rangeMeasurement[range];
const reverse = type === CONFIG.DH.GENERAL.rangeInclusion.outsideRange.id;
const inRange =
required === 5
? userTarget.isAdjacentWith(token.object)
: userTarget.distanceTo(token.object) <= required;
if (reverse ? inRange : !inRange) {
enabledEffect = false;
break;
}
}
await effect.update({ disabled: !enabledEffect });
}
};
const updateAllRangeDependentEffects = async () => {
const effectsAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).effects;
if (!effectsAutomation.rangeDependent) return;
const rangeDependantEffects = movedToken.actor.effects.filter(effect => effect.system.rangeDependence?.enabled);
const updateEffects = async (disposition, token, effects, effectUpdates) => {
const rangeMeasurement = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.variantRules
).rangeMeasurement;
for (let effect of effects.filter(x => x.system.rangeDependence?.enabled)) {
const { target, range, type } = effect.system.rangeDependence;
if ((target === 'friendly' && disposition !== 1) || (target === 'hostile' && disposition !== -1))
return false;
const distanceBetween = canvas.grid.measurePath([
{ ...movedToken.toObject(), x: data.destination.x, y: data.destination.y },
token
]).distance;
const distance = rangeMeasurement[range];
const reverse = type === CONFIG.DH.GENERAL.rangeInclusion.outsideRange.id;
const newDisabled = reverse ? distanceBetween <= distance : distanceBetween > distance;
const oldDisabled = effectUpdates[effect.uuid] ? effectUpdates[effect.uuid].disabled : newDisabled;
effectUpdates[effect.uuid] = {
disabled: oldDisabled || newDisabled,
value: effect
};
const tokens = canvas.scene.tokens;
if (game.user.character) {
// The character updates their character's token. There can be only one token.
const characterToken = tokens.find(x => x.actor === game.user.character);
updateActorsRangeDependentEffects(characterToken);
} else if (game.user.isActiveGM) {
// The GM is responsible for all other tokens.
const playerCharacters = game.users.players.filter(x => x.active).map(x => x.character);
for (const token of tokens.filter(x => !playerCharacters.includes(x.actor))) {
updateActorsRangeDependentEffects(token);
}
};
const effectUpdates = {};
for (let token of game.scenes.find(x => x.active).tokens) {
if (token.id !== movedToken.id) {
await updateEffects(token.disposition, token, rangeDependantEffects, effectUpdates);
}
if (token.actor) await updateEffects(movedToken.disposition, token, token.actor.effects, effectUpdates);
}
};
for (let key in effectUpdates) {
const effect = effectUpdates[key];
await effect.value.update({ disabled: effect.disabled });
const debouncedRangeEffectCall = foundry.utils.debounce(updateAllRangeDependentEffects, 50);
Hooks.on('targetToken', () => {
debouncedRangeEffectCall();
});
Hooks.on('refreshToken', (_, options) => {
if (options.refreshPosition) {
debouncedRangeEffectCall();
}
});

View file

@ -36,6 +36,7 @@
},
"DAGGERHEART": {
"CharacterSheet": "Character Sheet",
"ACTIONS": {
"TYPES": {
"attack": {
@ -225,6 +226,7 @@
"confirmText": "Would you like to level up your companion {name} by {levelChange} levels at this time? (You can do it manually later)"
},
"viewLevelups": "View Levelups",
"viewParty": "View Party",
"InvalidOldCharacterImportTitle": "Old Character Import",
"InvalidOldCharacterImportText": "Character data exported prior to system version 1.1 will not generate a complete character. Do you wish to continue?",
"cancelBeastform": "Cancel Beastform"
@ -325,6 +327,7 @@
"equip": "Equip",
"sendToChat": "Send To Chat",
"toLoadout": "Send to Loadout",
"recall": "Recall",
"toVault": "Send to Vault",
"unequip": "Unequip",
"useItem": "Use Item"
@ -386,7 +389,8 @@
"hideNewCountdowns": "Hide New Countdowns"
},
"DaggerheartMenu": {
"title": "GM Tools"
"title": "GM Tools",
"refreshFeatures": "Refresh Features"
},
"DeleteConfirmation": {
"title": "Delete {type} - {name}",
@ -504,6 +508,7 @@
},
"navigateLevel": "To Level {level}",
"navigateToLevelup": "Return To Levelup",
"finishLevelup": "Finish Levelup",
"navigateToSummary": "To Summary",
"options": {
"trait": "Gain a +1 bonus to two unmarked character traits and mark them.",
@ -583,6 +588,7 @@
},
"OwnershipSelection": {
"title": "Ownership Selection - {name}",
"noPlayers": "No players to assign ownership to",
"default": "Default Ownership"
},
"ReactionRoll": {
@ -609,6 +615,9 @@
"insufficientHope": "The initiating character doesn't have enough hope",
"createTagTeam": "Create TagTeam Roll",
"chatMessageRollTitle": "Roll"
},
"TokenConfig": {
"actorSizeUsed": "Actor size is set, determining the dimensions"
}
},
"CLASS": {
@ -618,11 +627,6 @@
}
},
"CONFIG": {
"ActionType": {
"passive": "Passive",
"action": "Action",
"reaction": "Reaction"
},
"AdversaryTrait": {
"relentless": {
"name": "Relentless",
@ -1031,6 +1035,12 @@
"description": ""
}
},
"FeatureForm": {
"label": "Feature Form",
"passive": "Passive",
"action": "Action",
"reaction": "Reaction"
},
"Gold": {
"title": "Gold",
"coins": "Coins",
@ -1144,6 +1154,14 @@
"rect": "Rectangle",
"ray": "Ray"
},
"TokenSize": {
"tiny": "Tiny",
"small": "Small",
"medium": "Medium",
"large": "Large",
"huge": "Huge",
"gargantuan": "Gargantuan"
},
"Traits": {
"agility": {
"name": "Agility",
@ -1784,7 +1802,9 @@
"label": "Long Rest: Bonus Long Rest Moves",
"hint": "The number of extra Long Rest Moves the character can take during a Long Rest."
}
}
},
"target": "Target",
"targetSelf": "Self"
},
"maxLoadout": {
"label": "Max Loadout Cards Bonus"
@ -1799,6 +1819,7 @@
"plural": "Costs"
},
"Damage": {
"massive": "Massive",
"severe": "Severe",
"major": "Major",
"minor": "Minor",
@ -2091,6 +2112,7 @@
"fear": "Fear",
"features": "Features",
"formula": "Formula",
"general": "General",
"gm": "GM",
"healing": "Healing",
"healingRoll": "Healing Roll",
@ -2105,6 +2127,7 @@
},
"hope": "Hope",
"hordeHp": "Horde HP",
"icon": "Icon",
"identify": "Identity",
"imagePath": "Image Path",
"inactiveEffects": "Inactive Effects",
@ -2141,6 +2164,7 @@
"recovery": "Recovery",
"refresh": "Refresh",
"reroll": "Reroll",
"rerolled": "Rerolled",
"rerollThing": "Reroll {thing}",
"resource": "Resource",
"roll": "Roll",
@ -2161,10 +2185,12 @@
"plural": "Targets"
},
"title": "Title",
"tokenSize": "Token Size",
"total": "Total",
"traitModifier": "Trait Modifier",
"true": "True",
"type": "Type",
"typeSheet": "System {type} Sheet",
"unarmed": "Unarmed",
"unarmedAttack": "Unarmed Attack",
"unarmored": "Unarmored",
@ -2217,6 +2243,7 @@
"tokenRingImg": { "label": "Subject Texture" },
"tokenSize": {
"placeholder": "Using character dimensions",
"disabledPlaceholder": "Set by character size",
"height": { "label": "Height" },
"width": { "label": "Width" }
},
@ -2240,7 +2267,11 @@
"evolvedDrag": "Drag a form here to evolve it.",
"hybridize": "Hybridize",
"hybridizeFeatureTitle": "Hybrid Features",
"hybridizeDrag": "Drag a form here to hybridize it."
"hybridizeDrag": "Drag a form here to hybridize it.",
"mainTrait": "Main Trait",
"traitBonus": "Trait Bonus",
"evolvedTokenHint": "An evolved beastform's token is based on that of the form you evolve",
"evolvedImagePlaceholder": "The image for the form selected for evolution will be used"
},
"Class": {
"hopeFeatures": "Hope Features",
@ -2434,9 +2465,12 @@
},
"currency": {
"title": "Currency Overrides",
"changeIcon": "Change Currency Icon",
"currencyName": "Currency Name",
"coinName": "Coin Name",
"handfulName": "Handful Name",
"iconName": "Icon Name",
"iconNameHint": "Icons are from fontawesome",
"bagName": "Bag Name",
"chestName": "Chest Name"
},
@ -2498,6 +2532,11 @@
"hint": "Apply variant rules from the Daggerheart system",
"name": "Variant Rules",
"actionTokens": "Action Tokens"
},
"SpotlightRequestQueue": {
"name": "Spotlight Request Queue",
"label": "Spotlight Request Queue",
"hint": "Adds more structure to spotlight requests by ordering them from oldest to newest"
}
},
"Resources": {
@ -2511,6 +2550,10 @@
"actionTokens": {
"enabled": { "label": "Enabled" },
"tokens": { "label": "Tokens" }
},
"massiveDamage": {
"title": "Massive Damage",
"enabled": { "label": "Enabled" }
}
}
},
@ -2583,7 +2626,8 @@
"selectMember": "Select a Member",
"rerollTitle": "Reroll Group Roll",
"rerollContent": "Are you sure you want to reroll your {trait} check?",
"rerollTooltip": "Reroll"
"rerollTooltip": "Reroll",
"wholePartySelected": "The whole party is selected"
},
"healingRoll": {
"title": "Heal - {damage}",
@ -2737,7 +2781,9 @@
"gmRequired": "This action requires an online GM",
"gmOnly": "This can only be accessed by the GM",
"noActorOwnership": "You do not have permissions for this character",
"documentIsMissing": "The {documentType} is missing from the world."
"documentIsMissing": "The {documentType} is missing from the world.",
"tokenActorMissing": "{name} is missing an Actor",
"tokenActorsMissing": "[{names}] missing Actors"
},
"Sidebar": {
"actorDirectory": {
@ -2780,7 +2826,9 @@
"companionPartnerLevelBlock": "The companion needs an assigned partner to level up.",
"configureAttribution": "Configure Attribution",
"deleteItem": "Delete Item",
"immune": "Immune"
"immune": "Immune",
"middleClick": "[Middle Click] Keep tooltip view",
"tokenSize": "The token size used on the canvas"
}
}
}

View file

@ -278,19 +278,26 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
'close',
async () => {
const selected = app.selected.toObject();
const evolved = app.evolved.form ? app.evolved.form.toObject() : null;
const data = await game.system.api.data.items.DHBeastform.getWildcardImage(
app.configData.data.parent,
app.selected
evolved ?? app.selected
);
if (data) {
if (!data.selectedImage) selected = null;
else {
if (data.usesDynamicToken) selected.system.tokenRingImg = data.selectedImage;
else selected.system.tokenImg = data.selectedImage;
const imageSource = evolved ?? selected;
if (imageSource.usesDynamicToken) imageSource.system.tokenRingImg = data.selectedImage;
else imageSource.system.tokenImg = data.selectedImage;
}
}
resolve({ selected: selected, evolved: app.evolved, hybrid: app.hybrid, item: featureItem });
resolve({
selected: selected,
evolved: { ...app.evolved, form: evolved },
hybrid: app.hybrid,
item: featureItem
});
},
{ once: true }
);

View file

@ -104,7 +104,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.roll = this.roll;
context.rollType = this.roll?.constructor.name;
context.rallyDie = this.roll.rallyChoices;
const experiences = this.config.data?.experiences || {};
const experiences = this.config.data?.system?.experiences || {};
context.experiences = Object.keys(experiences).map(id => ({
id,
...experiences[id]
@ -185,7 +185,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
extKey: button.dataset.key,
key: this.config?.data?.parent?.isNPC ? 'fear' : 'hope',
value: 1,
name: this.config.data?.experiences?.[button.dataset.key]?.name
name: this.config.data?.system.experiences?.[button.dataset.key]?.name
}
];
this.render();
@ -195,9 +195,9 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
if (this.config.roll) {
this.reactionOverride = !this.reactionOverride;
this.config.actionType = this.reactionOverride
? CONFIG.DH.ITEM.actionTypes.reaction.id
: this.config.actionType === CONFIG.DH.ITEM.actionTypes.reaction.id
? CONFIG.DH.ITEM.actionTypes.action.id
? 'reaction'
: this.config.actionType === 'reaction'
? 'action'
: this.config.actionType;
this.render();
}

View file

@ -93,7 +93,7 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
}
getRefreshables() {
const actionItems = this.actor.items.reduce((acc, x) => {
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)) {
@ -118,7 +118,7 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
if (
x.system.resource &&
x.system.resource.type &&
refreshIsAllowed([this.shortrest ? 'shortRest' : 'longRest'], action.uses.recovery)
refreshIsAllowed([this.shortrest ? 'shortRest' : 'longRest'], x.system.resource.recovery)
) {
acc.push({
title: game.i18n.localize(`TYPES.Item.${x.type}`),
@ -181,12 +181,17 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
.filter(x => category.moves[x].selected)
.flatMap(key => {
const move = category.moves[key];
const needsTarget = move.actions.filter(x => x.target?.type && x.target.type !== 'self').length > 0;
return [...Array(move.selected).keys()].map(_ => ({
...move,
movePath: `${categoryKey}.moves.${key}`
movePath: `${categoryKey}.moves.${key}`,
needsTarget: needsTarget
}));
});
});
const characters = game.actors.filter(x => x.type === 'character')
.filter(x => x.testUserPermission(game.user, 'LIMITED'))
.filter(x => x.uuid !== this.actor.uuid);
const cls = getDocumentClass('ChatMessage');
const msg = {
@ -206,7 +211,9 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
`DAGGERHEART.APPLICATIONS.Downtime.${this.shortrest ? 'shortRest' : 'longRest'}.title`
),
actor: { name: this.actor.name, img: this.actor.img },
moves: moves
moves: moves,
characters: characters,
selfId: this.actor.uuid
}
),
flags: {

View file

@ -1,69 +1,61 @@
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class ItemTransferDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(item) {
constructor(data) {
super({});
this.item = item;
this.quantity = item.system.quantity;
this.data = data;
}
get title() {
return this.item.name;
return this.data.title;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'dh-style', 'dialog', 'item-transfer'],
position: { width: 300, height: 'auto' },
position: { width: 400, height: 'auto' },
window: { icon: 'fa-solid fa-hand-holding-hand' },
actions: {
finish: ItemTransferDialog.#finish
},
form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false }
}
};
static PARTS = {
main: { template: 'systems/daggerheart/templates/dialogs/item-transfer.hbs' }
main: { template: 'systems/daggerheart/templates/dialogs/item-transfer.hbs', root: true }
};
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement.querySelector('.number-display').addEventListener('change', event => {
this.quantity = isNaN(event.target.value) ? this.quantity : Number(event.target.value);
this.render();
});
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.item = this.item;
context.quantity = this.quantity;
return context;
}
static async updateData(_event, _element, formData) {
const { quantity } = foundry.utils.expandObject(formData.object);
this.quantity = quantity;
this.render();
return foundry.utils.mergeObject(context, this.data);
}
static async #finish() {
this.close({ submitted: true });
this.selected = this.form.elements.quantity.valueAsNumber || null;
this.close();
}
close(options = {}) {
if (!options.submitted) this.quantity = null;
static #determineTransferOptions({ originActor, targetActor, item, currency }) {
originActor ??= item?.actor;
const homebrewKey = CONFIG.DH.SETTINGS.gameSettings.Homebrew;
const currencySetting = game.settings.get(CONFIG.DH.id, homebrewKey).currency?.[currency] ?? null;
super.close();
return {
originActor,
targetActor,
itemImage: item?.img,
currencyIcon: currencySetting?.icon,
max: item?.system.quantity ?? originActor.system.gold[currency] ?? 0,
title: item?.name ?? currencySetting?.label
};
}
static async configure(item) {
static async configure(options) {
return new Promise(resolve => {
const app = new this(item);
app.addEventListener('close', () => resolve(app.quantity), { once: true });
const data = this.#determineTransferOptions(options);
if (data.max <= 1) return resolve(data.max);
const app = new this(data);
app.addEventListener('close', () => resolve(app.selected), { once: true });
app.render({ force: true });
});
}

View file

@ -38,7 +38,6 @@ export default class OwnershipSelection extends HandlebarsApplicationMixin(Appli
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.ownershipDefaultOptions = CONFIG.DH.GENERAL.basicOwnershiplevels;
context.ownershipOptions = CONFIG.DH.GENERAL.simpleOwnershiplevels;
context.defaultOwnership = this.defaultOwnership;
context.ownership = game.users.reduce((acc, user) => {
@ -52,6 +51,7 @@ export default class OwnershipSelection extends HandlebarsApplicationMixin(Appli
return acc;
}, {});
context.showOwnership = Boolean(Object.keys(context.ownership).length);
return context;
}

View file

@ -77,7 +77,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
cost: this.data.initiator.cost
};
const selectedMember = Object.values(context.members).find(x => x.selected);
const selectedMember = Object.values(context.members).find(x => x.selected && x.roll);
const selectedIsCritical = selectedMember?.roll?.system?.isCritical;
context.selectedData = {
result: selectedMember
@ -220,8 +220,8 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
!roll.system.isCritical && criticalRoll
? (await getCritDamageBonus(damage.formula)) + damage.total
: damage.total;
const updatedDamageParts = damage.parts;
if (systemData.damage[key]) {
const updatedDamageParts = damage.parts;
if (!roll.system.isCritical && criticalRoll) {
for (let part of updatedDamageParts) {
const criticalDamage = await getCritDamageBonus(part.formula);

View file

@ -21,6 +21,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
async _prepareContext(options) {
const context = await super._prepareContext(options);
if (!this.actor) return context;
context.partyOnCanvas =
this.actor.type === 'party' &&
this.actor.system.partyMembers.some(member => member.getActiveTokens().length > 0);
@ -58,14 +60,33 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
}
static async #onToggleCombat() {
const tokensWithoutActors = canvas.tokens.controlled.filter(t => !t.actor);
const warning =
tokensWithoutActors.length === 1
? game.i18n.format('DAGGERHEART.UI.Notifications.tokenActorMissing', {
name: tokensWithoutActors[0].name
})
: game.i18n.format('DAGGERHEART.UI.Notifications.tokenActorsMissing', {
names: tokensWithoutActors.map(x => x.name).join(', ')
});
const tokens = canvas.tokens.controlled
.filter(t => !t.actor || !DHTokenHUD.#nonCombatTypes.includes(t.actor.type))
.filter(t => t.actor && !DHTokenHUD.#nonCombatTypes.includes(t.actor.type))
.map(t => t.document);
if (!this.object.controlled) tokens.push(this.document);
if (!this.object.controlled && this.document.actor) tokens.push(this.document);
try {
if (this.document.inCombat) await TokenDocument.implementation.deleteCombatants(tokens);
else await TokenDocument.implementation.createCombatants(tokens);
if (this.document.inCombat) {
const tokensInCombat = tokens.filter(t => t.inCombat);
await TokenDocument.implementation.deleteCombatants([...tokensInCombat, ...tokensWithoutActors]);
} else {
if (tokensWithoutActors.length) {
ui.notifications.warn(warning);
}
const tokensOutOfCombat = tokens.filter(t => !t.inCombat);
await TokenDocument.implementation.createCombatants(tokensOutOfCombat);
}
} catch (err) {
ui.notifications.warn(err.message);
}
@ -197,6 +218,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
for (const effect of activeEffects) {
for (const statusId of effect.statuses) {
const status = choices[statusId];
if (!status) continue;
status.instances = 1 + (status.instances ?? 0);
status.locked = status.locked || effect.condition || status.instances > 1;
if (!status) continue;

View file

@ -280,11 +280,19 @@ export default class DhCharacterLevelUp extends LevelUpBase {
break;
case 'experience':
if (!advancement[choiceKey]) advancement[choiceKey] = [];
const allExperiences = {
...this.actor.system.experiences,
...Object.values(this.levelup.levels).reduce((acc, level) => {
for (const key of Object.keys(level.achievements.experiences)) {
acc[key] = level.achievements.experiences[key];
}
return acc;
}, {})
};
const data = checkbox.data.map(data => {
const experience = Object.keys(this.actor.system.experiences).find(
x => x === data
);
return this.actor.system.experiences[experience]?.name ?? '';
const experience = Object.keys(allExperiences).find(x => x === data);
return allExperiences[experience]?.name ?? '';
});
advancement[choiceKey].push({ data: data, value: checkbox.value });
break;

View file

@ -357,11 +357,23 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const experienceIncreaseTagify = htmlElement.querySelector('.levelup-experience-increases');
if (experienceIncreaseTagify) {
const allExperiences = {
...this.actor.system.experiences,
...Object.values(this.levelup.levels).reduce((acc, level) => {
for (const key of Object.keys(level.achievements.experiences)) {
acc[key] = level.achievements.experiences[key];
}
return acc;
}, {})
};
tagifyElement(
experienceIncreaseTagify,
Object.keys(this.actor.system.experiences).reduce((acc, id) => {
const experience = this.actor.system.experiences[id];
acc.push({ id: id, label: experience.name });
Object.keys(allExperiences).reduce((acc, id) => {
const experience = allExperiences[id];
if (experience.name) {
acc.push({ id: id, label: experience.name });
}
return acc;
}, []),

View file

@ -32,6 +32,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
icon: 'fa-solid fa-gears'
},
actions: {
editCurrencyIcon: this.changeCurrencyIcon,
addItem: this.addItem,
editItem: this.editItem,
removeItem: this.removeItem,
@ -43,6 +44,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
deleteAdversaryType: this.deleteAdversaryType,
selectAdversaryType: this.selectAdversaryType,
save: this.save,
resetTokenSizes: this.resetTokenSizes,
reset: this.reset
},
form: { handler: this.updateData, submitOnChange: true }
@ -115,6 +117,45 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
this.render();
}
static async changeCurrencyIcon(_, target) {
const type = target.dataset.currency;
const currentIcon = this.settings.currency[type].icon;
const icon = await foundry.applications.api.DialogV2.input({
classes: ['daggerheart', 'dh-style', 'change-currency-icon'],
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/settings/homebrew-settings/change-currency-icon.hbs',
{ currentIcon }
),
window: {
title: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.currency.changeIcon'),
icon: 'fa-solid fa-coins'
},
render: (_, dialog) => {
const icon = dialog.element.querySelector('.displayed-icon i');
const input = dialog.element.querySelector('input');
const reset = dialog.element.querySelector('button[data-action=reset]');
input.addEventListener('input', () => {
icon.classList.value = input.value;
});
reset.addEventListener('click', () => {
const currencyField = DhHomebrew.schema.fields.currency.fields[type];
const initial = currencyField.fields.icon.getInitialValue();
input.value = icon.classList.value = initial;
});
},
ok: {
callback: (_, button) => button.form.elements.icon.value
}
});
if (icon !== null) {
await this.settings.updateSource({
[`currency.${type}.icon`]: icon
});
this.render();
}
}
static async addItem(_, target) {
const { type } = target.dataset;
if (['shortRest', 'longRest'].includes(type)) {
@ -384,6 +425,14 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
this.close();
}
static async resetTokenSizes() {
await this.settings.updateSource({
tokenSizes: this.settings.schema.fields.tokenSizes.initial
});
this.render();
}
static async reset() {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {

View file

@ -98,7 +98,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
async _prepareContext(_options) {
const context = await super._prepareContext(_options, 'action');
context.source = this.action.toObject(false);
context.source = this.action.toObject(true);
context.openSection = this.openSection;
context.tabs = this._getTabs(this.constructor.TABS);
context.config = CONFIG.DH;

View file

@ -9,6 +9,9 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
if (!ignoredActorKeys.includes(key)) {
const model = game.system.api.models.actors[key];
const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model);
// As per DHToken._getTrackedAttributesFromSchema, attributes.bar have a max version as well.
const maxAttributes = attributes.bar.map(x => [...x, 'max']);
attributes.value.push(...maxAttributes);
const group = game.i18n.localize(model.metadata.label);
const choices = CONFIG.Token.documentClass
.getTrackedAttributeChoices(attributes, model)

View file

@ -51,6 +51,19 @@ export default class DHAdversarySettings extends DHBaseActorSettings {
}
};
async _prepareContext(options) {
const context = await super._prepareContext(options);
const featureForms = ['passive', 'action', 'reaction'];
context.features = context.document.system.features.sort((a, b) =>
a.system.featureForm !== b.system.featureForm
? featureForms.indexOf(a.system.featureForm) - featureForms.indexOf(b.system.featureForm)
: a.sort - b.sort
);
return context;
}
/* -------------------------------------------- */
/**
@ -98,16 +111,16 @@ export default class DHAdversarySettings extends DHBaseActorSettings {
async _onDrop(event) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const item = await fromUuid(data.uuid);
if (item?.type === 'feature') {
if (data.fromInternal && item.parent?.uuid === this.actor.uuid) {
return;
}
const itemData = item.toObject();
delete itemData._id;
await this.actor.createEmbeddedDocuments('Item', [itemData]);
}
}

View file

@ -49,6 +49,19 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
}
};
async _prepareContext(options) {
const context = await super._prepareContext(options);
const featureForms = ['passive', 'action', 'reaction'];
context.features = context.document.system.features.sort((a, b) =>
a.system.featureForm !== b.system.featureForm
? featureForms.indexOf(a.system.featureForm) - featureForms.indexOf(b.system.featureForm)
: a.sort - b.sort
);
return context;
}
/**
* Adds a new category entry to the actor.
* @type {ApplicationClickAction}

View file

@ -1,20 +1,30 @@
export default class DhPrototypeTokenConfig extends foundry.applications.sheets.PrototypeTokenConfig {
import DHTokenConfigMixin from './token-config-mixin.mjs';
import { getActorSizeFromForm } from './token-config-mixin.mjs';
export default class DhPrototypeTokenConfig extends DHTokenConfigMixin(
foundry.applications.sheets.PrototypeTokenConfig
) {
/** @inheritDoc */
async _prepareResourcesTab() {
const token = this.token;
const usesTrackableAttributes = !foundry.utils.isEmpty(CONFIG.Actor.trackableAttributes);
const attributeSource =
this.actor?.system instanceof foundry.abstract.DataModel && usesTrackableAttributes
? this.actor?.type
: this.actor?.system;
const TokenDocument = foundry.utils.getDocumentClass('Token');
const attributes = TokenDocument.getTrackedAttributes(attributeSource);
return {
barAttributes: TokenDocument.getTrackedAttributeChoices(attributes, attributeSource),
bar1: token.getBarAttribute?.('bar1'),
bar2: token.getBarAttribute?.('bar2'),
turnMarkerModes: DhPrototypeTokenConfig.TURN_MARKER_MODES,
turnMarkerAnimations: CONFIG.Combat.settings.turnMarkerAnimations
};
static DEFAULT_OPTIONS = {
...super.DEFAULT_OPTIONS,
form: { handler: DhPrototypeTokenConfig.#onSubmit }
};
/**
* Process form submission for the sheet
* @this {PrototypeTokenConfig}
* @type {ApplicationFormSubmission}
*/
static async #onSubmit(event, form, formData) {
const submitData = this._processFormData(event, form, formData);
submitData.detectionModes ??= []; // Clear detection modes array
this._processChanges(submitData);
const changes = { prototypeToken: submitData };
const changedTokenSizeValue = getActorSizeFromForm(this.element, this.actor);
if (changedTokenSizeValue) changes.system = { size: changedTokenSizeValue };
this.actor.validate({ changes, clean: true, fallback: false });
await this.actor.update(changes);
}
}

View file

@ -0,0 +1,114 @@
export default function DHTokenConfigMixin(Base) {
class DHTokenConfigBase extends Base {
/** @override */
static PARTS = {
tabs: super.PARTS.tabs,
identity: super.PARTS.identity,
appearance: {
template: 'systems/daggerheart/templates/sheets-settings/token-config/appearance.hbs',
scrollable: ['']
},
vision: super.PARTS.vision,
light: super.PARTS.light,
resources: super.PARTS.resources,
footer: super.PARTS.footer
};
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
switch (partId) {
case 'appearance':
htmlElement
.querySelector('#dhTokenSize')
?.addEventListener('change', this.onTokenSizeChange.bind(this));
break;
}
}
/** @inheritDoc */
async _prepareResourcesTab() {
const token = this.token;
const usesTrackableAttributes = !foundry.utils.isEmpty(CONFIG.Actor.trackableAttributes);
const attributeSource =
this.actor?.system instanceof foundry.abstract.DataModel && usesTrackableAttributes
? this.actor?.type
: this.actor?.system;
const TokenDocument = foundry.utils.getDocumentClass('Token');
const attributes = TokenDocument.getTrackedAttributes(attributeSource);
return {
barAttributes: TokenDocument.getTrackedAttributeChoices(attributes, attributeSource),
bar1: token.getBarAttribute?.('bar1'),
bar2: token.getBarAttribute?.('bar2'),
turnMarkerModes: DHTokenConfigBase.TURN_MARKER_MODES,
turnMarkerAnimations: CONFIG.Combat.settings.turnMarkerAnimations
};
}
async _prepareAppearanceTab() {
const context = await super._prepareAppearanceTab();
context.tokenSizes = CONFIG.DH.ACTOR.tokenSize;
context.tokenSize = this.actor?.system?.size;
context.usesActorSize = this.actor?.system?.metadata?.usesSize;
context.actorSizeDisable = context.usesActorSize && this.actor.system.size !== 'custom';
return context;
}
/** @inheritDoc */
_previewChanges(changes) {
if (!changes || !this._preview) return;
const tokenSizeSelect = this.element?.querySelector('#dhTokenSize');
if (this.actor && tokenSizeSelect && tokenSizeSelect.value !== 'custom') {
const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes;
const tokenSize = tokenSizes[tokenSizeSelect.value];
changes.width = tokenSize;
changes.height = tokenSize;
}
const deletions = { '-=actorId': null, '-=actorLink': null };
const mergeOptions = { inplace: false, performDeletions: true };
this._preview.updateSource(mergeObject(changes, deletions, mergeOptions));
if (this._preview?.object?.destroyed === false) {
this._preview.object.initializeSources();
this._preview.object.renderFlags.set({ refresh: true });
}
}
async onTokenSizeChange(event) {
const value = event.target.value;
const tokenSizeDimensions = this.element.querySelector('#tokenSizeDimensions');
if (tokenSizeDimensions) {
const disabled = value !== 'custom';
tokenSizeDimensions.dataset.tooltip = disabled
? game.i18n.localize('DAGGERHEART.APPLICATIONS.TokenConfig.actorSizeUsed')
: '';
const disabledIcon = tokenSizeDimensions.querySelector('i');
if (disabledIcon) {
disabledIcon.style.opacity = disabled ? '' : '0';
}
const dimensionsInputs = tokenSizeDimensions.querySelectorAll('.form-fields input');
for (const input of dimensionsInputs) {
input.disabled = disabled;
}
}
}
}
return DHTokenConfigBase;
}
export function getActorSizeFromForm(element, actor) {
const tokenSizeSelect = element.querySelector('#dhTokenSize');
const isSizeDifferent = tokenSizeSelect?.value !== actor?.system?.size;
if (tokenSizeSelect && actor && isSizeDifferent) {
return tokenSizeSelect.value;
}
return null;
}

View file

@ -1,20 +1,11 @@
export default class DhTokenConfig extends foundry.applications.sheets.TokenConfig {
/** @inheritDoc */
async _prepareResourcesTab() {
const token = this.token;
const usesTrackableAttributes = !foundry.utils.isEmpty(CONFIG.Actor.trackableAttributes);
const attributeSource =
this.actor?.system instanceof foundry.abstract.DataModel && usesTrackableAttributes
? this.actor?.type
: this.actor?.system;
const TokenDocument = foundry.utils.getDocumentClass('Token');
const attributes = TokenDocument.getTrackedAttributes(attributeSource);
return {
barAttributes: TokenDocument.getTrackedAttributeChoices(attributes, attributeSource),
bar1: token.getBarAttribute?.('bar1'),
bar2: token.getBarAttribute?.('bar2'),
turnMarkerModes: DhTokenConfig.TURN_MARKER_MODES,
turnMarkerAnimations: CONFIG.Combat.settings.turnMarkerAnimations
};
import DHTokenConfigMixin from './token-config-mixin.mjs';
import { getActorSizeFromForm } from './token-config-mixin.mjs';
export default class DhTokenConfig extends DHTokenConfigMixin(foundry.applications.sheets.TokenConfig) {
async _processSubmitData(event, form, submitData, options) {
const changedTokenSizeValue = getActorSizeFromForm(this.element, this.actor);
if (changedTokenSizeValue) this.token.actor.update({ 'system.size': changedTokenSizeValue });
super._processSubmitData(event, form, submitData, options);
}
}

View file

@ -26,7 +26,12 @@ export default class AdversarySheet extends DHBaseActorSheet {
}
]
},
dragDrop: [{ dragSelector: '[data-item-id][draggable="true"]', dropSelector: null }]
dragDrop: [
{
dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
dropSelector: null
}
],
};
static PARTS = {
@ -88,6 +93,13 @@ export default class AdversarySheet extends DHBaseActorSheet {
context.resources.stress.emptyPips =
context.resources.stress.max < maxResource ? maxResource - context.resources.stress.max : 0;
const featureForms = ['passive', 'action', 'reaction'];
context.features = this.document.system.features.sort((a, b) =>
a.system.featureForm !== b.system.featureForm
? featureForms.indexOf(a.system.featureForm) - featureForms.indexOf(b.system.featureForm)
: a.sort - b.sort
);
return context;
}
@ -164,6 +176,16 @@ export default class AdversarySheet extends DHBaseActorSheet {
});
}
/** @inheritdoc */
async _onDragStart(event) {
const inventoryItem = event.currentTarget.closest('.inventory-item');
if (inventoryItem) {
event.dataTransfer.setDragImage(inventoryItem.querySelector('img'), 60, 0);
}
super._onDragStart(event);
}
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */

View file

@ -32,7 +32,8 @@ export default class CharacterSheet extends DHBaseActorSheet {
handleResourceDice: CharacterSheet.#handleResourceDice,
advanceResourceDie: CharacterSheet.#advanceResourceDie,
cancelBeastform: CharacterSheet.#cancelBeastform,
useDowntime: this.useDowntime
useDowntime: this.useDowntime,
viewParty: CharacterSheet.#viewParty,
},
window: {
resizable: true,
@ -46,7 +47,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
},
dragDrop: [
{
dragSelector: '[data-item-id][draggable="true"]',
dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
dropSelector: null
}
],
@ -139,10 +140,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
element.addEventListener('change', this.updateItemResource.bind(this));
element.addEventListener('click', e => e.stopPropagation());
});
htmlElement.querySelectorAll('.inventory-item-quantity').forEach(element => {
element.addEventListener('change', this.updateItemQuantity.bind(this));
element.addEventListener('click', e => e.stopPropagation());
});
// Add listener for armor marks input
htmlElement.querySelectorAll('.armor-marks-input').forEach(element => {
@ -322,6 +319,40 @@ export default class CharacterSheet extends DHBaseActorSheet {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.loadoutMaxReached'));
}
},
{
name: 'recall',
icon: 'fa-solid fa-bolt-lightning',
condition: target => {
const doc = getDocFromElementSync(target);
return doc && doc.system.inVault;
},
callback: async (target, event) => {
const doc = await getDocFromElement(target);
const actorLoadout = doc.actor.system.loadoutSlot;
if (!actorLoadout.available) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.loadoutMaxReached'));
return;
}
if (doc.system.recallCost == 0) {
return doc.update({ 'system.inVault': false });
}
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 config = await action.use(event);
if (config) {
return doc.update({ 'system.inVault': false });
}
}
},
{
name: 'toVault',
icon: 'fa-solid fa-arrow-down',
@ -593,14 +624,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
this.render();
}
async updateItemQuantity(event) {
const item = await getDocFromElement(event.currentTarget);
if (!item) return;
await item.update({ 'system.quantity': event.currentTarget.value });
this.render();
}
async updateArmorMarks(event) {
const armor = this.document.system.armor;
if (!armor) return;
@ -687,18 +710,21 @@ export default class CharacterSheet extends DHBaseActorSheet {
roll: {
trait: button.dataset.attribute
},
hasRoll: true
};
const result = await this.document.diceRoll({
...config,
hasRoll: true,
actionType: 'action',
headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`,
title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: abilityLabel
})
});
};
const result = await this.document.diceRoll(config);
if (result) game.system.api.fields.ActionFields.CostField.execute.call(this, result);
/* 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
.filter(x => x.enabled)
.map(cost => ({ ...cost, value: -cost.value, total: -cost.total }));
config.resourceUpdates.addResources(costResources);
await config.resourceUpdates.updateResources();
}
//TODO: redo toggleEquipItem method
@ -867,6 +893,41 @@ export default class CharacterSheet extends DHBaseActorSheet {
game.system.api.fields.ActionFields.BeastformField.handleActiveTransformations.call(item);
}
static async #viewParty(_, target) {
const parties = this.document.parties;
if (parties.size <= 1) {
parties.first()?.sheet.render({ force: true });
return;
}
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");
name.textContent = p.name;
button.append(name);
button.addEventListener("click", () => {
p.sheet?.render({ force: true });
game.tooltip.dismissLockedTooltips();
});
return button;
});
const html = document.createElement("div");
html.classList.add("party-list");
html.append(...buttons);
game.tooltip.dismissLockedTooltips();
game.tooltip.activate(target, {
html,
locked: true,
})
}
/**
* Open the downtime application.
* @type {ApplicationClickAction}
@ -877,6 +938,15 @@ export default class CharacterSheet extends DHBaseActorSheet {
});
}
/** @inheritdoc */
async _onDragStart(event) {
const inventoryItem = event.currentTarget.closest('.inventory-item');
if (inventoryItem) {
event.dataTransfer.setDragImage(inventoryItem.querySelector('img'), 60, 0);
}
super._onDragStart(event);
}
async _onDropItem(event, item) {
if (this.document.uuid === item.parent?.uuid) {
return super._onDropItem(event, item);

View file

@ -25,7 +25,12 @@ export default class DhpEnvironment extends DHBaseActorSheet {
toggleResourceDice: DhpEnvironment.#toggleResourceDice,
handleResourceDice: DhpEnvironment.#handleResourceDice
},
dragDrop: [{ dragSelector: '.inventory-item', dropSelector: null }]
dragDrop: [
{
dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
dropSelector: null
}
]
};
/**@override */
@ -74,6 +79,9 @@ export default class DhpEnvironment extends DHBaseActorSheet {
case 'header':
await this._prepareHeaderContext(context, options);
break;
case 'features':
await this._prepareFeaturesContext(context, options);
break;
case 'notes':
await this._prepareNotesContext(context, options);
@ -110,6 +118,22 @@ export default class DhpEnvironment extends DHBaseActorSheet {
}
}
/**
* Prepare render context for the features part.
* @param {ApplicationRenderContext} context
* @param {ApplicationRenderOptions} options
* @returns {Promise<void>}
* @protected
*/
async _prepareFeaturesContext(context, _options) {
const featureForms = ['passive', 'action', 'reaction'];
context.features = this.document.system.features.sort((a, b) =>
a.system.featureForm !== b.system.featureForm
? featureForms.indexOf(a.system.featureForm) - featureForms.indexOf(b.system.featureForm)
: a.sort - b.sort
);
}
/**
* Prepare render context for the Header part.
* @param {ApplicationRenderContext} context

View file

@ -40,7 +40,7 @@ export default class Party extends DHBaseActorSheet {
selectRefreshable: DaggerheartMenu.selectRefreshable,
refreshActors: DaggerheartMenu.refreshActors
},
dragDrop: [{ dragSelector: '[data-item-id][draggable="true"]', dropSelector: null }]
dragDrop: [{ dragSelector: '[data-item-id]', dropSelector: null }]
};
/**@override */

View file

@ -178,6 +178,79 @@ export default function DHApplicationMixin(Base) {
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
this._dragDrop.forEach(d => d.bind(htmlElement));
// Handle delta inputs
for (const deltaInput of htmlElement.querySelectorAll('input[data-allow-delta]')) {
deltaInput.dataset.numValue = deltaInput.value;
deltaInput.inputMode = 'numeric';
const handleUpdate = (delta = 0) => {
const min = Number(deltaInput.min) || 0;
const max = Number(deltaInput.max) || Infinity;
const current = Number(deltaInput.dataset.numValue);
const rawNumber = Number(deltaInput.value);
if (Number.isNaN(rawNumber)) {
deltaInput.value = delta ? Math.clamp(current + delta, min, max) : current;
return;
}
const newValue =
deltaInput.value.startsWith('+') || deltaInput.value.startsWith('-')
? Math.clamp(current + rawNumber + delta, min, max)
: Math.clamp(rawNumber + delta, min, max);
deltaInput.value = deltaInput.dataset.numValue = newValue;
};
// Force valid characters while inputting
deltaInput.addEventListener('input', () => {
deltaInput.value = /[+=\-]?\d*/.exec(deltaInput.value)?.at(0) ?? deltaInput.value;
});
// Recreate Keyup/Keydown support
deltaInput.addEventListener('keydown', event => {
const step = event.key === 'ArrowUp' ? 1 : event.key === 'ArrowDown' ? -1 : 0;
if (step !== 0) {
handleUpdate(step);
deltaInput.dispatchEvent(new Event("change", { bubbles: true }));
}
});
// Mousewheel while focused support
deltaInput.addEventListener(
'wheel',
event => {
if (deltaInput === document.activeElement) {
event.preventDefault();
handleUpdate(Math.sign(-1 * event.deltaY));
deltaInput.dispatchEvent(new Event("change", { bubbles: true }));
}
},
{ passive: false }
);
deltaInput.addEventListener('change', () => {
handleUpdate();
});
}
// Handle contenteditable
for (const input of htmlElement.querySelectorAll('[contenteditable][data-property]')) {
const property = input.dataset.property;
input.addEventListener("blur", () => {
const selection = document.getSelection();
if (input.contains(selection.anchorNode)) {
selection.empty();
}
this.document.update({ [property]: input.textContent });
});
input.addEventListener("keydown", event => {
if (event.key === "Enter") input.blur();
});
// Chrome sometimes add <br>, which aren't a problem for the value but are for the placeholder
input.addEventListener("input", () => input.querySelectorAll("br").forEach((i) => i.remove()));
}
}
/**@inheritdoc */
@ -325,7 +398,8 @@ export default function DHApplicationMixin(Base) {
if (data.type === 'ActiveEffect' && data.fromInternal !== this.document.uuid) {
this.document.createEmbeddedDocuments('ActiveEffect', [data.data]);
} else {
return super._onDrop(event);
// Fallback to super, but note that item sheets do not have this function
return super._onDrop?.(event);
}
}

View file

@ -34,7 +34,10 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
}
}
],
dragDrop: [{ dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null }]
dragDrop: [
{ dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null },
{ dragSelector: ".currency[data-currency] .drag-handle", dropSelector: null }
]
};
/* -------------------------------------------- */
@ -89,6 +92,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
value: context.source.system.gold[key]
};
}
context.inventory.hasCurrency = Object.values(context.inventory.currencies).some((c) => c.enabled);
}
return context;
@ -134,6 +138,10 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement.querySelectorAll('.inventory-item-quantity').forEach(element => {
element.addEventListener('change', this.updateItemQuantity.bind(this));
element.addEventListener('click', e => e.stopPropagation());
});
htmlElement.querySelectorAll('.item-button .action-uses-button').forEach(element => {
element.addEventListener('contextmenu', DHBaseActorSheet.#modifyActionUses);
});
@ -172,6 +180,15 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true });
}
/* -------------------------------------------- */
/* Application Listener Actions */
/* -------------------------------------------- */
async updateItemQuantity(event) {
const item = await getDocFromElement(event.currentTarget);
await item?.update({ 'system.quantity': event.currentTarget.value });
}
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
@ -240,14 +257,35 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
/* Application Drag/Drop */
/* -------------------------------------------- */
async _onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (data.type === 'Currency' && ['character', 'party'].includes(this.document.type)) {
const originActor = await foundry.utils.fromUuid(data.originActor);
if (!originActor || originActor.uuid === this.document.uuid) return;
const currency = data.currency;
const quantity = await game.system.api.applications.dialogs.ItemTransferDialog.configure({
originActor,
targetActor: this.document,
currency
});
if (quantity) {
originActor.update({ [`system.gold.${currency}`]: Math.max(0, originActor.system.gold[currency] - quantity) });
this.document.update({ [`system.gold.${currency}`]: this.document.system.gold[currency] + quantity });
}
return;
}
return super._onDrop(event);
}
async _onDropItem(event, item) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const physicalActorTypes = ['character', 'party'];
const originActor = item.actor;
if (
item.actor?.uuid === this.document.uuid ||
!originActor ||
!physicalActorTypes.includes(this.document.type)
!['character', 'party'].includes(this.document.type)
) {
return super._onDropItem(event, item);
}
@ -256,10 +294,10 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
if (item.system.metadata.isInventoryItem) {
if (item.system.metadata.isQuantifiable) {
const actorItem = originActor.items.get(data.originId);
const quantityTransfered =
actorItem.system.quantity === 1
? 1
: await game.system.api.applications.dialogs.ItemTransferDialog.configure(item);
const quantityTransfered = await game.system.api.applications.dialogs.ItemTransferDialog.configure({
item,
targetActor: this.document
});
if (quantityTransfered) {
if (quantityTransfered === actorItem.system.quantity) {
@ -300,6 +338,16 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
* @param {DragEvent} event - The drag event
*/
async _onDragStart(event) {
// Handle drag/dropping currencies
const currencyEl = event.currentTarget.closest(".currency[data-currency]");
if (currencyEl) {
const currency = currencyEl.dataset.currency;
const data = { type: 'Currency', currency, originActor: this.document.uuid };
event.dataTransfer.setData('text/plain', JSON.stringify(data));
return;
}
// Handle drag/dropping attacks
const attackItem = event.currentTarget.closest('.inventory-item[data-type="attack"]');
if (attackItem) {
const attackData = {

View file

@ -77,6 +77,7 @@ export default class BeastformSheet extends DHBaseItemSheet {
name: context.document.system.advantageOn[key].value
}))
);
context.dimensionsDisabled = context.document.system.tokenSize.size !== 'custom';
break;
case 'effects':
context.effects.actives = context.effects.actives.map(effect => {

View file

@ -31,4 +31,11 @@ export default class FeatureSheet extends DHBaseItemSheet {
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}
};
//Might be wrong location but testing out if here is okay.
/**@override */
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.featureFormChoices = CONFIG.DH.ITEM.featureForm;
return context;
}
}

View file

@ -17,4 +17,30 @@ export default class DhActorDirectory extends foundry.applications.sidebar.tabs.
: null;
};
}
/** @inheritDoc */
_onDragStart(event) {
let actor;
const { entryId } = event.currentTarget.dataset;
if (entryId) {
actor = this.collection.get(entryId);
if (!actor?.visible) return false;
}
super._onDragStart(event);
// Create the drag preview.
if (actor && canvas.ready) {
const img = event.currentTarget.querySelector('img');
const pt = actor.prototypeToken;
const usesSize = actor.system.metadata.usesSize;
const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes;
const width = usesSize ? tokenSizes[actor.system.size] : pt.width;
const height = usesSize ? tokenSizes[actor.system.size] : pt.height;
const w = width * canvas.dimensions.size * Math.abs(pt.texture.scaleX) * canvas.stage.scale.x;
const h = height * canvas.dimensions.size * Math.abs(pt.texture.scaleY) * canvas.stage.scale.y;
const preview = foundry.applications.ux.DragDrop.implementation.createDragImage(img, w, h);
event.dataTransfer.setDragImage(preview, w / 2, h / 2);
}
}
}

View file

@ -55,27 +55,28 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
];
}
addChatListeners = async (app, html, data) => {
addChatListeners = async (document, html, data) => {
const message = data?.message ?? document.toObject(false);
html.querySelectorAll('.simple-roll-button').forEach(element =>
element.addEventListener('click', event => this.onRollSimple(event, data.message))
element.addEventListener('click', event => this.onRollSimple(event, message))
);
html.querySelectorAll('.ability-use-button').forEach(element =>
element.addEventListener('click', event => this.abilityUseButton(event, data.message))
element.addEventListener('click', event => this.abilityUseButton(event, message))
);
html.querySelectorAll('.action-use-button').forEach(element =>
element.addEventListener('click', event => this.actionUseButton(event, data.message))
element.addEventListener('click', event => this.actionUseButton(event, message))
);
html.querySelectorAll('.reroll-button').forEach(element =>
element.addEventListener('click', event => this.rerollEvent(event, data.message))
element.addEventListener('click', event => this.rerollEvent(event, message))
);
html.querySelectorAll('.group-roll-button').forEach(element =>
element.addEventListener('click', event => this.groupRollButton(event, data.message))
element.addEventListener('click', event => this.groupRollButton(event, message))
);
html.querySelectorAll('.group-roll-reroll').forEach(element =>
element.addEventListener('click', event => this.groupRollReroll(event, data.message))
element.addEventListener('click', event => this.groupRollReroll(event, message))
);
html.querySelectorAll('.group-roll-success').forEach(element =>
element.addEventListener('click', event => this.groupRollSuccessEvent(event, data.message))
element.addEventListener('click', event => this.groupRollSuccessEvent(event, message))
);
html.querySelectorAll('.group-roll-header-expand-section').forEach(element =>
element.addEventListener('click', this.groupRollExpandSection)
@ -133,7 +134,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
async actionUseButton(event, message) {
const { moveIndex, actionIndex, movePath } = event.currentTarget.dataset;
const parent = await foundry.utils.fromUuid(message.system.actor);
const targetUuid = event.currentTarget.closest('.action-use-button-parent').querySelector('select')?.value;
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];
const action = new cls(
@ -145,7 +148,8 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
type: CONFIG.DH.ITEM.originItemType.restMove,
itemPath: movePath,
actionIndex: actionIndex
}
},
targetUuid: targetUuid
},
{ parent: parent.system }
);
@ -245,7 +249,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
});
if (!result) return;
await game.system.api.fields.ActionFields.CostField.execute.call({ actor }, result);
const newMessageData = foundry.utils.deepClone(message.system);
foundry.utils.setProperty(newMessageData, `${path}.result`, result.roll);

View file

@ -5,8 +5,7 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
actions: {
requestSpotlight: this.requestSpotlight,
toggleSpotlight: this.toggleSpotlight,
setActionTokens: this.setActionTokens,
openCountdowns: this.openCountdowns
setActionTokens: this.setActionTokens
}
};
@ -42,13 +41,13 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
const modifierBP =
this.combats
.find(x => x.active)
?.system?.extendedBattleToggles?.reduce((acc, toggle) => acc + toggle.category, 0) ?? 0;
?.system?.extendedBattleToggles?.reduce((acc, toggle) => (acc ?? 0) + toggle.category, null) ?? null;
const maxBP = CONFIG.DH.ENCOUNTER.BaseBPPerEncounter(context.characters.length) + modifierBP;
const currentBP = AdversaryBPPerEncounter(context.adversaries, context.characters);
Object.assign(context, {
fear: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
battlepoints: { max: maxBP, current: currentBP, hasModifierBP: Boolean(modifierBP) }
battlepoints: { max: maxBP, current: currentBP, hasModifierBP: modifierBP !== null }
});
}
@ -57,21 +56,26 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
const adversaries = context.turns?.filter(x => x.isNPC) ?? [];
const characters = context.turns?.filter(x => !x.isNPC) ?? [];
const spotlightQueueEnabled = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.SpotlightRequestQueue
);
const spotlightRequests = characters
?.filter(x => !x.isNPC)
?.filter(x => !x.isNPC && spotlightQueueEnabled)
.filter(x => x.system.spotlight.requestOrderIndex > 0)
.sort((a, b) => {
const valueA = a.system.spotlight.requestOrderIndex;
const valueB = b.system.spotlight.requestOrderIndex;
return valueA - valueB;
});
Object.assign(context, {
actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens,
adversaries,
characters: characters?.filter(x => !x.isNPC).filter(x => x.system.spotlight.requestOrderIndex == 0),
characters: characters
?.filter(x => !x.isNPC)
.filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0),
spotlightRequests
});
}
@ -123,7 +127,7 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
resource,
active: index === combat.turn,
canPing: combatant.sceneId === canvas.scene?.id && game.user.hasPermission('PING_CANVAS'),
type: combatant.actor.system.type,
type: combatant.actor?.system?.type,
img: await this._getCombatantThumbnail(combatant)
};
@ -161,9 +165,13 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
if (this.viewed.turn !== toggleTurn) {
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.spotlight.id);
if (combatant.actor.type === 'character') {
await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.characterSpotlight.id);
if (combatant.actor?.type === 'character') {
await updateCountdowns(
CONFIG.DH.GENERAL.countdownProgressionTypes.spotlight.id,
CONFIG.DH.GENERAL.countdownProgressionTypes.characterSpotlight.id
);
} else {
await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.spotlight.id);
}
const autoPoints = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).actionPoints;

View file

@ -245,14 +245,20 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
return super.close(options);
}
static async updateCountdowns(progressType) {
/**
* Sends updates of the countdowns to the GM player. Since this is asynchronous, be sure to
* update all the countdowns at the same time.
*
* @param {...any} progressTypes Countdowns to be updated
*/
static async updateCountdowns(...progressTypes) {
const { countdownAutomation } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
if (!countdownAutomation) return;
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const updatedCountdowns = Object.keys(countdownSetting.countdowns).reduce((acc, key) => {
const countdown = countdownSetting.countdowns[key];
if (countdown.progress.type === progressType && countdown.progress.current > 0) {
if (progressTypes.indexOf(countdown.progress.type) !== -1 && countdown.progress.current > 0) {
acc.push(key);
}
@ -260,7 +266,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
}, []);
const countdownData = countdownSetting.toObject();
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, {
const settings = {
...countdownData,
countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => {
const countdown = foundry.utils.deepClone(countdownData.countdowns[key]);
@ -271,14 +277,12 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
acc[key] = countdown;
return acc;
}, {})
};
await emitAsGM(GMUpdateEvent.UpdateCountdowns,
DhCountdowns.gmSetSetting.bind(settings),
settings, null, {
refreshType: RefreshType.Countdown
});
const data = { refreshType: RefreshType.Countdown };
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data
});
Hooks.callAll(socketEvent.Refresh, data);
}
async _onRender(context, options) {

View file

@ -87,7 +87,7 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica
async removeEffect(event) {
const element = event.target.closest('.effect-container');
const effects = DhEffectsDisplay.getTokenEffects();
const effect = effects.find(x => x.id === element.id);
const effect = effects.find(x => x.id === element.dataset.effectId);
await effect.delete();
this.render();
}

View file

@ -34,6 +34,69 @@ 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.
*/
distanceTo(target) {
if (!canvas.ready) return NaN;
if (this === target) return 0;
const originPoint = this.center;
const destinationPoint = target.center;
// Compute for gridless. This version returns circular edge to edge + grid distance,
// 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 distance = canvas.grid.measurePath([originPoint, destinationPoint]).distance;
return distance - originRadius - targetRadius + canvas.grid.distance;
}
// Compute what the closest grid space of each token is, then compute that distance
const originEdge = this.#getEdgeBoundary(this.bounds, originPoint, destinationPoint);
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)
});
const adjustDestinationPoint = canvas.grid.getTopLeftPoint({
x: targetEdge.x + Math.sign(destinationPoint.x - targetEdge.x),
y: targetEdge.y + Math.sign(destinationPoint.y - targetEdge.y)
});
return canvas.grid.measurePath([adjustedOriginPoint, adjustDestinationPoint]).distance;
}
/** Returns the point at which a line starting at origin and ending at destination intersects the edge of the bounds */
#getEdgeBoundary(bounds, originPoint, destinationPoint) {
const points = [
{ x: bounds.x, y: bounds.y },
{ x: bounds.x + bounds.width, y: bounds.y },
{ x: bounds.x + bounds.width, y: bounds.y + bounds.height },
{ x: bounds.x, y: bounds.y + bounds.height }
];
const pairsToTest = [
[points[0], points[1]],
[points[1], points[2]],
[points[2], points[3]],
[points[3], points[0]]
];
for (const pair of pairsToTest) {
const result = foundry.utils.lineSegmentIntersection(originPoint, destinationPoint, pair[0], pair[1]);
if (result) return result;
}
return null;
}
/** 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);
}
/** @inheritDoc */
_drawBar(number, bar, data) {
const val = Number(data.value);

View file

@ -2,7 +2,7 @@ export const actionTypes = {
attack: {
id: 'attack',
name: 'DAGGERHEART.ACTIONS.TYPES.attack.name',
icon: 'fa-khanda',
icon: 'fa-hand-fist',
tooltip: 'DAGGERHEART.ACTIONS.TYPES.attack.tooltip'
},
countdown: {

View file

@ -211,6 +211,44 @@ export const adversaryTraits = {
}
};
export const tokenSize = {
custom: {
id: 'custom',
value: 0,
label: 'DAGGERHEART.GENERAL.custom'
},
tiny: {
id: 'tiny',
value: 1,
label: 'DAGGERHEART.CONFIG.TokenSize.tiny'
},
small: {
id: 'small',
value: 2,
label: 'DAGGERHEART.CONFIG.TokenSize.small'
},
medium: {
id: 'medium',
value: 3,
label: 'DAGGERHEART.CONFIG.TokenSize.medium'
},
large: {
id: 'large',
value: 4,
label: 'DAGGERHEART.CONFIG.TokenSize.large'
},
huge: {
id: 'huge',
value: 5,
label: 'DAGGERHEART.CONFIG.TokenSize.huge'
},
gargantuan: {
id: 'gargantuan',
value: 6,
label: 'DAGGERHEART.CONFIG.TokenSize.gargantuan'
}
};
export const levelChoices = {
attributes: {
name: 'attributes',

View file

@ -9,7 +9,7 @@ export const AdversaryBPPerEncounter = (adversaries, characters) => {
);
if (existingEntry) {
existingEntry.nr += 1;
} else {
} else if (adversary.type) {
acc.push({ adversary, nr: 1 });
}
return acc;
@ -21,7 +21,7 @@ export const AdversaryBPPerEncounter = (adversaries, characters) => {
if (type.partyAmountPerBP) {
acc += characters.length === 0 ? 0 : Math.ceil(entry.nr / characters.length);
} else {
acc += bpCost;
acc += bpCost * entry.nr;
}
return acc;
@ -84,6 +84,7 @@ export const BPModifiers = {
increaseDamage: {
sort: 2,
description: 'DAGGERHEART.CONFIG.BPModifiers.increaseDamage.description',
effectTargetTypes: ['adversary'],
effects: [
{
name: 'DAGGERHEART.CONFIG.BPModifiers.increaseDamage.effect.name',

View file

@ -232,7 +232,7 @@ export const defaultRestOptions = {
actionType: 'action',
chatDisplay: false,
target: {
type: 'self'
type: 'friendly'
},
damage: {
parts: [
@ -298,7 +298,7 @@ export const defaultRestOptions = {
actionType: 'action',
chatDisplay: false,
target: {
type: 'self'
type: 'friendly'
},
damage: {
parts: [
@ -341,7 +341,7 @@ export const defaultRestOptions = {
actionType: 'action',
chatDisplay: false,
target: {
type: 'self'
type: 'friendly'
},
damage: {
parts: [
@ -407,7 +407,7 @@ export const defaultRestOptions = {
actionType: 'action',
chatDisplay: false,
target: {
type: 'self'
type: 'friendly'
},
damage: {
parts: [

View file

@ -5,7 +5,6 @@ export const armorFeatures = {
actions: [
{
type: 'damage',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.burning.actions.burn.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.burning.actions.burn.description',
@ -174,7 +173,6 @@ export const armorFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.hopeful.actions.hope.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.hopeful.actions.hope.description',
@ -188,7 +186,6 @@ export const armorFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.impenetrable.actions.impenetrable.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.impenetrable.actions.impenetrable.description',
@ -231,7 +228,6 @@ export const armorFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.painful.actions.pain.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.painful.actions.pain.description',
@ -269,7 +265,6 @@ export const armorFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.quiet.actions.quiet.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.quiet.actions.quiet.description',
@ -306,7 +301,6 @@ export const armorFeatures = {
actions: [
{
type: 'attack',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.resilient.actions.resilient.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.resilient.actions.resilient.description',
@ -353,7 +347,6 @@ export const armorFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.shifting.actions.shift.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.shifting.actions.shift.description',
@ -373,7 +366,6 @@ export const armorFeatures = {
actions: [
{
type: 'attack',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.timeslowing.actions.slowTime.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.timeslowing.actions.slowTime.description',
@ -401,7 +393,6 @@ export const armorFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.truthseeking.actions.truthseeking.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.truthseeking.actions.truthseeking.description',
@ -444,7 +435,8 @@ export const armorFeatures = {
{
key: 'system.resistance.magical.reduction',
mode: 2,
value: '@system.armorScore'
value: '@system.armorScore',
priority: 21
}
]
}
@ -537,7 +529,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.bouncing.actions.bounce.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.bouncing.actions.bounce.description',
@ -582,7 +573,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.brutal.actions.addDamage.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.brutal.actions.addDamage.description',
@ -596,7 +586,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.burning.actions.burn.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.burning.actions.burn.description',
@ -610,7 +599,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.charged.actions.markStress.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.charged.actions.markStress.description',
@ -647,7 +635,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.concussive.actions.attack.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.concussive.actions.attack.description',
@ -688,7 +675,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.deadly.actions.extraDamage.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.deadly.actions.extraDamage.description',
@ -702,7 +688,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.deflecting.actions.deflect.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.deflecting.actions.deflect.description',
@ -725,7 +710,8 @@ export const weaponFeatures = {
{
key: 'system.evasion',
mode: 2,
value: '@system.armorScore'
value: '@system.armorScore',
priority: 21
}
]
}
@ -739,7 +725,6 @@ export const weaponFeatures = {
actions: [
{
type: 'damage',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.destructive.actions.attack.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.destructive.actions.attack.descriptive',
@ -784,7 +769,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.devastating.actions.devastate.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.devastating.actions.devastate.description',
@ -835,7 +819,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.doubledUp.actions.doubleUp.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.doubledUp.actions.doubleUp.description',
@ -849,7 +832,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.dueling.actions.duel.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.dueling.actions.duel.description',
@ -863,7 +845,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect', // Should prompt a dc 14 reaction save on adversaries
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.eruptive.actions.erupt.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.eruptive.actions.erupt.description',
@ -877,7 +858,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.grappling.actions.grapple.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.grappling.actions.grapple.description',
@ -897,7 +877,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.greedy.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.greedy.description',
@ -929,7 +908,6 @@ export const weaponFeatures = {
actions: [
{
type: 'healing',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.healing.actions.heal.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.healing.actions.heal.description',
@ -977,7 +955,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.hooked.actions.hook.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.hooked.actions.hook.description',
@ -991,7 +968,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.hot.actions.hot.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.hot.actions.hot.description',
@ -1005,7 +981,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.invigorating.actions.invigorate.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.invigorating.actions.invigorate.description',
@ -1019,7 +994,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.lifestealing.actions.lifesteal.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.lifestealing.actions.lifesteal.description',
@ -1033,7 +1007,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.lockedOn.actions.lockOn.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.lockedOn.actions.lockOn.description',
@ -1047,7 +1020,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.long.actions.long.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.long.actions.long.description',
@ -1061,7 +1033,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.lucky.actions.luck.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.lucky.actions.luck.description',
@ -1099,7 +1070,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.painful.actions.pain.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.painful.actions.pain.description',
@ -1145,7 +1115,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.parry.actions.parry.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.parry.actions.parry.description',
@ -1159,7 +1128,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.persuasive.actions.persuade.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.persuasive.actions.persuade.description',
@ -1196,7 +1164,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.pompous.actions.pompous.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.pompous.actions.pompous.description',
@ -1240,7 +1207,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.quick.actions.quick.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.quick.actions.quick.description',
@ -1278,7 +1244,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.reloading.actions.reload.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.reloading.actions.reload.description',
@ -1292,7 +1257,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.retractable.actions.retract.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.retractable.actions.retract.description',
@ -1306,7 +1270,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.returning.actions.return.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.returning.actions.return.description',
@ -1320,7 +1283,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.scary.actions.scare.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.scary.actions.scare.description',
@ -1364,7 +1326,8 @@ export const weaponFeatures = {
{
key: 'system.bonuses.damage.primaryWeapon.bonus',
mode: 2,
value: '@system.traits.agility.value'
value: '@system.traits.agility.value',
priority: 21
}
]
}
@ -1376,7 +1339,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.sheltering.actions.shelter.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.sheltering.actions.shelter.description',
@ -1390,7 +1352,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.startling.actions.startle.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.startling.actions.startle.description',
@ -1410,7 +1371,6 @@ export const weaponFeatures = {
actions: [
{
type: 'effect',
actionType: 'action',
chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.timebending.actions.bendTime.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.timebending.actions.bendTime.description',
@ -1458,6 +1418,12 @@ export const orderedWeaponFeatures = () => {
return Object.values(all).sort((a, b) => game.i18n.localize(a.label).localeCompare(game.i18n.localize(b.label)));
};
export const featureForm = {
passive: 'DAGGERHEART.CONFIG.FeatureForm.passive',
action: 'DAGGERHEART.CONFIG.FeatureForm.action',
reaction: 'DAGGERHEART.CONFIG.FeatureForm.reaction'
};
export const featureTypes = {
ancestry: {
id: 'ancestry',
@ -1515,21 +1481,6 @@ export const featureSubTypes = {
mastery: 'mastery'
};
export const actionTypes = {
passive: {
id: 'passive',
label: 'DAGGERHEART.CONFIG.ActionType.passive'
},
action: {
id: 'action',
label: 'DAGGERHEART.CONFIG.ActionType.action'
},
reaction: {
id: 'reaction',
label: 'DAGGERHEART.CONFIG.ActionType.reaction'
}
};
export const itemResourceTypes = {
simple: {
id: 'simple',

View file

@ -28,7 +28,8 @@ export const gameSettings = {
LevelTiers: 'LevelTiers',
Countdowns: 'Countdowns',
LastMigrationVersion: 'LastMigrationVersion',
TagTeamRoll: 'TagTeamRoll'
TagTeamRoll: 'TagTeamRoll',
SpotlightRequestQueue: 'SpotlightRequestQueue',
};
export const actionAutomationChoices = {

View file

@ -33,7 +33,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
initial: 'action',
nullable: false,
required: true
})
}),
targetUuid: new fields.StringField({ initial: undefined })
};
this.extraSchemas.forEach(s => {
@ -95,6 +96,9 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
prepareData() {
this.name = this.name || game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[this.type].name);
this.img = this.img ?? this.parent?.parent?.img;
/* Fallback to feature description */
this.description = this.description || this.parent?.description;
}
/**
@ -159,12 +163,9 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
* @returns {object}
*/
getRollData(data = {}) {
if (!this.actor) return null;
const actorData = this.actor.getRollData(false);
const actorData = this.actor ? this.actor.getRollData(false) : {};
// Add Roll results to RollDatas
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)
: 1;
@ -193,8 +194,6 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
async use(event) {
if (!this.actor) throw new Error("An Action can't be used outside of an Actor context.");
if (this.chatDisplay) await this.toChat();
let config = this.prepareConfig(event);
if (!config) return;
@ -208,9 +207,12 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
// Execute the Action Worflow in order based of schema fields
await this.executeWorkflow(config);
await config.resourceUpdates.updateResources();
if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return;
if (this.chatDisplay) await this.toChat();
return config;
}
@ -239,8 +241,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
isDirect: !!this.damage?.direct,
selectedRollMode: game.settings.get('core', 'rollMode'),
data: this.getRollData(),
evaluate: this.hasRoll
evaluate: this.hasRoll,
resourceUpdates: new ResourceUpdateMap(this.actor),
targetUuid: this.targetUuid
};
DHBaseAction.applyKeybindings(config);
return config;
}
@ -322,11 +327,46 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
* @returns {string[]} An array of localized tag strings.
*/
_getTags() {
const tags = [
game.i18n.localize(`DAGGERHEART.ACTIONS.TYPES.${this.type}.name`),
game.i18n.localize(`DAGGERHEART.CONFIG.ActionType.${this.actionType}`)
];
const tags = [game.i18n.localize(`DAGGERHEART.ACTIONS.TYPES.${this.type}.name`)];
return tags;
}
}
export class ResourceUpdateMap extends Map {
#actor;
constructor(actor) {
super();
this.#actor = actor;
}
addResources(resources) {
for (const resource of resources) {
if (!resource.key) continue;
const existing = this.get(resource.key);
if (existing) {
this.set(resource.key, {
...existing,
value: existing.value + (resource.value ?? 0),
total: existing.total + (resource.total ?? 0)
});
} else {
this.set(resource.key, resource);
}
}
}
#getResources() {
return Array.from(this.values());
}
async updateResources() {
if (this.#actor) {
const target = this.#actor.system.partner ?? this.#actor;
await target.modifyResource(this.#getResources());
}
}
}

View file

@ -1,3 +1,17 @@
/** -- Changes Type Priorities --
* - Base Number -
* Custom: 0
* Multiply: 10
* Add: 20
* Downgrade: 30
* Upgrade: 40
* Override: 50
*
* - Changes Value Priorities -
* Standard: +0
* "Anything that uses another data model value as its value": +1 - Effects that increase traits have to be calculated first at Base priority. (EX: Raise evasion by half your agility)
*/
export default class BaseEffect extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;

View file

@ -65,20 +65,38 @@ export default class BeastformEffect extends BaseEffect {
}
};
const updateToken = token => ({
...baseUpdate,
'texture': {
enabled: this.characterTokenData.usesDynamicToken,
src: token.flags.daggerheart?.beastformTokenImg ?? this.characterTokenData.tokenImg
},
'ring': {
subject: {
texture:
token.flags.daggerheart?.beastformSubjectTexture ?? this.characterTokenData.tokenRingImg
}
},
'flags.daggerheart': { '-=beastformTokenImg': null, '-=beastformSubjectTexture': null }
});
const updateToken = token => {
let x = null,
y = null;
if (token.object?.scene?.grid) {
const positionData = game.system.api.documents.DhToken.getSnappedPositionInSquareGrid(
token.object.scene.grid,
{ x: token.x, y: token.y, elevation: token.elevation },
baseUpdate.width,
baseUpdate.height
);
x = positionData.x;
y = positionData.y;
}
return {
...baseUpdate,
x,
y,
'texture': {
enabled: this.characterTokenData.usesDynamicToken,
src: token.flags.daggerheart?.beastformTokenImg ?? this.characterTokenData.tokenImg
},
'ring': {
subject: {
texture:
token.flags.daggerheart?.beastformSubjectTexture ?? this.characterTokenData.tokenRingImg
}
},
'flags.daggerheart': { '-=beastformTokenImg': null, '-=beastformSubjectTexture': null }
};
};
await updateActorTokens(this.parent.parent, update, updateToken);

View file

@ -1,6 +1,6 @@
import DHAdversarySettings from '../../applications/sheets-configs/adversary-settings.mjs';
import { ActionField } from '../fields/actionField.mjs';
import BaseDataActor from './base.mjs';
import BaseDataActor, { commonActorRules } from './base.mjs';
import { resourceField, bonusField } from '../fields/actorField.mjs';
export default class DhpAdversary extends BaseDataActor {
@ -11,7 +11,8 @@ export default class DhpAdversary extends BaseDataActor {
label: 'TYPES.Actor.adversary',
type: 'adversary',
settingSheet: DHAdversarySettings,
hasAttribution: true
hasAttribution: true,
usesSize: true
});
}
@ -55,27 +56,11 @@ export default class DhpAdversary extends BaseDataActor {
})
}),
resources: new fields.SchemaField({
hitPoints: resourceField(
0,
0,
'DAGGERHEART.GENERAL.HitPoints.plural',
true,
game.i18n.localize('DAGGERHEART.GENERAL.max')
),
stress: resourceField(
0,
0,
'DAGGERHEART.GENERAL.stress',
true,
game.i18n.localize('DAGGERHEART.GENERAL.max')
)
hitPoints: resourceField(0, 0, 'DAGGERHEART.GENERAL.HitPoints.plural', true),
stress: resourceField(0, 0, 'DAGGERHEART.GENERAL.stress', true)
}),
rules: new fields.SchemaField({
conditionImmunities: new fields.SchemaField({
hidden: new fields.BooleanField({ initial: false }),
restrained: new fields.BooleanField({ initial: false }),
vulnerable: new fields.BooleanField({ initial: false })
})
...commonActorRules()
}),
attack: new ActionField({
initial: {
@ -142,7 +127,7 @@ export default class DhpAdversary extends BaseDataActor {
}
isItemValid(source) {
return source.type === "feature";
return source.type === 'feature';
}
async _preUpdate(changes, options, user) {

View file

@ -1,21 +1,24 @@
import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs';
import DHItem from '../../documents/item.mjs';
import { getScrollTextData } from '../../helpers/utils.mjs';
const fields = foundry.data.fields;
const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) =>
new foundry.data.fields.SchemaField({
resistance: new foundry.data.fields.BooleanField({
new fields.SchemaField({
resistance: new fields.BooleanField({
initial: false,
label: `${resistanceLabel}.label`,
hint: `${resistanceLabel}.hint`,
isAttributeChoice: true
}),
immunity: new foundry.data.fields.BooleanField({
immunity: new fields.BooleanField({
initial: false,
label: `${immunityLabel}.label`,
hint: `${immunityLabel}.hint`,
isAttributeChoice: true
}),
reduction: new foundry.data.fields.NumberField({
reduction: new fields.NumberField({
integer: true,
initial: 0,
label: `${reductionLabel}.label`,
@ -23,6 +26,25 @@ const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) =>
})
});
/* Common rules applying to Characters and Adversaries */
export const commonActorRules = (extendedData = { damageReduction: {} }) => ({
conditionImmunities: new fields.SchemaField({
hidden: new fields.BooleanField({ initial: false }),
restrained: new fields.BooleanField({ initial: false }),
vulnerable: new fields.BooleanField({ initial: false })
}),
damageReduction: new fields.SchemaField({
thresholdImmunities: new fields.SchemaField({
minor: new fields.BooleanField({ initial: false })
}),
reduceSeverity: new fields.SchemaField({
magical: new fields.NumberField({ initial: 0, min: 0 }),
physical: new fields.NumberField({ initial: 0, min: 0 })
}),
...extendedData.damageReduction
})
});
/**
* Describes metadata about the actor data model type
* @typedef {Object} ActorDataModelMetadata
@ -41,7 +63,8 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
settingSheet: null,
hasResistances: true,
hasAttribution: false,
hasLimitedView: true
hasLimitedView: true,
usesSize: false
};
}
@ -52,7 +75,6 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
/** @inheritDoc */
static defineSchema() {
const fields = foundry.data.fields;
const schema = {};
if (this.metadata.hasAttribution) {
@ -76,6 +98,13 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
'DAGGERHEART.GENERAL.DamageResistance.magicalReduction'
)
});
if (this.metadata.usesSize)
schema.size = new fields.StringField({
required: true,
nullable: false,
choices: CONFIG.DH.ACTOR.tokenSize,
initial: CONFIG.DH.ACTOR.tokenSize.custom.id
});
return schema;
}
@ -106,6 +135,17 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
return data;
}
/**
* Checks if an item is available for use, such as multiclass features being disabled
* on a character.
*
* @param {DHItem} item The item being checked for availability
* @return {boolean} whether the item is available
*/
isItemAvailable(item) {
return true;
}
async _preDelete() {
/* Clear all partyMembers from tagTeam setting.*/
/* Revisit this when tagTeam is improved for many parties */

View file

@ -1,7 +1,7 @@
import { burden } from '../../config/generalConfig.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import DhLevelData from '../levelData.mjs';
import BaseDataActor from './base.mjs';
import BaseDataActor, { commonActorRules } from './base.mjs';
import { attributeField, resourceField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs';
import { ActionField } from '../fields/actionField.mjs';
import DHCharacterSettings from '../../applications/sheets-configs/character-settings.mjs';
@ -217,44 +217,41 @@ export default class DhCharacter extends BaseDataActor {
}),
companion: new ForeignDocumentUUIDField({ type: 'Actor', nullable: true, initial: null }),
rules: new fields.SchemaField({
damageReduction: new fields.SchemaField({
maxArmorMarked: new fields.SchemaField({
value: new fields.NumberField({
required: true,
...commonActorRules({
damageReduction: {
magical: new fields.BooleanField({ initial: false }),
physical: new fields.BooleanField({ initial: false }),
maxArmorMarked: new fields.SchemaField({
value: new fields.NumberField({
required: true,
integer: true,
initial: 1,
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.maxArmorMarkedBonus'
}),
stressExtra: new fields.NumberField({
required: true,
integer: true,
initial: 0,
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.maxArmorMarkedStress.label',
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.maxArmorMarkedStress.hint'
})
}),
stressDamageReduction: new fields.SchemaField({
severe: stressDamageReductionRule(
'DAGGERHEART.GENERAL.Rules.damageReduction.stress.severe'
),
major: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.major'),
minor: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.minor'),
any: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.any')
}),
increasePerArmorMark: new fields.NumberField({
integer: true,
initial: 1,
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.maxArmorMarkedBonus'
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.label',
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint'
}),
stressExtra: new fields.NumberField({
required: true,
integer: true,
initial: 0,
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.maxArmorMarkedStress.label',
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.maxArmorMarkedStress.hint'
})
}),
stressDamageReduction: new fields.SchemaField({
severe: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.severe'),
major: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.major'),
minor: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.minor'),
any: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.any')
}),
increasePerArmorMark: new fields.NumberField({
integer: true,
initial: 1,
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.label',
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint'
}),
magical: new fields.BooleanField({ initial: false }),
physical: new fields.BooleanField({ initial: false }),
thresholdImmunities: new fields.SchemaField({
minor: new fields.BooleanField({ initial: false })
}),
reduceSeverity: new fields.SchemaField({
magical: new fields.NumberField({ initial: 0, min: 0 }),
physical: new fields.NumberField({ initial: 0, min: 0 })
}),
disabledArmor: new fields.BooleanField({ intial: false })
disabledArmor: new fields.BooleanField({ intial: false })
}
}),
attack: new fields.SchemaField({
damage: new fields.SchemaField({
@ -283,11 +280,6 @@ export default class DhCharacter extends BaseDataActor {
})
})
}),
conditionImmunities: new fields.SchemaField({
hidden: new fields.BooleanField({ initial: false }),
restrained: new fields.BooleanField({ initial: false }),
vulnerable: new fields.BooleanField({ initial: false })
}),
runeWard: new fields.BooleanField({ initial: false }),
burden: new fields.SchemaField({
ignore: new fields.BooleanField()
@ -435,6 +427,33 @@ export default class DhCharacter extends BaseDataActor {
return attack;
}
/** @inheritDoc */
isItemAvailable(item) {
if (!super.isItemAvailable(this)) return false;
/**
* Preventing subclass features from being available if the chacaracter does not
* have the right subclass advancement
*/
if (item.system.originItemType !== CONFIG.DH.ITEM.featureTypes.subclass.id) {
return true;
}
if (!this.class.subclass) return false;
const prop = item.system.multiclassOrigin ? 'multiclass' : 'class';
const subclassState = this[prop].subclass?.system?.featureState;
if (!subclassState) return false;
if (
item.system.identifier === CONFIG.DH.ITEM.featureSubTypes.foundation ||
(item.system.identifier === CONFIG.DH.ITEM.featureSubTypes.specialization && subclassState >= 2) ||
(item.system.identifier === CONFIG.DH.ITEM.featureSubTypes.mastery && subclassState >= 3)
) {
return true;
} else {
return false;
}
}
get sheetLists() {
const ancestryFeatures = [],
communityFeatures = [],
@ -443,7 +462,7 @@ export default class DhCharacter extends BaseDataActor {
companionFeatures = [],
features = [];
for (let item of this.parent.items) {
for (let item of this.parent.items.filter(x => this.isItemAvailable(x))) {
if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.ancestry.id) {
ancestryFeatures.push(item);
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.community.id) {
@ -451,20 +470,7 @@ export default class DhCharacter extends BaseDataActor {
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.class.id) {
classFeatures.push(item);
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.subclass.id) {
if (this.class.subclass) {
const prop = item.system.multiclassOrigin ? 'multiclass' : 'class';
const subclassState = this[prop].subclass?.system?.featureState;
if (!subclassState) continue;
if (
item.system.identifier === CONFIG.DH.ITEM.featureSubTypes.foundation ||
(item.system.identifier === CONFIG.DH.ITEM.featureSubTypes.specialization &&
subclassState >= 2) ||
(item.system.identifier === CONFIG.DH.ITEM.featureSubTypes.mastery && subclassState >= 3)
) {
subclassFeatures.push(item);
}
}
subclassFeatures.push(item);
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.companion.id) {
companionFeatures.push(item);
} else if (item.type === 'feature' && !item.system.type) {

View file

@ -26,7 +26,7 @@ export default class DhParty extends BaseDataActor {
/* -------------------------------------------- */
isItemValid(source) {
return ["weapon", "armor", "consumable", "loot"].includes(source.type);
return ['weapon', 'armor', 'consumable', 'loot'].includes(source.type);
}
prepareBaseData() {

View file

@ -15,8 +15,9 @@ export default class DhCombat extends foundry.abstract.TypeDataModel {
get extendedBattleToggles() {
const modifiers = CONFIG.DH.ENCOUNTER.BPModifiers;
const adversaries =
this.parent.turns?.filter(x => x.isNPC)?.map(x => ({ ...x.actor, type: x.actor.system.type })) ?? [];
const characters = this.parent.turns?.filter(x => !x.isNPC) ?? [];
this.parent.turns?.filter(x => x.actor && x.isNPC)?.map(x => ({ ...x.actor, type: x.actor.system.type })) ??
[];
const characters = this.parent.turns?.filter(x => x.actor && !x.isNPC) ?? [];
const activeAutomatic = Object.keys(modifiers).reduce((acc, categoryKey) => {
const category = modifiers[categoryKey];

View file

@ -76,7 +76,7 @@ export default class BeastformField extends fields.SchemaField {
* @returns
*/
static async transform(selectedForm, evolvedData, hybridData) {
const formData = evolvedData?.form ? evolvedData.form.toObject() : selectedForm;
const formData = evolvedData?.form ?? selectedForm;
const beastformEffect = formData.effects.find(x => x.type === 'beastform');
if (!beastformEffect) {
ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect');
@ -92,6 +92,18 @@ export default class BeastformField extends fields.SchemaField {
beastformEffect.changes = [...beastformEffect.changes, ...evolvedForm.changes];
formData.system.features = [...formData.system.features, ...selectedForm.system.features.map(x => x.uuid)];
const baseSize = evolvedData.form.system.tokenSize.size;
const evolvedSize =
baseSize === 'custom'
? 'custom'
: (Object.keys(CONFIG.DH.ACTOR.tokenSize).find(
x => CONFIG.DH.ACTOR.tokenSize[x].value === CONFIG.DH.ACTOR.tokenSize[baseSize].value + 1
) ?? baseSize);
formData.system.tokenSize = {
...evolvedData.form.system.tokenSize,
size: evolvedSize
};
}
if (selectedForm.system.beastformType === CONFIG.DH.ITEM.beastformTypes.hybrid.id) {

View file

@ -75,7 +75,7 @@ export default class CostField extends fields.ArrayField {
}
}, []);
await actor.modifyResource(resources);
config.resourceUpdates.addResources(resources);
}
/**

View file

@ -124,29 +124,21 @@ export default class SaveField extends fields.SchemaField {
*/
static async updateSaveMessage(result, message, targetId) {
if (!result) return;
const updateMsg = async function (message, targetId, result) {
// setTimeout(async () => {
const chatMessage = ui.chat.collection.get(message._id),
changes = {
flags: {
[game.system.id]: {
reactionRolls: {
[targetId]: {
result: result.roll.total,
success: result.roll.success
}
const chatMessage = ui.chat.collection.get(message._id),
changes = {
flags: {
[game.system.id]: {
reactionRolls: {
[targetId]: {
result: result.roll.total,
success: result.roll.success
}
}
}
};
await chatMessage.update(changes);
// }, 100);
};
if (game.modules.get('dice-so-nice')?.active)
game.dice3d
.waitFor3DAnimationByMessageID(result.message.id ?? result.message._id)
.then(async () => await updateMsg(message, targetId, result));
else await updateMsg(message, targetId, result);
}
};
await chatMessage.update(changes);
}
/**

View file

@ -25,9 +25,12 @@ export default class TargetField extends fields.SchemaField {
config.hasTarget = true;
let targets;
// If the Action is configured as self-targeted, set targets as the owner. Probably better way than to fallback to getDependentTokens
if (this.target?.type === CONFIG.DH.GENERAL.targetTypes.self.id)
if (this.target?.type === CONFIG.DH.GENERAL.targetTypes.self.id) {
targets = [this.actor.token ?? this.actor.prototypeToken];
else {
} else if (config.targetUuid) {
const actor = fromUuidSync(config.targetUuid);
targets = [actor.token ?? actor.prototypeToken];
} else {
targets = Array.from(game.user.targets);
if (this.target.type !== CONFIG.DH.GENERAL.targetTypes.any.id) {
targets = targets.filter(target => TargetField.isTargetFriendly(this.actor, target, this.target.type));
@ -79,7 +82,7 @@ export default class TargetField extends fields.SchemaField {
return {
id: token.id,
actorId: token.actor.uuid,
name: token.actor.name,
name: token.name,
img: token.actor.img,
difficulty: token.actor.system.difficulty,
evasion: token.actor.system.evasion,

View file

@ -141,6 +141,12 @@ export function ActionMixin(Base) {
return this.documentName;
}
//Getter for icons
get typeIcon() {
const config = CONFIG.DH.ACTIONS.actionTypes[this.type];
return config?.icon || 'fa-question'; // Fallback icon just in case
}
get relativeUUID() {
return `.Item.${this.item.id}.Action.${this.id}`;
}
@ -256,7 +262,7 @@ export function ActionMixin(Base) {
async toChat(origin) {
const cls = getDocumentClass('ChatMessage');
const systemData = {
title: game.i18n.localize('DAGGERHEART.CONFIG.ActionType.action'),
title: game.i18n.localize('DAGGERHEART.CONFIG.FeatureForm.action'),
origin: origin,
action: {
name: this.name,
@ -266,12 +272,17 @@ export function ActionMixin(Base) {
itemOrigin: this.item,
description: this.description || (this.item instanceof Item ? this.item.system.description : '')
};
const speaker = cls.getSpeaker();
const msg = {
type: 'abilityUse',
user: game.user.id,
actor: { name: this.actor.name, img: this.actor.img },
author: this.author,
speaker: cls.getSpeaker(),
speaker: {
speaker,
actor: speaker.actor ?? this.actor
},
title: game.i18n.localize('DAGGERHEART.UI.Chat.action.title'),
system: systemData,
content: await foundry.applications.handlebars.renderTemplate(

View file

@ -43,6 +43,12 @@ export default class DHBeastform extends BaseDataItem {
base64: false
}),
tokenSize: new fields.SchemaField({
size: new fields.StringField({
required: true,
nullable: false,
choices: CONFIG.DH.ACTOR.tokenSize,
initial: CONFIG.DH.ACTOR.tokenSize.custom.id
}),
height: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }),
width: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true })
}),
@ -190,9 +196,18 @@ export default class DHBeastform extends BaseDataItem {
await this.parent.parent.createEmbeddedDocuments('ActiveEffect', [beastformEffect.toObject()]);
const autoTokenSize =
this.tokenSize.size !== 'custom'
? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes[
this.tokenSize.size
]
: null;
const width = autoTokenSize ?? this.tokenSize.width;
const height = autoTokenSize ?? this.tokenSize.height;
const prototypeTokenUpdate = {
height: this.tokenSize.height,
width: this.tokenSize.width,
height,
width,
texture: {
src: this.tokenImg
},
@ -202,16 +217,33 @@ export default class DHBeastform extends BaseDataItem {
}
}
};
const tokenUpdate = token => {
let x = null,
y = null;
if (token.object?.scene?.grid) {
const positionData = game.system.api.documents.DhToken.getSnappedPositionInSquareGrid(
token.object.scene.grid,
{ x: token.x, y: token.y, elevation: token.elevation },
width ?? token.width,
height ?? token.height
);
const tokenUpdate = token => ({
...prototypeTokenUpdate,
flags: {
daggerheart: {
beastformTokenImg: token.texture.src,
beastformSubjectTexture: token.ring.subject.texture
}
x = positionData.x;
y = positionData.y;
}
});
return {
...prototypeTokenUpdate,
x,
y,
flags: {
daggerheart: {
beastformTokenImg: token.texture.src,
beastformSubjectTexture: token.ring.subject.texture
}
}
};
};
await updateActorTokens(this.parent.parent, prototypeTokenUpdate, tokenUpdate);

View file

@ -30,7 +30,13 @@ export default class DHFeature extends BaseDataItem {
initial: null
}),
multiclassOrigin: new fields.BooleanField({ initial: false }),
identifier: new fields.StringField()
identifier: new fields.StringField(),
featureForm: new fields.StringField({
required: true,
initial: 'passive',
choices: CONFIG.DH.ITEM.featureForm,
label: 'DAGGERHEART.CONFIG.FeatureForm.label'
})
};
}
}

View file

@ -1,14 +1,15 @@
import { defaultRestOptions } from '../../config/generalConfig.mjs';
import { ActionsField } from '../fields/actionField.mjs';
const currencyField = (initial, label) =>
const currencyField = (initial, label, icon) =>
new foundry.data.fields.SchemaField({
enabled: new foundry.data.fields.BooleanField({ required: true, initial: true }),
label: new foundry.data.fields.StringField({
required: true,
initial,
label
})
}),
icon: new foundry.data.fields.StringField({ required: true, nullable: false, blank: true, initial: icon })
});
export default class DhHomebrew extends foundry.abstract.DataModel {
@ -39,16 +40,60 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
traitArray: new fields.ArrayField(new fields.NumberField({ required: true, integer: true }), {
initial: () => [2, 1, 1, 0, 0, -1]
}),
tokenSizes: new fields.SchemaField({
tiny: new fields.NumberField({
integer: false,
initial: 0.5,
label: 'DAGGERHEART.CONFIG.TokenSize.tiny'
}),
small: new fields.NumberField({
integer: false,
initial: 0.8,
label: 'DAGGERHEART.CONFIG.TokenSize.small'
}),
medium: new fields.NumberField({
integer: false,
initial: 1,
label: 'DAGGERHEART.CONFIG.TokenSize.medium'
}),
large: new fields.NumberField({
integer: false,
initial: 2,
label: 'DAGGERHEART.CONFIG.TokenSize.large'
}),
huge: new fields.NumberField({
integer: false,
initial: 3,
label: 'DAGGERHEART.CONFIG.TokenSize.huge'
}),
gargantuan: new fields.NumberField({
integer: false,
initial: 4,
label: 'DAGGERHEART.CONFIG.TokenSize.gargantuan'
})
}),
currency: new fields.SchemaField({
title: new fields.StringField({
required: true,
initial: 'Gold',
label: 'DAGGERHEART.SETTINGS.Homebrew.currency.currencyName'
}),
coins: currencyField('Coins', 'DAGGERHEART.SETTINGS.Homebrew.currency.coinName'),
handfuls: currencyField('Handfuls', 'DAGGERHEART.SETTINGS.Homebrew.currency.handfulName'),
bags: currencyField('Bags', 'DAGGERHEART.SETTINGS.Homebrew.currency.bagName'),
chests: currencyField('Chests', 'DAGGERHEART.SETTINGS.Homebrew.currency.chestName')
coins: currencyField(
'Coins',
'DAGGERHEART.SETTINGS.Homebrew.currency.coinName',
'fa-solid fa-coin-front'
),
handfuls: currencyField(
'Handfuls',
'DAGGERHEART.SETTINGS.Homebrew.currency.handfulName',
'fa-solid fa-coins'
),
bags: currencyField('Bags', 'DAGGERHEART.SETTINGS.Homebrew.currency.bagName', 'fa-solid fa-sack'),
chests: currencyField(
'Chests',
'DAGGERHEART.SETTINGS.Homebrew.currency.chestName',
'fa-solid fa-treasure-chest'
)
}),
restMoves: new fields.SchemaField({
longRest: new fields.SchemaField({
@ -139,22 +184,10 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
/** @inheritDoc */
_initializeSource(source, options = {}) {
source = super._initializeSource(source, options);
source.currency.coins = {
enabled: source.currency.coins.enabled ?? true,
label: source.currency.coins.label || source.currency.coins
};
source.currency.handfuls = {
enabled: source.currency.handfuls.enabled ?? true,
label: source.currency.handfuls.label || source.currency.handfuls
};
source.currency.bags = {
enabled: source.currency.bags.enabled ?? true,
label: source.currency.bags.label || source.currency.bags
};
source.currency.chests = {
enabled: source.currency.chests.enabled ?? true,
label: source.currency.chests.label || source.currency.chests
};
for (const type of ['coins', 'handfuls', 'bags', 'chests']) {
const initial = this.schema.fields.currency.fields[type].getInitialValue();
source.currency[type] = foundry.utils.mergeObject(initial, source.currency[type], { inplace: false });
}
return source;
}
}

View file

@ -39,6 +39,13 @@ export default class DhVariantRules extends foundry.abstract.DataModel {
label: 'DAGGERHEART.CONFIG.Range.close.name'
}),
far: new fields.NumberField({ required: true, initial: 60, label: 'DAGGERHEART.CONFIG.Range.far.name' })
}),
massiveDamage: new fields.SchemaField({
enabled: new fields.BooleanField({
required: true,
initial: false,
label: 'DAGGERHEART.SETTINGS.VariantRules.FIELDS.massiveDamage.enabled.label'
})
})
};
}

View file

@ -98,10 +98,10 @@ export default class D20Roll extends DHRoll {
this.options.roll.modifiers = this.applyBaseBonus();
this.options.experiences?.forEach(m => {
if (this.options.data.experiences?.[m])
if (this.options.data.system?.experiences?.[m])
this.options.roll.modifiers.push({
label: this.options.data.experiences[m].name,
value: this.options.data.experiences[m].value
label: this.options.data.system.experiences[m].name,
value: this.options.data.system.experiences[m].value
});
});

View file

@ -236,67 +236,3 @@ export default class DHRoll extends Roll {
return {};
}
}
export const registerRollDiceHooks = () => {
Hooks.on(`${CONFIG.DH.id}.postRollDuality`, async (config, message) => {
const automationSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
if (
automationSettings.countdownAutomation &&
config.actionType !== CONFIG.DH.ITEM.actionTypes.reaction.id &&
!config.tagTeamSelected &&
!config.skips?.updateCountdowns
) {
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.actionRoll.id);
if (config.roll.result.duality === -1) {
await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.fear.id);
}
}
const hopeFearAutomation = automationSettings.hopeFear;
if (
!config.source?.actor ||
(game.user.isGM ? !hopeFearAutomation.gm : !hopeFearAutomation.players) ||
config.actionType === 'reaction' ||
config.tagTeamSelected ||
config.skips?.resources
)
return;
const actor = await fromUuid(config.source.actor);
let updates = [];
if (!actor) return;
if (config.roll.isCritical || config.roll.result.duality === 1)
updates.push({ key: 'hope', value: 1, total: -1, enabled: true });
if (config.roll.isCritical) updates.push({ key: 'stress', value: 1, total: -1, enabled: true });
if (config.roll.result.duality === -1) updates.push({ key: 'fear', value: 1, total: -1, enabled: true });
if (config.rerolledRoll) {
if (config.rerolledRoll.isCritical || config.rerolledRoll.result.duality === 1)
updates.push({ key: 'hope', value: -1, total: 1, enabled: true });
if (config.rerolledRoll.isCritical) updates.push({ key: 'stress', value: -1, total: 1, enabled: true });
if (config.rerolledRoll.result.duality === -1)
updates.push({ key: 'fear', value: -1, total: 1, enabled: true });
}
if (updates.length) {
const target = actor.system.partner ?? actor;
if (!['dead', 'defeated', 'unconscious'].some(x => actor.statuses.has(x))) {
if (config.rerolledRoll) target.modifyResource(updates);
else config.costs = [...(config.costs ?? []), ...updates];
}
}
if (!config.roll.hasOwnProperty('success') && !config.targets?.length) return;
const rollResult = config.roll.success || config.targets.some(t => t.hit),
looseSpotlight = !rollResult || config.roll.result.duality === -1;
if (looseSpotlight && game.combat?.active) {
const currentCombatant = game.combat.combatants.get(game.combat.current?.combatantId);
if (currentCombatant?.actorId == actor.id) ui.combat.setCombatantSpotlight(currentCombatant.id);
}
return;
});
};

View file

@ -2,6 +2,7 @@ import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
import D20Roll from './d20Roll.mjs';
import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
import { getDiceSoNicePresets } from '../config/generalConfig.mjs';
import { ResourceUpdateMap } from '../data/action/baseAction.mjs';
export default class DualityRoll extends D20Roll {
_advantageFaces = 6;
@ -19,7 +20,7 @@ export default class DualityRoll extends D20Roll {
get title() {
return game.i18n.localize(
`DAGGERHEART.GENERAL.${this.options?.actionType === CONFIG.DH.ITEM.actionTypes.reaction.id ? 'reactionRoll' : 'dualityRoll'}`
`DAGGERHEART.GENERAL.${this.options?.actionType === 'reaction' ? 'reactionRoll' : 'dualityRoll'}`
);
}
@ -219,6 +220,88 @@ export default class DualityRoll extends D20Roll {
return data;
}
static async buildPost(roll, config, message) {
await super.buildPost(roll, config, message);
await DualityRoll.dualityUpdate(config);
}
static async addDualityResourceUpdates(config) {
const automationSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
const hopeFearAutomation = automationSettings.hopeFear;
if (
!config.source?.actor ||
(game.user.isGM ? !hopeFearAutomation.gm : !hopeFearAutomation.players) ||
config.actionType === 'reaction' ||
config.tagTeamSelected ||
config.skips?.resources
)
return;
const actor = await fromUuid(config.source.actor);
let updates = [];
if (!actor) return;
if (config.rerolledRoll) {
if (config.roll.result.duality != config.rerolledRoll.result.duality) {
const hope =
(config.roll.isCritical || config.roll.result.duality === 1 ? 1 : 0) -
(config.rerolledRoll.isCritical || config.rerolledRoll.result.duality === 1 ? 1 : 0);
const stress = (config.roll.isCritical ? 1 : 0) - (config.rerolledRoll.isCritical ? 1 : 0);
const fear =
(config.roll.result.duality === -1 ? 1 : 0) - (config.rerolledRoll.result.duality === -1 ? 1 : 0);
if (hope !== 0) updates.push({ key: 'hope', value: hope, total: -1 * hope, enabled: true });
if (stress !== 0) updates.push({ key: 'stress', value: -1 * stress, total: stress, enabled: true });
if (fear !== 0) updates.push({ key: 'fear', value: fear, total: -1 * fear, enabled: true });
}
} else {
if (config.roll.isCritical || config.roll.result.duality === 1)
updates.push({ key: 'hope', value: 1, total: -1, enabled: true });
if (config.roll.isCritical) updates.push({ key: 'stress', value: -1, total: 1, enabled: true });
if (config.roll.result.duality === -1) updates.push({ key: 'fear', value: 1, total: -1, enabled: true });
}
if (updates.length) {
// const target = actor.system.partner ?? actor;
if (!['dead', 'defeated', 'unconscious'].some(x => actor.statuses.has(x))) {
config.resourceUpdates.addResources(updates);
}
}
}
static async dualityUpdate(config) {
const automationSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
if (
automationSettings.countdownAutomation &&
config.actionType !== 'reaction' &&
!config.tagTeamSelected &&
!config.skips?.updateCountdowns
) {
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
if (config.roll.result.duality === -1) {
await updateCountdowns(
CONFIG.DH.GENERAL.countdownProgressionTypes.actionRoll.id,
CONFIG.DH.GENERAL.countdownProgressionTypes.fear.id
);
} else {
await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.actionRoll.id);
}
}
await DualityRoll.addDualityResourceUpdates(config);
if (!config.roll.hasOwnProperty('success') && !config.targets?.length) return;
const rollResult = config.roll.success || config.targets?.some(t => t.hit),
looseSpotlight = !rollResult || config.roll.result.duality === -1;
if (looseSpotlight && game.combat?.active) {
const currentCombatant = game.combat.combatants.get(game.combat.current?.combatantId);
if (currentCombatant?.actorId == config.data.id) ui.combat.setCombatantSpotlight(currentCombatant.id);
}
}
static async reroll(rollString, target, message) {
let parsedRoll = game.system.api.dice.DualityRoll.fromData({ ...rollString, evaluated: false });
const term = parsedRoll.terms[target.dataset.dieIndex];
@ -257,14 +340,20 @@ export default class DualityRoll extends D20Roll {
newRoll.extra = newRoll.extra.slice(2);
const tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
Hooks.call(`${CONFIG.DH.id}.postRollDuality`, {
const actor = message.system.source.actor ? await foundry.utils.fromUuid(message.system.source.actor) : null;
const config = {
source: { actor: message.system.source.actor ?? '' },
targets: message.system.targets,
tagTeamSelected: Object.values(tagTeamSettings.members).some(x => x.messageId === message._id),
roll: newRoll,
rerolledRoll:
newRoll.result.duality !== message.system.roll.result.duality ? message.system.roll : undefined
});
rerolledRoll: message.system.roll,
resourceUpdates: new ResourceUpdateMap(actor)
};
await DualityRoll.addDualityResourceUpdates(config);
await config.resourceUpdates.updateResources();
return { newRoll, parsedRoll };
}
}

View file

@ -4,6 +4,7 @@ export { default as DhpCombat } from './combat.mjs';
export { default as DHCombatant } from './combatant.mjs';
export { default as DhActiveEffect } from './activeEffect.mjs';
export { default as DhChatMessage } from './chatMessage.mjs';
export { default as DhScene } from './scene.mjs';
export { default as DhToken } from './token.mjs';
export { default as DhTooltipManager } from './tooltipManager.mjs';
export { default as DhTemplateManager } from './templateManager.mjs';

View file

@ -194,27 +194,10 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
}
prepareDerivedData() {
/* Preventing subclass features from transferring to actor if they do not have the right subclass advancement */
if (this.parent?.type === 'feature') {
const origSubclassParent = this.parent.system.originItemType === 'subclass';
if (origSubclassParent) {
const subclass = this.parent.parent.items.find(
x =>
x.type === 'subclass' &&
x.system.isMulticlass === (this.parent.system.identifier === 'multiclass')
);
if (subclass) {
const featureState = subclass.system.featureState;
if (
(this.parent.system.identifier === CONFIG.DH.ITEM.featureSubTypes.specialization &&
featureState < 2) ||
(this.parent.system.identifier === CONFIG.DH.ITEM.featureSubTypes.mastery && featureState < 3)
) {
this.transfer = false;
}
}
/* Check for item availability such as in the case of subclass advancement. */
if (this.parent?.parent?.system?.isItemAvailable) {
if (!this.parent.parent.system.isItemAvailable(this.parent)) {
this.transfer = false;
}
}
}

View file

@ -1,8 +1,9 @@
import { emitAsGM, GMUpdateEvent } from '../systemRegistration/socket.mjs';
import { LevelOptionType } from '../data/levelTier.mjs';
import DHFeature from '../data/item/feature.mjs';
import { createScrollText, damageKeyToNumber } from '../helpers/utils.mjs';
import { createScrollText, damageKeyToNumber, getDamageKey } from '../helpers/utils.mjs';
import DhCompanionLevelUp from '../applications/levelup/companionLevelup.mjs';
import { ResourceUpdateMap } from '../data/action/baseAction.mjs';
export default class DhpActor extends Actor {
parties = new Set();
@ -73,16 +74,27 @@ export default class DhpActor extends Actor {
/**@inheritdoc */
async _preCreate(data, options, user) {
if ((await super._preCreate(data, options, user)) === false) return false;
const update = {};
// Set default token size. Done here as we do not want to set a datamodel default, since that would apply the sizing to third party actor modules that aren't set up with the size system.
if (this.system.metadata.usesSize && !data.system?.size) {
Object.assign(update, {
system: {
size: CONFIG.DH.ACTOR.tokenSize.medium.id
}
});
}
// Configure prototype token settings
const prototypeToken = {};
if (['character', 'companion', 'party'].includes(this.type))
Object.assign(prototypeToken, {
sight: { enabled: true },
actorLink: true,
disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY
Object.assign(update, {
prototypeToken: {
sight: { enabled: true },
actorLink: true,
disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY
}
});
this.updateSource({ prototypeToken });
this.updateSource(update);
}
_onUpdate(changes, options, userId) {
@ -477,6 +489,7 @@ export default class DhpActor extends Actor {
async diceRoll(config) {
config.source = { ...(config.source ?? {}), actor: this.uuid };
config.data = this.getRollData();
config.resourceUpdates = new ResourceUpdateMap(this);
const rollClass = config.roll.lite ? CONFIG.Dice.daggerheart['DHRoll'] : this.rollClass;
return await rollClass.build(config);
}
@ -526,7 +539,11 @@ export default class DhpActor extends Actor {
/**@inheritdoc */
getRollData() {
const rollData = super.getRollData();
const rollData = foundry.utils.deepClone(super.getRollData());
/* system gets repeated infinately which causes issues when trying to use the data for document creation */
delete rollData.system;
rollData.id = this.id;
rollData.name = this.name;
rollData.system = this.system.getRollData();
rollData.prof = this.system.proficiency ?? 1;
@ -614,6 +631,19 @@ export default class DhpActor extends Actor {
}
}
}
if (this.type === 'adversary') {
const reducedSeverity = hpDamage.damageTypes.reduce((value, curr) => {
return Math.max(this.system.rules.damageReduction.reduceSeverity[curr], value);
}, 0);
hpDamage.value = Math.max(hpDamage.value - reducedSeverity, 0);
if (
hpDamage.value &&
this.system.rules.damageReduction.thresholdImmunities[getDamageKey(hpDamage.value)]
) {
hpDamage.value -= 1;
}
}
}
updates.forEach(
@ -679,6 +709,10 @@ export default class DhpActor extends Actor {
return updates;
}
/**
* Resources are modified asynchronously, so be careful not to update the same resource in
* quick succession.
*/
async modifyResource(resources) {
if (!resources?.length) return;
@ -761,6 +795,11 @@ export default class DhpActor extends Actor {
}
convertDamageToThreshold(damage) {
const massiveDamageEnabled = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules)
.massiveDamage.enabled;
if (massiveDamageEnabled && damage >= this.system.damageThresholds.severe * 2) {
return 4;
}
return damage >= this.system.damageThresholds.severe ? 3 : damage >= this.system.damageThresholds.major ? 2 : 1;
}
@ -854,7 +893,7 @@ export default class DhpActor extends Actor {
acc.push(effect);
const currentStatusActiveEffects = acc.filter(
x => x.statuses.size === 1 && x.name === game.i18n.localize(statusMap.get(x.statuses.first()).name)
x => x.statuses.size === 1 && x.name === game.i18n.localize(statusMap.get(x.statuses.first())?.name)
);
for (var status of effect.statuses) {

View file

@ -28,6 +28,7 @@ export default class DhpCombat extends Combat {
...effect,
name: game.i18n.localize(effect.name),
description: game.i18n.localize(effect.description),
effectTargetTypes: grouping.effectTargetTypes ?? [],
flags: {
[`${CONFIG.DH.id}.${CONFIG.DH.FLAGS.combatToggle}`]: {
category: toggle.category,
@ -45,11 +46,7 @@ export default class DhpCombat extends Combat {
for (let actor of actors) {
await actor.createEmbeddedDocuments(
'ActiveEffect',
effects.map(effect => ({
...effect,
name: game.i18n.localize(effect.name),
description: game.i18n.localize(effect.description)
}))
effects.filter(x => x.effectTargetTypes.includes(actor.type))
);
}
} else {

View file

@ -0,0 +1,40 @@
import DHToken from './token.mjs';
export default class DhScene extends Scene {
/** A map of `TokenDocument` IDs embedded in this scene long with new dimensions from actor size-category changes */
#sizeSyncBatch = new Map();
/** Synchronize a token's dimensions with its actor's size category. */
syncTokenDimensions(tokenDoc, tokenSize) {
if (!tokenDoc.parent?.tokens.has(tokenDoc.id)) return;
const prototype = tokenDoc.actor?.prototypeToken ?? tokenDoc;
this.#sizeSyncBatch.set(tokenDoc.id, {
size: tokenSize,
prototypeSize: { width: prototype.width, height: prototype.height },
position: { x: tokenDoc.x, y: tokenDoc.y, elevation: tokenDoc.elevation }
});
this.#processSyncBatch();
}
/** Retrieve size and clear size-sync batch, make updates. */
#processSyncBatch = foundry.utils.debounce(() => {
const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes;
const entries = this.#sizeSyncBatch
.entries()
.toArray()
.map(([_id, { size, prototypeSize, position }]) => {
const tokenSize = tokenSizes[size];
const width = size !== CONFIG.DH.ACTOR.tokenSize.custom.id ? tokenSize : prototypeSize.width;
const height = size !== CONFIG.DH.ACTOR.tokenSize.custom.id ? tokenSize : prototypeSize.height;
const updatedPosition = DHToken.getSnappedPositionInSquareGrid(this.grid, position, width, height);
return {
_id,
width,
height,
...updatedPosition
};
});
this.#sizeSyncBatch.clear();
this.updateEmbeddedDocuments('Token', entries, { animation: { movementSpeed: 1.5 } });
}, 0);
}

View file

@ -1,4 +1,4 @@
export default class DHToken extends TokenDocument {
export default class DHToken extends CONFIG.Token.documentClass {
/**
* Inspect the Actor data model and identify the set of attributes which could be used for a Token Bar.
* @param {object} attributes The tracked attributes which can be chosen from
@ -83,7 +83,7 @@ export default class DHToken extends TokenDocument {
if (combat?.system?.battleToggles?.length) {
await combat.toggleModifierEffects(
true,
tokens.map(x => x.actor)
tokens.filter(x => x.actor).map(x => x.actor)
);
}
super.createCombatants(tokens, combat ?? {});
@ -95,9 +95,445 @@ export default class DHToken extends TokenDocument {
if (combat?.system?.battleToggles?.length) {
await combat.toggleModifierEffects(
false,
tokens.map(x => x.actor)
tokens.filter(x => x.actor).map(x => x.actor)
);
}
super.deleteCombatants(tokens, combat ?? {});
}
/**@inheritdoc */
static async _preCreateOperation(documents, operation, user) {
const allowed = await super._preCreateOperation(documents, operation, user);
if (allowed === false) return false;
const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes;
for (const document of documents) {
const actor = document.actor;
if (actor?.system.metadata.usesSize) {
const tokenSize = tokenSizes[actor.system.size];
if (tokenSize && actor.system.size !== CONFIG.DH.ACTOR.tokenSize.custom.id) {
document.updateSource({
width: tokenSize,
height: tokenSize
});
}
}
}
}
/**@inheritdoc */
_onRelatedUpdate(update = {}, operation = {}) {
super._onRelatedUpdate(update, operation);
if (!this.actor?.isOwner) return;
const updates = Array.isArray(update) ? update : [update];
const activeGM = game.users.activeGM; // Let the active GM take care of updates if available
for (let update of updates) {
if (
this.actor.system.metadata.usesSize &&
update.system?.size &&
activeGM &&
game.user.id === activeGM.id
) {
const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes;
const tokenSize = tokenSizes[update.system.size];
if (tokenSize !== this.width || tokenSize !== this.height) {
this.parent?.syncTokenDimensions(this, update.system.size);
}
}
}
}
/**@inheritdoc */
getSnappedPosition(data = {}) {
const grid = this.parent?.grid ?? BaseScene.defaultGrid;
const x = data.x ?? this.x;
const y = data.y ?? this.y;
let elevation = data.elevation ?? this.elevation;
const unsnapped = { x, y, elevation };
// Gridless grid
if (grid.isGridless) return unsnapped;
// Get position and elevation
elevation = Math.round(elevation / grid.distance) * grid.distance;
let width = data.width ?? this.width;
let height = data.height ?? this.height;
if (this.actor?.system.metadata.usesSize) {
const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes;
const tokenSize = tokenSizes[this.actor.system.size];
if (tokenSize && this.actor.system.size !== CONFIG.DH.ACTOR.tokenSize.custom.id) {
width = tokenSize ?? width;
height = tokenSize ?? height;
}
}
// Round width and height to nearest multiple of 0.5 if not small
width = width < 1 ? width : Math.round(width * 2) / 2;
height = height < 1 ? height : Math.round(height * 2) / 2;
const shape = data.shape ?? this.shape;
// Square grid
let snapped;
if (grid.isSquare) snapped = DHToken.getSnappedPositionInSquareGrid(grid, unsnapped, width, height);
// Hexagonal grid
else snapped = DHToken.getSnappedPositionInHexagonalGrid(grid, unsnapped, width, height, shape);
return { x: snapped.x, y: snapped.y, elevation };
}
static getSnappedPositionInSquareGrid(grid, position, width, height) {
const M = CONST.GRID_SNAPPING_MODES;
// Small tokens snap to any vertex of the subgrid with resolution 4
// where the token is fully contained within the grid space
const isTiny = (width === 0.5 && height <= 1) || (width <= 1 && height === 0.5);
if (isTiny) {
let x = position.x / grid.size;
let y = position.y / grid.size;
if (width === 1) x = Math.round(x);
else {
x = Math.floor(x * 8);
const k = ((x % 8) + 8) % 8;
if (k >= 6) x = Math.ceil(x / 8);
else if (k === 5) x = Math.floor(x / 8) + 0.5;
else x = Math.round(x / 2) / 4;
}
if (height === 1) y = Math.round(y);
else {
y = Math.floor(y * 8);
const k = ((y % 8) + 8) % 8;
if (k >= 6) y = Math.ceil(y / 8);
else if (k === 5) y = Math.floor(y / 8) + 0.5;
else y = Math.round(y / 2) / 4;
}
x *= grid.size;
y *= grid.size;
return { x, y };
} else if (width < 1 && height < 1) {
// isSmall
let xGrid = Math.round(position.x / grid.size);
let yGrid = Math.round(position.y / grid.size);
const x = xGrid * grid.size + grid.size / 2 - (width * grid.size) / 2;
const y = yGrid * grid.size + grid.size / 2 - (height * grid.size) / 2;
return { x, y };
}
const modeX = Number.isInteger(width) ? M.VERTEX : M.VERTEX | M.EDGE_MIDPOINT | M.CENTER;
const modeY = Number.isInteger(height) ? M.VERTEX : M.VERTEX | M.EDGE_MIDPOINT | M.CENTER;
if (modeX === modeY) return grid.getSnappedPoint(position, { mode: modeX });
return {
x: grid.getSnappedPoint(position, { mode: modeX }).x,
y: grid.getSnappedPoint(position, { mode: modeY }).y
};
}
//#region CopyPasta for mean private methods that have to be duplicated
static getSnappedPositionInHexagonalGrid(grid, position, width, height, shape) {
// Hexagonal shape
const hexagonalShape = DHToken.#getHexagonalShape(width, height, shape, grid.columns);
if (hexagonalShape) {
const offsetX = hexagonalShape.anchor.x * grid.sizeX;
const offsetY = hexagonalShape.anchor.y * grid.sizeY;
position = grid.getCenterPoint({ x: position.x + offsetX, y: position.y + offsetY });
position.x -= offsetX;
position.y -= offsetY;
return position;
}
// Rectagular shape
const M = CONST.GRID_SNAPPING_MODES;
return grid.getSnappedPoint(position, { mode: M.CENTER | M.VERTEX | M.CORNER | M.SIDE_MIDPOINT });
}
/**
* The cache of hexagonal shapes.
* @type {Map<string, DeepReadonly<TokenHexagonalShapeData>>}
*/
static #hexagonalShapes = new Map();
static #getHexagonalShape(width, height, shape, columns) {
if (!Number.isInteger(width * 2) || !Number.isInteger(height * 2)) return null;
// TODO: can we set a max of 2^13 on width and height so that we may use an integer key?
const key = `${width},${height},${shape}${columns ? 'C' : 'R'}`;
let data = DHToken.#hexagonalShapes.get(key);
if (data) return data;
// Hexagon symmetry
if (columns) {
const rowData = BaseToken.#getHexagonalShape(height, width, shape, false);
if (!rowData) return null;
// Transpose the offsets/points of the shape in row orientation
const offsets = { even: [], odd: [] };
for (const { i, j } of rowData.offsets.even) offsets.even.push({ i: j, j: i });
for (const { i, j } of rowData.offsets.odd) offsets.odd.push({ i: j, j: i });
offsets.even.sort(({ i: i0, j: j0 }, { i: i1, j: j1 }) => j0 - j1 || i0 - i1);
offsets.odd.sort(({ i: i0, j: j0 }, { i: i1, j: j1 }) => j0 - j1 || i0 - i1);
const points = [];
for (let i = rowData.points.length; i > 0; i -= 2) {
points.push(rowData.points[i - 1], rowData.points[i - 2]);
}
data = {
offsets,
points,
center: { x: rowData.center.y, y: rowData.center.x },
anchor: { x: rowData.anchor.y, y: rowData.anchor.x }
};
}
// Small hexagon
else if (width === 0.5 && height === 0.5) {
data = {
offsets: { even: [{ i: 0, j: 0 }], odd: [{ i: 0, j: 0 }] },
points: [0.25, 0.0, 0.5, 0.125, 0.5, 0.375, 0.25, 0.5, 0.0, 0.375, 0.0, 0.125],
center: { x: 0.25, y: 0.25 },
anchor: { x: 0.25, y: 0.25 }
};
}
// Normal hexagon
else if (width === 1 && height === 1) {
data = {
offsets: { even: [{ i: 0, j: 0 }], odd: [{ i: 0, j: 0 }] },
points: [0.5, 0.0, 1.0, 0.25, 1, 0.75, 0.5, 1.0, 0.0, 0.75, 0.0, 0.25],
center: { x: 0.5, y: 0.5 },
anchor: { x: 0.5, y: 0.5 }
};
}
// Hexagonal ellipse or trapezoid
else if (shape <= CONST.TOKEN_SHAPES.TRAPEZOID_2) {
data = DHToken.#createHexagonalEllipseOrTrapezoid(width, height, shape);
}
// Hexagonal rectangle
else if (shape <= CONST.TOKEN_SHAPES.RECTANGLE_2) {
data = DHToken.#createHexagonalRectangle(width, height, shape);
}
// Cache the shape
if (data) {
foundry.utils.deepFreeze(data);
DHToken.#hexagonalShapes.set(key, data);
}
return data;
}
static #createHexagonalEllipseOrTrapezoid(width, height, shape) {
if (!Number.isInteger(width) || !Number.isInteger(height)) return null;
const points = [];
let top;
let bottom;
switch (shape) {
case CONST.TOKEN_SHAPES.ELLIPSE_1:
if (height >= 2 * width) return null;
top = Math.floor(height / 2);
bottom = Math.floor((height - 1) / 2);
break;
case CONST.TOKEN_SHAPES.ELLIPSE_2:
if (height >= 2 * width) return null;
top = Math.floor((height - 1) / 2);
bottom = Math.floor(height / 2);
break;
case CONST.TOKEN_SHAPES.TRAPEZOID_1:
if (height > width) return null;
top = height - 1;
bottom = 0;
break;
case CONST.TOKEN_SHAPES.TRAPEZOID_2:
if (height > width) return null;
top = 0;
bottom = height - 1;
break;
}
const offsets = { even: [], odd: [] };
for (let i = bottom; i > 0; i--) {
for (let j = 0; j < width - i; j++) {
offsets.even.push({ i: bottom - i, j: j + (((bottom & 1) + i + 1) >> 1) });
offsets.odd.push({ i: bottom - i, j: j + (((bottom & 1) + i) >> 1) });
}
}
for (let i = 0; i <= top; i++) {
for (let j = 0; j < width - i; j++) {
offsets.even.push({ i: bottom + i, j: j + (((bottom & 1) + i + 1) >> 1) });
offsets.odd.push({ i: bottom + i, j: j + (((bottom & 1) + i) >> 1) });
}
}
let x = 0.5 * bottom;
let y = 0.25;
for (let k = width - bottom; k--; ) {
points.push(x, y);
x += 0.5;
y -= 0.25;
points.push(x, y);
x += 0.5;
y += 0.25;
}
points.push(x, y);
for (let k = bottom; k--; ) {
y += 0.5;
points.push(x, y);
x += 0.5;
y += 0.25;
points.push(x, y);
}
y += 0.5;
for (let k = top; k--; ) {
points.push(x, y);
x -= 0.5;
y += 0.25;
points.push(x, y);
y += 0.5;
}
for (let k = width - top; k--; ) {
points.push(x, y);
x -= 0.5;
y += 0.25;
points.push(x, y);
x -= 0.5;
y -= 0.25;
}
points.push(x, y);
for (let k = top; k--; ) {
y -= 0.5;
points.push(x, y);
x -= 0.5;
y -= 0.25;
points.push(x, y);
}
y -= 0.5;
for (let k = bottom; k--; ) {
points.push(x, y);
x += 0.5;
y -= 0.25;
points.push(x, y);
y -= 0.5;
}
return {
offsets,
points,
// We use the centroid of the polygon for ellipse and trapzoid shapes
center: foundry.utils.polygonCentroid(points),
anchor: bottom % 2 ? { x: 0.0, y: 0.5 } : { x: 0.5, y: 0.5 }
};
}
/**
* Create the row-based hexagonal rectangle given the type, width, and height.
* @param {number} width The width of the Token (positive)
* @param {number} height The height of the Token (positive)
* @param {TokenShapeType} shape The shape type (must be RECTANGLE_1 or RECTANGLE_2)
* @returns {TokenHexagonalShapeData|null} The hexagonal shape or null if there is no shape
* for the given combination of arguments
*/
static #createHexagonalRectangle(width, height, shape) {
if (width < 1 || !Number.isInteger(height)) return null;
if (width === 1 && height > 1) return null;
if (!Number.isInteger(width) && height === 1) return null;
const even = shape === CONST.TOKEN_SHAPES.RECTANGLE_1 || height === 1;
const offsets = { even: [], odd: [] };
for (let i = 0; i < height; i++) {
const j0 = even ? 0 : (i + 1) & 1;
const j1 = ((width + (i & 1) * 0.5) | 0) - (even ? i & 1 : 0);
for (let j = j0; j < j1; j++) {
offsets.even.push({ i, j: j + (i & 1) });
offsets.odd.push({ i, j });
}
}
let x = even ? 0.0 : 0.5;
let y = 0.25;
const points = [x, y];
while (x + 1 <= width) {
x += 0.5;
y -= 0.25;
points.push(x, y);
x += 0.5;
y += 0.25;
points.push(x, y);
}
if (x !== width) {
y += 0.5;
points.push(x, y);
x += 0.5;
y += 0.25;
points.push(x, y);
}
while (y + 1.5 <= 0.75 * height) {
y += 0.5;
points.push(x, y);
x -= 0.5;
y += 0.25;
points.push(x, y);
y += 0.5;
points.push(x, y);
x += 0.5;
y += 0.25;
points.push(x, y);
}
if (y + 0.75 < 0.75 * height) {
y += 0.5;
points.push(x, y);
x -= 0.5;
y += 0.25;
points.push(x, y);
}
y += 0.5;
points.push(x, y);
while (x - 1 >= 0) {
x -= 0.5;
y += 0.25;
points.push(x, y);
x -= 0.5;
y -= 0.25;
points.push(x, y);
}
if (x !== 0) {
y -= 0.5;
points.push(x, y);
x -= 0.5;
y -= 0.25;
points.push(x, y);
}
while (y - 1.5 > 0) {
y -= 0.5;
points.push(x, y);
x += 0.5;
y -= 0.25;
points.push(x, y);
y -= 0.5;
points.push(x, y);
x -= 0.5;
y -= 0.25;
points.push(x, y);
}
if (y - 0.75 > 0) {
y -= 0.5;
points.push(x, y);
x += 0.5;
y -= 0.25;
points.push(x, y);
}
return {
offsets,
points,
// We use center of the rectangle (and not the centroid of the polygon) for the rectangle shapes
center: {
x: width / 2,
y: (0.75 * Math.floor(height) + 0.5 * (height % 1) + 0.25) / 2
},
anchor: even ? { x: 0.5, y: 0.5 } : { x: 0.0, y: 0.5 }
};
}
//#endregion
}

View file

@ -10,6 +10,7 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
let html = options.html;
if (element.dataset.tooltip?.startsWith('#battlepoints#')) {
this.#wide = true;
this.#bordered = true;
html = await this.getBattlepointHTML(element.dataset.combatId);
options.direction = this._determineItemTooltipDirection(element);
@ -22,6 +23,7 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
return;
} else {
this.#wide = false;
this.#bordered = false;
}
if (element.dataset.tooltip === '#effect-display#') {
@ -73,7 +75,8 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
{
item: item,
description: item.system?.enrichedDescription ?? item.enrichedDescription,
config: CONFIG.DH
config: CONFIG.DH,
allDomains: CONFIG.DH.DOMAIN.allDomains()
}
);
@ -168,14 +171,6 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
super.activate(element, { ...options, html: html });
}
_setStyle(position = {}) {
super._setStyle(position);
if (this.#bordered) {
this.tooltip.classList.add('bordered-tooltip');
}
}
_determineItemTooltipDirection(element, prefered = this.constructor.TOOLTIP_DIRECTIONS.LEFT) {
const pos = element.getBoundingClientRect();
const dirs = this.constructor.TOOLTIP_DIRECTIONS;
@ -247,12 +242,17 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
if (this.#wide) {
this.tooltip.classList.add('wide');
}
if (this.#bordered) {
this.tooltip.classList.add('bordered-tooltip');
}
}
/**@inheritdoc */
lockTooltip() {
const clone = super.lockTooltip();
clone.classList.add('wide');
if (this.#wide) clone.classList.add('wide');
if (this.#bordered) clone.classList.add('bordered-tooltip');
return clone;
}
@ -262,7 +262,7 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
const combat = game.combats.get(combatId);
const adversaries =
combat.turns?.filter(x => x.actor?.isNPC)?.map(x => ({ ...x.actor, type: x.actor.system.type })) ?? [];
const characters = combat.turns?.filter(x => !x.isNPC) ?? [];
const characters = combat.turns?.filter(x => !x.isNPC && x.actor) ?? [];
const nrCharacters = characters.length;
const currentBP = AdversaryBPPerEncounter(adversaries, characters);
@ -272,7 +272,7 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
);
const categories = combat.combatants.reduce((acc, combatant) => {
if (combatant.actor.type === 'adversary') {
if (combatant.actor?.type === 'adversary') {
const keyData = Object.keys(acc).reduce((identifiers, categoryKey) => {
if (identifiers) return identifiers;
const category = acc[categoryKey];
@ -352,7 +352,7 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
await combat.toggleModifierEffects(
event.target.checked,
combat.combatants.filter(x => x.actor.type === 'adversary').map(x => x.actor),
combat.combatants.filter(x => x.actor?.type === 'adversary').map(x => x.actor),
category,
grouping
);

View file

@ -3,6 +3,11 @@ import { parseInlineParams } from './parser.mjs';
export default function DhLookupEnricher(match, { rollData }) {
const results = parseInlineParams(match[1], { first: 'formula' });
const element = document.createElement('span');
element.textContent = Roll.replaceFormulaData(String(results.formula), rollData);
const lookupCommand = match[0];
const lookupParam = match[1];
const lookupText = Roll.replaceFormulaData(String(results.formula), rollData);
element.textContent = lookupText === lookupParam ? lookupCommand : lookupText;
return element;
}

View file

@ -119,7 +119,7 @@ export const tagifyElement = (element, baseOptions, onChange, tagifyOptions = {}
spellcheck='false'
tabIndex="${this.settings.a11y.focusableTags ? 0 : -1}"
class="${this.settings.classNames.tag} ${tagData.class ? tagData.class : ''}"
data-tooltip="${tagData.description || tagData.name}"
data-tooltip="${tagData.description ? htmlToText(tagData.description) : tagData.name}"
${this.getAttributes(tagData)}>
<x class="${this.settings.classNames.tagX}" role='button' aria-label='remove tag'></x>
<div>
@ -198,7 +198,7 @@ foundry.dice.terms.Die.prototype.selfCorrecting = function (modifier) {
};
export const getDamageKey = damage => {
return ['none', 'minor', 'major', 'severe', 'any'][damage];
return ['none', 'minor', 'major', 'severe', 'massive', 'any'][damage];
};
export const getDamageLabel = damage => {
@ -211,7 +211,8 @@ export const damageKeyToNumber = key => {
minor: 1,
major: 2,
severe: 3,
any: 4
massive: 4,
any: 5
}[key];
};
@ -471,5 +472,12 @@ export function refreshIsAllowed(allowedTypes, typeToCheck) {
export async function getCritDamageBonus(formula) {
const critRoll = new Roll(formula);
return critRoll.dice.reduce((acc, dice) => acc + dice.faces, 0);
return critRoll.dice.reduce((acc, dice) => acc + dice.faces * dice.number, 0);
}
export function htmlToText(html) {
var tempDivElement = document.createElement('div');
tempDivElement.innerHTML = html;
return tempDivElement.textContent || tempDivElement.innerText || '';
}

View file

@ -13,6 +13,16 @@ export const registerDHSettings = () => {
registerMenuSettings();
registerMenus();
registerNonConfigSettings();
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.SpotlightRequestQueue, {
name: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.SpotlightRequestQueue.name'),
label: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.SpotlightRequestQueue.label'),
hint: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.SpotlightRequestQueue.hint'),
scope: 'world',
config: true,
type: Boolean,
onChange: () => ui.combat.render()
});
};
const registerMenuSettings = () => {
@ -36,6 +46,9 @@ const registerMenuSettings = () => {
if (value.maxFear) {
if (ui.resources) ui.resources.render({ force: true });
}
// Some homebrew settings may change sheets in various ways, so trigger a re-render
resetActors();
}
});
@ -130,3 +143,25 @@ const registerNonConfigSettings = () => {
type: DhTagTeamRoll
});
};
/**
* Triggers a reset and non-forced re-render on all given actors (if given)
* or all world actors and actors in all scenes to show immediate results for a changed setting.
*/
function resetActors(actors) {
actors ??= [
game.actors.contents,
game.scenes.contents.flatMap(s => s.tokens.contents).flatMap(t => t.actor ?? [])
].flat();
actors = new Set(actors);
for (const actor of actors) {
for (const app of Object.values(actor.apps)) {
for (const element of app.element?.querySelectorAll('prose-mirror.active')) {
element.open = false; // This triggers a save
}
}
actor.reset();
actor.render();
}
}

View file

@ -73,10 +73,13 @@ export const registerSocketHooks = () => {
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
break;
case GMUpdateEvent.UpdateSaveMessage:
const action = await fromUuid(data.update.action),
message = game.messages.get(data.update.message);
if (!action || !message) return;
action.updateSaveMessage(data.update.result, message, data.update.token);
const message = game.messages.get(data.update.message);
if (!message) return;
game.system.api.fields.ActionFields.SaveField.updateSaveMessage(
data.update.result,
message,
data.update.token
);
break;
}

View file

@ -40,7 +40,8 @@
"experiences": {
"pe7OIoJsqlpMXEvs": {
"name": "Tremor Sense",
"value": 2
"value": 2,
"description": ""
}
},
"bonuses": {
@ -148,7 +149,8 @@
"source": "Daggerheart SRD",
"page": 75,
"artist": ""
}
},
"size": "large"
},
"flags": {},
"ownership": {
@ -258,14 +260,14 @@
"_id": "MFmGN6Tbf5GYxrQ9",
"img": "icons/magic/unholy/silhouette-evil-horned-giant.webp",
"system": {
"description": "<p>The Burrower can be spotlighted up to three times per GM turn. Spend Fear as usual to spotlight them.</p>",
"description": "<p>The @Lookup[@name] can be spotlighted up to three times per GM turn. Spend Fear as usual to spotlight them.</p>",
"resource": null,
"actions": {
"3lGGgkxnzgUwHGIp": {
"type": "effect",
"_id": "3lGGgkxnzgUwHGIp",
"systemPath": "actions",
"description": "<p>The Burrower can be spotlighted up to three times per GM turn. Spend Fear as usual to spotlight them.</p>",
"description": "",
"chatDisplay": false,
"actionType": "passive",
"cost": [
@ -317,14 +319,14 @@
"_id": "ctXYwil2D1zfsekT",
"img": "icons/magic/earth/barrier-stone-explosion-red.webp",
"system": {
"description": "<p><strong>Mark a Stress</strong> to have the Burrower burst out of the ground. All creatures within Very Close range must succeed on an Agility Reaction Roll or be knocked over, making them Vulnerable until they next act.</p><p>@Template[type:emanation|range:vc]</p>",
"description": "<p><strong>Mark a Stress</strong> to have the @Lookup[@name] burst out of the ground. All creatures within Very Close range must succeed on an Agility Reaction Roll or be knocked over, making them Vulnerable until they next act.</p><p>@Template[type:emanation|range:vc]</p>",
"resource": null,
"actions": {
"4ppSeiTdbqnMzWAs": {
"type": "attack",
"_id": "4ppSeiTdbqnMzWAs",
"systemPath": "actions",
"description": "<p><strong>Mark a Stress</strong> to have the Burrower burst out of the ground. All creatures within Very Close range must succeed on an Agility Reaction Roll or be knocked over, making them Vulnerable until they next act.</p><p>@Template[type:emanation|range:vc]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -381,7 +383,8 @@
},
"originItemType": null,
"subType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"effects": [
{
@ -441,14 +444,14 @@
"_id": "UpFsnlbZkyvM2Ftv",
"img": "icons/magic/acid/projectile-smoke-glowing.webp",
"system": {
"description": "<p>Make an attack against all targets in front of the Burrower within Close range. Targets the Burrower succeeds against take <strong>2d6</strong> physical damage and must mark an Armor Slot without receiving its benefits (they can still use armor to reduce the damage). If they cant mark an Armor Slot, they must mark an additional HP and you gain a Fear.</p><p>@Template[type:inFront|range:c]</p>",
"description": "<p>Make an attack against all targets in front of the @Lookup[@name] within Close range. Targets the @Lookup[@name] succeeds against take <strong>2d6</strong> physical damage and must mark an Armor Slot without receiving its benefits (they can still use armor to reduce the damage). If they cant mark an Armor Slot, they must mark an additional HP and you gain a Fear.</p><p>@Template[type:inFront|range:c]</p>",
"resource": null,
"actions": {
"yd10HwK6Wa3OEvv2": {
"type": "attack",
"_id": "yd10HwK6Wa3OEvv2",
"systemPath": "actions",
"description": "<p>Make an attack against all targets in front of the Burrower within Close range. Targets the Burrower succeeds against take <strong>2d6</strong> physical damage and must mark an Armor Slot without receiving its benefi ts (they can still use armor to reduce the damage). If they cant mark an Armor Slot, they must mark an additional HP and you gain a Fear.</p><p>@Template[type:inFront|range:c]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -546,7 +549,8 @@
},
"originItemType": null,
"subType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"effects": [],
"folder": null,
@ -567,14 +571,14 @@
"_id": "aNIVT5LKhwLyjKpI",
"img": "icons/magic/acid/dissolve-drip-droplet-smoke.webp",
"system": {
"description": "<p>When the Burrower takes Severe damage, all creatures within Close range are bathed in their acidic blood, taking <strong>1d10</strong> physical damage. This splash covers the ground within Very Close range with blood, and all creatures other than the Burrower who move through it take <strong>1d6</strong> physical damage.</p><p>@Template[type:emanation|range:c]</p>",
"description": "<p>When the @Lookup[@name] takes Severe damage, all creatures within Close range are bathed in their acidic blood, taking <strong>1d10</strong> physical damage. This splash covers the ground within Very Close range with blood, and all creatures other than the @Lookup[@name] who move through it take <strong>1d6</strong> physical damage.</p><p>@Template[type:emanation|range:c]</p>",
"resource": null,
"actions": {
"XbtTzOBvlTaxOKTy": {
"type": "damage",
"_id": "XbtTzOBvlTaxOKTy",
"systemPath": "actions",
"description": "<p>When the Burrower takes Severe damage, all creatures within Close range are bathed in their acidic blood, taking <strong>1d10</strong> physical damage. This splash covers the ground within Very Close range with blood, and all creatures other than the Burrower who move through it take <strong>1d6</strong> physical damage.</p><p>@Template[type:emanation|range:c]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -628,7 +632,7 @@
"type": "damage",
"_id": "xpcp1ECTWF20kxve",
"systemPath": "actions",
"description": "<p>This splash covers the ground within Very Close range with blood, and all creatures other than the Burrower who move through it take <strong>1d6</strong> physical damage.</p>",
"description": "<p>This splash covers the ground within Very Close range with blood, and all creatures other than the @Lookup[@name] who move through it take <strong>1d6</strong> physical damage.</p>",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -681,7 +685,8 @@
},
"originItemType": null,
"subType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"effects": [],
"folder": null,

View file

@ -111,7 +111,8 @@
"source": "Daggerheart SRD",
"page": 91,
"artist": ""
}
},
"size": "gargantuan"
},
"flags": {},
"_id": "G7jiltRjgvVhZewm",
@ -220,14 +221,14 @@
"name": "Relentless (4)",
"type": "feature",
"system": {
"description": "<p>The Flickerfly can be spotlighted up to four times per GM turn. Spend Fear as usual to spotlight them.</p>",
"description": "<p>The @Lookup[@name] can be spotlighted up to four times per GM turn. Spend Fear as usual to spotlight them.</p>",
"resource": null,
"actions": {
"poUhJdSkhjiVL2Vp": {
"type": "effect",
"_id": "poUhJdSkhjiVL2Vp",
"systemPath": "actions",
"description": "<p>The Flickerfly can be spotlighted up to four times per GM turn. Spend Fear as usual to spotlight them.</p>",
"description": "",
"chatDisplay": true,
"actionType": "passive",
"cost": [
@ -263,7 +264,7 @@
"img": "icons/magic/unholy/silhouette-evil-horned-giant.webp",
"effects": [],
"folder": null,
"sort": 0,
"sort": 100000,
"ownership": {
"default": 0,
"MQSznptE5yLT7kj8": 3
@ -278,14 +279,14 @@
"name": "Never Misses",
"type": "feature",
"system": {
"description": "<p>When the Flickerfly makes an attack, the targets Evasion is halved against the attack.</p>",
"description": "<p>When the @Lookup[@name] makes an attack, the targets Evasion is halved against the attack.</p>",
"resource": null,
"actions": {
"VRGPnDhDpReXUZZF": {
"type": "effect",
"_id": "VRGPnDhDpReXUZZF",
"systemPath": "actions",
"description": "<p>When the Flickerfly makes an attack, the targets Evasion is halved against the attack.</p>",
"description": "",
"chatDisplay": true,
"actionType": "passive",
"cost": [],
@ -360,7 +361,7 @@
}
],
"folder": null,
"sort": 0,
"sort": 200000,
"ownership": {
"default": 0,
"MQSznptE5yLT7kj8": 3
@ -375,14 +376,14 @@
"name": "Whirlwind",
"type": "feature",
"system": {
"description": "<p><strong>Spend a Fear</strong> to whirl, making an attack against all targets within Very Close range. Targets the Flickerfly succeeds against take <strong>3d8</strong> direct physical damage.</p><p>@Template[type:emanation|range:vc]</p>",
"description": "<p><strong>Spend a Fear</strong> to whirl, making an attack against all targets within Very Close range. Targets the @Lookup[@name] succeeds against take <strong>3d8</strong> direct physical damage.</p><p>@Template[type:emanation|range:vc]</p>",
"resource": null,
"actions": {
"RV1wKufKrMPN6MOo": {
"type": "attack",
"_id": "RV1wKufKrMPN6MOo",
"systemPath": "actions",
"description": "<p><strong>Spend a Fear</strong> to whirl, making an attack against all targets within Very Close range. Targets the Flickerfly succeeds against take <strong>3d8</strong> direct physical damage.</p><p>@Template[type:emanation|range:vc]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -427,7 +428,8 @@
}
}
],
"includeBase": false
"includeBase": false,
"direct": true
},
"target": {
"type": "any",
@ -460,13 +462,14 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "BuL6ndgaiJtjaM2T",
"img": "icons/skills/melee/strike-slashes-orange.webp",
"effects": [],
"folder": null,
"sort": 0,
"sort": 300000,
"ownership": {
"default": 0,
"MQSznptE5yLT7kj8": 3
@ -481,14 +484,14 @@
"name": "Mind Dance",
"type": "feature",
"system": {
"description": "<p><strong>Mark a Stress</strong> to create a magically dazzling display that grapples the minds of nearby foes. All targets within Close range must make an Instinct Reaction Roll. For each target who failed, you gain a Fear and the Flickerfl y learns one of the targets fears.</p><p>@Template[type:emanation|range:c]</p>",
"description": "<p><strong>Mark a Stress</strong> to create a magically dazzling display that grapples the minds of nearby foes. All targets within Close range must make an Instinct Reaction Roll. For each target who failed, you gain a Fear and the @Lookup[@name] learns one of the targets fears.</p><p>@Template[type:emanation|range:c]</p>",
"resource": null,
"actions": {
"GNwsDlCabx3fiG4g": {
"type": "attack",
"_id": "GNwsDlCabx3fiG4g",
"systemPath": "actions",
"description": "<p><strong>Mark a Stress</strong> to create a magically dazzling display that grapples the minds of nearby foes. All targets within Close range must make an Instinct Reaction Roll. For each target who failed, you gain a Fear and the Flickerfl y learns one of the targets fears.</p><p>@Template[type:emanation|range:c]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -539,13 +542,14 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "bOTsfXr9yNIGkIzK",
"img": "icons/magic/light/explosion-glow-spiral-yellow.webp",
"effects": [],
"folder": null,
"sort": 0,
"sort": 400000,
"ownership": {
"default": 0,
"MQSznptE5yLT7kj8": 3
@ -560,14 +564,14 @@
"name": "Hallucinatory Breath",
"type": "feature",
"system": {
"description": "<p><em>Countdown (Loop 1d6)</em>. When the Flickerfly takes damage for the first time, activate the countdown. When it triggers, the Flickerfly breathes hallucinatory gas on all targets in front of them up to Far range. Targets must make an Instinct Reaction Roll or be tormented by fearful hallucinations. Targets whose fears are known to the Flickerfl y have disadvantage on this roll. Targets who fail lose 2 Hope and take <strong>3d8+3</strong> direct magic damage.</p><p>@Template[type:inFront|range:f]</p>",
"description": "<p><em>Countdown (Loop 1d6)</em>. When the @Lookup[@name] takes damage for the first time, activate the countdown. When it triggers, the @Lookup[@name] breathes hallucinatory gas on all targets in front of them up to Far range. Targets must make an Instinct Reaction Roll or be tormented by fearful hallucinations. Targets whose fears are known to the @Lookup[@name] have disadvantage on this roll. Targets who fail lose 2 Hope and take <strong>3d8+3</strong> direct magic damage.</p><p>@Template[type:inFront|range:f]</p>",
"resource": null,
"actions": {
"YOyKyKGTUEWkMmJe": {
"type": "attack",
"_id": "YOyKyKGTUEWkMmJe",
"systemPath": "actions",
"description": "<p>The Flickerfly breathes hallucinatory gas on all targets in front of them up to Far range. Targets must make an Instinct Reaction Roll or be tormented by fearful hallucinations. Targets whose fears are known to the Flickerfly have disadvantage on this roll. Targets who fail lose 2 Hope and take <strong>3d8+3</strong> direct magic damage.</p><p>@Template[type:inFront|range:f]</p>",
"description": "<p>The @Lookup[@name] breathes hallucinatory gas on all targets in front of them up to Far range. Targets must make an Instinct Reaction Roll or be tormented by fearful hallucinations. Targets whose fears are known to the @Lookup[@name] have disadvantage on this roll. Targets who fail lose 2 Hope and take <strong>3d8+3</strong> direct magic damage.</p><p>@Template[type:inFront|range:f]</p>",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -665,7 +669,7 @@
"type": "countdown",
"_id": "lBhmLc33pcXzJHT3",
"systemPath": "actions",
"description": "<p><em>Countdown (Loop 1d6)</em>. When the Flickerfly takes damage for the first time, activate the countdown.</p>",
"description": "<p><em>Countdown (Loop 1d6)</em>. When the @Lookup[@name] takes damage for the first time, activate the countdown.</p>",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
@ -700,13 +704,14 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"_id": "49cIxZRFiAM6jDva",
"img": "icons/magic/air/fog-gas-smoke-purple.webp",
"effects": [],
"folder": null,
"sort": 0,
"sort": 500000,
"ownership": {
"default": 0,
"MQSznptE5yLT7kj8": 3
@ -721,14 +726,14 @@
"name": "Uncanny Reflexes",
"type": "feature",
"system": {
"description": "<p>When the Flickerfly takes damage from an attack within Close range, you can mark a Stress to take half damage.</p>",
"description": "<p>When the @Lookup[@name] takes damage from an attack within Close range, you can mark a Stress to take half damage.</p>",
"resource": null,
"actions": {
"FocbilGTpvUjlb7m": {
"type": "effect",
"_id": "FocbilGTpvUjlb7m",
"systemPath": "actions",
"description": "<p>When the Flickerfly takes damage from an attack within Close range, you can mark a Stress to take half damage.</p>",
"description": "",
"chatDisplay": true,
"actionType": "reaction",
"cost": [
@ -755,13 +760,14 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"_id": "KLdLRKoJHBJlHwYe",
"img": "icons/skills/movement/arrow-upward-yellow.webp",
"effects": [],
"folder": null,
"sort": 0,
"sort": 600000,
"ownership": {
"default": 0,
"MQSznptE5yLT7kj8": 3
@ -776,7 +782,7 @@
"name": "Deadly Flight",
"type": "feature",
"system": {
"description": "<p>While flying the Flickerfly can move up to Far range instead of Close range before taking an action.</p>",
"description": "<p>While flying the @Lookup[@name] can move up to Far range instead of Close range before taking an action.</p>",
"resource": null,
"actions": {},
"originItemType": null,
@ -786,7 +792,7 @@
"img": "icons/skills/movement/feet-winged-boots-blue.webp",
"effects": [],
"folder": null,
"sort": 0,
"sort": 700000,
"ownership": {
"default": 0,
"fBcTgyTzoARBvohY": 3

View file

@ -110,7 +110,8 @@
"source": "Daggerheart SRD",
"page": 84,
"artist": ""
}
},
"size": "medium"
},
"flags": {},
"_id": "vNIbYQ4YSzNf0WPE",
@ -219,11 +220,12 @@
"name": "Minion (6)",
"type": "feature",
"system": {
"description": "<p>The Assassin is defeated when they take any damage. For every 6 damage a PC deals to the Assassin, defeat an additional Minion within range the attack would succeed against.</p>",
"description": "<p>The @Lookup[@name] is defeated when they take any damage. For every 6 damage a PC deals to the @Lookup[@name], defeat an additional Minion within range the attack would succeed against.</p>",
"resource": null,
"actions": {},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "passive"
},
"_id": "2yREz60uPY80tAa4",
"img": "icons/magic/symbols/runes-carved-stone-yellow.webp",
@ -244,14 +246,14 @@
"name": "Group Attack",
"type": "feature",
"system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all Apprentice Assassins within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 4 physical damage each. Combine this damage.</p>",
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 4 physical damage each. Combine this damage.</p>",
"resource": null,
"actions": {
"vgguNWz8vG8aoLXR": {
"type": "effect",
"_id": "vgguNWz8vG8aoLXR",
"systemPath": "actions",
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all Apprentice Assassins within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 4 physical damage each. Combine this damage.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -278,7 +280,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "4wT7CmM1DJEPcraF",
"img": "icons/creatures/abilities/tail-strike-bone-orange.webp",

View file

@ -122,7 +122,8 @@
"source": "Daggerheart SRD",
"page": 97,
"artist": ""
}
},
"size": "medium"
},
"flags": {},
"_id": "WPEOIGfclNJxWb87",
@ -238,7 +239,7 @@
"type": "attack",
"_id": "wi2DDvBhlg6sxQoc",
"systemPath": "actions",
"description": "<p><strong>Mark a Stress</strong> to spotlight <strong>1d4</strong> allies. Attacks they make while spotlighted in this way deal half damage, or full damage if you spend a Fear.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -289,7 +290,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "jNmMyq5QI2HNgffy",
"img": "icons/magic/death/skull-weapon-staff-glow-pink.webp",
@ -317,7 +319,7 @@
"type": "attack",
"_id": "vaXLESD4sRkQ3Ahn",
"systemPath": "actions",
"description": "<p><strong>Mark 2 Stress</strong> to cause all targets within Far range to make a Strength Reaction Roll. Targets who fail take <strong>2d20+12</strong> magic damage and you gain a Fear. Targets who succeed take half damage. A target who marks 2 or more HP must also <strong>mark 2 Stress</strong> and becomes <em>Vulnerable</em> until they roll with Hope.</p><p>@Template[type:emanation|range:f]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -467,7 +469,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "4EECsXzHFG0RoIg0",
"img": "icons/magic/unholy/projectile-missile-green.webp",
@ -534,7 +537,7 @@
"type": "effect",
"_id": "gZg3AkzCYUTExjE6",
"systemPath": "actions",
"description": "<p><strong>Spend a Fear</strong> to summon a @UUID[Compendium.daggerheart.adversaries.Actor.YhJrP7rTBiRdX5Fp]{Zombie Legion}, which appears at Close range and immediately takes the spotlight.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -561,7 +564,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "XxXOrFovbCz9zFxR",
"img": "icons/magic/death/undead-zombie-grave-green.webp",
@ -582,14 +586,14 @@
"name": "Not Today, My Dears",
"type": "feature",
"system": {
"description": "<p>When the Necromancer has marked 7 or more of their HP, you can <strong>spend a Fear</strong> to have them teleport away to a safe location to recover. A PC who succeeds on an Instinct Roll can trace the teleportation magic to their destination.</p>",
"description": "<p>When the @Lookup[@name] has marked 7 or more of their HP, you can <strong>spend a Fear</strong> to have them teleport away to a safe location to recover. A PC who succeeds on an Instinct Roll can trace the teleportation magic to their destination.</p>",
"resource": null,
"actions": {
"DX8WPeLVrRBB2CdM": {
"type": "attack",
"_id": "DX8WPeLVrRBB2CdM",
"systemPath": "actions",
"description": "<p>When the Necromancer has marked 7 or more of their HP, you can <strong>spend a Fear</strong> to have them teleport away to a safe location to recover. A PC who succeeds on an Instinct Roll can trace the teleportation magic to their destination.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -640,7 +644,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"_id": "k4MSykLRoW3qp7Lk",
"img": "icons/magic/death/skull-horned-worn-fire-blue.webp",
@ -661,14 +666,14 @@
"name": "Your Life Is Mine",
"type": "feature",
"system": {
"description": "<p><em>Countdown (Loop 2d6)</em>. When the Necromancer has marked 6 or more of their HP, activate the countdown. When it triggers, deal <strong>2d10+6</strong> direct magic damage to a target within Close range. The Necromancer then <strong>clears a number of Stress or HP</strong> equal to the number of HP marked by the target from this attack.</p>",
"description": "<p><em>Countdown (Loop 2d6)</em>. When the @Lookup[@name] has marked 6 or more of their HP, activate the countdown. When it triggers, deal <strong>2d10+6</strong> direct magic damage to a target within Close range. The @Lookup[@name] then <strong>clears a number of Stress or HP</strong> equal to the number of HP marked by the target from this attack.</p>",
"resource": null,
"actions": {
"YzepYov9vEMcBPU1": {
"type": "damage",
"_id": "YzepYov9vEMcBPU1",
"systemPath": "actions",
"description": "<p>Deal <strong>2d10+6</strong> direct magic damage to a target within Close range. The Necromancer then <strong>clears a number of Stress or HP</strong> equal to the number of HP marked by the target from this attack.</p>",
"description": "<p>Deal <strong>2d10+6</strong> direct magic damage to a target within Close range. The @Lookup[@name] then <strong>clears a number of Stress or HP</strong> equal to the number of HP marked by the target from this attack.</p>",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -721,7 +726,7 @@
"type": "countdown",
"_id": "LXhwkNCDFeUric8D",
"systemPath": "actions",
"description": "<p><em>Countdown (Loop 2d6)</em>. When the Necromancer has marked 6 or more of their HP, activate the countdown.</p>",
"description": "<p><em>Countdown (Loop 2d6)</em>. When the @Lookup[@name] has marked 6 or more of their HP, activate the countdown.</p>",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
@ -756,7 +761,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"_id": "FKcuCo0v2U7fVkqq",
"img": "icons/magic/unholy/hand-claw-fire-green.webp",

View file

@ -39,7 +39,8 @@
"experiences": {
"Gtr9I2G39GcXT2Si": {
"name": "Local Knowledge",
"value": 3
"value": 3,
"description": ""
}
},
"bonuses": {
@ -116,7 +117,8 @@
"source": "Daggerheart SRD",
"page": 77,
"artist": ""
}
},
"size": "medium"
},
"flags": {},
"_id": "JRhrrEg5UroURiAD",
@ -234,7 +236,7 @@
"type": "attack",
"_id": "84rwldOFvTPrrHJJ",
"systemPath": "actions",
"description": "<p>Make an attack against a target within Far range. On a success, <strong>mark a Stress</strong> to deal <strong>1d12+3</strong> physical damage. If the target marks HP from this attack, they have disadvantage on Agility Rolls until they clear at least 1 HP.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -311,7 +313,8 @@
},
"originItemType": null,
"subType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"effects": [
{

View file

@ -111,7 +111,8 @@
"source": "Daggerheart SRD",
"page": 84,
"artist": ""
}
},
"size": "medium"
},
"flags": {},
"_id": "0ts6CGd93lLqGZI5",
@ -220,7 +221,7 @@
"name": "Horde (1d6+3)",
"type": "feature",
"system": {
"description": "<p>When the Squadron has marked half or more of their HP, their standard attack deals <strong>1d6+3</strong> physical damage instead.</p>",
"description": "<p>When the @Lookup[@name] has marked half or more of their HP, their standard attack deals <strong>1d6+3</strong> physical damage instead.</p>",
"resource": null,
"actions": {},
"originItemType": null,
@ -245,14 +246,14 @@
"name": "Focused Volley",
"type": "feature",
"system": {
"description": "<p><strong>Spend a Fear</strong> to target a point within Far range. Make an attack with advantage against all targets within Close range of that point. Targets the Squadron succeeds against take <strong>1d10+4</strong> physical damage.</p><p>@Template[type:circle|range:c]</p>",
"description": "<p><strong>Spend a Fear</strong> to target a point within Far range. Make an attack with advantage against all targets within Close range of that point. Targets the @Lookup[@name] succeeds against take <strong>1d10+4</strong> physical damage.</p><p>@Template[type:circle|range:c]</p>",
"resource": null,
"actions": {
"uG7Hl2DqaT69aNs1": {
"type": "attack",
"_id": "uG7Hl2DqaT69aNs1",
"systemPath": "actions",
"description": "<p><strong>Spend a Fear</strong> to target a point within Far range. Make an attack with advantage against all targets within Close range of that point. Targets the Squadron succeeds against take <strong>1d10+4</strong> physical damage.</p><p>@Template[type:circle|range:c]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -328,7 +329,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "Wuf5y9tJ88BwzLv2",
"img": "icons/skills/ranged/arrows-flying-triple-brown.webp",
@ -356,7 +358,7 @@
"type": "attack",
"_id": "mH6mmJIMM1fwzePt",
"systemPath": "actions",
"description": "<p><strong>Mark a Stress</strong> to target a point within Far range. Until the next roll with Fear, a creature who moves within Close range of that point must make an Agility Reaction Roll. On a failure, they take <strong>2d6+3</strong> physical damage. On a success, they take half damage.</p><p>@Template[type:circle|range:c]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -425,7 +427,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "ayGHTtyjSuIR4BrV",
"img": "icons/skills/ranged/arrows-flying-salvo-blue.webp",

View file

@ -117,7 +117,8 @@
"source": "Daggerheart SRD",
"page": 84,
"artist": ""
}
},
"size": "medium"
},
"flags": {},
"_id": "h5RuhzGL17dW5FBT",
@ -226,14 +227,14 @@
"name": "Grindletooth Venom",
"type": "feature",
"system": {
"description": "<p>Targets who mark HP from the Assassins attacks are Vulnerable until they clear a HP.</p>",
"description": "<p>Targets who mark HP from the @Lookup[@name]s attacks are Vulnerable until they clear a HP.</p>",
"resource": null,
"actions": {
"L83tU1TgmqoH9SSn": {
"type": "effect",
"_id": "L83tU1TgmqoH9SSn",
"systemPath": "actions",
"description": "<p>Targets who mark HP from the Assassins attacks are Vulnerable until they clear a HP.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -318,7 +319,7 @@
"name": "Assassin Poisoner",
"type": "feature",
"system": {
"description": "<p>The Assassin has advantage on attacks if they are <em>Hidden</em>.</p>",
"description": "<p>The @Lookup[@name] has advantage on attacks if they are <em>Hidden</em>.</p>",
"resource": null,
"actions": {},
"originItemType": null,
@ -394,7 +395,7 @@
"type": "effect",
"_id": "sp7RfJRQJsEUm09m",
"systemPath": "actions",
"description": "<p>Drop a smoke bomb that fills the air within Close range with smoke, Dizzying all targets in this area. Dizzied targets have disadvantage on their next action roll, then clear the condition.</p><p>@Template[type:emanation|range:c]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -419,7 +420,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "lAmiK8wVxjyHwKlp",
"img": "icons/magic/air/fog-gas-smoke-green.webp",

View file

@ -117,7 +117,8 @@
"source": "Daggerheart SRD",
"page": 85,
"artist": ""
}
},
"size": "large"
},
"flags": {},
"_id": "dgH3fW9FTYLaIDvS",
@ -226,14 +227,14 @@
"name": "Relentless (2)",
"type": "feature",
"system": {
"description": "<p>The Box can be spotlighted up to two times times per GM turn. Spend Fear as usual to spotlight them.</p>",
"description": "<p>The @Lookup[@name] can be spotlighted up to two times times per GM turn. Spend Fear as usual to spotlight them.</p>",
"resource": null,
"actions": {
"2JfPSV3pw6pv0BXd": {
"type": "effect",
"_id": "2JfPSV3pw6pv0BXd",
"systemPath": "actions",
"description": "<p>The Box can be spotlighted up to two times times per GM turn. Spend Fear as usual to spotlight them.</p>",
"description": "",
"chatDisplay": true,
"actionType": "passive",
"cost": [
@ -284,14 +285,14 @@
"name": "Randomized Tactics",
"type": "feature",
"system": {
"description": "<p>Mark a Stress and roll a <strong>d6</strong>. The Box uses the corresponding move:</p><ol><li><p><strong>Mana Beam</strong></p></li><li><p><strong>Fire Jets</strong></p></li><li><p><strong>Trample</strong></p></li><li><p><strong>Shocking Gas</strong></p></li><li><p><strong>Stunning Clap</strong></p></li><li><p><strong>Psionic Whine</strong></p></li></ol>",
"description": "<p>Mark a Stress and roll a <strong>d6</strong>. The @Lookup[@name] uses the corresponding move:</p><ol><li><p><strong>Mana Beam</strong></p></li><li><p><strong>Fire Jets</strong></p></li><li><p><strong>Trample</strong></p></li><li><p><strong>Shocking Gas</strong></p></li><li><p><strong>Stunning Clap</strong></p></li><li><p><strong>Psionic Whine</strong></p></li></ol>",
"resource": null,
"actions": {
"FX9jwg5ZNjAWnti3": {
"type": "attack",
"_id": "FX9jwg5ZNjAWnti3",
"systemPath": "actions",
"description": "<p>Mark a Stress and roll a <strong>d6</strong>. The Box uses the corresponding move:</p><ol><li><p><strong>Mana Beam</strong></p></li><li><p><strong>Fire Jets</strong></p></li><li><p><strong>Trample</strong></p></li><li><p><strong>Shocking Gas</strong></p></li><li><p><strong>Stunning Clap</strong></p></li><li><p><strong>Psionic Whine</strong></p></li></ol>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -342,7 +343,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "ZqfLMjVkbUwDw4p6",
"img": "icons/commodities/tech/transmission.webp",
@ -363,14 +365,14 @@
"name": "Mana Beam",
"type": "feature",
"system": {
"description": "<p>The Box fires a searing beam. Make an attack against a target within Far range. On a success, deal <strong>2d10+2</strong> magic damage. </p>",
"description": "<p>The @Lookup[@name] fires a searing beam. Make an attack against a target within Far range. On a success, deal <strong>2d10+2</strong> magic damage.</p>",
"resource": null,
"actions": {
"Co09oXMw0yBjGaws": {
"type": "attack",
"_id": "Co09oXMw0yBjGaws",
"systemPath": "actions",
"description": "<p>The Box fires a searing beam. Make an attack against a target within Far range. On a success, deal <strong>2d10+2</strong> magic damage.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -441,7 +443,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "lqyN4CQop53BzarW",
"img": "icons/magic/light/beam-rays-blue.webp",
@ -462,14 +465,14 @@
"name": "Fire Jets",
"type": "feature",
"system": {
"description": "<p>The Box shoots into the air, spinning and releasing jets of flame. Make an attack against all targets within Close range. Targets the Box succeeds against take <strong>2d8</strong> physical damage.</p><p>@Template[type:emanation|range:c]</p>",
"description": "<p>The @Lookup[@name] shoots into the air, spinning and releasing jets of flame. Make an attack against all targets within Close range. Targets the @Lookup[@name] succeeds against take <strong>2d8</strong> physical damage.</p><p>@Template[type:emanation|range:c]</p>",
"resource": null,
"actions": {
"hRAKaOdzQXLYBNVV": {
"type": "attack",
"_id": "hRAKaOdzQXLYBNVV",
"systemPath": "actions",
"description": "<p>The Box shoots into the air, spinning and releasing jets of flame. Make an attack against all targets within Close range. Targets the Box succeeds against take <strong>2d8</strong> physical damage.</p><p>@Template[type:emanation|range:c]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -540,7 +543,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "IHQoqt39T772FVMs",
"img": "icons/magic/fire/explosion-embers-orange.webp",
@ -561,14 +565,14 @@
"name": "Trample",
"type": "feature",
"system": {
"description": "<p>The Box rockets around erratically. Make an attack against all PCs within Close range. Targets the Box succeeds against take <strong>1d6+5</strong> physical damage and are Vulnerable until their next roll with Hope. </p><p>@Template[type:emanation|range:c]</p>",
"description": "<p>The @Lookup[@name] rockets around erratically. Make an attack against all PCs within Close range. Targets the @Lookup[@name] succeeds against take <strong>1d6+5</strong> physical damage and are Vulnerable until their next roll with Hope.</p><p>@Template[type:emanation|range:c]</p>",
"resource": null,
"actions": {
"IOgPMu12Xnn33TfG": {
"type": "attack",
"_id": "IOgPMu12Xnn33TfG",
"systemPath": "actions",
"description": "<p>The Box rockets around erratically. Make an attack against all PCs within Close range. Targets the Box succeeds against take <strong>1d6+5</strong> physical damage and are Vulnerable until their next roll with Hope.</p><p>@Template[type:emanation|range:c]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -644,7 +648,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "XtnByqUr9AuYU9Ip",
"img": "icons/skills/movement/arrow-upward-yellow.webp",
@ -704,14 +709,14 @@
"name": "Shocking Gas",
"type": "feature",
"system": {
"description": "<p>The Box sprays out a silver gas sparking with lightning. All targets within Close range must succeed on a Finesse Reaction Roll or mark 3 Stress. </p><p>@Template[type:emanation|range:c]</p>",
"description": "<p>The @Lookup[@name] sprays out a silver gas sparking with lightning. All targets within Close range must succeed on a Finesse Reaction Roll or mark 3 Stress.</p><p>@Template[type:emanation|range:c]</p>",
"resource": null,
"actions": {
"ky4OMl558J5wCbDp": {
"type": "attack",
"_id": "ky4OMl558J5wCbDp",
"systemPath": "actions",
"description": "<p>The Box sprays out a silver gas sparking with lightning. All targets within Close range must succeed on a Finesse Reaction Roll or mark 3 Stress.</p><p>@Template[type:emanation|range:c]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -781,7 +786,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "3bPURmuwQs06fThQ",
"img": "icons/magic/lightning/bolt-strike-embers-teal.webp",
@ -802,14 +808,14 @@
"name": "Stunning Clap",
"type": "feature",
"system": {
"description": "<p>The Box leaps and their sides clap, creating a small sonic boom. All targets within Very Close range must succeed on a Strength Reaction Roll or become <em>Vulnerable</em> until the cube is defeated.</p><p>@Template[type:emanation|range:vc]</p>",
"description": "<p>The @Lookup[@name] leaps and their sides clap, creating a small sonic boom. All targets within Very Close range must succeed on a Strength Reaction Roll or become <em>Vulnerable</em> until the cube is defeated.</p><p>@Template[type:emanation|range:vc]</p>",
"resource": null,
"actions": {
"LQtopkrtSlCQ5MAr": {
"type": "attack",
"_id": "LQtopkrtSlCQ5MAr",
"systemPath": "actions",
"description": "<p>The Box leaps and their sides clap, creating a small sonic boom. All targets within Very Close range must succeed on a Strength Reaction Roll or become <em>Vulnerable</em> until the cube is defeated.</p><p>@Template[type:emanation|range:vc]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -858,7 +864,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "ijIaKjroxq3xZd9Z",
"img": "icons/magic/sonic/explosion-impact-shock-wave.webp",
@ -918,14 +925,14 @@
"name": "Psionic Whine",
"type": "feature",
"system": {
"description": "<p>The Box releases a cluster of mechanical bees whose buzz rattles mortal minds. All targets within Close range must succeed on a Presence Reaction Roll or take <strong>2d4+9</strong> direct magic damage.</p><p>@Template[type:emanation|range:c]</p>",
"description": "<p>The @Lookup[@name] releases a cluster of mechanical bees whose buzz rattles mortal minds. All targets within Close range must succeed on a Presence Reaction Roll or take <strong>2d4+9</strong> direct magic damage.</p><p>@Template[type:emanation|range:c]</p>",
"resource": null,
"actions": {
"3R3pGOUj4rHaUzPK": {
"type": "attack",
"_id": "3R3pGOUj4rHaUzPK",
"systemPath": "actions",
"description": "<p>The Box releases a cluster of mechanical bees whose buzz rattles mortal minds. All targets within Close range must succeed on a Presence Reaction Roll or take <strong>2d4+9</strong> direct magic damage.</p><p>@Template[type:emanation|range:c]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -996,7 +1003,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "JCue4ko61bjhedXv",
"img": "icons/creatures/invertebrates/wasp-swarm-tan.webp",
@ -1017,14 +1025,14 @@
"name": "Overcharge",
"type": "feature",
"system": {
"description": "<p>Before rolling damage for the Boxs attack, you can <strong>mark a Stress</strong> to add a <strong>d6</strong> to the damage roll. Additionally, you gain a Fear.</p>",
"description": "<p>Before rolling damage for the @Lookup[@name]s attack, you can <strong>mark a Stress</strong> to add a <strong>d6</strong> to the damage roll. Additionally, you gain a Fear.</p>",
"resource": null,
"actions": {
"3XOvKoYz4CqMNrU9": {
"type": "healing",
"_id": "3XOvKoYz4CqMNrU9",
"systemPath": "actions",
"description": "<p>Before rolling damage for the Boxs attack, you can <strong>mark a Stress</strong> to add a <strong>d6</strong> to the damage roll. Additionally, you gain a Fear.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -1096,7 +1104,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"_id": "ITzpRJr2jWK0Ksmp",
"img": "icons/creatures/magical/construct-golem-stone-blue.webp",
@ -1117,14 +1126,14 @@
"name": "Death Quake",
"type": "feature",
"system": {
"description": "<p>When the Box marks their last HP, the magic powering them ruptures in an explosion of force. All targets within Close range must succeed on an Instinct Reaction Roll or take <strong>2d8+1</strong> magic damage</p><p>@Template[type:emanation|range:c]</p>",
"description": "<p>When the @Lookup[@name] marks their last HP, the magic powering them ruptures in an explosion of force. All targets within Close range must succeed on an Instinct Reaction Roll or take <strong>2d8+1</strong> magic damage</p><p>@Template[type:emanation|range:c]</p>",
"resource": null,
"actions": {
"oCpv4zi9jtEpo0K1": {
"type": "attack",
"_id": "oCpv4zi9jtEpo0K1",
"systemPath": "actions",
"description": "<p>When the Box marks their last HP, the magic powering them ruptures in an explosion of force. All targets within Close range must succeed on an Instinct Reaction Roll or take <strong>2d8+1</strong> magic damage</p><p>@Template[type:emanation|range:c]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -1195,7 +1204,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"_id": "YvfzPyJbbv2ia6Yp",
"img": "icons/magic/sonic/explosion-shock-wave-teal.webp",

View file

@ -122,7 +122,8 @@
"source": "Daggerheart SRD",
"page": 75,
"artist": ""
}
},
"size": "large"
},
"flags": {},
"_id": "71qKDLKO3CsrNkdy",
@ -233,7 +234,7 @@
"_id": "2fXzhh2qil8dw3vw",
"img": "icons/skills/melee/strike-slashes-orange.webp",
"system": {
"description": "<p>Targets who mark HP from the Bears standard attack are knocked back to Very Close range.</p>",
"description": "<p>Targets who mark HP from the @Lookup[@name]s standard attack are knocked back to Very Close range.</p>",
"resource": null,
"actions": {},
"originItemType": null,
@ -266,7 +267,7 @@
"type": "attack",
"_id": "PXL3e51eBYZ4O2lb",
"systemPath": "actions",
"description": "<p><strong>Mark a Stress</strong> to make an attack against a target within Melee range. On a success, deal <strong>3d4+10</strong> physical damage and the target is <em>Restrained</em> until they break free with a successful Strength Roll.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -350,7 +351,8 @@
},
"originItemType": null,
"subType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"effects": [
{
@ -485,7 +487,8 @@
},
"originItemType": null,
"subType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"effects": [],
"folder": null,

View file

@ -117,7 +117,8 @@
"source": "Daggerheart SRD",
"page": 77,
"artist": ""
}
},
"size": "medium"
},
"flags": {},
"_id": "B4LZcGuBAHzyVdzy",
@ -228,14 +229,14 @@
"_id": "qEn4baWgkjKtmILp",
"img": "icons/equipment/shield/shield-round-boss-wood-brown.webp",
"system": {
"description": "<p>A creature who tries to move within Very Close range of the Guard must succeed on an Agility Roll. If additional Bladed Guards are standing in a line alongside the first, and each is within Melee range of another guard in the line, the Difficulty increases by the total number of guards in that line.</p>",
"description": "<p>A creature who tries to move within Very Close range of the @Lookup[@name] must succeed on an Agility Roll. If additional @Lookup[@name]s are standing in a line alongside the first, and each is within Melee range of another guard in the line, the Difficulty increases by the total number of guards in that line.</p>",
"resource": null,
"actions": {
"3lbeEeJdjzPn0MoG": {
"type": "attack",
"_id": "3lbeEeJdjzPn0MoG",
"systemPath": "actions",
"description": "<p>A creature who tries to move within Very Close range of the Guard must succeed on an Agility Roll. If additional Bladed Guards are standing in a line alongside the first, and each is within Melee range of another guard in the line, the Difficulty increases by the total number of guards in that line.</p>",
"description": "",
"chatDisplay": true,
"actionType": "passive",
"cost": [],
@ -308,7 +309,7 @@
"type": "attack",
"_id": "TK5R00afB1RIA6gp",
"systemPath": "actions",
"description": "<p>Make an attack against a target within Very Close range. On a success, <strong>mark a Stress</strong> to Restrain the target until they break free with a successful attack, Finesse Roll, or Strength Roll.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -358,7 +359,8 @@
},
"originItemType": null,
"subType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"effects": [
{

View file

@ -120,7 +120,8 @@
"source": "Daggerheart SRD",
"page": 83,
"artist": ""
}
},
"size": "medium"
},
"flags": {},
"_id": "2UeZ0tEe7AzgSJNd",
@ -231,7 +232,7 @@
"_id": "yBaLF9DwPH2GSRKf",
"img": "icons/magic/time/hourglass-brown-orange.webp",
"system": {
"description": "<p>When you spotlight the Zombie and they dont have a token on their stat block, they cant act yet. Place a token on their stat block and describe what theyre preparing to do. When you spotlight the Zombie and they have a token on their stat block, clear the token and they can act.</p>",
"description": "<p>When you spotlight the @Lookup[@name] and they dont have a token on their stat block, they cant act yet. Place a token on their stat block and describe what theyre preparing to do. When you spotlight the @Lookup[@name] and they have a token on their stat block, clear the token and they can act.</p>",
"resource": {
"type": "simple",
"value": 0,
@ -262,14 +263,14 @@
"_id": "LP7xVLMTkJsmiIvl",
"img": "icons/skills/melee/strike-slashes-red.webp",
"system": {
"description": "<p>Make a standard attack with advantage against a target the Zombie has <em>Restrained</em>. On a success, the attack deals direct damage.</p>",
"description": "<p>Make a standard attack with advantage against a target the @Lookup[@name] has <em>Restrained</em>. On a success, the attack deals direct damage.</p>",
"resource": null,
"actions": {
"qCcWw60cPZnEWbpG": {
"type": "attack",
"_id": "qCcWw60cPZnEWbpG",
"systemPath": "actions",
"description": "<p>Make a standard attack with advantage against a target the Zombie has <em>Restrained</em>. On a success, the attack deals direct damage.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -307,7 +308,8 @@
}
}
],
"includeBase": false
"includeBase": false,
"direct": true
},
"target": {
"type": "any",
@ -319,7 +321,7 @@
"trait": null,
"difficulty": null,
"bonus": null,
"advState": "neutral",
"advState": "advantage",
"diceRolling": {
"multiplier": "prof",
"flatMultiplier": 1,
@ -341,7 +343,8 @@
},
"originItemType": null,
"subType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"effects": [],
"folder": null,
@ -362,14 +365,14 @@
"_id": "69reUZ5tv3splqyO",
"img": "icons/creatures/abilities/mouth-teeth-lamprey-red.webp",
"system": {
"description": "<p>When the Zombies makes a successful standard attack, you can <strong>mark a Stress</strong> to temporarily Restrain the target and force them to mark 2 Stress.</p>",
"description": "<p>When the @Lookup[@name] makes a successful standard attack, you can <strong>mark a Stress</strong> to temporarily Restrain the target and force them to mark 2 Stress.</p>",
"resource": null,
"actions": {
"xV1z3dk9c7jIkk7v": {
"type": "damage",
"_id": "xV1z3dk9c7jIkk7v",
"systemPath": "actions",
"description": "<p>When the Zombies makes a successful standard attack, you can <strong>mark a Stress</strong> to temporarily Restrain the target and force them to mark 2 Stress.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -432,7 +435,8 @@
},
"originItemType": null,
"subType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"effects": [
{

View file

@ -39,7 +39,8 @@
"experiences": {
"7GpgCWSe6hNwnOO7": {
"name": "Throw",
"value": 2
"value": 2,
"description": ""
}
},
"bonuses": {
@ -105,7 +106,8 @@
},
"base": false
}
]
],
"direct": true
},
"name": "Club",
"img": "icons/weapons/clubs/club-banded-barbed-black.webp",
@ -116,7 +118,8 @@
"source": "Daggerheart SRD",
"page": 75,
"artist": ""
}
},
"size": "large"
},
"flags": {},
"_id": "8Zkqk1jU09nKL2fy",
@ -227,14 +230,14 @@
"_id": "ynuyMl1sMQYINfcQ",
"img": "icons/weapons/clubs/club-spiked-glowing.webp",
"system": {
"description": "<p>You must <strong>spend a Fear</strong> to spotlight the Ogre. While spotlighted, they can make their standard attack against all targets within range.</p>",
"description": "<p>You must <strong>spend a Fear</strong> to spotlight the @Lookup[@name]. While spotlighted, they can make their standard attack against all targets within range.</p>",
"resource": null,
"actions": {
"UoZ6vXRXvWYjpJpZ": {
"type": "effect",
"_id": "UoZ6vXRXvWYjpJpZ",
"systemPath": "actions",
"description": "<p>You must <strong>spend a Fear</strong> to spotlight the Ogre. While spotlighted, they can make their standard attack against all targets within range.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -283,7 +286,7 @@
"_id": "szu5YYQ6klkDbqAT",
"img": "icons/skills/wounds/bone-broken-marrow-red.webp",
"system": {
"description": "<p>The Ogres attacks deal direct damage.</p>",
"description": "<p>The @Lookup[@name]s attacks deal direct damage.</p>",
"resource": null,
"actions": {},
"originItemType": null,
@ -309,14 +312,14 @@
"_id": "zGvaBYJPOOnQVQEn",
"img": "icons/magic/earth/projectile-stone-boulder-orange.webp",
"system": {
"description": "<p><strong>Mark a Stress</strong> to pick up heavy objects and throw them at all targets in front of the Ogre within Far range. Make an attack against these targets. Targets the Ogre succeeds against take <strong>1d10+2</strong> physical damage. If they succeed against more than one target, you gain a Fear.</p><p>@Template[type:inFront|range:f]</p>",
"description": "<p><strong>Mark a Stress</strong> to pick up heavy objects and throw them at all targets in front of the @Lookup[@name] within Far range. Make an attack against these targets. Targets the @Lookup[@name] succeeds against take <strong>1d10+2</strong> physical damage. If they succeed against more than one target, you gain a Fear.</p><p>@Template[type:inFront|range:f]</p>",
"resource": null,
"actions": {
"3p1qfHy5uHe4H2hB": {
"type": "attack",
"_id": "3p1qfHy5uHe4H2hB",
"systemPath": "actions",
"description": "<p><strong>Mark a Stress</strong> to pick up heavy objects and throw them at all targets in front of the Ogre within Far range. Make an attack against these targets. Targets the Ogre succeeds against take <strong>1d10+2</strong> physical damage. If they succeed against more than one target, you gain a Fear.</p><p>@Template[type:inFront|range:f]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -337,10 +340,11 @@
{
"value": {
"custom": {
"enabled": false
"enabled": false,
"formula": ""
},
"flatMultiplier": 1,
"dice": "d12",
"dice": "d10",
"bonus": 2,
"multiplier": "flat"
},
@ -356,12 +360,14 @@
"dice": "d6",
"bonus": null,
"custom": {
"enabled": false
"enabled": false,
"formula": ""
}
}
}
],
"includeBase": false
"includeBase": false,
"direct": true
},
"target": {
"type": "any",
@ -462,7 +468,8 @@
},
"originItemType": null,
"subType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"effects": [],
"folder": null,
@ -483,14 +490,14 @@
"_id": "Qxkddj6nQc4RDExW",
"img": "icons/skills/melee/strike-flail-destructive-yellow.webp",
"system": {
"description": "<p>When the Ogre marks 2 or more HP, they can rampage. Move the Ogre to a point within Close range and deal <strong>2d6+3</strong> direct physical damage to all targets in their path.</p>",
"description": "<p>When the @Lookup[@name] marks 2 or more HP, they can rampage. Move the @Lookup[@name] to a point within Close range and deal <strong>2d6+3</strong> direct physical damage to all targets in their path.</p>",
"resource": null,
"actions": {
"PtTu9bnCJKMySBSV": {
"type": "damage",
"_id": "PtTu9bnCJKMySBSV",
"systemPath": "actions",
"description": "<p>When the Ogre marks 2 or more HP, they can rampage. Move the Ogre to a point within Close range and deal <strong>2d6+3</strong> direct physical damage to all targets in their path.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -528,7 +535,8 @@
}
}
],
"includeBase": false
"includeBase": false,
"direct": true
},
"target": {
"type": "any",
@ -542,7 +550,8 @@
},
"originItemType": null,
"subType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"effects": [],
"folder": null,

View file

@ -111,7 +111,8 @@
"source": "Daggerheart SRD",
"page": 85,
"artist": ""
}
},
"size": "tiny"
},
"flags": {},
"_id": "jDmHqGvzg5wjgmxE",
@ -220,7 +221,7 @@
"name": "Levitation",
"type": "feature",
"system": {
"description": "<p>The Skull levitates several feet off the ground and cant be <em>Restrained</em>.</p>",
"description": "<p>The @Lookup[@name] levitates several feet off the ground and cant be <em>Restrained</em>.</p>",
"resource": null,
"actions": {},
"originItemType": null,
@ -289,7 +290,7 @@
"name": "Wards",
"type": "feature",
"system": {
"description": "<p> The Skull is resistant to magic damage.</p>",
"description": "<p>The @Lookup[@name] is resistant to magic damage.</p>",
"resource": null,
"actions": {},
"originItemType": null,
@ -358,14 +359,14 @@
"name": "Magic Burst",
"type": "feature",
"system": {
"description": "<p><strong>Mark a Stress</strong> to make an attack against all targets within Close range. Targets the Skull succeeds against take <strong>2d6+4</strong> magic damage.</p><p>@Template[type:emanation|range:c]</p>",
"description": "<p><strong>Mark a Stress</strong> to make an attack against all targets within Close range. Targets the @Lookup[@name] succeeds against take <strong>2d6+4</strong> magic damage.</p><p>@Template[type:emanation|range:c]</p>",
"resource": null,
"actions": {
"iF0PD1t3yovKMTfy": {
"type": "attack",
"_id": "iF0PD1t3yovKMTfy",
"systemPath": "actions",
"description": "<p><strong>Mark a Stress</strong> to make an attack against all targets within Close range. Targets the Skull succeeds against take <strong>2d6+4</strong> magic damage.</p><p>@Template[type:emanation|range:c]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -443,7 +444,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "Zn25zBr96y1hrmnr",
"img": "icons/magic/lightning/bolt-strike-purple.webp",
@ -464,14 +466,14 @@
"name": "Siphon Magic",
"type": "feature",
"system": {
"description": "<p><strong>Spend a Fear</strong> to make an attack against a PC with a Spellcast trait within Very Close range. On a success, the target marks <strong>1d4</strong> Stress and the Skull clears that many Stress. Additionally, on a success, the Skull can immediately be spotlighted again.</p>",
"description": "<p><strong>Spend a Fear</strong> to make an attack against a PC with a Spellcast trait within Very Close range. On a success, the target marks <strong>1d4</strong> Stress and the @Lookup[@name] clears that many Stress. Additionally, on a success, the @Lookup[@name] can immediately be spotlighted again.</p>",
"resource": null,
"actions": {
"872Fq88Hitwc6f3W": {
"type": "attack",
"_id": "872Fq88Hitwc6f3W",
"systemPath": "actions",
"description": "<p><strong>Spend a Fear</strong> to make an attack against a PC with a Spellcast trait within Very Close range. On a success, the target marks <strong>1d4</strong> Stress and the Skull clears that many Stress. Additionally, on a success, the Skull can immediately be spotlighted again.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -540,7 +542,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "urXRi4bdBfvl8U6K",
"img": "icons/magic/control/sihouette-hold-beam-green.webp",

View file

@ -104,7 +104,8 @@
"source": "Daggerheart SRD",
"page": 85,
"artist": ""
}
},
"size": "medium"
},
"flags": {},
"_id": "99TqczuQipBmaB8i",
@ -213,7 +214,7 @@
"name": "Minion (6)",
"type": "feature",
"system": {
"description": "<p>The Conscript is defeated when they take any damage. For every 6 damage a PC deals to the Conscript, defeat an additional Minion within range the attack would succeed against.</p>",
"description": "<p>The @Lookup[@name] is defeated when they take any damage. For every 6 damage a PC deals to the @Lookup[@name], defeat an additional Minion within range the attack would succeed against.</p>",
"resource": null,
"actions": {},
"originItemType": null,
@ -238,14 +239,14 @@
"name": "Group Attack",
"type": "feature",
"system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all Conscripts within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 6 physical damage each. Combine this damage.</p>",
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 6 physical damage each. Combine this damage.</p>",
"resource": null,
"actions": {
"cbAvPSIhwBMBTI3D": {
"type": "effect",
"_id": "cbAvPSIhwBMBTI3D",
"systemPath": "actions",
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all Conscripts within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 6 physical damage each. Combine this damage.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -272,7 +273,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "MWfKUGzT1YBmLvpn",
"img": "icons/creatures/abilities/tail-strike-bone-orange.webp",

View file

@ -111,7 +111,8 @@
"source": "Daggerheart SRD",
"page": 75,
"artist": ""
}
},
"size": "medium"
},
"flags": {},
"_id": "uOP5oT9QzXPlnf3p",
@ -222,14 +223,14 @@
"_id": "y3oUmDLGkcSjOO5Q",
"img": "icons/magic/unholy/silhouette-evil-horned-giant.webp",
"system": {
"description": "<p>The Construct can be spotlighted up to two times per GM turn. Spend Fear as usual to spotlight them.</p>",
"description": "<p>The @Lookup[@name] can be spotlighted up to two times per GM turn. Spend Fear as usual to spotlight them.</p>",
"resource": null,
"actions": {
"bay0pyPsCyDEZKuk": {
"type": "effect",
"_id": "bay0pyPsCyDEZKuk",
"systemPath": "actions",
"description": "<p>The Construct can be spotlighted up to two times per GM turn. Spend Fear as usual to spotlight them.</p>",
"description": "",
"chatDisplay": true,
"actionType": "passive",
"cost": [
@ -281,7 +282,7 @@
"_id": "p4HLIkiM3HsglRoA",
"img": "icons/commodities/metal/barstock-broken-steel.webp",
"system": {
"description": "<p>When the Construct marks HP from physical damage, they must mark an additional HP.</p>",
"description": "<p>When the @Lookup[@name] marks HP from physical damage, they must mark an additional HP.</p>",
"resource": null,
"actions": {},
"originItemType": null,
@ -307,14 +308,14 @@
"_id": "93m085bEaKFzvEWT",
"img": "icons/skills/movement/arrow-upward-blue.webp",
"system": {
"description": "<p><strong>Mark a Stress</strong> to make an attack against all targets in the Constructs path when they move. Targets the Construct succeeds against take <strong>1d8</strong> physical damage.</p>",
"description": "<p><strong>Mark a Stress</strong> to make an attack against all targets in the @Lookup[@name]s path when they move. Targets the @Lookup[@name] succeeds against take <strong>1d8</strong> physical damage.</p>",
"resource": null,
"actions": {
"OswphW4Z1B5oa4ts": {
"type": "attack",
"_id": "OswphW4Z1B5oa4ts",
"systemPath": "actions",
"description": "<p><strong>Mark a Stress</strong> to make an attack against all targets in the Constructs path when they move. Targets the Construct succeeds against take <strong>1d8</strong> physical damage.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -393,7 +394,8 @@
},
"originItemType": null,
"subType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"effects": [],
"folder": null,
@ -414,14 +416,14 @@
"_id": "EF6YIDjQ0liFubGA",
"img": "icons/creatures/magical/construct-golem-stone-blue.webp",
"system": {
"description": "<p>Before rolling damage for the Constructs attack, you can <strong>mark a Stress</strong> to gain a +10 bonus to the damage roll. The Construct can then take the spotlight again.</p>",
"description": "<p>Before rolling damage for the @Lookup[@name]s attack, you can <strong>mark a Stress</strong> to gain a +10 bonus to the damage roll. The @Lookup[@name] can then take the spotlight again.</p>",
"resource": null,
"actions": {
"xYACTiZzApmCXXmf": {
"type": "effect",
"_id": "xYACTiZzApmCXXmf",
"systemPath": "actions",
"description": "<p>Before rolling damage for the Constructs attack, you can <strong>mark a Stress</strong> to gain a +10 bonus to the damage roll. The Construct can then take the spotlight again.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -449,7 +451,8 @@
},
"originItemType": null,
"subType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"effects": [
{
@ -514,14 +517,14 @@
"_id": "UlGLuV1L33tDWkli",
"img": "icons/magic/sonic/explosion-shock-wave-teal.webp",
"system": {
"description": "<p>When the Construct marks their last HP, the magic powering them ruptures in an explosion of force. Make an attack with advantage against all targets within Very Close range. Targets the Construct succeeds against take <strong>1d12+2</strong> magic damage.</p>",
"description": "<p>When the @Lookup[@name] marks their last HP, the magic powering them ruptures in an explosion of force. Make an attack with advantage against all targets within Very Close range. Targets the @Lookup[@name] succeeds against take <strong>1d12+2</strong> magic damage.</p>",
"resource": null,
"actions": {
"fkIWRdcGPgHgm6VC": {
"type": "attack",
"_id": "fkIWRdcGPgHgm6VC",
"systemPath": "actions",
"description": "<p>When the Construct marks their last HP, the magic powering them ruptures in an explosion of force. Make an attack with advantage against all targets within Very Close range. Targets the Construct succeeds against take <strong>1d12+2</strong> magic damage.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -593,7 +596,8 @@
},
"originItemType": null,
"subType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"effects": [],
"folder": null,

View file

@ -122,7 +122,8 @@
"source": "Daggerheart SRD",
"page": 85,
"artist": ""
}
},
"size": "medium"
},
"flags": {},
"_id": "ZxWaWPdzFIUPNC62",
@ -231,14 +232,14 @@
"name": "Searing Glance",
"type": "feature",
"system": {
"description": "<p>When a PC within Close range makes a Presence Roll, you can <strong>mark a Stress</strong> to cast a gaze toward the aftermath. On the targets failure, they must mark 2 Stress and are <em>Vulnerable</em> until the scene ends or they succeed on a social action against the Courtesan. On the targets success, they must mark a Stress.</p>",
"description": "<p>When a PC within Close range makes a Presence Roll, you can <strong>mark a Stress</strong> to cast a gaze toward the aftermath. On the targets failure, they must mark 2 Stress and are <em>Vulnerable</em> until the scene ends or they succeed on a social action against the @Lookup[@name]. On the targets success, they must mark a Stress.</p>",
"resource": null,
"actions": {
"dRtDCrAPLc1GYqBs": {
"type": "damage",
"_id": "dRtDCrAPLc1GYqBs",
"systemPath": "actions",
"description": "<p>When a PC within Close range makes a Presence Roll, you can <strong>mark a Stress</strong> to cast a gaze toward the aftermath. On the targets failure, they must mark 2 Stress and are <em>Vulnerable</em> until the scene ends or they succeed on a social action against the Courtesan. On the targets success, they must mark a Stress.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -300,7 +301,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"_id": "rSMUPC5GhR982ifg",
"img": "icons/magic/perception/eye-slit-orange.webp",

View file

@ -117,7 +117,8 @@
"source": "Daggerheart SRD",
"page": 76,
"artist": ""
}
},
"size": "medium"
},
"flags": {},
"_id": "CBBuEXAlLKFMJdjg",
@ -235,7 +236,7 @@
"type": "attack",
"_id": "Yi3rvjj0Umqt5Z8j",
"systemPath": "actions",
"description": "<p><strong>Mark a Stress</strong> to say something mocking and force a target within Close range to make a Presence Reaction Roll (14) to see if they can save face. On a failure, the target must mark 2 Stress and is Vulnerable until the scene ends.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -318,7 +319,8 @@
},
"originItemType": null,
"subType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"effects": [
{
@ -378,14 +380,14 @@
"_id": "Ux42ELBBuSYwm4yW",
"img": "icons/skills/social/diplomacy-unity-alliance.webp",
"system": {
"description": "<p><strong>Spend a Fear</strong> and target a PC. The Courtier convinces a crowd or prominent individual that the target is the cause of their current conflict or misfortune.</p>",
"description": "<p><strong>Spend a Fear</strong> and target a PC. The @Lookup[@name] convinces a crowd or prominent individual that the target is the cause of their current conflict or misfortune.</p>",
"resource": null,
"actions": {
"IwuFowlcXyjvfOxp": {
"type": "effect",
"_id": "IwuFowlcXyjvfOxp",
"systemPath": "actions",
"description": "<p><strong>Spend a Fear</strong> and target a PC. The Courtier convinces a crowd or prominent individual that the target is the cause of their current conflict or misfortune.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -413,7 +415,8 @@
},
"originItemType": null,
"subType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"effects": [],
"folder": null,

View file

@ -122,7 +122,8 @@
"source": "Daggerheart SRD",
"page": 85,
"artist": ""
}
},
"size": "medium"
},
"flags": {},
"_id": "0NxCSugvKQ4W8OYZ",
@ -238,7 +239,7 @@
"type": "attack",
"_id": "TQv3o9sRnlDNbPyu",
"systemPath": "actions",
"description": "<p><strong>Spend a Fear</strong> to make a standard attack against a target within range. On a success, the target must mark a Stress.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -341,7 +342,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "kCffzM8rX8NEr9d2",
"img": "icons/magic/unholy/beam-ringed-impact-purple.webp",
@ -362,14 +364,14 @@
"name": "Shroud of the Fallen",
"type": "feature",
"system": {
"description": "<p><strong>Mark a Stress</strong> to wrap an ally within Close range in a shroud of Protection until the Adept marks their last HP. While Protected, the target has resistance to all damage.</p>",
"description": "<p><strong>Mark a Stress</strong> to wrap an ally within Close range in a shroud of Protection until the @Lookup[@name] marks their last HP. While Protected, the target has resistance to all damage.</p>",
"resource": null,
"actions": {
"8yRj7EpEI4PlKNhl": {
"type": "effect",
"_id": "8yRj7EpEI4PlKNhl",
"systemPath": "actions",
"description": "<p><strong>Mark a Stress</strong> to wrap an ally within Close range in a shroud of Protection until the Adept marks their last HP. While Protected, the target has resistance to all damage.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -401,7 +403,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "IHWDn097sRgjlZXO",
"img": "icons/magic/unholy/orb-contained-pink.webp",
@ -479,7 +482,7 @@
"type": "effect",
"_id": "g4RDHrY0AEYXjH52",
"systemPath": "actions",
"description": "<p><strong>Spend a Fear</strong> and choose a point within Far range. All targets within Close range of that point are <em>Restrained</em> in smoky chains until they break free with a successful Strength or Instinct Roll. A target Restrained by this feature must spend a Hope to make an action roll.</p><p>@Template[type:circle|range:c]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -511,7 +514,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "JpSrduK3vjd9h098",
"img": "icons/magic/air/fog-gas-smoke-dense-pink.webp",
@ -578,7 +582,7 @@
"type": "healing",
"_id": "3tibqB97ooJesxf0",
"systemPath": "actions",
"description": "<p>Twice per scene, when a PC rolls a failure with Fear, clear a Stress.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -643,7 +647,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"_id": "x6FbcrfOscb3er6P",
"img": "icons/magic/unholy/silhouette-robe-evil-glow.webp",

View file

@ -111,7 +111,8 @@
"source": "Daggerheart SRD",
"page": 86,
"artist": ""
}
},
"size": "medium"
},
"flags": {},
"_id": "tyBOpLfigAhI9bU3",
@ -220,14 +221,14 @@
"name": "Shadow's Embrace",
"type": "feature",
"system": {
"description": "<p>The Fang can climb and walk on vertical surfaces. <strong>Mark a Stress</strong> to move from one shadow to another within Far range.</p>",
"description": "<p>The @Lookup[@name] can climb and walk on vertical surfaces. <strong>Mark a Stress</strong> to move from one shadow to another within Far range.</p>",
"resource": null,
"actions": {
"hjuqvsMB7KNLNvjg": {
"type": "effect",
"_id": "hjuqvsMB7KNLNvjg",
"systemPath": "actions",
"description": "<p>The Fang can climb and walk on vertical surfaces. <strong>Mark a Stress</strong> to move from one shadow to another within Far range.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -275,14 +276,14 @@
"name": "Pick Off the Straggler",
"type": "feature",
"system": {
"description": "<p><strong>Mark a Stress</strong> to cause a target within Melee range to make an Instinct Reaction Roll. On a failure, the target must mark 2 Stress and is teleported with the Fang to a shadow within Far range, making them temporarily Vulnerable. On a success, the target must mark a Stress.</p>",
"description": "<p><strong>Mark a Stress</strong> to cause a target within Melee range to make an Instinct Reaction Roll. On a failure, the target must mark 2 Stress and is teleported with the @Lookup[@name] to a shadow within Far range, making them temporarily Vulnerable. On a success, the target must mark a Stress.</p>",
"resource": null,
"actions": {
"QjQ04SAwfjrxliNI": {
"type": "attack",
"_id": "QjQ04SAwfjrxliNI",
"systemPath": "actions",
"description": "<p><strong>Mark a Stress</strong> to cause a target within Melee range to make an Instinct Reaction Roll. On a failure, the target must mark 2 Stress and is teleported with the Fang to a shadow within Far range, making them temporarily Vulnerable. On a success, the target must mark a Stress.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -364,7 +365,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "ohASSruBxcvuItIK",
"img": "icons/magic/unholy/barrier-fire-pink.webp",

View file

@ -104,7 +104,8 @@
"source": "Daggerheart SRD",
"page": 86,
"artist": ""
}
},
"size": "medium"
},
"flags": {},
"_id": "zx99sOGTXicP4SSD",
@ -213,7 +214,7 @@
"name": "Minion (6)",
"type": "feature",
"system": {
"description": "<p>The Initiate is defeated when they take any damage. For every 6 damage a PC deals to the Initiate, defeat an additional Minion within range the attack would succeed against.</p>",
"description": "<p>The @Lookup[@name] is defeated when they take any damage. For every 6 damage a PC deals to the @Lookup[@name], defeat an additional Minion within range the attack would succeed against.</p>",
"resource": null,
"actions": {},
"originItemType": null,
@ -238,14 +239,14 @@
"name": "Group Attack",
"type": "feature",
"system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all Cult Initiates within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 5 physical damage each. Combine this damage.</p>",
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all Cult @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 5 physical damage each. Combine this damage.</p>",
"resource": null,
"actions": {
"EH1preaTWBD4rOvx": {
"type": "effect",
"_id": "EH1preaTWBD4rOvx",
"systemPath": "actions",
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all Cult Initiates within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 5 physical damage each. Combine this damage.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -272,7 +273,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "WP6xQtYzouPEFr82",
"img": "icons/creatures/abilities/tail-strike-bone-orange.webp",

View file

@ -116,7 +116,8 @@
"source": "Daggerheart SRD",
"page": 76,
"artist": ""
}
},
"size": "medium"
},
"flags": {},
"_id": "9x2xY9zwc3xzbXo5",
@ -234,7 +235,7 @@
"type": "damage",
"_id": "55hCZsJQhJNcZ0lX",
"systemPath": "actions",
"description": "<p>Slam the ground, knocking all targets within Very Close range back to Far range. Each target knocked back this way must mark a Stress.</p><p>@Template[type:emanation|range:vc]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -285,7 +286,8 @@
},
"originItemType": null,
"subType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"effects": [],
"folder": null,
@ -306,14 +308,14 @@
"_id": "rreGFW5TbhUoZf2T",
"img": "icons/magic/nature/root-vine-entangled-hand.webp",
"system": {
"description": "<p>Make an attack against a target within Close range. On a success, <strong>spend a Fear</strong> to pull them into Melee range, deal <strong>1d6+2</strong> physical damage, and <em>Restrain</em> them until the Defender takes Severe damage.</p>",
"description": "<p>Make an attack against a target within Close range. On a success, <strong>spend a Fear</strong> to pull them into Melee range, deal <strong>1d6+2</strong> physical damage, and <em>Restrain</em> them until the @Lookup[@name] takes Severe damage.</p>",
"resource": null,
"actions": {
"nQ3vXrrKBizZoaDt": {
"type": "attack",
"_id": "nQ3vXrrKBizZoaDt",
"systemPath": "actions",
"description": "<p>Make an attack against a target within Close range. On a success, <strong>spend a Fear</strong> to pull them into Melee range, deal <strong>1d6+2</strong> physical damage, and <em>Restrain</em> them until the Defender takes Severe damage.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -388,7 +390,8 @@
},
"originItemType": null,
"subType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"effects": [
{

View file

@ -117,7 +117,8 @@
"source": "Daggerheart SRD",
"page": 91,
"artist": ""
}
},
"size": "medium"
},
"flags": {},
"_id": "pnyjIGxxvurcWmTv",
@ -226,7 +227,7 @@
"name": "Money Talks",
"type": "feature",
"system": {
"description": "<p>Attacks against the Demon are made with disadvantage unless the attacker spends a handful of gold. This Demon starts with a number of handfuls equal to the number of PCs. When a target marks HP from the Demons standard attack, they can spend a handful of gold instead of marking HP (1 handful per HP). Add a handful of gold to the Demon for each handful of gold spent by PCs on this feature.</p>",
"description": "<p>Attacks against the @Lookup[@name] are made with disadvantage unless the attacker spends a handful of gold. This @Lookup[@name] starts with a number of handfuls equal to the number of PCs. When a target marks HP from the @Lookup[@name]s standard attack, they can spend a handful of gold instead of marking HP (1 handful per HP). Add a handful of gold to the @Lookup[@name] for each handful of gold spent by PCs on this feature.</p>",
"resource": null,
"actions": {},
"originItemType": null,
@ -251,7 +252,7 @@
"name": "Number Must Go Up",
"type": "feature",
"system": {
"description": "<p>Add a bonus to the Demons attack rolls equal to the number of handfuls of gold they have.</p>",
"description": "<p>Add a bonus to the @Lookup[@name]s attack rolls equal to the number of handfuls of gold they have.</p>",
"resource": {
"type": "simple",
"value": 0,
@ -283,7 +284,7 @@
"key": "system.bonuses.roll.attack.bonus",
"mode": 2,
"value": "ITEM.@system.resource.value",
"priority": null
"priority": 21
}
],
"disabled": false,
@ -329,7 +330,8 @@
"resource": null,
"actions": {},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "BKgv2D1IdI813R8k",
"img": "icons/skills/movement/arrows-up-trio-red.webp",

View file

@ -117,7 +117,8 @@
"source": "Daggerheart SRD",
"page": 92,
"artist": ""
}
},
"size": "medium"
},
"flags": {},
"_id": "kE4dfhqmIQpNd44e",
@ -226,7 +227,7 @@
"name": "Depths of Despair",
"type": "feature",
"system": {
"description": "<p>The Demon deals double damage to PCs with 0 Hope.</p>",
"description": "<p>The @Lookup[@name] deals double damage to PCs with 0 Hope.</p>",
"resource": null,
"actions": {},
"originItemType": null,
@ -258,7 +259,7 @@
"type": "effect",
"_id": "n0RYO05pROFU6ov3",
"systemPath": "actions",
"description": "<p><strong>Spend a Fear</strong> to weigh down the spirits of all PCs within Far range. All targets affected replace their Hope Die with a <strong>d8</strong> until they roll a success with Hope or their next rest.</p><p>@Template[type:emanation|range:f]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -290,7 +291,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "FC8PIf4BVkhmoJX8",
"img": "icons/magic/death/skull-flames-white-blue.webp",
@ -355,7 +357,7 @@
"type": "damage",
"_id": "urrp8SCFgqbmSTvm",
"systemPath": "actions",
"description": "<p>When a PC fails with Fear, you can <strong>mark a Stress</strong> to cause all other PCs within Close range to lose a Hope.</p><p>@Template[type:emanation|range:c]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -412,7 +414,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"_id": "dlMdfUjy2GaqgeOJ",
"img": "icons/creatures/unholy/demon-fire-horned-mask.webp",
@ -433,14 +436,14 @@
"name": "Momentum",
"type": "feature",
"system": {
"description": "<p>When the Demon makes a successful attack against a PC, you gain a Fear.</p>",
"description": "<p>When the @Lookup[@name] makes a successful attack against a PC, you gain a Fear.</p>",
"resource": null,
"actions": {
"N0Xx6GnijLXIMGBw": {
"type": "healing",
"_id": "N0Xx6GnijLXIMGBw",
"systemPath": "actions",
"description": "<p>When the Demon makes a successful attack against a PC, you gain a Fear.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -507,7 +510,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"_id": "7qjx1c4C1fUfvXnu",
"img": "icons/skills/melee/strike-weapons-orange.webp",

View file

@ -117,7 +117,8 @@
"source": "Daggerheart SRD",
"page": 92,
"artist": ""
}
},
"size": "medium"
},
"flags": {},
"_id": "2VN3BftageoTTIzu",
@ -226,14 +227,14 @@
"name": "Terrifying",
"type": "feature",
"system": {
"description": "<p>When the Demon makes a successful attack, all PCs within Far range must lose a Hope and you gain a Fear.</p><p>@Template[type:emanation|range:f]</p>",
"description": "<p>When the @Lookup[@name] makes a successful attack, all PCs within Far range must lose a Hope and you gain a Fear.</p><p>@Template[type:emanation|range:f]</p>",
"resource": null,
"actions": {
"v3XbljQeHEyfuSXz": {
"type": "damage",
"_id": "v3XbljQeHEyfuSXz",
"systemPath": "actions",
"description": "<p>When the Demon makes a successful attack, all PCs within Far range must lose a Hope and you gain a Fear.</p><p>@Template[type:emanation|range:f]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -318,7 +319,7 @@
"type": "damage",
"_id": "nNfWqBgysVPtFh4w",
"systemPath": "actions",
"description": "<p>When a PC within Far range fails a roll, they can choose to reroll their Fear Die and take the new result. If they still fail, they mark 2 Stress and the Demon clears a Stress.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -389,14 +390,14 @@
"name": "Unparalleled Skill",
"type": "feature",
"system": {
"description": "<p><strong>Mark a Stress</strong> to deal the Demons standard attack damage to a target within Close range.</p>",
"description": "<p><strong>Mark a Stress</strong> to deal the @Lookup[@name]s standard attack damage to a target within Close range.</p>",
"resource": null,
"actions": {
"MYOD2VAfdVC6hMCs": {
"type": "damage",
"_id": "MYOD2VAfdVC6hMCs",
"systemPath": "actions",
"description": "<p><strong>Mark a Stress</strong> to deal the Demons standard attack damage to a target within Close range.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -454,7 +455,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "Y3W44ifKIcoYpONN",
"img": "icons/skills/melee/spear-tips-quintuple-orange.webp",
@ -482,7 +484,7 @@
"type": "effect",
"_id": "ozGST8UY2MJnrd3w",
"systemPath": "actions",
"description": "<p><strong>Spend a Fear</strong> to spotlight two other Demons within Far range.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -509,7 +511,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "6BKWOTuxQWJd5RP5",
"img": "icons/creatures/unholy/demon-fire-horned-winged-roar.webp",
@ -530,14 +533,14 @@
"name": "You Pale in Comparison",
"type": "feature",
"system": {
"description": "<p>When a PC fails a roll within Close range of the Demon, they must mark a Stress.</p><p>@Template[type:emanation|range:c]</p>",
"description": "<p>When a PC fails a roll within Close range of the @Lookup[@name], they must mark a Stress.</p><p>@Template[type:emanation|range:c]</p>",
"resource": null,
"actions": {
"kuCPWb9cu3pZdAhh": {
"type": "damage",
"_id": "kuCPWb9cu3pZdAhh",
"systemPath": "actions",
"description": "<p>When a PC fails a roll within Close range of the Demon, they must mark a Stress.</p><p>@Template[type:emanation|range:c]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -587,7 +590,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"_id": "FLp1dPSJz1ezY0gD",
"img": "icons/magic/control/fear-fright-shadow-monster-red.webp",

View file

@ -107,7 +107,8 @@
},
"base": false
}
]
],
"direct": true
},
"img": "icons/magic/symbols/rune-sigil-rough-white-teal.webp",
"type": "attack",
@ -117,7 +118,8 @@
"source": "Daggerheart SRD",
"page": 92,
"artist": ""
}
},
"size": "medium"
},
"flags": {},
"_id": "SxSOkM4bcVOFyjbo",
@ -226,7 +228,7 @@
"name": "Unprotected Mind",
"type": "feature",
"system": {
"description": "<p> The Demons standard attack deals direct damage.</p>",
"description": "<p>The @Lookup[@name]s standard attack deals direct damage.</p>",
"resource": null,
"actions": {},
"originItemType": null,
@ -251,11 +253,12 @@
"name": "My Turn",
"type": "feature",
"system": {
"description": "<p>When the Demon marks HP from an attack, <strong>spend a number of Fear equal to the HP marked by the Demon</strong> to cause the attacker to mark the same number of HP.</p>",
"description": "<p>When the @Lookup[@name] marks HP from an attack, <strong>spend a number of Fear equal to the HP marked by the @Lookup[@name]</strong> to cause the attacker to mark the same number of HP.</p>",
"resource": null,
"actions": {},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"_id": "KVyhgMJSSHTwRISA",
"img": "icons/magic/control/fear-fright-monster-grin-purple-blue.webp",
@ -283,7 +286,7 @@
"type": "effect",
"_id": "UU3H5aPQejOSoFZw",
"systemPath": "actions",
"description": "<p>When a creature within Close range takes damage from a different adversary, you can <strong>mark a Stress</strong> to add a <strong>d4</strong> to the damage roll.</p><p>@Template[type:emanation|range:c]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -310,7 +313,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"_id": "pSAupMWw1eYqm84z",
"img": "icons/magic/perception/eye-ringed-glow-angry-small-teal.webp",
@ -331,14 +335,14 @@
"name": "What's Yours Is Mine",
"type": "feature",
"system": {
"description": "<p>When a PC takes Severe damage within Very Close range of the Demon, you can <strong>spend a Fear</strong> to cause the target to make a Finesse Reaction Roll. On a failure, the Demon seizes one item or consumable of their choice from the targets inventory.</p><p>@Template[type:emanation|range:vc]</p>",
"description": "<p>When a PC takes Severe damage within Very Close range of the @Lookup[@name], you can <strong>spend a Fear</strong> to cause the target to make a Finesse Reaction Roll. On a failure, the @Lookup[@name] seizes one item or consumable of their choice from the targets inventory.</p><p>@Template[type:emanation|range:vc]</p>",
"resource": null,
"actions": {
"3cGZ2CofM9HUlELH": {
"type": "attack",
"_id": "3cGZ2CofM9HUlELH",
"systemPath": "actions",
"description": "<p>When a PC takes Severe damage within Very Close range of the Demon, you can <strong>spend a Fear</strong> to cause the target to make a Finesse Reaction Roll. On a failure, the Demon seizes one item or consumable of their choice from the targets inventory.</p><p>@Template[type:emanation|range:vc]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -382,7 +386,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"_id": "589tCxFc8KZ3rdzP",
"img": "icons/magic/perception/hand-eye-black.webp",

View file

@ -108,7 +108,8 @@
},
"base": false
}
]
],
"direct": true
},
"type": "attack",
"chatDisplay": false
@ -117,7 +118,8 @@
"source": "Daggerheart SRD",
"page": 92,
"artist": ""
}
},
"size": "large"
},
"flags": {},
"_id": "5lphJAgzoqZI3VoG",
@ -226,7 +228,7 @@
"name": "Anger Unrelenting",
"type": "feature",
"system": {
"description": "<p>The Demons attacks deal direct damage.</p>",
"description": "<p>The @Lookup[@name]s attacks deal direct damage.</p>",
"resource": null,
"actions": {},
"originItemType": null,
@ -258,7 +260,7 @@
"type": "effect",
"_id": "V142qYppCGJn8OiN",
"systemPath": "actions",
"description": "<p><strong>Spend a Fear</strong> to boil the blood of all PCs within Far range. They use a d20 as their Fear Die until the end of the scene.</p><p>@Template[type:emanation|range:f]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -285,7 +287,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "a33PW8UkziliowlR",
"img": "icons/skills/melee/maneuver-greatsword-yellow.webp",
@ -306,14 +309,14 @@
"name": "Retalliation",
"type": "feature",
"system": {
"description": "<p>When the Demon takes damage from an attack within Close range, you can <strong>mark a Stress</strong> to make a standard attack against the attacker.</p><p>@Template[type:emanation|range:c]</p>",
"description": "<p>When the @Lookup[@name] takes damage from an attack within Close range, you can <strong>mark a Stress</strong> to make a standard attack against the attacker.</p><p>@Template[type:emanation|range:c]</p>",
"resource": null,
"actions": {
"hxrdtBm4dYN7KGZm": {
"type": "attack",
"_id": "hxrdtBm4dYN7KGZm",
"systemPath": "actions",
"description": "<p>When the Demon takes damage from an attack within Close range, you can <strong>mark a Stress</strong> to make a standard attack against the attacker.</p><p>@Template[type:emanation|range:c]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -358,7 +361,8 @@
}
}
],
"includeBase": false
"includeBase": false,
"direct": true
},
"target": {
"type": "any",
@ -391,7 +395,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"_id": "DjGydqLXT4rDa7Av",
"img": "icons/skills/melee/blood-slash-foam-red.webp",
@ -412,14 +417,14 @@
"name": "Blood and Souls",
"type": "feature",
"system": {
"description": "<p><em>Countdown (Loop 6)</em>. Activate the first time an attack is made within sight of the Demon. It ticks down when a PC takes a violent action. When it triggers, summon [[/r 1d4]]@UUID[Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb]{Minor Demons}, who appear at Close range.</p>",
"description": "<p><em>Countdown (Loop 6)</em>. Activate the first time an attack is made within sight of the @Lookup[@name]. It ticks down when a PC takes a violent action. When it triggers, summon [[/r 1d4]]@UUID[Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb]{Minor Demons}, who appear at Close range.</p>",
"resource": null,
"actions": {
"szg3qA09aJUt9WKS": {
"type": "countdown",
"_id": "szg3qA09aJUt9WKS",
"systemPath": "actions",
"description": "<p><em>Countdown (Loop 6)</em>. Activate the first time an attack is made within sight of the Demon. It ticks down when a PC takes a violent action.</p>",
"description": "<p><em>Countdown (Loop 6)</em>. Activate the first time an attack is made within sight of the @Lookup[@name]. It ticks down when a PC takes a violent action.</p>",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
@ -480,7 +485,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"_id": "2F75BO0xEU8Zlj7T",
"img": "icons/creatures/unholy/demon-fire-horned-clawed.webp",

View file

@ -117,7 +117,8 @@
"source": "Daggerheart SRD",
"page": 86,
"artist": ""
}
},
"size": "large"
},
"flags": {},
"_id": "NoRZ1PqB8N5wcIw0",
@ -226,7 +227,7 @@
"name": "Horde (2d4+1)",
"type": "feature",
"system": {
"description": "<p>When the Pack has marked half or more of their HP, their standard attack deals <strong>2d4+1</strong> physical damage instead.</p>",
"description": "<p>When the @Lookup[@name] has marked half or more of their HP, their standard attack deals <strong>2d4+1</strong> physical damage instead.</p>",
"resource": null,
"actions": {},
"originItemType": null,
@ -258,7 +259,7 @@
"type": "damage",
"_id": "XyLlX9RWSxciZ7oV",
"systemPath": "actions",
"description": "<p><strong>Mark a Stress</strong> to make all targets within Very Close range lose a Hope. If a target is not able to lose a Hope, they must instead mark 2 Stress.</p><p>@Template[type:emanation|range:vc]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -308,7 +309,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "WEbHwamS5ZBphiKq",
"img": "icons/creatures/unholy/demons-horned-glowing-pink.webp",
@ -329,14 +331,14 @@
"name": "Momentum",
"type": "feature",
"system": {
"description": "<p>When the Pack makes a successful attack against a PC, you gain a Fear.</p>",
"description": "<p>When the @Lookup[@name] makes a successful attack against a PC, you gain a Fear.</p>",
"resource": null,
"actions": {
"BApDkAKPfyBkqrKY": {
"type": "healing",
"_id": "BApDkAKPfyBkqrKY",
"systemPath": "actions",
"description": "<p>When the Pack makes a successful attack against a PC, you gain a Fear.</p>",
"description": "",
"chatDisplay": true,
"actionType": "reaction",
"cost": [],
@ -403,7 +405,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"_id": "3mOBJE5c3cP2cGP1",
"img": "icons/skills/melee/strike-weapons-orange.webp",

View file

@ -116,7 +116,8 @@
"source": "Daggerheart SRD",
"page": 93,
"artist": ""
}
},
"size": "medium"
},
"flags": {},
"_id": "tBWHW00epmMnkawe",
@ -225,7 +226,7 @@
"name": "Flying",
"type": "feature",
"system": {
"description": "<p>While flying, the Bat gains a +3 bonus to their Difficulty.</p>",
"description": "<p>While flying, the @Lookup[@name] gains a +3 bonus to their Difficulty.</p>",
"resource": null,
"actions": {},
"originItemType": null,
@ -294,14 +295,14 @@
"name": "Screech",
"type": "feature",
"system": {
"description": "<p><strong>Mark a Stress</strong> to send a high-pitch screech out toward all targets in front of the Bat within Far range. Those targets must mark <strong>1d4</strong> Stress.</p><p>@Template[type:inFront|range:f]</p>",
"description": "<p><strong>Mark a Stress</strong> to send a high-pitch screech out toward all targets in front of the @Lookup[@name] within Far range. Those targets must mark <strong>1d4</strong> Stress.</p><p>@Template[type:inFront|range:f]</p>",
"resource": null,
"actions": {
"2ILfoiBoMyBCtBsL": {
"type": "damage",
"_id": "2ILfoiBoMyBCtBsL",
"systemPath": "actions",
"description": "<p><strong>Mark a Stress</strong> to send a high-pitch screech out toward all targets in front of the Bat within Far range. Those targets must mark <strong>1d4</strong> Stress.</p><p>@Template[type:inFront|range:f]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -350,7 +351,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "o69lipskvBwGVhe4",
"img": "icons/magic/sonic/projectile-sound-rings-wave.webp",
@ -378,7 +380,7 @@
"type": "attack",
"_id": "wW7WGisUBzyxjsH2",
"systemPath": "actions",
"description": "<p>When an allied Vampire marks HP, you can <strong>mark a Stress</strong> to fl y into Melee range of the attacker and make an attack with advantage against them. On a success, deal <strong>2d6+2</strong> physical damage.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -456,7 +458,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"_id": "BQPGgbNzKbNkGDJb",
"img": "icons/skills/melee/strike-slashes-red.webp",

View file

@ -117,7 +117,8 @@
"source": "Daggerheart SRD",
"page": 76,
"artist": ""
}
},
"size": "medium"
},
"flags": {},
"_id": "wNzeuQLfLUMvgHlQ",
@ -228,14 +229,14 @@
"_id": "wQXEnMqrl2jo91oy",
"img": "icons/creatures/abilities/wolf-howl-moon-purple.webp",
"system": {
"description": "<p>If the Wolf makes a successful standard attack and another Dire Wolf is within Melee range of the target, deal <strong>1d6+5</strong> physical damage instead of their standard damage and you gain a Fear.</p>",
"description": "<p>If the @Lookup[@name] makes a successful standard attack and another @Lookup[@name] is within Melee range of the target, deal <strong>1d6+5</strong> physical damage instead of their standard damage and you gain a Fear.</p>",
"resource": null,
"actions": {
"FFQvt3sMfuwXxIrf": {
"type": "attack",
"_id": "FFQvt3sMfuwXxIrf",
"systemPath": "actions",
"description": "<p>If the Wolf makes a successful standard attack and another Dire Wolf is within Melee range of the target, deal <strong>1d6+5</strong> physical damage instead of their standard damage and you gain a Fear.</p>",
"description": "",
"chatDisplay": true,
"actionType": "passive",
"cost": [],
@ -334,7 +335,7 @@
"type": "attack",
"_id": "Tvizq1jEfG8FyfNc",
"systemPath": "actions",
"description": "<p><strong>Mark a Stress</strong> to make an attack against a target within Melee range. On a success, deal <strong>3d4+10</strong> direct physical damage and make them <em>Vulnerable</em> until they clear at least 1 HP.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -377,7 +378,8 @@
}
}
],
"includeBase": false
"includeBase": false,
"direct": true
},
"target": {
"type": "any",
@ -416,7 +418,8 @@
},
"originItemType": null,
"subType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"effects": [
{

View file

@ -117,7 +117,8 @@
"source": "Daggerheart SRD",
"page": 93,
"artist": ""
}
},
"size": "medium"
},
"flags": {},
"_id": "wR7cFKrHvRzbzhBT",
@ -226,14 +227,14 @@
"name": "Bramble Patch",
"type": "feature",
"system": {
"description": "<p><strong>Mark a Stress</strong> to target a point within Far range. Create a patch of thorns that covers an area within Close range of that point. All targets within that area take <strong>2d6+2</strong> physical damage when they act. A target must succeed on a Finesse Roll or deal more than 20 damage to the Dryad with an attack to leave the area.</p><p>@Template[type:circle|range:c]</p>",
"description": "<p><strong>Mark a Stress</strong> to target a point within Far range. Create a patch of thorns that covers an area within Close range of that point. All targets within that area take <strong>2d6+2</strong> physical damage when they act. A target must succeed on a Finesse Roll or deal more than 20 damage to the @Lookup[@name] with an attack to leave the area.</p><p>@Template[type:circle|range:c]</p>",
"resource": null,
"actions": {
"iCJdIs57hfh5Cb0u": {
"type": "attack",
"_id": "iCJdIs57hfh5Cb0u",
"systemPath": "actions",
"description": "<p><strong>Mark a Stress</strong> to target a point within Far range. Create a patch of thorns that covers an area within Close range of that point. All targets within that area take <strong>2d6+2</strong> physical damage when they act. A target must succeed on a Finesse Roll or deal more than 20 damage to the Dryad with an attack to leave the area.</p><p>@Template[type:circle|range:c]</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -337,7 +338,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "i9HbArl09dX2BvzY",
"img": "icons/magic/nature/root-vines-grow-brown.webp",
@ -365,7 +367,7 @@
"type": "effect",
"_id": "84Q2b0zIY9c7Yhho",
"systemPath": "actions",
"description": "<p><strong>Spend a Fear</strong> to grow three @UUID[Compendium.daggerheart.adversaries.Actor.o63nS0k3wHu6EgKP]{Treant Sapling Minions}, who appear at Close range and immediately take the spotlight.</p>",
"description": "",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -392,7 +394,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "action"
},
"_id": "yKWQLL3qsEZlQjyb",
"img": "icons/magic/nature/tree-animated-stump-mushrooms-teal.webp",
@ -517,7 +520,8 @@
}
},
"originItemType": null,
"originId": null
"originId": null,
"featureForm": "reaction"
},
"_id": "z4JbqiHuxrWy6Cpu",
"img": "icons/magic/nature/vines-thorned-curled-glow-teal-purple.webp",

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