Compare commits

...

119 commits
1.2.7 ... main

Author SHA1 Message Date
WBHarry
9393bab6cf
Added so that beastforms can set a scale to handle specific images (#1532) 2026-01-15 09:45:10 +01:00
Carlos Fernandez
b92a474069
Fix bottom gap of item settings and make header rows more rhythmic (#1529) 2026-01-14 20:04:00 -05:00
WBHarry
4aa414be00
[Fix] Daggerheart Menu LightMode (#1534)
* Fixed so the menu and menuIcon coloration correctly changes with mode change

* .

* .

* .
2026-01-15 00:53:33 +01:00
WBHarry
5b61340fee Fixed /dr without target failing resource update 2026-01-13 22:36:50 +01:00
WBHarry
acafd2c8b9 Added fallback for defaultHopeDice and defaultFearDice 2026-01-13 21:00:24 +01:00
WBHarry
9473250f0c Fixed critical threshold applying to reactions 2026-01-13 20:47:43 +01:00
WBHarry
c958acabe6
[Fix] Release Fixes (#1530)
* Various fixes

* .

* .

* .
2026-01-13 19:12:50 +01:00
WBHarry
3c9ef75645
[Feature] ActiveEffect Default Hope/Fear dice (#1522)
* Added ActiveEffect rules for default hope and fear dice

* .

* Raised system version
2026-01-13 10:58:03 +01:00
WBHarry
d282a81594
[Feature] Summon Action Implementation (#1514)
* Schema definition for DHSummonAction

* Will reimplement

* HBS idea formed. Need to recheck drag drop implementation

* Tried to refine drag drop

* drag drop implemented (css tbd)

* phase 1 complete

* tbd work on summon action type

* Improved Schema and now it works

* .

* Dialog created. Tokens not dragged(tbd).

* Bare minimum implementation

* Finalized functionality

* Cleanup

* .

* Added optional summon render to chat message

* Updated SRD

* bugfix: fix title lines not rendering in chat messages

* Added summon actions to the easily doable environments in the SRD

* Update module/data/fields/action/summonField.mjs

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

---------

Co-authored-by: Nikhil Nagarajan <potter.nikhil@gmail.com>
Co-authored-by: Murilo Brito <dev.murilobrito@gmail.com>
Co-authored-by: Carlos Fernandez <CarlosFdez@users.noreply.github.com>
2026-01-13 10:36:29 +01:00
WBHarry
d823501d91
[Feature] Advanced Effect Settings (#1523)
* Added VaultActive and LoadoutIgnore

* Added DomainTouched support

* Naming change

* .

* Improved labels
2026-01-13 10:32:16 +01:00
WBHarry
27b7758f7d
[Feature] Roll Effect Toggles (#1510)
* Initial

* .

* .

* Update module/dice/dhRoll.mjs

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

---------

Co-authored-by: Carlos Fernandez <CarlosFdez@users.noreply.github.com>
2026-01-13 01:15:00 +01:00
WBHarry
883aaeec02
[Feature] Armor/Weapon Features In Description (#1521)
* ItemFeatures are now prepended to the description

* .

* Better separation of concerns

* .

* .
2026-01-12 14:16:19 +01:00
WBHarry
8de12551ad
[Feature] Scene Environments (#1499)
* Initial

* Remade to array structure

* Fixed context menu for multiples

* .

* Cleanup

* .

* Fixed so that users can access sceneEnvironments if they have permissions

* Update module/documents/scene.mjs

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

* .

---------

Co-authored-by: Carlos Fernandez <CarlosFdez@users.noreply.github.com>
2026-01-12 00:26:27 +01:00
WBHarry
454507ba7b
[Feature] Trigger System (#1500)
* Initial

* .

* Added StrangePattern trigger

* Set command codeblock to expandable

* Added automation setting

* Added ferocity trigger

* Improved StrangePatterns trigger to handle multiple matches
2026-01-11 11:51:05 +01:00
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
WBHarry
14ac8977af
Standardized what gets refreshed by which refreshType (#1374) 2025-12-07 00:53:33 +01:00
WBHarry
2a622a7363
[Fix] TagTeamRoll Critical Damage (#1371)
* Made some handlign for the damage when a tagTeamRoll crits

* Fixed so hope is taken from the initiator if the roll is with fear

* Raised verison
2025-12-07 00:03:18 +01:00
Carlos Fernandez
6ae00e15bd
Set card created on actor to first valid domain (#1372) 2025-12-06 23:07:43 +01:00
WBHarry
c846c5bc85
. (#1373) 2025-12-06 23:06:31 +01:00
WBHarry
451bef4c92
[Feature] Encounter Battlepoints (#1346)
* Added BP calculation and tooltip breakdown of BP sources

* Added Modifiers

* Fixed automatic battleToggles

* Corrected 'NoToughies' conditional

* Fixed GM-only visibility

* Fixed combatant isNPC
2025-12-06 21:11:34 +01:00
WBHarry
c3cb9121af
[Fix] Beastform Effect Size Issue (#1368)
* Fixed so beastform effects handle actors being smaller than 1

* .
2025-12-06 21:08:08 +01:00
WBHarry
6d8d773a26
[Fix] 1325 - Weapon Tooltip Damage Formula (#1355)
* Fixed so the damage calculation is correct. Fixed so the right tooltip is shown for attacks

* Moved the damageFormula function to damageAction
2025-12-06 15:16:34 +01:00
Carlos Fernandez
b57e98071f
[Feature] Sortable inventories and adversary/environment drag/drop (#1357)
* Add ability to sort inventories in player and party sheets

* Format base actor sheet

* Check item validity when creating on an actor

* Block dragdrop on adversaries and environments

* Support drag and drop in adversary and environment sheets

* Fix regression with dropping to character sheet

* Move vault when created handling to domain card preCreate
2025-12-06 15:05:10 +01:00
Carlos Fernandez
2171c1b433
[Fix] grid view of domain cards when more than 5 (#1369)
* Fix grid view of domain cards when more than 5

* Also fit 5 when there is a scrollbar
2025-12-06 14:58:41 +01:00
Carlos Fernandez
1fbce2507a
Prune stats when extracting (#1359) 2025-12-06 02:36:03 +01:00
Carlos Fernandez
e3f244d8d7
Fix scroll preservation in environment sheet (#1362) 2025-12-06 02:30:50 +01:00
WBHarry
9fa4627b19
[Fix] Homebrew Item Features (#1365)
* Fixed adding and editing effects on a homebrew feature

* .

* Fixed effect removal
2025-12-06 02:29:56 +01:00
Carlos Fernandez
cb10b18e06
Fix height scaling of environment sheets when changing window size (#1360) 2025-12-04 22:29:25 +01:00
Carlos Fernandez
5163bf9788
Fix dragging features between different environment settings applications (#1361) 2025-12-04 22:26:40 +01:00
Carlos Fernandez
82d39a3d70
Stamp Compendiums with minimum core version on build (#1358) 2025-12-03 19:11:15 +01:00
Murilo Brito
01a91724ed
[SUPPORT] Add css vars (#1343)
* support: add css vars

* support: add css vars to fonts family

* fix font vars
2025-12-03 08:45:44 -05:00
WBHarry
8f917c3640
Fixed so that AttackActions with a save will not stop the workflow of the fields if save automation is not on (#1353) 2025-12-01 15:41:47 +01:00
Carlos Fernandez
e57e7327d6
Make editing the biography take the entire tab and tweak rendering (#1352) 2025-11-30 18:36:19 +01:00
WBHarry
165068a9ee
[Feature] EffectsDisplay (#1340)
* Merged with main

* Added the display

* .

* Removed unused override function

* Fixed layout for generic effects

* feat: add basic style to effects tooltip

* Corrected distancing

* Use CSS based solution for shifting the countdowns

* Centered tooltip header

---------

Co-authored-by: moliloo <dev.murilobrito@gmail.com>
Co-authored-by: Carlos Fernandez <cfern1990@gmail.com>
2025-11-30 18:30:57 +01:00
WBHarry
f41f0b20b7
Fixed so that attribute rolls for characters are set as Action type (#1347) 2025-11-30 00:37:25 +01:00
WBHarry
f29198e81f
. (#1351) 2025-11-29 22:49:28 +01:00
Carlos Fernandez
630ba5ab7d
[Feature] Actor Directory subtitles (#1332)
* Show subtitles for various actor types in actor directory

* Show adversary and environment type

* Update companion subtitles

* Fallback adversay and environment label to item type label
2025-11-27 00:38:11 -03:00
1415 changed files with 13724 additions and 24797 deletions

View file

@ -16,10 +16,10 @@ import {
settingsRegistration,
socketRegistration
} from './module/systemRegistration/_module.mjs';
import { placeables } from './module/canvas/_module.mjs';
import { registerRollDiceHooks } from './module/dice/dhRoll.mjs';
import { placeables, DhTokenLayer } from './module/canvas/_module.mjs';
import './node_modules/@yaireo/tagify/dist/tagify.css';
import TemplateManager from './module/documents/templateManager.mjs';
import TokenManager from './module/documents/tokenManager.mjs';
CONFIG.DH = SYSTEM;
CONFIG.TextEditor.enrichers.push(...enricherConfig);
@ -43,6 +43,7 @@ CONFIG.ActiveEffect.dataModels = models.activeEffects.config;
CONFIG.Combat.documentClass = documents.DhpCombat;
CONFIG.Combat.dataModels = { base: models.DhCombat };
CONFIG.Combatant.documentClass = documents.DHCombatant;
CONFIG.Combatant.dataModels = { base: models.DhCombatant };
CONFIG.ChatMessage.dataModels = models.chatMessages.config;
@ -51,8 +52,12 @@ CONFIG.ChatMessage.template = 'systems/daggerheart/templates/ui/chat/chat-messag
CONFIG.Canvas.rulerClass = placeables.DhRuler;
CONFIG.Canvas.layers.templates.layerClass = placeables.DhTemplateLayer;
CONFIG.Canvas.layers.tokens.layerClass = DhTokenLayer;
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;
@ -60,15 +65,19 @@ CONFIG.Token.rulerClass = placeables.DhTokenRuler;
CONFIG.Token.hudClass = applications.hud.DHTokenHUD;
CONFIG.ui.combat = applications.ui.DhCombatTracker;
CONFIG.ui.nav = applications.ui.DhSceneNavigation;
CONFIG.ui.chat = applications.ui.DhChatLog;
CONFIG.ui.effectsDisplay = applications.ui.DhEffectsDisplay;
CONFIG.ui.hotbar = applications.ui.DhHotbar;
CONFIG.ui.sidebar = applications.sidebar.DhSidebar;
CONFIG.ui.actors = applications.sidebar.DhActorDirectory;
CONFIG.ui.daggerheartMenu = applications.sidebar.DaggerheartMenu;
CONFIG.ui.resources = applications.ui.DhFearTracker;
CONFIG.ui.countdowns = applications.ui.DhCountdowns;
CONFIG.ux.ContextMenu = applications.ux.DHContextMenu;
CONFIG.ux.TooltipManager = documents.DhTooltipManager;
CONFIG.ux.TemplateManager = new TemplateManager();
CONFIG.ux.TokenManager = new TokenManager();
Hooks.once('init', () => {
game.system.api = {
@ -80,40 +89,102 @@ Hooks.once('init', () => {
fields
};
game.system.registeredTriggers = new RegisteredTriggers();
const { DocumentSheetConfig } = foundry.applications.apps;
DocumentSheetConfig.unregisterSheet(TokenDocument, 'core', foundry.applications.sheets.TokenConfig);
DocumentSheetConfig.registerSheet(TokenDocument, SYSTEM.id, applications.sheetConfigs.DhTokenConfig, {
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(
@ -126,7 +197,8 @@ Hooks.once('init', () => {
SYSTEM.id,
applications.sheetConfigs.ActiveEffectConfig,
{
makeDefault: true
makeDefault: true,
label: sheetLabel('DOCUMENT.ActiveEffect')
}
);
@ -135,9 +207,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();
@ -167,11 +240,13 @@ Hooks.on('ready', async () => {
ui.countdowns.render({ force: true });
}
ui.effectsDisplay = new CONFIG.ui.effectsDisplay();
ui.effectsDisplay.render({ force: true });
if (!(ui.compendiumBrowser instanceof applications.ui.ItemBrowser))
ui.compendiumBrowser = new applications.ui.ItemBrowser();
socketRegistration.registerSocketHooks();
registerRollDiceHooks();
socketRegistration.registerUserQueries();
if (!game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.welcomeMessage)) {
@ -187,9 +262,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));
});
@ -234,7 +309,7 @@ Hooks.on('chatMessage', (_, message) => {
target,
difficulty,
title,
label: 'test',
label: game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll'),
actionType: null,
advantage
});
@ -242,53 +317,121 @@ Hooks.on('chatMessage', (_, message) => {
}
});
Hooks.on('moveToken', async (movedToken, data) => {
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 updateActorsRangeDependentEffects = async token => {
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)) {
for (let effect of token.actor?.allApplicableEffects() ?? []) {
if (!effect.system.rangeDependence?.enabled) continue;
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];
// 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 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 inRange =
required === 5
? userTarget.isAdjacentWith(token.object)
: userTarget.distanceTo(token.object) <= required;
if (reverse ? inRange : !inRange) {
enabledEffect = false;
break;
}
};
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);
await effect.update({ disabled: !enabledEffect });
}
};
for (let key in effectUpdates) {
const effect = effectUpdates[key];
await effect.value.update({ disabled: effect.disabled });
const updateAllRangeDependentEffects = async () => {
const effectsAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).effects;
if (!effectsAutomation.rangeDependent) return;
const tokens = canvas.scene?.tokens;
if (!tokens) return;
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 debouncedRangeEffectCall = foundry.utils.debounce(updateAllRangeDependentEffects, 50);
Hooks.on('targetToken', () => {
debouncedRangeEffectCall();
});
Hooks.on('refreshToken', (_, options) => {
if (options.refreshPosition) {
debouncedRangeEffectCall();
}
});
Hooks.on('renderCompendiumDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html));
Hooks.on('renderDocumentDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html));
class RegisteredTriggers extends Map {
constructor() {
super();
}
async registerTriggers(trigger, actor, triggeringActorType, uuid, commands) {
const existingTrigger = this.get(trigger);
if (!existingTrigger) this.set(trigger, new Map());
this.get(trigger).set(uuid, { actor, triggeringActorType, commands });
}
async runTrigger(trigger, currentActor, ...args) {
const updates = [];
const triggerSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).triggers;
if (!triggerSettings.enabled) return updates;
const dualityTrigger = this.get(trigger);
if (dualityTrigger) {
for (let { actor, triggeringActorType, commands } of dualityTrigger.values()) {
const triggerData = CONFIG.DH.TRIGGER.triggers[trigger];
if (triggerData.usesActor && triggeringActorType !== 'any') {
if (triggeringActorType === 'self' && currentActor?.uuid !== actor) continue;
else if (triggeringActorType === 'other' && currentActor?.uuid === actor) continue;
}
for (let command of commands) {
try {
const result = await command(...args);
if (result?.updates?.length) updates.push(...result.updates);
} catch (_) {
const triggerName = game.i18n.localize(triggerData.label);
ui.notifications.error(
game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerError', {
trigger: triggerName,
actor: currentActor?.name
})
);
}
}
}
}
return updates;
}
}

View file

@ -36,6 +36,7 @@
},
"DAGGERHEART": {
"CharacterSheet": "Character Sheet",
"ACTIONS": {
"TYPES": {
"attack": {
@ -68,7 +69,11 @@
},
"summon": {
"name": "Summon",
"tooltip": "Create tokens in the scene."
"tooltip": "Create tokens in the scene.",
"error": "You do not have permission to summon tokens or there is no active scene.",
"invalidDrop": "You can only drop Actor entities to summon.",
"chatMessageTitle": "Test2",
"chatMessageHeaderTitle": "Summoning"
}
},
"Config": {
@ -89,7 +94,9 @@
"customFormula": "Custom Formula",
"formula": "Formula"
},
"displayInChat": "Display in chat"
"displayInChat": "Display in chat",
"deleteTriggerTitle": "Delete Trigger",
"deleteTriggerContent": "Are you sure you want to delete the {trigger} trigger?"
},
"RollField": {
"diceRolling": {
@ -119,6 +126,9 @@
},
"cost": {
"stepTooltip": "+{step} per step"
},
"summon": {
"dropSummonsHere": "Drop Summons Here"
}
}
},
@ -193,6 +203,8 @@
"unequip": "Unequip",
"useItem": "Use Item"
},
"defaultHopeDice": "Default Hope Dice",
"defaultFearDice": "Default Fear Dice",
"disadvantageSources": {
"label": "Disadvantage Sources",
"hint": "Add single words or short text as reminders and hints of what a character has disadvantage on."
@ -225,6 +237,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 +338,7 @@
"equip": "Equip",
"sendToChat": "Send To Chat",
"toLoadout": "Send to Loadout",
"recall": "Recall",
"toVault": "Send to Vault",
"unequip": "Unequip",
"useItem": "Use Item"
@ -386,7 +400,8 @@
"hideNewCountdowns": "Hide New Countdowns"
},
"DaggerheartMenu": {
"title": "GM Tools"
"title": "GM Tools",
"refreshFeatures": "Refresh Features"
},
"DeleteConfirmation": {
"title": "Delete {type} - {name}",
@ -504,6 +519,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 +599,7 @@
},
"OwnershipSelection": {
"title": "Ownership Selection - {name}",
"noPlayers": "No players to assign ownership to",
"default": "Default Ownership"
},
"ReactionRoll": {
@ -609,6 +626,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 +638,6 @@
}
},
"CONFIG": {
"ActionType": {
"passive": "Passive",
"action": "Action",
"reaction": "Reaction"
},
"AdversaryTrait": {
"relentless": {
"name": "Relentless",
@ -682,6 +697,14 @@
"description": "Enemies that enhance their allies and/or disrupt their opponents."
}
},
"AdversaryTypeCost": {
"minion": "for each group of Minions equal to the size of the party.",
"support": "for each Social or Support adversary.",
"standard": "for each Horde, Ranged, Skulk, or Standard adversary.",
"leader": "for each Leader adversary.",
"bruiser": "for each Bruiser adversary.",
"solo": "for each Solo adversary."
},
"ArmorFeature": {
"burning": {
"name": "Burning",
@ -905,6 +928,30 @@
"evolved": "Evolved",
"hybrid": "Hybrid"
},
"BPModifiers": {
"increaseDamage": {
"description": "if you add +1d4 (or a static +2) to all adversaries' damage rolls (to increase the challenge without lengthening the battle)",
"effect": {
"name": "Increase Damage",
"description": "Add 1d4 to damage"
}
},
"lessDifficult": {
"description": "if the fight should be less difficult or shorter."
},
"lowerTier": {
"description": "if you choose an adversary from a lower tier."
},
"manySolos": {
"description": "if you're using 2 or more Solo adversaries."
},
"moreDangerous": {
"description": "if the fight should be more dangerous or last longer"
},
"noToughies": {
"description": "if you don't include any Bruisers, Hordes, Leaders, or Solos"
}
},
"Burden": {
"oneHanded": "One-Handed",
"twoHanded": "Two-Handed"
@ -999,6 +1046,12 @@
"description": ""
}
},
"FeatureForm": {
"label": "Feature Form",
"passive": "Passive",
"action": "Action",
"reaction": "Reaction"
},
"Gold": {
"title": "Gold",
"coins": "Coins",
@ -1102,7 +1155,8 @@
"any": "Any",
"friendly": "Friendly",
"hostile": "Hostile",
"self": "Self"
"self": "Self",
"other": "Other"
},
"TemplateTypes": {
"circle": "Circle",
@ -1112,6 +1166,14 @@
"rect": "Rectangle",
"ray": "Ray"
},
"TokenSize": {
"tiny": "Tiny",
"small": "Small",
"medium": "Medium",
"large": "Large",
"huge": "Huge",
"gargantuan": "Gargantuan"
},
"Traits": {
"agility": {
"name": "Agility",
@ -1168,6 +1230,29 @@
}
}
},
"Triggers": {
"postDamageReduction": {
"label": "After Damage Reduction"
},
"preDamageReduction": {
"label": "Before Damage Reduction"
},
"dualityRoll": {
"label": "Duality Roll"
},
"fearRoll": {
"label": "Fear Roll"
},
"triggerTexts": {
"strangePatternsContentTitle": "Matched {nr} times.",
"strangePatternsContentSubTitle": "Increase hope and stress to a total of {nr}.",
"ferocityContent": "Spend 2 Hope to gain {bonus} bonus Evasion until after the next attack against you?",
"ferocityEffectDescription": "Your evasion is increased by {bonus}. This bonus lasts until after the next attack made against you."
},
"triggerType": "Trigger Type",
"triggeringActorType": "Triggering Actor Type",
"triggerError": "{trigger} trigger failed for {actor}. It's probably configured wrong."
},
"WeaponFeature": {
"barrier": {
"name": "Barrier",
@ -1752,7 +1837,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"
@ -1767,6 +1854,7 @@
"plural": "Costs"
},
"Damage": {
"massive": "Massive",
"severe": "Severe",
"major": "Major",
"minor": "Minor",
@ -2002,10 +2090,10 @@
"partyMembers": "Party Members",
"projects": "Projects",
"types": "Types",
"itemFeatures": "Item Features",
"questions": "Questions",
"configuration": "Configuration",
"base": "Base"
"base": "Base",
"triggers": "Triggers"
},
"Tiers": {
"singular": "Tier",
@ -2032,6 +2120,7 @@
"basics": "Basics",
"bonus": "Bonus",
"burden": "Burden",
"condition": "Condition",
"continue": "Continue",
"criticalSuccess": "Critical Success",
"criticalShort": "Critical",
@ -2058,6 +2147,7 @@
"fear": "Fear",
"features": "Features",
"formula": "Formula",
"general": "General",
"gm": "GM",
"healing": "Healing",
"healingRoll": "Healing Roll",
@ -2072,6 +2162,7 @@
},
"hope": "Hope",
"hordeHp": "Horde HP",
"icon": "Icon",
"identify": "Identity",
"imagePath": "Image Path",
"inactiveEffects": "Inactive Effects",
@ -2093,6 +2184,7 @@
"missingDragDropThing": "Drop {thing} here",
"multiclass": "Multiclass",
"newCategory": "New Category",
"newThing": "New {thing}",
"none": "None",
"noTarget": "No current target",
"partner": "Partner",
@ -2107,6 +2199,7 @@
"recovery": "Recovery",
"refresh": "Refresh",
"reroll": "Reroll",
"rerolled": "Rerolled",
"rerollThing": "Reroll {thing}",
"resource": "Resource",
"roll": "Roll",
@ -2121,16 +2214,22 @@
"stress": "Stress",
"subclasses": "Subclasses",
"success": "Success",
"summon": {
"single": "Summon",
"plural": "Summons"
},
"take": "Take",
"Target": {
"single": "Target",
"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",
@ -2183,8 +2282,10 @@
"tokenRingImg": { "label": "Subject Texture" },
"tokenSize": {
"placeholder": "Using character dimensions",
"disabledPlaceholder": "Set by character size",
"height": { "label": "Height" },
"width": { "label": "Width" }
"width": { "label": "Width" },
"scale": { "label": "Token Scale" }
},
"evolved": {
"maximumTier": { "label": "Maximum Tier" },
@ -2206,7 +2307,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",
@ -2229,6 +2334,9 @@
"DomainCard": {
"type": "Type",
"recallCost": "Recall Cost",
"vaultActive": "Active In Vault",
"loadoutIgnore": "Ignores Loadout Limits",
"domainTouched": "Domain Touched",
"foundationTitle": "Foundation",
"specializationTitle": "Specialization",
"masteryTitle": "Mastery"
@ -2367,6 +2475,12 @@
"hint": "Automatically apply effects. Targets must be selected before the action is made and Reaction Roll Automation must be different than Never. Bypass users permissions."
}
},
"triggers": {
"enabled": {
"label": "Enabled",
"hint": "Advanced automation such as triggering a popup for a wizard's Strange Patterns"
}
},
"summaryMessages": {
"label": "Summary Messages"
}
@ -2376,14 +2490,17 @@
},
"roll": {
"title": "Actions"
},
"trigger": {
"title": "Triggers"
}
},
"Homebrew": {
"newDowntimeMove": "Downtime Move",
"downtimeMove": "Downtime Move",
"armorFeature": "Armor Feature",
"weaponFeature": "Weapon Feaure",
"newFeature": "New ItemFeature",
"weaponFeature": "Weapon Feature",
"newFeature": "New Item Feature",
"downtimeMoves": "Downtime Moves",
"itemFeatures": "Item Features",
"nrChoices": "# Moves Per Rest",
@ -2400,9 +2517,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"
},
@ -2464,6 +2584,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": {
@ -2477,6 +2602,10 @@
"actionTokens": {
"enabled": { "label": "Enabled" },
"tokens": { "label": "Tokens" }
},
"massiveDamage": {
"title": "Massive Damage",
"enabled": { "label": "Enabled" }
}
}
},
@ -2491,7 +2620,9 @@
}
},
"disabledText": "Daggerheart Measurements are disabled in System Settings - Variant Rules",
"rangeMeasurement": "Range Measurement"
"rangeMeasurement": "Range Measurement",
"sceneEnvironments": "Scene Environments",
"dragEnvironmentHere": "Drag environments here"
}
},
"UI": {
@ -2549,7 +2680,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}",
@ -2585,6 +2717,10 @@
"decreasingLoop": "Decreasing Looping",
"increasingLoop": "Increasing Looping"
},
"EffectsDisplay": {
"removeThing": "[Right Click] Remove {thing}",
"appliedBy": "Applied By: {by}"
},
"ItemBrowser": {
"title": "Daggerheart Compendium Browser",
"hint": "Select a Folder in sidebar to start browsing through the compendium",
@ -2699,9 +2835,18 @@
"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",
"domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used"
},
"Sidebar": {
"actorDirectory": {
"tier": "Tier {tier} {type}",
"character": "Level {level} Character",
"companion": "Level {level} - {partner}",
"companionNoPartner": "No Partner"
},
"daggerheartMenu": {
"title": "Daggerheart Menu",
"startSession": "Start Session",
@ -2736,7 +2881,10 @@
"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",
"previewTokenHelp": "Left-click to place, right-click to cancel"
}
}
}

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

@ -10,6 +10,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.config = config;
this.config.experiences = [];
this.reactionOverride = config.actionType === 'reaction';
this.selectedEffects = this.config.bonusEffects;
if (config.source?.action) {
this.item = config.data.parent.items.get(config.source.item) ?? config.data.parent;
@ -35,6 +36,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
selectExperience: this.selectExperience,
toggleReaction: this.toggleReaction,
toggleTagTeamRoll: this.toggleTagTeamRoll,
toggleSelectedEffect: this.toggleSelectedEffect,
submitRoll: this.submitRoll
},
form: {
@ -76,6 +78,9 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
icon
}));
context.hasSelectedEffects = Boolean(this.selectedEffects && Object.keys(this.selectedEffects).length);
context.selectedEffects = this.selectedEffects;
this.config.costs ??= [];
if (this.config.costs?.length) {
const updatedCosts = game.system.api.fields.ActionFields.CostField.calcCosts.call(
@ -104,7 +109,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]
@ -116,14 +121,14 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.isLite = this.config.roll?.lite;
context.extraFormula = this.config.extraFormula;
context.formula = this.roll.constructFormula(this.config);
if (this.actor.system.traits) context.abilities = this.getTraitModifiers();
if (this.actor?.system?.traits) context.abilities = this.getTraitModifiers();
context.showReaction = !this.config.roll?.type && context.rollType === 'DualityRoll';
context.reactionOverride = this.reactionOverride;
}
const tagTeamSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
if (tagTeamSetting.members[this.actor.id] && !this.config.skips?.createMessage) {
if (this.actor && tagTeamSetting.members[this.actor.id] && !this.config.skips?.createMessage) {
context.activeTagTeamRoll = true;
context.tagTeamSelected = this.config.tagTeamSelected;
}
@ -185,7 +190,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 +200,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
? null
? 'reaction'
: this.config.actionType === 'reaction'
? 'action'
: this.config.actionType;
this.render();
}
@ -208,6 +213,11 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.render();
}
static toggleSelectedEffect(_event, button) {
this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected;
this.render();
}
static async submitRoll() {
await this.close({ submitted: true });
}

View file

@ -6,6 +6,7 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
this.roll = roll;
this.config = config;
this.selectedEffects = this.config.bonusEffects;
}
static DEFAULT_OPTIONS = {
@ -20,6 +21,7 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
icon: 'fa-solid fa-dice'
},
actions: {
toggleSelectedEffect: this.toggleSelectedEffect,
submitRoll: this.submitRoll
},
form: {
@ -57,6 +59,9 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
icon
}));
context.modifiers = this.config.modifiers;
context.hasSelectedEffects = Boolean(Object.keys(this.selectedEffects).length);
context.selectedEffects = this.selectedEffects;
return context;
}
@ -69,6 +74,11 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
this.render();
}
static toggleSelectedEffect(_event, button) {
this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected;
this.render();
}
static async submitRoll() {
await this.close({ submitted: true });
}

View file

@ -1,3 +1,5 @@
import { refreshIsAllowed } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV2) {
@ -91,14 +93,12 @@ 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 (
action.uses.recovery &&
((action.uses.recovery === 'longRest' && !this.shortrest) ||
action.uses.recovery === 'shortRest')
) {
if (refreshIsAllowed([this.shortrest ? 'shortRest' : 'longRest'], action.uses.recovery)) {
acc.push({
title: x.name,
name: action.name,
@ -120,8 +120,7 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
if (
x.system.resource &&
x.system.resource.type &&
((x.system.resource.recovery === 'longRest') === !this.shortrest ||
x.system.resource.recovery === 'shortRest')
refreshIsAllowed([this.shortrest ? 'shortRest' : 'longRest'], x.system.resource.recovery)
) {
acc.push({
title: game.i18n.localize(`TYPES.Item.${x.type}`),
@ -184,12 +183,18 @@ 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 = {
@ -209,7 +214,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

@ -1,3 +1,4 @@
import { getCritDamageBonus } from '../../helpers/utils.mjs';
import { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -76,28 +77,37 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
cost: this.data.initiator.cost
};
context.selectedData = Object.values(context.members).reduce(
(acc, member) => {
if (!member.roll) return acc;
if (member.selected) {
acc.result = `${member.roll.system.roll.total} ${member.roll.system.roll.result.label}`;
}
const selectedMember = Object.values(context.members).find(x => x.selected && x.roll);
const selectedIsCritical = selectedMember?.roll?.system?.isCritical;
context.selectedData = {
result: selectedMember
? `${selectedMember.roll.system.roll.total} ${selectedMember.roll.system.roll.result.label}`
: null,
damageValues: null
};
for (const member of Object.values(context.members)) {
if (!member.roll) continue;
if (context.usesDamage) {
if (!acc.damageValues) acc.damageValues = {};
if (!context.selectedData.damageValues) context.selectedData.damageValues = {};
for (let damage of member.damageValues) {
if (acc.damageValues[damage.key]) {
acc.damageValues[damage.key].total += damage.total;
const damageTotal = member.roll.system.isCritical
? damage.total
: selectedIsCritical
? damage.total + (await getCritDamageBonus(member.roll.system.damage[damage.key].formula))
: damage.total;
if (context.selectedData.damageValues[damage.key]) {
context.selectedData.damageValues[damage.key].total += damageTotal;
} else {
acc.damageValues[damage.key] = foundry.utils.deepClone(damage);
context.selectedData.damageValues[damage.key] = {
...foundry.utils.deepClone(damage),
total: damageTotal
};
}
}
}
}
return acc;
},
{ result: null, damageValues: null }
);
context.showResult = Object.values(context.members).reduce((enabled, member) => {
if (!member.roll) return enabled;
if (context.usesDamage) {
@ -201,21 +211,41 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
.map(key => game.messages.get(this.data.members[key].messageId));
const systemData = foundry.utils.deepClone(mainRoll).system.toObject();
const criticalRoll = systemData.roll.isCritical;
for (let roll of secondaryRolls) {
if (roll.system.hasDamage) {
for (let key in roll.system.damage) {
var damage = roll.system.damage[key];
const damageTotal =
!roll.system.isCritical && criticalRoll
? (await getCritDamageBonus(damage.formula)) + damage.total
: damage.total;
const updatedDamageParts = damage.parts;
if (systemData.damage[key]) {
systemData.damage[key].total += damage.total;
systemData.damage[key].parts = [...systemData.damage[key].parts, ...damage.parts];
} else {
systemData.damage[key] = damage;
if (!roll.system.isCritical && criticalRoll) {
for (let part of updatedDamageParts) {
const criticalDamage = await getCritDamageBonus(part.formula);
if (criticalDamage) {
damage.formula = `${damage.formula} + ${criticalDamage}`;
part.formula = `${part.formula} + ${criticalDamage}`;
part.modifierTotal = part.modifierTotal + criticalDamage;
part.total += criticalDamage;
part.roll = new Roll(part.formula);
}
}
}
}
systemData.title = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.chatMessageRollTitle');
systemData.damage[key].formula = `${systemData.damage[key].formula} + ${damage.formula}`;
systemData.damage[key].total += damageTotal;
systemData.damage[key].parts = [...systemData.damage[key].parts, ...updatedDamageParts];
} else {
systemData.damage[key] = { ...damage, total: damageTotal, parts: updatedDamageParts };
}
}
}
}
systemData.title = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.chatMessageRollTitle');
const cls = getDocumentClass('ChatMessage'),
msgData = {
type: 'dualityRoll',
@ -233,14 +263,16 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
const fearUpdate = { key: 'fear', value: null, total: null, enabled: true };
for (let memberId of Object.keys(this.data.members)) {
const resourceUpdates = [];
if (systemData.roll.isCritical || systemData.roll.result.duality === 1) {
const value =
memberId !== this.data.initiator.id
? 1
: this.data.initiator.cost
const rollGivesHope = systemData.roll.isCritical || systemData.roll.result.duality === 1;
if (memberId === this.data.initiator.id) {
const value = this.data.initiator.cost
? rollGivesHope
? 1 - this.data.initiator.cost
: -this.data.initiator.cost
: 1;
resourceUpdates.push({ key: 'hope', value: value, total: -value, enabled: true });
} else if (rollGivesHope) {
resourceUpdates.push({ key: 'hope', value: 1, total: -1, enabled: true });
}
if (systemData.roll.isCritical) resourceUpdates.push({ key: 'stress', value: -1, total: 1, enabled: true });
if (systemData.roll.result.duality === -1) {

View file

@ -21,6 +21,7 @@ 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' &&
@ -31,6 +32,7 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
context.canToggleCombat = DHTokenHUD.#nonCombatTypes.includes(this.actor.type)
? false
: context.canToggleCombat;
context.systemStatusEffects = Object.keys(context.statusEffects).reduce((acc, key) => {
const effect = context.statusEffects[key];
if (effect.systemEffect) {
@ -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);
}
@ -193,16 +214,20 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
}
// Update the status of effects which are active for the token actor
const activeEffects = this.actor?.effects || [];
const activeEffects = this.actor?.getActiveEffects() || [];
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;
if (status._id) {
if (status._id !== effect.id) continue;
}
status.isActive = true;
if (effect.getFlag('core', 'overlay')) status.isOverlay = true;
if (effect.getFlag?.('core', 'overlay')) status.isOverlay = true;
}
}

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];
Object.keys(allExperiences).reduce((acc, id) => {
const experience = allExperiences[id];
if (experience.name) {
acc.push({ id: id, label: experience.name });
}
return acc;
}, []),

View file

@ -1,16 +1,25 @@
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
export default class DhSceneConfigSettings extends foundry.applications.sheets.SceneConfig {
// static DEFAULT_OPTIONS = {
// ...super.DEFAULT_OPTIONS,
// form: {
// handler: this.updateData,
// closeOnSubmit: true
// }
// };
constructor(options) {
super(options);
Hooks.on(socketEvent.Refresh, ({ refreshType }) => {
if (refreshType === RefreshType.Scene) this.render();
});
}
static DEFAULT_OPTIONS = {
...super.DEFAULT_OPTIONS,
actions: {
...super.DEFAULT_OPTIONS.actions,
removeSceneEnvironment: DhSceneConfigSettings.#removeSceneEnvironment
}
};
static buildParts() {
const { footer, tabs, ...parts } = super.PARTS;
const tmpParts = {
// tabs,
tabs: { template: 'systems/daggerheart/templates/scene/tabs.hbs' },
...parts,
dh: { template: 'systems/daggerheart/templates/scene/dh-config.hbs' },
@ -28,27 +37,47 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
static TABS = DhSceneConfigSettings.buildTabs();
async _preRender(context, options) {
await super._preFirstRender(context, options);
if (!options.internalRefresh)
this.daggerheartFlag = new game.system.api.data.scenes.DHScene(this.document.flags.daggerheart);
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
switch (partId) {
case 'dh':
htmlElement.querySelector('#rangeMeasurementSetting')?.addEventListener('change', async event => {
const flagData = foundry.utils.mergeObject(this.document.flags.daggerheart, {
rangeMeasurement: { setting: event.target.value }
});
this.document.flags.daggerheart = flagData;
this.render();
this.daggerheartFlag.updateSource({ rangeMeasurement: { setting: event.target.value } });
this.render({ internalRefresh: true });
});
const dragArea = htmlElement.querySelector('.scene-environments');
if (dragArea) dragArea.ondrop = this._onDrop.bind(this);
break;
}
}
async _onDrop(event) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const item = await foundry.utils.fromUuid(data.uuid);
if (item instanceof game.system.api.documents.DhpActor && item.type === 'environment') {
await this.daggerheartFlag.updateSource({
sceneEnvironments: [...this.daggerheartFlag.sceneEnvironments, data.uuid]
});
this.render({ internalRefresh: true });
}
}
/** @inheritDoc */
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
switch (partId) {
case 'dh':
context.data = new game.system.api.data.scenes.DHScene(canvas.scene.flags.daggerheart);
context.data = this.daggerheartFlag;
context.variantRules = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules);
break;
}
@ -56,8 +85,24 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
return context;
}
// static async updateData(event, _, formData) {
// const data = foundry.utils.expandObject(formData.object);
// this.close(data);
// }
static async #removeSceneEnvironment(_event, button) {
await this.daggerheartFlag.updateSource({
sceneEnvironments: this.daggerheartFlag.sceneEnvironments.filter(
(_, index) => index !== Number.parseInt(button.dataset.index)
)
});
this.render({ internalRefresh: true });
}
/** @override */
async _processSubmitData(event, form, submitData, options) {
submitData.flags.daggerheart = this.daggerheartFlag.toObject();
for (const key of Object.keys(this.document._source.flags.daggerheart?.sceneEnvironments ?? {})) {
if (!submitData.flags.daggerheart.sceneEnvironments[key]) {
submitData.flags.daggerheart.sceneEnvironments[`-=${key}`] = null;
}
}
super._processSubmitData(event, form, submitData, options);
}
}

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

@ -1,4 +1,5 @@
export { default as ActionConfig } from './action-config.mjs';
export { default as ActionSettingsConfig } from './action-settings-config.mjs';
export { default as CharacterSettings } from './character-settings.mjs';
export { default as AdversarySettings } from './adversary-settings.mjs';
export { default as CompanionSettings } from './companion-settings.mjs';

View file

@ -0,0 +1,382 @@
import DaggerheartSheet from '../sheets/daggerheart-sheet.mjs';
const { ApplicationV2 } = foundry.applications.api;
export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) {
constructor(action) {
super({});
this.action = action;
this.openSection = null;
this.openTrigger = this.action.triggers.length > 0 ? 0 : null;
}
get title() {
return `${game.i18n.localize('DAGGERHEART.GENERAL.Tabs.settings')}: ${this.action.name}`;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'dh-style', 'action-config', 'dialog', 'max-800'],
window: {
icon: 'fa-solid fa-wrench',
resizable: false
},
position: { width: 600, height: 'auto' },
actions: {
toggleSection: this.toggleSection,
addEffect: this.addEffect,
removeEffect: this.removeEffect,
addElement: this.addElement,
removeElement: this.removeElement,
editEffect: this.editEffect,
addDamage: this.addDamage,
removeDamage: this.removeDamage,
editDoc: this.editDoc,
addTrigger: this.addTrigger,
removeTrigger: this.removeTrigger,
expandTrigger: this.expandTrigger
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
},
dragDrop: [{ dragSelector: null, dropSelector: '#summon-drop-zone', handlers: ['_onDrop'] }]
};
static PARTS = {
header: {
id: 'header',
template: 'systems/daggerheart/templates/sheets-settings/action-settings/header.hbs'
},
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
base: {
id: 'base',
template: 'systems/daggerheart/templates/sheets-settings/action-settings/base.hbs'
},
configuration: {
id: 'configuration',
template: 'systems/daggerheart/templates/sheets-settings/action-settings/configuration.hbs'
},
effect: {
id: 'effect',
template: 'systems/daggerheart/templates/sheets-settings/action-settings/effect.hbs'
},
trigger: {
id: 'trigger',
template: 'systems/daggerheart/templates/sheets-settings/action-settings/trigger.hbs'
}
};
static TABS = {
base: {
active: true,
cssClass: '',
group: 'primary',
id: 'base',
icon: null,
label: 'DAGGERHEART.GENERAL.Tabs.base'
},
config: {
active: false,
cssClass: '',
group: 'primary',
id: 'config',
icon: null,
label: 'DAGGERHEART.GENERAL.Tabs.configuration'
},
effect: {
active: false,
cssClass: '',
group: 'primary',
id: 'effect',
icon: null,
label: 'DAGGERHEART.GENERAL.Tabs.effects'
},
trigger: {
active: false,
cssClass: '',
group: 'primary',
id: 'trigger',
icon: null,
label: 'DAGGERHEART.GENERAL.Tabs.triggers'
}
};
static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects', 'summon'];
_getTabs(tabs) {
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active;
v.cssClass = v.active ? 'active' : '';
}
return tabs;
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement.querySelectorAll('.summon-count-wrapper input').forEach(element => {
element.addEventListener('change', this.updateSummonCount.bind(this));
});
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options, 'action');
context.source = this.action.toObject(true);
context.summons = [];
for (const summon of context.source.summon ?? []) {
const actor = await foundry.utils.fromUuid(summon.actorUUID);
context.summons.push({ actor, count: summon.count });
}
context.openSection = this.openSection;
context.tabs = this._getTabs(this.constructor.TABS);
context.config = CONFIG.DH;
if (this.action.damage?.hasOwnProperty('includeBase') && this.action.type === 'attack')
context.hasBaseDamage = !!this.action.parent.attack;
context.costOptions = this.getCostOptions();
context.getRollTypeOptions = this.getRollTypeOptions();
context.disableOption = this.disableOption.bind(this);
context.isNPC = this.action.actor?.isNPC;
context.baseSaveDifficulty = this.action.actor?.baseSaveDifficulty;
context.baseAttackBonus = this.action.actor?.system.attack?.roll.bonus;
context.hasRoll = this.action.hasRoll;
context.triggers = context.source.triggers.map((trigger, index) => {
const { hint, returns, usesActor } = CONFIG.DH.TRIGGER.triggers[trigger.trigger];
return {
...trigger,
hint,
returns,
usesActor,
revealed: this.openTrigger === index
};
});
const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers;
context.tierOptions = [
{ key: 1, label: game.i18n.localize('DAGGERHEART.GENERAL.Tiers.1') },
...Object.values(settingsTiers).map(x => ({ key: x.tier, label: x.name }))
];
return context;
}
static toggleSection(_, button) {
this.openSection = button.dataset.section === this.openSection ? null : button.dataset.section;
this.render(true);
}
getCostOptions() {
const options = foundry.utils.deepClone(CONFIG.DH.GENERAL.abilityCosts);
const resource = this.action.parent.resource;
if (resource) {
options.resource = {
label: 'DAGGERHEART.GENERAL.itemResource',
group: 'Global'
};
}
if (this.action.parent.metadata?.isQuantifiable) {
options.quantity = {
label: 'DAGGERHEART.GENERAL.itemQuantity',
group: 'Global'
};
}
return options;
}
getRollTypeOptions() {
const types = foundry.utils.deepClone(CONFIG.DH.GENERAL.rollTypes);
if (!this.action.actor) return types;
Object.values(types).forEach(t => {
if (this.action.actor.type !== 'character' && t.playerOnly) delete types[t.id];
});
return types;
}
disableOption(index, costOptions, choices) {
const filtered = foundry.utils.deepClone(costOptions);
Object.keys(filtered).forEach(o => {
if (choices.find((c, idx) => c.type === o && index !== idx)) filtered[o].disabled = true;
});
return filtered;
}
_prepareSubmitData(_event, formData) {
const submitData = foundry.utils.expandObject(formData.object);
const itemAbilityCostKeys = Object.keys(CONFIG.DH.GENERAL.itemAbilityCosts);
for (const keyPath of this.constructor.CLEAN_ARRAYS) {
const data = foundry.utils.getProperty(submitData, keyPath);
const dataValues = data ? Object.values(data) : [];
if (keyPath === 'cost') {
for (var value of dataValues) {
value.itemId = itemAbilityCostKeys.includes(value.key) ? this.action.parent.parent.id : null;
}
}
if (data) foundry.utils.setProperty(submitData, keyPath, dataValues);
}
return submitData;
}
static async updateForm(event, _, formData) {
const submitData = this._prepareSubmitData(event, formData);
const data = foundry.utils.mergeObject(this.action.toObject(), submitData);
this.action = await this.action.update(data);
this.sheetUpdate?.(this.action);
this.render();
}
static addElement(event) {
const data = this.action.toObject(),
key = event.target.closest('[data-key]').dataset.key;
if (!this.action[key]) return;
data[key].push(this.action.defaultValues[key] ?? {});
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static removeElement(event, button) {
event.stopPropagation();
const data = this.action.toObject(),
key = event.target.closest('[data-key]').dataset.key;
// Prefer explicit index, otherwise find by uuid
let index = button?.dataset.index;
if (index === undefined || index === null || index === '') {
const uuid = button?.dataset.uuid ?? button?.dataset.itemUuid;
index = data[key].findIndex(e => (e?.actorUUID ?? e?.uuid) === uuid);
if (index === -1) return;
} else index = Number(index);
data[key].splice(index, 1);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static async editDoc(_event, target) {
const element = target.closest('[data-item-uuid]');
const doc = (await foundry.utils.fromUuid(element.dataset.itemUuid)) ?? null;
if (doc) return doc.sheet.render({ force: true });
}
static addDamage(_event) {
if (!this.action.damage.parts) return;
const data = this.action.toObject(),
part = {};
if (this.action.actor?.isNPC) part.value = { multiplier: 'flat' };
data.damage.parts.push(part);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static removeDamage(_event, button) {
if (!this.action.damage.parts) return;
const data = this.action.toObject(),
index = button.dataset.index;
data.damage.parts.splice(index, 1);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static addTrigger() {
const data = this.action.toObject();
data.triggers.push({
trigger: CONFIG.DH.TRIGGER.triggers.dualityRoll.id,
triggeringActor: CONFIG.DH.TRIGGER.triggerActorTargetType.any.id
});
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static async removeTrigger(_event, button) {
const trigger = CONFIG.DH.TRIGGER.triggers[this.action.triggers[button.dataset.index].trigger];
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize('DAGGERHEART.ACTIONS.Config.deleteTriggerTitle')
},
content: game.i18n.format('DAGGERHEART.ACTIONS.Config.deleteTriggerContent', {
trigger: game.i18n.localize(trigger.label)
})
});
if (!confirmed) return;
const data = this.action.toObject();
data.triggers = data.triggers.filter((_, index) => index !== Number.parseInt(button.dataset.index));
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static async expandTrigger(_event, button) {
const index = Number.parseInt(button.dataset.index);
const toggle = (element, codeMirror) => {
codeMirror.classList.toggle('revealed');
const button = element.querySelector('a > i');
button.classList.toggle('fa-angle-up');
button.classList.toggle('fa-angle-down');
};
const fieldset = button.closest('fieldset');
const codeMirror = fieldset.querySelector('.code-mirror-wrapper');
toggle(fieldset, codeMirror);
if (this.openTrigger !== null && this.openTrigger !== index) {
const previouslyExpanded = fieldset
.closest(`section`)
.querySelector(`fieldset[data-index="${this.openTrigger}"]`);
const codeMirror = previouslyExpanded.querySelector('.code-mirror-wrapper');
toggle(previouslyExpanded, codeMirror);
this.openTrigger = index;
} else if (this.openTrigger === index) {
this.openTrigger = null;
} else {
this.openTrigger = index;
}
}
updateSummonCount(event) {
event.stopPropagation();
const wrapper = event.target.closest('.summon-count-wrapper');
const index = wrapper.dataset.index;
const data = this.action.toObject();
data.summon[index].count = event.target.value;
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
/** Specific implementation in extending classes **/
static async addEffect(_event) {}
static removeEffect(_event, _button) {}
static editEffect(_event) {}
async close(options) {
this.tabGroups.primary = 'base';
await super.close(options);
}
async _onDrop(event) {
const data = foundry.applications.ux.TextEditor.getDragEventData(event);
const item = await foundry.utils.fromUuid(data.uuid);
if (!(item instanceof game.system.api.documents.DhpActor)) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.summon.invalidDrop'));
return;
}
const actionData = this.action.toObject();
let countvalue = 1;
for (const entry of actionData.summon) {
if (entry.actorUUID === data.uuid) {
entry.count += 1;
countvalue = entry.count;
await this.constructor.updateForm.bind(this)(null, null, {
object: foundry.utils.flattenObject(actionData)
});
return;
}
}
actionData.summon.push({ actorUUID: data.uuid, count: countvalue });
await this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(actionData) });
}
}

View file

@ -1,241 +1,32 @@
import DaggerheartSheet from '../sheets/daggerheart-sheet.mjs';
const { ApplicationV2 } = foundry.applications.api;
export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
constructor(action, sheetUpdate) {
super({});
this.action = action;
this.sheetUpdate = sheetUpdate;
this.openSection = null;
}
get title() {
return `${game.i18n.localize('DAGGERHEART.GENERAL.Tabs.settings')}: ${this.action.name}`;
}
import DHActionBaseConfig from './action-base-config.mjs';
export default class DHActionConfig extends DHActionBaseConfig {
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'dh-style', 'dialog', 'max-800'],
window: {
icon: 'fa-solid fa-wrench',
resizable: false
},
position: { width: 600, height: 'auto' },
...DHActionBaseConfig.DEFAULT_OPTIONS,
actions: {
toggleSection: this.toggleSection,
...DHActionBaseConfig.DEFAULT_OPTIONS.actions,
addEffect: this.addEffect,
removeEffect: this.removeEffect,
addElement: this.addElement,
removeElement: this.removeElement,
editEffect: this.editEffect,
addDamage: this.addDamage,
removeDamage: this.removeDamage
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
editEffect: this.editEffect
}
};
static PARTS = {
header: {
id: 'header',
template: 'systems/daggerheart/templates/sheets-settings/action-settings/header.hbs'
},
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
base: {
id: 'base',
template: 'systems/daggerheart/templates/sheets-settings/action-settings/base.hbs'
},
configuration: {
id: 'configuration',
template: 'systems/daggerheart/templates/sheets-settings/action-settings/configuration.hbs'
},
effect: {
id: 'effect',
template: 'systems/daggerheart/templates/sheets-settings/action-settings/effect.hbs'
}
};
static TABS = {
base: {
active: true,
cssClass: '',
group: 'primary',
id: 'base',
icon: null,
label: 'DAGGERHEART.GENERAL.Tabs.base'
},
config: {
active: false,
cssClass: '',
group: 'primary',
id: 'config',
icon: null,
label: 'DAGGERHEART.GENERAL.Tabs.configuration'
},
effect: {
active: false,
cssClass: '',
group: 'primary',
id: 'effect',
icon: null,
label: 'DAGGERHEART.GENERAL.Tabs.effects'
}
};
static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects'];
_getTabs(tabs) {
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active;
v.cssClass = v.active ? 'active' : '';
}
return tabs;
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options, 'action');
context.source = this.action.toObject(false);
context.openSection = this.openSection;
context.tabs = this._getTabs(this.constructor.TABS);
context.config = CONFIG.DH;
async _prepareContext(options) {
const context = await super._prepareContext(options);
if (!!this.action.effects) context.effects = this.action.effects.map(e => this.action.item.effects.get(e._id));
if (this.action.damage?.hasOwnProperty('includeBase') && this.action.type === 'attack')
context.hasBaseDamage = !!this.action.parent.attack;
context.getEffectDetails = this.getEffectDetails.bind(this);
context.costOptions = this.getCostOptions();
context.getRollTypeOptions = this.getRollTypeOptions();
context.disableOption = this.disableOption.bind(this);
context.isNPC = this.action.actor?.isNPC;
context.baseSaveDifficulty = this.action.actor?.baseSaveDifficulty;
context.baseAttackBonus = this.action.actor?.system.attack?.roll.bonus;
context.hasRoll = this.action.hasRoll;
const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers;
context.tierOptions = [
{ key: 1, label: game.i18n.localize('DAGGERHEART.GENERAL.Tiers.1') },
...Object.values(settingsTiers).map(x => ({ key: x.tier, label: x.name }))
];
return context;
}
static toggleSection(_, button) {
this.openSection = button.dataset.section === this.openSection ? null : button.dataset.section;
this.render(true);
}
getCostOptions() {
const options = foundry.utils.deepClone(CONFIG.DH.GENERAL.abilityCosts);
const resource = this.action.parent.resource;
if (resource) {
options.resource = {
label: 'DAGGERHEART.GENERAL.itemResource',
group: 'Global'
};
}
if (this.action.parent.metadata?.isQuantifiable) {
options.quantity = {
label: 'DAGGERHEART.GENERAL.itemQuantity',
group: 'Global'
};
}
return options;
}
getRollTypeOptions() {
const types = foundry.utils.deepClone(CONFIG.DH.GENERAL.rollTypes);
if (!this.action.actor) return types;
Object.values(types).forEach(t => {
if (this.action.actor.type !== 'character' && t.playerOnly) delete types[t.id];
});
return types;
}
disableOption(index, costOptions, choices) {
const filtered = foundry.utils.deepClone(costOptions);
Object.keys(filtered).forEach(o => {
if (choices.find((c, idx) => c.type === o && index !== idx)) filtered[o].disabled = true;
});
return filtered;
}
getEffectDetails(id) {
return this.action.item.effects.get(id);
}
_prepareSubmitData(_event, formData) {
const submitData = foundry.utils.expandObject(formData.object);
const itemAbilityCostKeys = Object.keys(CONFIG.DH.GENERAL.itemAbilityCosts);
for (const keyPath of this.constructor.CLEAN_ARRAYS) {
const data = foundry.utils.getProperty(submitData, keyPath);
const dataValues = data ? Object.values(data) : [];
if (keyPath === 'cost') {
for (var value of dataValues) {
value.itemId = itemAbilityCostKeys.includes(value.key) ? this.action.parent.parent.id : null;
}
}
if (data) foundry.utils.setProperty(submitData, keyPath, dataValues);
}
return submitData;
}
static async updateForm(event, _, formData) {
const submitData = this._prepareSubmitData(event, formData),
data = foundry.utils.mergeObject(this.action.toObject(), submitData);
this.action = await this.action.update(data);
this.sheetUpdate?.(this.action);
this.render();
}
static addElement(event) {
const data = this.action.toObject(),
key = event.target.closest('[data-key]').dataset.key;
if (!this.action[key]) return;
data[key].push(this.action.defaultValues[key] ?? {});
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static removeElement(event, button) {
event.stopPropagation();
const data = this.action.toObject(),
key = event.target.closest('[data-key]').dataset.key,
index = button.dataset.index;
data[key].splice(index, 1);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static addDamage(event) {
if (!this.action.damage.parts) return;
const data = this.action.toObject(),
part = {};
if (this.action.actor?.isNPC) part.value = { multiplier: 'flat' };
data.damage.parts.push(part);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static removeDamage(event, button) {
if (!this.action.damage.parts) return;
const data = this.action.toObject(),
index = button.dataset.index;
data.damage.parts.splice(index, 1);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static async addEffect(event) {
static async addEffect(_event) {
if (!this.action.effects) return;
const effectData = this._addEffectData.bind(this)(),
[created] = await this.action.item.createEmbeddedDocuments('ActiveEffect', [effectData], { render: false }),
data = this.action.toObject();
const effectData = this._addEffectData.bind(this)();
const data = this.action.toObject();
const [created] = await this.action.item.createEmbeddedDocuments('ActiveEffect', [effectData], {
render: false
});
data.effects.push({ _id: created._id });
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
this.action.item.effects.get(created._id).sheet.render(true);
@ -255,6 +46,10 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
};
}
getEffectDetails(id) {
return this.action.item.effects.get(id);
}
static removeEffect(event, button) {
if (!this.action.effects) return;
const index = button.dataset.index,
@ -267,9 +62,4 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
const id = event.target.closest('[data-effect-id]')?.dataset?.effectId;
this.action.item.effects.get(id).sheet.render(true);
}
async close(options) {
this.tabGroups.primary = 'base';
await super.close(options);
}
}

View file

@ -0,0 +1,66 @@
import DHActionBaseConfig from './action-base-config.mjs';
export default class DHActionSettingsConfig extends DHActionBaseConfig {
constructor(action, effects, sheetUpdate) {
super(action);
this.effects = effects;
this.sheetUpdate = sheetUpdate;
}
static DEFAULT_OPTIONS = {
...DHActionBaseConfig.DEFAULT_OPTIONS,
actions: {
...DHActionBaseConfig.DEFAULT_OPTIONS.actions,
addEffect: this.addEffect,
removeEffect: this.removeEffect,
editEffect: this.editEffect
}
};
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.effects = this.effects;
context.getEffectDetails = this.getEffectDetails.bind(this);
return context;
}
getEffectDetails(id) {
return this.effects.find(x => x.id === id);
}
static async addEffect(_event) {
if (!this.action.effects) return;
const effectData = game.system.api.data.activeEffects.BaseEffect.getDefaultObject();
const data = this.action.toObject();
this.sheetUpdate(data, effectData);
this.effects = [...this.effects, effectData];
data.effects.push({ _id: effectData.id });
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static removeEffect(event, button) {
if (!this.action.effects) return;
const index = button.dataset.index,
effectId = this.action.effects[index]._id;
this.constructor.removeElement.bind(this)(event, button);
this.sheetUpdate(
this.action.toObject(),
this.effects.find(x => x.id === effectId),
true
);
}
static async editEffect(event) {
const id = event.target.closest('[data-effect-id]')?.dataset?.effectId;
const updatedEffect = await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(
this.getEffectDetails(id)
);
if (!updatedEffect) return;
this.effects = await this.sheetUpdate(this.action.toObject(), { ...updatedEffect, id });
this.render();
}
}

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;
}
/* -------------------------------------------- */
/**

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}
@ -109,9 +122,9 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
async _onDrop(event) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (data.fromInternal) return;
const item = await fromUuid(data.uuid);
if (data.fromInternal && item?.parent?.uuid === this.actor.uuid) return;
if (item.type === 'adversary' && event.target.closest('.category-container')) {
const target = event.target.closest('.category-container');
const path = `system.potentialAdversaries.${target.dataset.potentialAdversary}.adversaries`;

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

@ -23,7 +23,7 @@ export default class SettingActiveEffectConfig extends HandlebarsApplicationMixi
}
static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'sheet', 'dh-style', 'active-effect-config'],
classes: ['daggerheart', 'sheet', 'dh-style', 'active-effect-config', 'standard-form'],
tag: 'form',
position: {
width: 560
@ -131,6 +131,7 @@ export default class SettingActiveEffectConfig extends HandlebarsApplicationMixi
if (partId in context.tabs) context.tab = context.tabs[partId];
switch (partId) {
case 'details':
context.statuses = CONFIG.statusEffects.map(s => ({ value: s.id, label: game.i18n.localize(s.name) }));
context.isActorEffect = false;
context.isItemEffect = true;
const useGeneric = game.settings.get(
@ -138,10 +139,13 @@ export default class SettingActiveEffectConfig extends HandlebarsApplicationMixi
CONFIG.DH.SETTINGS.gameSettings.appearance
).showGenericStatusEffects;
if (!useGeneric) {
context.statuses = Object.values(CONFIG.DH.GENERAL.conditions).map(status => ({
context.statuses = [
...context.statuses,
Object.values(CONFIG.DH.GENERAL.conditions).map(status => ({
value: status.id,
label: game.i18n.localize(status.name)
}));
}))
];
}
break;
case 'changes':
@ -157,7 +161,7 @@ export default class SettingActiveEffectConfig extends HandlebarsApplicationMixi
return context;
}
static async #onSubmit(event, form, formData) {
static async #onSubmit(_event, _form, formData) {
this.data = foundry.utils.expandObject(formData.object);
this.close();
}
@ -193,11 +197,11 @@ export default class SettingActiveEffectConfig extends HandlebarsApplicationMixi
* @type {ApplicationClickAction}
*/
static async #addChange() {
const submitData = foundry.utils.expandObject(new FormDataExtended(this.form).object);
const changes = Object.values(submitData.changes ?? {});
changes.push({});
const { changes, ...rest } = foundry.utils.expandObject(new FormDataExtended(this.form).object);
const updatedChanges = Object.values(changes ?? {});
updatedChanges.push({});
this.effect.changes = changes;
this.effect = { ...rest, changes: updatedChanges };
this.render();
}
@ -208,12 +212,12 @@ export default class SettingActiveEffectConfig extends HandlebarsApplicationMixi
*/
static async #deleteChange(event) {
const submitData = foundry.utils.expandObject(new FormDataExtended(this.form).object);
const changes = Object.values(submitData.changes);
const updatedChanges = Object.values(submitData.changes);
const row = event.target.closest('li');
const index = Number(row.dataset.index) || 0;
changes.splice(index, 1);
updatedChanges.splice(index, 1);
this.effect.changes = changes;
this.effect = { ...submitData, changes: updatedChanges };
this.render();
}

View file

@ -1,5 +1,5 @@
import { actionsTypes } from '../../data/action/_module.mjs';
import DHActionConfig from './action-config.mjs';
import ActionSettingsConfig from './action-settings-config.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -102,6 +102,8 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
return (
(await foundry.applications.api.DialogV2.input({
window: { title: game.i18n.localize('DAGGERHEART.CONFIG.SelectAction.selectType') },
position: { width: 300 },
classes: ['daggerheart', 'dh-style'],
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/actionTypes/actionType.hbs',
{ types: CONFIG.DH.ACTIONS.actionTypes }
@ -158,16 +160,55 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
this.render();
} else {
const action = this.move.actions.get(id);
await new DHActionConfig(action, async updatedMove => {
await new ActionSettingsConfig(action, this.move.effects, async (updatedMove, effectData, deleteEffect) => {
let updatedEffects = null;
if (effectData) {
const currentEffects = foundry.utils.getProperty(this.settings, `${this.movePath}.effects`);
const existingEffectIndex = currentEffects.findIndex(x => x.id === effectData.id);
updatedEffects = deleteEffect
? currentEffects.filter(x => x.id !== effectData.id)
: existingEffectIndex === -1
? [...currentEffects, effectData]
: currentEffects.with(existingEffectIndex, effectData);
await this.settings.updateSource({
[`${this.movePath}.effects`]: updatedEffects
});
}
await this.settings.updateSource({ [`${this.actionsPath}.${id}`]: updatedMove });
this.move = foundry.utils.getProperty(this.settings, this.movePath);
this.render();
return updatedEffects;
}).render(true);
}
}
static async removeItem(_, target) {
const { type, id } = target.dataset;
if (type === 'effect') {
const move = foundry.utils.getProperty(this.settings, this.movePath);
for (const action of move.actions) {
const remainingEffects = action.effects.filter(x => x._id !== id);
if (action.effects.length !== remainingEffects.length) {
await action.update({
effects: remainingEffects.map(x => {
const { _id, ...rest } = x;
return { ...rest, _id: _id };
})
});
}
}
await this.settings.updateSource({
[this.movePath]: {
effects: move.effects.filter(x => x.id !== id),
actions: move.actions
}
});
} else {
await this.settings.updateSource({ [`${this.actionsPath}.-=${target.dataset.id}`]: null });
}
this.move = foundry.utils.getProperty(this.settings, this.movePath);
this.render();
}

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

@ -25,7 +25,13 @@ export default class AdversarySheet extends DHBaseActorSheet {
action: 'editAttribution'
}
]
},
dragDrop: [
{
dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
dropSelector: null
}
]
};
static PARTS = {
@ -87,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;
}
@ -163,6 +176,15 @@ 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 => {
@ -214,34 +211,8 @@ export default class CharacterSheet extends DHBaseActorSheet {
context.resources.stress.emptyPips =
context.resources.stress.max < maxResource ? maxResource - context.resources.stress.max : 0;
context.inventory = { currencies: {} };
const { title, ...currencies } = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Homebrew
).currency;
for (let key in currencies) {
context.inventory.currencies[key] = {
...currencies[key],
field: context.systemFields.gold.fields[key],
value: context.source.system.gold[key]
};
}
// context.inventory = {
// currency: {
// title: game.i18n.localize('DAGGERHEART.CONFIG.Gold.title'),
// coins: game.i18n.localize('DAGGERHEART.CONFIG.Gold.coins'),
// handfuls: game.i18n.localize('DAGGERHEART.CONFIG.Gold.handfuls'),
// bags: game.i18n.localize('DAGGERHEART.CONFIG.Gold.bags'),
// chests: game.i18n.localize('DAGGERHEART.CONFIG.Gold.chests')
// }
// };
context.beastformActive = this.document.effects.find(x => x.type === 'beastform');
// if (context.inventory.length === 0) {
// context.inventory = Array(1).fill(Array(5).fill([]));
// }
return context;
}
@ -348,6 +319,45 @@ 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',
@ -619,14 +629,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;
@ -710,20 +712,27 @@ export default class CharacterSheet extends DHBaseActorSheet {
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: abilityLabel
}),
effects: Array.from(await this.document.allApplicableEffects()),
roll: {
trait: button.dataset.attribute
trait: button.dataset.attribute,
type: 'trait'
},
hasRoll: true
};
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) return;
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
@ -821,7 +830,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
static async #toggleVault(_event, button) {
const doc = await getDocFromElement(button);
const { available } = this.document.system.loadoutSlot;
if (doc.system.inVault && !available) {
if (doc.system.inVault && !available && !doc.system.loadoutIgnore) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.loadoutMaxReached'));
}
@ -892,6 +901,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}
@ -902,47 +946,18 @@ export default class CharacterSheet extends DHBaseActorSheet {
});
}
/** @inheritdoc */
async _onDragStart(event) {
const item = await getDocFromElement(event.target);
const dragData = {
originActor: this.document.uuid,
originId: item.id,
type: item.documentName,
uuid: item.uuid
};
event.dataTransfer.setData('text/plain', JSON.stringify(dragData));
const inventoryItem = event.currentTarget.closest('.inventory-item');
if (inventoryItem) {
event.dataTransfer.setDragImage(inventoryItem.querySelector('img'), 60, 0);
}
super._onDragStart(event);
}
async _onDrop(event) {
// Prevent event bubbling to avoid duplicate handling
event.preventDefault();
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const { cancel } = await super._onDrop(event);
if (cancel) return;
this._onDropItem(event, data);
}
async _onDropItem(event, data) {
const item = await Item.implementation.fromDropData(data);
const itemData = item.toObject();
if (item.type === 'domainCard' && !this.document.system.loadoutSlot.available) {
itemData.system.inVault = true;
}
const typesThatReplace = ['ancestry', 'community'];
if (typesThatReplace.includes(item.type)) {
await this.document.deleteEmbeddedDocuments(
'Item',
this.document.items.filter(x => x.type === item.type).map(x => x.id)
);
async _onDropItem(event, item) {
if (this.document.uuid === item.parent?.uuid) {
return super._onDropItem(event, item);
}
if (item.type === 'beastform') {
@ -952,20 +967,27 @@ export default class CharacterSheet extends DHBaseActorSheet {
);
}
const itemData = item.toObject();
const data = await game.system.api.data.items.DHBeastform.getWildcardImage(this.document, itemData);
if (data) {
if (!data.selectedImage) return;
else {
if (!data?.selectedImage) {
return;
} else if (data) {
if (data.usesDynamicToken) itemData.system.tokenRingImg = data.selectedImage;
else itemData.system.tokenImg = data.selectedImage;
}
return await this._onDropItemCreate(itemData);
}
}
if (this.document.uuid === item.parent?.uuid) return this._onSortItem(event, itemData);
const createdItem = await this._onDropItemCreate(itemData);
// If this is a type that gets deleted, delete it first (but still defer to super)
const typesThatReplace = ['ancestry', 'community'];
if (typesThatReplace.includes(item.type)) {
await this.document.deleteEmbeddedDocuments(
'Item',
this.document.items.filter(x => x.type === item.type).map(x => x.id)
);
}
return createdItem;
return super._onDropItem(event, item);
}
async _onDropItemCreate(itemData, event) {

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 */
@ -37,11 +42,11 @@ export default class DhpEnvironment extends DHBaseActorSheet {
header: { template: 'systems/daggerheart/templates/sheets/actors/environment/header.hbs' },
features: {
template: 'systems/daggerheart/templates/sheets/actors/environment/features.hbs',
scrollable: ['feature-section']
scrollable: ['.feature-section']
},
potentialAdversaries: {
template: 'systems/daggerheart/templates/sheets/actors/environment/potentialAdversaries.hbs',
scrollable: ['items-sections']
scrollable: ['.items-section']
},
notes: { template: 'systems/daggerheart/templates/sheets/actors/environment/notes.hbs' }
};
@ -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
@ -130,12 +154,13 @@ export default class DhpEnvironment extends DHBaseActorSheet {
/* -------------------------------------------- */
async _onDragStart(event) {
const item = event.currentTarget.closest('.inventory-item');
const item = event.currentTarget.closest('.inventory-item[data-type=adversary]');
if (item) {
const adversaryData = { type: 'Actor', uuid: item.dataset.itemUuid };
event.dataTransfer.setData('text/plain', JSON.stringify(adversaryData));
event.dataTransfer.setDragImage(item, 60, 0);
} else {
return super._onDragStart(event);
}
}

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 */
@ -93,25 +93,6 @@ export default class Party extends DHBaseActorSheet {
/* Prepare Context */
/* -------------------------------------------- */
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.inventory = { currencies: {} };
const { title, ...currencies } = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Homebrew
).currency;
for (let key in currencies) {
context.inventory.currencies[key] = {
...currencies[key],
field: context.systemFields.gold.fields[key],
value: context.source.system.gold[key]
};
}
return context;
}
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
switch (partId) {
@ -438,30 +419,9 @@ export default class Party extends DHBaseActorSheet {
}
/* -------------------------------------------- */
async _onDragStart(event) {
const item = await getDocFromElement(event.target);
const dragData = {
originActor: this.document.uuid,
originId: item.id,
type: item.documentName,
uuid: item.uuid
};
event.dataTransfer.setData('text/plain', JSON.stringify(dragData));
super._onDragStart(event);
}
async _onDrop(event) {
// Prevent event bubbling to avoid duplicate handling
event.preventDefault();
event.stopPropagation();
async _onDropActor(event, document) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const { cancel } = await super._onDrop(event);
if (cancel) return;
const document = await foundry.utils.fromUuid(data.uuid);
if (document instanceof DhpActor && Party.ALLOWED_ACTOR_TYPES.includes(document.type)) {
const currentMembers = this.document.system.partyMembers.map(x => x.uuid);
if (currentMembers.includes(data.uuid)) {
@ -469,11 +429,11 @@ export default class Party extends DHBaseActorSheet {
}
await this.document.update({ 'system.partyMembers': [...currentMembers, document.uuid] });
} else if (document instanceof DHItem) {
this.document.createEmbeddedDocuments('Item', [document.toObject()]);
} else {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.onlyCharactersInPartySheet'));
}
return null;
}
static async #deletePartyMember(event, target) {

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 */
@ -322,10 +395,11 @@ export default function DHApplicationMixin(Base) {
_onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (data.fromInternal === this.document.uuid) return;
if (data.type === 'ActiveEffect') {
if (data.type === 'ActiveEffect' && data.fromInternal !== this.document.uuid) {
this.document.createEmbeddedDocuments('ActiveEffect', [data.data]);
} else {
// Fallback to super, but note that item sheets do not have this function
return super._onDrop?.(event);
}
}
@ -431,6 +505,7 @@ export default function DHApplicationMixin(Base) {
const doc = await getDocFromElement(target),
action = doc?.system?.attack ?? doc;
const config = action.prepareConfig(event);
config.effects = Array.from(await this.document.allApplicableEffects());
config.hasRoll = false;
return action && action.workflow.get('damage').execute(config, null, true);
}
@ -511,7 +586,9 @@ export default function DHApplicationMixin(Base) {
if (!doc || !descriptionElement) continue;
// localize the description (idk if it's still necessary)
const description = game.i18n.localize(doc.system?.description ?? doc.description);
const description = doc.system?.getEnrichedDescription
? await doc.system.getEnrichedDescription()
: game.i18n.localize(doc.system?.description ?? doc.description);
// Enrich the description and attach it;
const isAction = doc.documentName === 'Action';
@ -662,6 +739,9 @@ export default function DHApplicationMixin(Base) {
};
if (inVault) data['system.inVault'] = true;
if (disabled) data.disabled = true;
if (type === 'domainCard' && parent?.system.domains?.length) {
data.system.domain = parent.system.domains[0];
}
const doc = await cls.create(data, { parent, renderSheet: !event.shiftKey });
if (parentIsItem && type === 'feature') {

View file

@ -1,4 +1,4 @@
import { itemIsIdentical } from '../../../helpers/utils.mjs';
import { getDocFromElement, itemIsIdentical } from '../../../helpers/utils.mjs';
import DHBaseActorSettings from './actor-setting.mjs';
import DHApplicationMixin from './application-mixin.mjs';
@ -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 }
]
};
/* -------------------------------------------- */
@ -69,6 +72,29 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
context.showAttribution = !game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
.hideAttribution;
// Prepare inventory data
if (['party', 'character'].includes(this.document.type)) {
context.inventory = {
currencies: {},
weapons: this.document.itemTypes.weapon.sort((a, b) => a.sort - b.sort),
armor: this.document.itemTypes.armor.sort((a, b) => a.sort - b.sort),
consumables: this.document.itemTypes.consumable.sort((a, b) => a.sort - b.sort),
loot: this.document.itemTypes.loot.sort((a, b) => a.sort - b.sort)
};
const { title, ...currencies } = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Homebrew
).currency;
for (const key in currencies) {
context.inventory.currencies[key] = {
...currencies[key],
field: context.systemFields.gold.fields[key],
value: context.source.system.gold[key]
};
}
context.inventory.hasCurrency = Object.values(context.inventory.currencies).some((c) => c.enabled);
}
return context;
}
@ -112,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);
});
@ -150,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 */
/* -------------------------------------------- */
@ -219,44 +258,63 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
/* -------------------------------------------- */
async _onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (data.originActor === this.document.uuid) return { cancel: true };
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 originActor = item.actor;
if (
item.actor?.uuid === this.document.uuid ||
!originActor ||
!['character', 'party'].includes(this.document.type)
) {
return super._onDropItem(event, item);
}
/* Handling transfer of inventoryItems */
let cancel = false;
const physicalActorTypes = ['character', 'party'];
if (physicalActorTypes.includes(this.document.type)) {
const originActor = data.originActor ? await foundry.utils.fromUuid(data.originActor) : null;
if (data.originId && originActor && physicalActorTypes.includes(originActor.type)) {
const dropDocument = await foundry.utils.fromUuid(data.uuid);
if (dropDocument.system.metadata.isInventoryItem) {
cancel = true;
if (dropDocument.system.metadata.isQuantifiable) {
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(dropDocument);
const quantityTransfered = await game.system.api.applications.dialogs.ItemTransferDialog.configure({
item,
targetActor: this.document
});
if (quantityTransfered) {
if (quantityTransfered === actorItem.system.quantity) {
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
} else {
cancel = true;
await actorItem.update({
'system.quantity': actorItem.system.quantity - quantityTransfered
});
}
const existingItem = this.document.items.find(x => itemIsIdentical(x, dropDocument));
const existingItem = this.document.items.find(x => itemIsIdentical(x, item));
if (existingItem) {
cancel = true;
await existingItem.update({
'system.quantity': existingItem.system.quantity + quantityTransfered
});
} else {
const createData = dropDocument.toObject();
const createData = item.toObject();
await this.document.createEmbeddedDocuments('Item', [
{
...createData,
@ -267,28 +325,30 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
}
]);
}
} else {
cancel = true;
}
} else {
await originActor.deleteEmbeddedDocuments('Item', [data.originId], { render: false });
const createData = dropDocument.toObject();
await this.document.createEmbeddedDocuments('Item', [createData]);
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
await this.document.createEmbeddedDocuments('Item', [item.toObject()]);
}
}
}
}
return { cancel };
}
/**
* On dragStart on the item.
* @param {DragEvent} event - The drag event
*/
async _onDragStart(event) {
const attackItem = event.currentTarget.closest('.inventory-item[data-type="attack"]');
// 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 = {
type: 'Attack',
@ -298,8 +358,20 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
};
event.dataTransfer.setData('text/plain', JSON.stringify(attackData));
event.dataTransfer.setDragImage(attackItem.querySelector('img'), 60, 0);
} else if (this.document.type !== 'environment') {
return;
}
const item = await getDocFromElement(event.target);
if (item) {
const dragData = {
originActor: this.document.uuid,
originId: item.id,
type: item.documentName,
uuid: item.uuid
};
event.dataTransfer.setData('text/plain', JSON.stringify(dragData));
}
super._onDragStart(event);
}
}
}

View file

@ -76,16 +76,10 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
/**@inheritdoc */
async _preparePartContext(partId, context, options) {
await super._preparePartContext(partId, context, options);
const { TextEditor } = foundry.applications.ux;
switch (partId) {
case 'description':
const value = foundry.utils.getProperty(this.document, 'system.description') ?? '';
context.enrichedDescription = await TextEditor.enrichHTML(value, {
relativeTo: this.item,
rollData: this.item.getRollData(),
secrets: this.item.isOwner
});
context.enrichedDescription = await this.document.system.getEnrichedDescription();
break;
case 'effects':
await this._prepareEffectsContext(context, options);

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

@ -1,2 +1,3 @@
export { default as DaggerheartMenu } from './tabs/daggerheartMenu.mjs';
export { default as DhActorDirectory } from './tabs/actorDirectory.mjs';
export { default as DhSidebar } from './sidebar.mjs';

View file

@ -0,0 +1,46 @@
export default class DhActorDirectory extends foundry.applications.sidebar.tabs.ActorDirectory {
static DEFAULT_OPTIONS = {
renderUpdateKeys: ['system.levelData.level.current', 'system.partner', 'system.tier']
};
static _entryPartial = 'systems/daggerheart/templates/ui/sidebar/actor-document-partial.hbs';
async _prepareDirectoryContext(context, options) {
await super._prepareDirectoryContext(context, options);
const adversaryTypes = CONFIG.DH.ACTOR.allAdversaryTypes();
const environmentTypes = CONFIG.DH.ACTOR.environmentTypes;
context.getTypeLabel = document => {
return document.type === 'adversary'
? game.i18n.localize(adversaryTypes[document.system.type]?.label ?? 'TYPES.Actor.adversary')
: document.type === 'environment'
? game.i18n.localize(environmentTypes[document.system.type]?.label ?? 'TYPES.Actor.environment')
: 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

@ -1,3 +1,5 @@
import { refreshIsAllowed } from '../../../helpers/utils.mjs';
const { HandlebarsApplicationMixin } = foundry.applications.api;
const { AbstractSidebarTab } = foundry.applications.sidebar;
/**
@ -58,7 +60,7 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
if (['character', 'adversary'].includes(actor.type) && actor.prototypeToken.actorLink) {
const updates = {};
for (let item of actor.items) {
if (item.system.metadata?.hasResource && types.includes(item.system.resource?.recovery)) {
if (item.system.metadata?.hasResource && refreshIsAllowed(types, item.system.resource?.recovery)) {
if (!refreshedActors[actor.id])
refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() };
refreshedActors[actor.id].refreshed.add(
@ -79,7 +81,7 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
if (item.system.metadata?.hasActions) {
const refreshTypes = new Set();
const actions = item.system.actions.filter(action => {
if (types.includes(action.uses.recovery)) {
if (refreshIsAllowed(types, action.uses.recovery)) {
refreshTypes.add(action.uses.recovery);
return true;
}

View file

@ -2,6 +2,8 @@ export { default as CountdownEdit } from './countdownEdit.mjs';
export { default as DhCountdowns } from './countdowns.mjs';
export { default as DhChatLog } from './chatLog.mjs';
export { default as DhCombatTracker } from './combatTracker.mjs';
export { default as DhEffectsDisplay } from './effectsDisplay.mjs';
export { default as DhFearTracker } from './fearTracker.mjs';
export { default as DhHotbar } from './hotbar.mjs';
export { default as DhSceneNavigation } from './sceneNavigation.mjs';
export { ItemBrowser } from './itemBrowser.mjs';

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

@ -1,10 +1,11 @@
import { AdversaryBPPerEncounter } from '../../config/encounterConfig.mjs';
export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
static DEFAULT_OPTIONS = {
actions: {
requestSpotlight: this.requestSpotlight,
toggleSpotlight: this.toggleSpotlight,
setActionTokens: this.setActionTokens,
openCountdowns: this.openCountdowns
setActionTokens: this.setActionTokens
}
};
@ -20,11 +21,33 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
}
};
/** @inheritDoc */
async _preparePartContext(_partId, context, _options) {
return context;
}
async _prepareContext(options) {
const context = await super._prepareContext(options);
await this._prepareTrackerContext(context, options);
await this._prepareCombatContext(context, options);
return context;
}
async _prepareCombatContext(context, options) {
await super._prepareCombatContext(context, options);
const modifierBP =
this.combats
.find(x => x.active)
?.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)
fear: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
battlepoints: { max: maxBP, current: currentBP, hasModifierBP: modifierBP !== null }
});
}
@ -33,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
});
}
@ -99,6 +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,
img: await this._getCombatantThumbnail(combatant)
};
@ -136,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;
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);
if (combatant.actor.type === 'character') {
await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.characterSpotlight.id);
}
const autoPoints = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).actionPoints;

View file

@ -14,7 +14,6 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
constructor(options = {}) {
super(options);
this.sidebarCollapsed = true;
this.setupHooks();
}
@ -98,11 +97,10 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.isGM = game.user.isGM;
context.sidebarCollapsed = this.sidebarCollapsed;
context.iconOnly =
game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode) ===
CONFIG.DH.GENERAL.countdownAppMode.iconOnly;
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
context.countdowns = this.#getCountdowns().reduce((acc, { key, countdown, ownership }) => {
const playersWithAccess = game.users.reduce((acc, user) => {
@ -247,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);
}
@ -262,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]);
@ -273,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

@ -0,0 +1,117 @@
import { RefreshType } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
/**
* A UI element which displays the Active Effects on a selected token.
*
* @extends ApplicationV2
* @mixes HandlebarsApplication
*/
export default class DhEffectsDisplay extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(options = {}) {
super(options);
this.setupHooks();
}
/** @inheritDoc */
static DEFAULT_OPTIONS = {
id: 'effects-display',
tag: 'div',
classes: ['daggerheart', 'dh-style', 'effects-display'],
window: {
frame: false,
positioned: false,
resizable: false,
minimizable: false
},
actions: {}
};
/** @override */
static PARTS = {
resources: {
root: true,
template: 'systems/daggerheart/templates/ui/effects-display.hbs'
}
};
get element() {
return document.body.querySelector('.daggerheart.dh-style.effects-display');
}
get hidden() {
return this.element.classList.contains('hidden');
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
if (this.element) {
this.element.querySelectorAll('.effect-container a').forEach(element => {
element.addEventListener('contextmenu', this.removeEffect.bind(this));
});
}
}
/** @override */
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.effects = DhEffectsDisplay.getTokenEffects();
return context;
}
static getTokenEffects = token => {
const actor = token
? token.actor
: canvas.tokens.controlled.length === 0
? !game.user.isGM
? game.user.character
: null
: canvas.tokens.controlled[0].actor;
return actor?.getActiveEffects() ?? [];
};
toggleHidden(token, focused) {
const effects = DhEffectsDisplay.getTokenEffects(focused ? token : null);
this.element.hidden = effects.length === 0;
Hooks.callAll(CONFIG.DH.HOOKS.effectDisplayToggle, this.element.hidden, token);
this.render();
}
async removeEffect(event) {
const element = event.target.closest('.effect-container');
const effects = DhEffectsDisplay.getTokenEffects();
const effect = effects.find(x => x.id === element.dataset.effectId);
await effect.delete();
this.render();
}
setupHooks() {
Hooks.on('controlToken', this.toggleHidden.bind(this));
Hooks.on(RefreshType.EffectsDisplay, this.toggleHidden.bind(this));
}
async close(options) {
/* Opt out of Foundry's standard behavior of closing all application windows marked as UI when Escape is pressed */
if (options.closeKey) return;
Hooks.off('controlToken', this.toggleHidden);
Hooks.off(RefreshType.EffectsDisplay, this.toggleHidden);
return super.close(options);
}
async _onRender(context, options) {
await super._onRender(context, options);
this.element.hidden = context.effects.length === 0;
if (options?.force) {
document.getElementById('ui-right-column-1')?.appendChild(this.element);
}
}
}

View file

@ -230,6 +230,14 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
result.flatMap(r => r),
'name'
);
/* If any noticeable slowdown occurs, consider replacing with enriching description on clicking to expand descriptions */
for (const item of this.items) {
item.system.enrichedDescription =
(await item.system.getEnrichedDescription?.()) ??
(await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.description));
}
this.fieldFilter = this._createFieldFilter();
if (this.presets?.filter) {

View file

@ -0,0 +1,89 @@
import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
export default class DhSceneNavigation extends foundry.applications.ui.SceneNavigation {
/** @inheritdoc */
static DEFAULT_OPTIONS = {
...super.DEFAULT_OPTIONS,
classes: ['faded-ui', 'flexcol', 'scene-navigation'],
actions: {
openSceneEnvironment: DhSceneNavigation.#openSceneEnvironment
}
};
/** @inheritdoc */
static PARTS = {
scenes: {
root: true,
template: 'systems/daggerheart/templates/ui/sceneNavigation/scene-navigation.hbs'
}
};
/** @inheritdoc */
async _prepareContext(options) {
const context = await super._prepareContext(options);
const extendScenes = scenes =>
scenes.map(x => {
const scene = game.scenes.get(x.id);
if (!scene.flags.daggerheart) return x;
const daggerheartInfo = new game.system.api.data.scenes.DHScene(scene.flags.daggerheart);
const environments = daggerheartInfo.sceneEnvironments.filter(
x => x && x.testUserPermission(game.user, 'LIMITED')
);
const hasEnvironments = environments.length > 0 && x.isView;
return {
...x,
hasEnvironments,
environmentImage: hasEnvironments ? environments[0].img : null,
environments: environments
};
});
context.scenes.active = extendScenes(context.scenes.active);
context.scenes.inactive = extendScenes(context.scenes.inactive);
return context;
}
static async #openSceneEnvironment(event, button) {
const scene = game.scenes.get(button.dataset.sceneId);
const sceneEnvironments = new game.system.api.data.scenes.DHScene(
scene.flags.daggerheart
).sceneEnvironments.filter(x => x.testUserPermission(game.user, 'LIMITED'));
if (sceneEnvironments.length === 1 || event.shiftKey) {
sceneEnvironments[0].sheet.render(true);
} else {
new foundry.applications.ux.ContextMenu.implementation(
button,
'.scene-environment',
sceneEnvironments.map(environment => ({
name: environment.name,
callback: () => {
if (scene.flags.daggerheart.sceneEnvironments[0] !== environment.uuid) {
const newEnvironments = scene.flags.daggerheart.sceneEnvironments;
const newFirst = newEnvironments.splice(
newEnvironments.findIndex(x => x === environment.uuid)
)[0];
newEnvironments.unshift(newFirst);
emitAsGM(
GMUpdateEvent.UpdateDocument,
scene.update.bind(scene),
{ 'flags.daggerheart.sceneEnvironments': newEnvironments },
scene.uuid
);
}
environment.sheet.render({ force: true });
}
})),
{
jQuery: false,
fixed: true
}
);
CONFIG.ux.ContextMenu.triggerContextMenu(event, '.scene-environment');
}
}
}

View file

@ -96,11 +96,11 @@ export default class DHContextMenu extends foundry.applications.ux.ContextMenu {
* Trigger a context menu event in response to a normal click on a additional options button.
* @param {PointerEvent} event
*/
static triggerContextMenu(event) {
static triggerContextMenu(event, altSelector) {
event.preventDefault();
event.stopPropagation();
const { clientX, clientY } = event;
const selector = '[data-item-uuid]';
const selector = altSelector ?? '[data-item-uuid]';
const target = event.target.closest(selector) ?? event.currentTarget.closest(selector);
target?.dispatchEvent(
new PointerEvent('contextmenu', {

View file

@ -1 +1,2 @@
export * as placeables from './placeables/_module.mjs';
export { default as DhTokenLayer } from './tokens.mjs';

View file

@ -1,4 +1,12 @@
export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
/** @inheritdoc */
async _draw(options) {
await super._draw(options);
if (this.document.flags.daggerheart?.createPlacement)
this.previewHelp ||= this.addChild(this.#drawPreviewHelp());
}
/** @inheritDoc */
async _drawEffects() {
this.effects.renderable = false;
@ -10,29 +18,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
this.effects.overlay = null;
// Categorize effects
const statusMap = new Map(foundry.CONFIG.statusEffects.map(status => [status.id, status]));
const activeEffects = (this.actor ? this.actor.effects.filter(x => !x.disabled) : []).reduce((acc, effect) => {
acc.push(effect);
const currentStatusActiveEffects = acc.filter(
x => x.statuses.size === 1 && x.name === game.i18n.localize(statusMap.get(x.statuses.first())?.name)
);
for (var status of effect.statuses) {
if (!currentStatusActiveEffects.find(x => x.statuses.has(status))) {
const statusData = statusMap.get(status);
if (statusData) {
acc.push({
name: game.i18n.localize(statusData.name),
statuses: [status],
img: statusData.icon ?? statusData.img,
tint: effect.tint
});
}
}
}
return acc;
}, []);
const activeEffects = this.actor?.getActiveEffects() ?? [];
const overlayEffect = activeEffects.findLast(e => e.img && e.getFlag?.('core', 'overlay'));
// Draw effects
@ -56,6 +42,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);
@ -91,4 +140,25 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
bar.position.set(0, posY);
return true;
}
/**
* Draw a helptext for previews as a text object
* @returns {PreciseText} The Text object for the preview helper
*/
#drawPreviewHelp() {
const { uiScale } = canvas.dimensions;
const textStyle = CONFIG.canvasTextStyle.clone();
textStyle.fontSize = 18;
textStyle.wordWrapWidth = this.w * 2.5;
textStyle.fontStyle = 'italic';
const helpText = new foundry.canvas.containers.PreciseText(
`(${game.i18n.localize('DAGGERHEART.UI.Tooltip.previewTokenHelp')})`,
textStyle
);
helpText.anchor.set(helpText.width / 900, 1);
helpText.scale.set(uiScale, uiScale);
return helpText;
}
}

16
module/canvas/tokens.mjs Normal file
View file

@ -0,0 +1,16 @@
export default class DhTokenLayer extends foundry.canvas.layers.TokenLayer {
async _createPreview(createData, options) {
if (options.actor) {
const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes;
if (options.actor?.system.metadata.usesSize) {
const tokenSize = tokenSizes[options.actor.system.size];
if (tokenSize && options.actor.system.size !== CONFIG.DH.ACTOR.tokenSize.custom.id) {
createData.width = tokenSize;
createData.height = tokenSize;
}
}
}
return super._createPreview(createData, options);
}
}

View file

@ -2,9 +2,12 @@ export * as actionConfig from './actionConfig.mjs';
export * as actorConfig from './actorConfig.mjs';
export * as domainConfig from './domainConfig.mjs';
export * as effectConfig from './effectConfig.mjs';
export * as encounterConfig from './encounterConfig.mjs';
export * as flagsConfig from './flagsConfig.mjs';
export * as generalConfig from './generalConfig.mjs';
export * as hooksConfig from './hooksConfig.mjs';
export * as itemConfig from './itemConfig.mjs';
export * as settingsConfig from './settingsConfig.mjs';
export * as systemConfig from './system.mjs';
export * as itemBrowserConfig from './itemBrowserConfig.mjs';
export * as triggerConfig from './triggerConfig.mjs';

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

@ -108,52 +108,64 @@ export const adversaryTypes = {
bruiser: {
id: 'bruiser',
label: 'DAGGERHEART.CONFIG.AdversaryType.bruiser.label',
description: 'DAGGERHEART.ACTORS.Adversary.bruiser.description'
description: 'DAGGERHEART.ACTORS.Adversary.bruiser.description',
bpCost: 4
},
horde: {
id: 'horde',
label: 'DAGGERHEART.CONFIG.AdversaryType.horde.label',
description: 'DAGGERHEART.ACTORS.Adversary.horde.description'
description: 'DAGGERHEART.ACTORS.Adversary.horde.description',
bpCost: 2
},
leader: {
id: 'leader',
label: 'DAGGERHEART.CONFIG.AdversaryType.leader.label',
description: 'DAGGERHEART.ACTORS.Adversary.leader.description'
description: 'DAGGERHEART.ACTORS.Adversary.leader.description',
bpCost: 3,
bpDescription: 'DAGGERHEART.CONFIG.AdversaryType.leader.'
},
minion: {
id: 'minion',
label: 'DAGGERHEART.CONFIG.AdversaryType.minion.label',
description: 'DAGGERHEART.ACTORS.Adversary.minion.description'
description: 'DAGGERHEART.ACTORS.Adversary.minion.description',
bpCost: 1,
partyAmountPerBP: true
},
ranged: {
id: 'ranged',
label: 'DAGGERHEART.CONFIG.AdversaryType.ranged.label',
description: 'DAGGERHEART.ACTORS.Adversary.ranged.description'
description: 'DAGGERHEART.ACTORS.Adversary.ranged.description',
bpCost: 2
},
skulk: {
id: 'skulk',
label: 'DAGGERHEART.CONFIG.AdversaryType.skulk.label',
description: 'DAGGERHEART.ACTORS.Adversary.skulk.description'
description: 'DAGGERHEART.ACTORS.Adversary.skulk.description',
bpCost: 2
},
social: {
id: 'social',
label: 'DAGGERHEART.CONFIG.AdversaryType.social.label',
description: 'DAGGERHEART.ACTORS.Adversary.social.description'
description: 'DAGGERHEART.ACTORS.Adversary.social.description',
bpCost: 1
},
solo: {
id: 'solo',
label: 'DAGGERHEART.CONFIG.AdversaryType.solo.label',
description: 'DAGGERHEART.ACTORS.Adversary.solo.description'
description: 'DAGGERHEART.ACTORS.Adversary.solo.description',
bpCost: 5
},
standard: {
id: 'standard',
label: 'DAGGERHEART.CONFIG.AdversaryType.standard.label',
description: 'DAGGERHEART.ACTORS.Adversary.standard.description'
description: 'DAGGERHEART.ACTORS.Adversary.standard.description',
bpCost: 2
},
support: {
id: 'support',
label: 'DAGGERHEART.CONFIG.AdversaryType.support.label',
description: 'DAGGERHEART.ACTORS.Adversary.support.description'
description: 'DAGGERHEART.ACTORS.Adversary.support.description',
bpCost: 1
}
};
@ -199,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

@ -0,0 +1,146 @@
export const BaseBPPerEncounter = nrCharacters => 3 * nrCharacters + 2;
export const AdversaryBPPerEncounter = (adversaries, characters) => {
const adversaryTypes = CONFIG.DH.ACTOR.allAdversaryTypes();
return adversaries
.reduce((acc, adversary) => {
const existingEntry = acc.find(
x => x.adversary.name === adversary.name && x.adversary.type === adversary.type
);
if (existingEntry) {
existingEntry.nr += 1;
} else if (adversary.type) {
acc.push({ adversary, nr: 1 });
}
return acc;
}, [])
.reduce((acc, entry) => {
const adversary = entry.adversary;
const type = adversaryTypes[adversary.type];
const bpCost = type.bpCost ?? 0;
if (type.partyAmountPerBP) {
acc += characters.length === 0 ? 0 : Math.ceil(entry.nr / characters.length);
} else {
acc += bpCost * entry.nr;
}
return acc;
}, 0);
};
export const adversaryTypeCostBrackets = {
1: [
{
sort: 1,
types: ['minion'],
description: 'DAGGERHEART.CONFIG.AdversaryTypeCost.minion'
},
{
sort: 2,
types: ['social', 'support'],
description: 'DAGGERHEART.CONFIG.AdversaryTypeCost.support'
}
],
2: [
{
sort: 1,
types: ['horde', 'ranged', 'skulk', 'standard'],
description: 'DAGGERHEART.CONFIG.AdversaryTypeCost.standard'
}
],
3: [
{
sort: 1,
types: ['leader'],
description: 'DAGGERHEART.CONFIG.AdversaryTypeCost.leader'
}
],
4: [
{
sort: 1,
types: ['bruiser'],
description: 'DAGGERHEART.CONFIG.AdversaryTypeCost.bruiser'
}
],
5: [
{
sort: 1,
types: ['solo'],
description: 'DAGGERHEART.CONFIG.AdversaryTypeCost.solo'
}
]
};
export const BPModifiers = {
[-2]: {
manySolos: {
sort: 1,
description: 'DAGGERHEART.CONFIG.BPModifiers.manySolos.description',
automatic: true,
conditional: (_combat, adversaries) => {
return adversaries.filter(x => x.system.type === 'solo').length > 1;
}
},
increaseDamage: {
sort: 2,
description: 'DAGGERHEART.CONFIG.BPModifiers.increaseDamage.description',
effectTargetTypes: ['adversary'],
effects: [
{
name: 'DAGGERHEART.CONFIG.BPModifiers.increaseDamage.effect.name',
description: 'DAGGERHEART.CONFIG.BPModifiers.increaseDamage.effect.description',
img: 'icons/magic/control/buff-flight-wings-red.webp',
changes: [
{
key: 'system.bonuses.damage.physical.dice',
mode: 2,
value: '1d4'
},
{
key: 'system.bonuses.damage.magical.dice',
mode: 2,
value: '1d4'
}
]
}
]
}
},
[-1]: {
lessDifficult: {
sort: 2,
description: 'DAGGERHEART.CONFIG.BPModifiers.lessDifficult.description'
}
},
1: {
lowerTier: {
sort: 1,
description: 'DAGGERHEART.CONFIG.BPModifiers.lowerTier.description',
automatic: true,
conditional: (_combat, adversaries, characters) => {
const characterMaxTier = characters.reduce((maxTier, character) => {
return character.system.tier > maxTier ? character.system.tier : maxTier;
}, 1);
return adversaries.some(adversary => adversary.system.tier < characterMaxTier);
}
},
noToughies: {
sort: 2,
description: 'DAGGERHEART.CONFIG.BPModifiers.noToughies.description',
automatic: true,
conditional: (_combat, adversaries) => {
const toughyTypes = ['bruiser', 'horde', 'leader', 'solo'];
return (
adversaries.length > 0 &&
!adversaries.some(adversary => toughyTypes.includes(adversary.system.type))
);
}
}
},
2: {
moreDangerous: {
sort: 2,
description: 'DAGGERHEART.CONFIG.BPModifiers.moreDangerous.description'
}
}
};

View file

@ -26,3 +26,5 @@ export const userFlags = {
welcomeMessage: 'welcome-message',
countdownMode: 'countdown-mode'
};
export const combatToggle = 'combat-toggle-origin';

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: [
@ -496,6 +496,8 @@ export const diceTypes = {
d20: 'd20'
};
export const dieFaces = [4, 6, 8, 10, 12, 20];
export const multiplierTypes = {
prof: 'Proficiency',
cast: 'Spellcast',

View file

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

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
}
]
}
@ -459,7 +451,16 @@ export const allArmorFeatures = () => {
...armorFeatures,
...Object.keys(homebrewFeatures).reduce((acc, key) => {
const feature = homebrewFeatures[key];
acc[key] = { ...feature, label: feature.name };
const actions = feature.actions.map(action => ({
...action,
effects: action.effects.map(effect => feature.effects.find(x => x.id === effect._id)),
type: action.type
}));
const actionEffects = actions.flatMap(a => a.effects);
const effects = feature.effects.filter(effect => !actionEffects.some(x => x.id === effect.id));
acc[key] = { ...feature, label: feature.name, effects, actions };
return acc;
}, {})
};
@ -528,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',
@ -573,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',
@ -587,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',
@ -601,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',
@ -638,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',
@ -679,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',
@ -693,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',
@ -716,7 +710,8 @@ export const weaponFeatures = {
{
key: 'system.evasion',
mode: 2,
value: '@system.armorScore'
value: '@system.armorScore',
priority: 21
}
]
}
@ -730,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',
@ -775,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',
@ -826,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',
@ -840,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',
@ -854,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',
@ -868,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',
@ -888,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',
@ -920,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',
@ -968,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',
@ -982,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',
@ -996,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',
@ -1010,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',
@ -1024,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',
@ -1038,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',
@ -1052,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',
@ -1090,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',
@ -1136,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',
@ -1150,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',
@ -1187,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',
@ -1231,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',
@ -1269,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',
@ -1283,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',
@ -1297,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',
@ -1311,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',
@ -1355,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
}
]
}
@ -1367,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',
@ -1381,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',
@ -1401,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',
@ -1414,11 +1383,21 @@ export const weaponFeatures = {
export const allWeaponFeatures = () => {
const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures
.weaponFeatures;
return {
...weaponFeatures,
...Object.keys(homebrewFeatures).reduce((acc, key) => {
const feature = homebrewFeatures[key];
acc[key] = { ...feature, label: feature.name };
const actions = feature.actions.map(action => ({
...action,
effects: action.effects.map(effect => feature.effects.find(x => x.id === effect._id)),
type: action.type
}));
const actionEffects = actions.flatMap(a => a.effects);
const effects = feature.effects.filter(effect => !actionEffects.some(x => x.id === effect.id));
acc[key] = { ...feature, label: feature.name, effects, actions };
return acc;
}, {})
};
@ -1439,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',
@ -1496,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

@ -1,17 +1,21 @@
import * as GENERAL from './generalConfig.mjs';
import * as DOMAIN from './domainConfig.mjs';
import * as ENCOUNTER from './encounterConfig.mjs';
import * as ACTOR from './actorConfig.mjs';
import * as ITEM from './itemConfig.mjs';
import * as SETTINGS from './settingsConfig.mjs';
import * as EFFECTS from './effectConfig.mjs';
import * as ACTIONS from './actionConfig.mjs';
import * as FLAGS from './flagsConfig.mjs';
import * as ITEMBROWSER from './itemBrowserConfig.mjs'
import * as HOOKS from './hooksConfig.mjs';
import * as TRIGGER from './triggerConfig.mjs';
import * as ITEMBROWSER from './itemBrowserConfig.mjs';
export const SYSTEM_ID = 'daggerheart';
export const SYSTEM = {
id: SYSTEM_ID,
ENCOUNTER,
GENERAL,
DOMAIN,
ACTOR,
@ -20,5 +24,7 @@ export const SYSTEM = {
EFFECTS,
ACTIONS,
FLAGS,
HOOKS,
TRIGGER,
ITEMBROWSER
};

View file

@ -0,0 +1,42 @@
/* hints and returns are intentionally not translated. They are programatical terms and best understood in english */
export const triggers = {
dualityRoll: {
id: 'dualityRoll',
usesActor: true,
args: ['roll', 'actor'],
label: 'DAGGERHEART.CONFIG.Triggers.dualityRoll.label',
hint: 'this: Action, roll: DhRoll, actor: DhActor',
returns: '{ updates: [{ key, value, total }] }'
},
fearRoll: {
id: 'fearRoll',
usesActor: true,
args: ['roll', 'actor'],
label: 'DAGGERHEART.CONFIG.Triggers.fearRoll.label',
hint: 'this: Action, roll: DhRoll, actor: DhActor',
returns: '{ updates: [{ key, value, total }] }'
},
postDamageReduction: {
id: 'postDamageReduction',
usesActor: true,
args: ['damageUpdates', 'actor'],
label: 'DAGGERHEART.CONFIG.Triggers.postDamageReduction.label',
hint: 'damageUpdates: ResourceUpdates, actor: DhActor',
returns: '{ updates: [{ originActor: this.actor, updates: [{ key, value, total }] }] }'
}
};
export const triggerActorTargetType = {
any: {
id: 'any',
label: 'DAGGERHEART.CONFIG.TargetTypes.any'
},
self: {
id: 'self',
label: 'DAGGERHEART.CONFIG.TargetTypes.self'
},
other: {
id: 'other',
label: 'DAGGERHEART.CONFIG.TargetTypes.other'
}
};

View file

@ -36,6 +36,7 @@ export default class DHAttackAction extends DHDamageAction {
async use(event, options) {
const result = await super.use(event, options);
if (!result.message) return;
if (result.message.system.action.roll?.type === 'attack') {
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;

View file

@ -2,6 +2,7 @@ import DhpActor from '../../documents/actor.mjs';
import D20RollDialog from '../../applications/dialogs/d20RollDialog.mjs';
import { ActionMixin } from '../fields/actionField.mjs';
import { originItemField } from '../chat-message/actorRoll.mjs';
import TriggerField from '../fields/triggerField.mjs';
const fields = foundry.data.fields;
@ -33,7 +34,9 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
initial: 'action',
nullable: false,
required: true
})
}),
targetUuid: new fields.StringField({ initial: undefined }),
triggers: new fields.ArrayField(new TriggerField())
};
this.extraSchemas.forEach(s => {
@ -95,6 +98,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 +165,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
* @returns {object}
*/
getRollData(data = {}) {
if (!this.actor) return null;
const actorData = this.actor.getRollData(false);
// Add Roll results to RollDatas
const actorData = this.actor ? this.actor.getRollData(false) : {};
actorData.result = data.roll?.total ?? 1;
actorData.scale = data.costs?.length // Right now only return the first scalable cost.
? (data.costs.find(c => c.scalable)?.total ?? 1)
: 1;
@ -193,11 +195,11 @@ 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;
await this.addEffects(config);
if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return;
// Display configuration window if necessary
@ -208,9 +210,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 +244,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;
}
@ -258,6 +266,16 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return config;
}
/** */
async addEffects(config) {
let effects = [];
if (this.actor) {
effects = Array.from(await this.actor.allApplicableEffects());
}
config.effects = effects;
}
/**
* Method used to know if a configuration dialog must be shown or not when there is no roll.
* @param {*} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
@ -322,11 +340,50 @@ 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) {
if (!resources?.length) return;
const invalidResources = resources.some(resource => !resource.key);
if (invalidResources) return;
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

@ -2,4 +2,17 @@ import DHBaseAction from './baseAction.mjs';
export default class DHDamageAction extends DHBaseAction {
static extraSchemas = [...super.extraSchemas, 'damage', 'target', 'effects'];
/**
* Return a display ready damage formula string
* @returns Formula string
*/
getDamageFormula() {
const strings = [];
for (const { value } of this.damage.parts) {
strings.push(Roll.replaceFormulaData(value.getFormula(), this.actor?.getRollData() ?? {}));
}
return strings.join(' + ');
}
}

View file

@ -1,19 +1,5 @@
import DHBaseAction from './baseAction.mjs';
export default class DHSummonAction extends DHBaseAction {
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
documentUUID: new fields.DocumentUUIDField({ type: 'Actor' })
};
}
async trigger(event, ...args) {
if (!this.canSummon || !canvas.scene) return;
}
get canSummon() {
return game.user.can('TOKEN_CREATE');
}
static extraSchemas = [...super.extraSchemas, 'summon'];
}

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

@ -19,8 +19,9 @@ export default class BeastformEffect extends BaseEffect {
base64: false
}),
tokenSize: new fields.SchemaField({
height: new fields.NumberField({ integer: true, nullable: true }),
width: new fields.NumberField({ integer: true, nullable: true })
scale: new fields.NumberField({ nullable: false, initial: 1 }),
height: new fields.NumberField({ integer: false, nullable: true }),
width: new fields.NumberField({ integer: false, nullable: true })
})
}),
advantageOn: new fields.ArrayField(new fields.StringField()),
@ -29,6 +30,14 @@ export default class BeastformEffect extends BaseEffect {
};
}
/** @inheritDoc */
static migrateData(source) {
if (!source.characterTokenData.tokenSize.height) source.characterTokenData.tokenSize.height = 1;
if (!source.characterTokenData.tokenSize.width) source.characterTokenData.tokenSize.width = 1;
return super.migrateData(source);
}
async _onCreate(_data, _options, userId) {
if (userId !== game.user.id) return;
@ -47,7 +56,9 @@ export default class BeastformEffect extends BaseEffect {
const update = {
...baseUpdate,
texture: {
src: this.characterTokenData.tokenImg
src: this.characterTokenData.tokenImg,
scaleX: this.characterTokenData.tokenSize.scale,
scaleY: this.characterTokenData.tokenSize.scale
},
ring: {
enabled: this.characterTokenData.usesDynamicToken,
@ -57,11 +68,30 @@ export default class BeastformEffect extends BaseEffect {
}
};
const updateToken = token => ({
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
src: token.flags.daggerheart?.beastformTokenImg ?? this.characterTokenData.tokenImg,
scaleX: this.characterTokenData.tokenSize.scale,
scaleY: this.characterTokenData.tokenSize.scale
},
'ring': {
subject: {
@ -70,7 +100,8 @@ export default class BeastformEffect extends BaseEffect {
}
},
'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: {
@ -141,6 +126,10 @@ export default class DhpAdversary extends BaseDataActor {
return this.parent.items.filter(x => x.type === 'feature');
}
isItemValid(source) {
return source.type === 'feature';
}
async _preUpdate(changes, options, user) {
const allowed = await super._preUpdate(changes, options, user);
if (allowed === false) return false;

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,7 +217,10 @@ export default class DhCharacter extends BaseDataActor {
}),
companion: new ForeignDocumentUUIDField({ type: 'Actor', nullable: true, initial: null }),
rules: new fields.SchemaField({
damageReduction: new fields.SchemaField({
...commonActorRules({
damageReduction: {
magical: new fields.BooleanField({ initial: false }),
physical: new fields.BooleanField({ initial: false }),
maxArmorMarked: new fields.SchemaField({
value: new fields.NumberField({
required: true,
@ -234,7 +237,9 @@ export default class DhCharacter extends BaseDataActor {
})
}),
stressDamageReduction: new fields.SchemaField({
severe: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.severe'),
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')
@ -245,16 +250,8 @@ export default class DhCharacter extends BaseDataActor {
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 })
}
}),
attack: new fields.SchemaField({
damage: new fields.SchemaField({
@ -283,10 +280,23 @@ 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 })
dualityRoll: new fields.SchemaField({
defaultHopeDice: new fields.NumberField({
nullable: false,
required: true,
integer: true,
choices: CONFIG.DH.GENERAL.dieFaces,
initial: 12,
label: 'DAGGERHEART.ACTORS.Character.defaultHopeDice'
}),
defaultFearDice: new fields.NumberField({
nullable: false,
required: true,
integer: true,
choices: CONFIG.DH.GENERAL.dieFaces,
initial: 12,
label: 'DAGGERHEART.ACTORS.Character.defaultFearDice'
})
}),
runeWard: new fields.BooleanField({ initial: false }),
burden: new fields.SchemaField({
@ -435,6 +445,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 +480,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 +488,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);
}
}
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.companion.id) {
companionFeatures.push(item);
} else if (item.type === 'feature' && !item.system.type) {

View file

@ -1,7 +1,7 @@
import BaseDataActor from './base.mjs';
import DhLevelData from '../levelData.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import { ActionField, ActionsField } from '../fields/actionField.mjs';
import { ActionField } from '../fields/actionField.mjs';
import { adjustDice, adjustRange } from '../../helpers/utils.mjs';
import DHCompanionSettings from '../../applications/sheets-configs/companion-settings.mjs';
import { resourceField, bonusField } from '../fields/actorField.mjs';
@ -109,6 +109,10 @@ export default class DhCompanion extends BaseDataActor {
return this.partner?.system?.proficiency ?? 1;
}
isItemValid() {
return false;
}
prepareBaseData() {
this.attack.roll.bonus = this.partner?.system?.spellcastModifier ?? 0;

View file

@ -1,8 +1,11 @@
import BaseDataActor from './base.mjs';
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
import DHEnvironmentSettings from '../../applications/sheets-configs/environment-settings.mjs';
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
export default class DhEnvironment extends BaseDataActor {
scenes = new Set();
/**@override */
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Environment'];
@ -51,4 +54,33 @@ export default class DhEnvironment extends BaseDataActor {
get features() {
return this.parent.items.filter(x => x.type === 'feature');
}
isItemValid(source) {
return source.type === 'feature';
}
_onUpdate(changes, options, userId) {
super._onUpdate(changes, options, userId);
for (const scene of this.scenes) {
scene.render();
}
}
_onDelete(options, userId) {
super._onDelete(options, userId);
for (const scene of this.scenes) {
if (game.user.isActiveGM) {
const newSceneEnvironments = scene.flags.daggerheart.sceneEnvironments.filter(
x => x !== this.parent.uuid
);
scene.update({ 'flags.daggerheart.sceneEnvironments': newSceneEnvironments }).then(() => {
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Scene });
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: { refreshType: RefreshType.TagTeamRoll }
});
});
}
}
}
}

View file

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

View file

@ -1,6 +1,40 @@
export default class DhCombat extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {};
return {
battleToggles: new fields.ArrayField(
new fields.SchemaField({
category: new fields.NumberField({ required: true, integer: true }),
grouping: new fields.StringField({ required: true })
})
)
};
}
/** Includes automatic BPModifiers */
get extendedBattleToggles() {
const modifiers = CONFIG.DH.ENCOUNTER.BPModifiers;
const adversaries =
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];
acc.push(
...Object.keys(category).reduce((acc, groupingKey) => {
const grouping = category[groupingKey];
if (grouping.automatic && grouping.conditional?.(this.parent, adversaries, characters)) {
acc.push({ category: Number(categoryKey), grouping: groupingKey });
}
return acc;
}, [])
);
return acc;
}, []);
return [...this.battleToggles, ...activeAutomatic];
}
}

View file

@ -2,5 +2,6 @@ export { ActionCollection } from './actionField.mjs';
export { default as FormulaField } from './formulaField.mjs';
export { default as ForeignDocumentUUIDField } from './foreignDocumentUUIDField.mjs';
export { default as ForeignDocumentUUIDArrayField } from './foreignDocumentUUIDArrayField.mjs';
export { default as TriggerField } from './triggerField.mjs';
export { default as MappingField } from './mappingField.mjs';
export * as ActionFields from './action/_module.mjs';

View file

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

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

@ -87,7 +87,7 @@ export class DHActionRollData extends foundry.abstract.DataModel {
if (this.type === CONFIG.DH.GENERAL.rollTypes.attack.id)
modifiers.push({
label: 'Bonus to Hit',
value: this.bonus ?? this.parent.actor.system.attack.roll.bonus
value: this.bonus ?? this.parent.actor.system.attack.roll.bonus ?? 0
});
break;
default:

View file

@ -46,7 +46,7 @@ export default class SaveField extends fields.SchemaField {
if (SaveField.getAutomation() !== CONFIG.DH.SETTINGS.actionAutomationChoices.never.id || force) {
targets ??= config.targets.filter(t => !config.hasRoll || t.hit);
await SaveField.rollAllSave.call(this, targets, config.event, message);
} else return false;
} else return;
}
/**
@ -124,8 +124,7 @@ 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: {
@ -140,13 +139,6 @@ export default class SaveField extends fields.SchemaField {
}
};
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);
}
/**

View file

@ -0,0 +1,89 @@
import FormulaField from '../formulaField.mjs';
const fields = foundry.data.fields;
export default class DHSummonField extends fields.ArrayField {
/**
* Action Workflow order
*/
static order = 120;
constructor(options = {}, context = {}) {
const summonFields = new fields.SchemaField({
actorUUID: new fields.DocumentUUIDField({
type: 'Actor',
required: true
}),
count: new FormulaField({
required: true,
default: '1'
})
});
super(summonFields, options, context);
}
static async execute() {
if (!canvas.scene) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.summon.error'));
return;
}
if (this.summon.length === 0) {
ui.notifications.warn('No actors configured for this Summon action.');
return;
}
const rolls = [];
const summonData = [];
for (const summon of this.summon) {
let count = summon.count;
const roll = new Roll(summon.count);
if (!roll.isDeterministic) {
await roll.evaluate();
if (game.modules.get('dice-so-nice')?.active) rolls.push(roll);
count = roll.total;
}
const actor = DHSummonField.getWorldActor(await foundry.utils.fromUuid(summon.actorUUID));
/* Extending summon data in memory so it's available in actionField.toChat. Think it's harmless, but ugly. Could maybe find a better way. */
summon.rolledCount = count;
summon.actor = actor.toObject();
summonData.push({ actor, count: count });
}
if (rolls.length) await Promise.all(rolls.map(roll => game.dice3d.showForRoll(roll, game.user, true)));
this.actor.sheet?.minimize();
DHSummonField.handleSummon(summonData, this.actor);
}
/* Check for any available instances of the actor present in the world if we're missing artwork in the compendium */
static getWorldActor(baseActor) {
const dataType = game.system.api.data.actors[`Dh${baseActor.type.capitalize()}`];
if (baseActor.inCompendium && dataType && baseActor.img === dataType.DEFAULT_ICON) {
const worldActorCopy = game.actors.find(x => x.name === baseActor.name);
return worldActorCopy ?? baseActor;
}
return baseActor;
}
static async handleSummon(summonData, actionActor, summonIndex = 0) {
const summon = summonData[summonIndex];
const result = await CONFIG.ux.TokenManager.createPreviewAsync(summon.actor, {
name: `${summon.actor.prototypeToken.name}${summon.count > 1 ? ` (${summon.count}x)` : ''}`
});
if (!result) return actionActor.sheet?.maximize();
summon.actor = result.actor;
summon.count--;
if (summon.count <= 0) {
summonIndex++;
if (summonIndex === summonData.length) return actionActor.sheet?.maximize();
}
DHSummonField.handleSummon(summonData, actionActor, summonIndex);
}
}

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,22 +262,28 @@ 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,
img: this.baseAction ? this.parent.parent.img : this.img,
tags: this.tags ? this.tags : ['Spell', 'Arcana', 'Lv 10']
tags: this.tags ? this.tags : ['Spell', 'Arcana', 'Lv 10'],
summon: this.summon
},
itemOrigin: this.item,
description: this.description || (this.item instanceof Item ? this.item.system.description : '')
};
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

@ -0,0 +1,24 @@
export default class TriggerField extends foundry.data.fields.SchemaField {
constructor(context) {
super(
{
trigger: new foundry.data.fields.StringField({
nullable: false,
blank: false,
initial: CONFIG.DH.TRIGGER.triggers.dualityRoll.id,
choices: CONFIG.DH.TRIGGER.triggers,
label: 'DAGGERHEART.CONFIG.Triggers.triggerType'
}),
triggeringActorType: new foundry.data.fields.StringField({
nullable: false,
blank: false,
initial: CONFIG.DH.TRIGGER.triggerActorTargetType.any.id,
choices: CONFIG.DH.TRIGGER.triggerActorTargetType,
label: 'DAGGERHEART.CONFIG.Triggers.triggeringActorType'
}),
command: new foundry.data.fields.JavaScriptField({ async: true })
},
context
);
}
}

View file

@ -54,6 +54,21 @@ export default class DHArmor extends AttachableItem {
);
}
/**@inheritdoc */
async getDescriptionData() {
const baseDescription = this.description;
const allFeatures = CONFIG.DH.ITEM.allArmorFeatures();
const features = this.armorFeatures.map(x => allFeatures[x.value]);
if (!features.length) return { prefix: null, value: baseDescription, suffix: null };
const prefix = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/items/armor/description.hbs',
{ features }
);
return { prefix, value: baseDescription, suffix: null };
}
/**@inheritdoc */
async _preUpdate(changes, options, user) {
const allowed = await super._preUpdate(changes, options, user);

View file

@ -8,7 +8,7 @@
* @property {boolean} isInventoryItem- Indicates whether items of this type is a Inventory Item
*/
import { addLinkedItemsDiff, createScrollText, getScrollTextData, updateLinkedItemApps } from '../../helpers/utils.mjs';
import { addLinkedItemsDiff, getScrollTextData, updateLinkedItemApps } from '../../helpers/utils.mjs';
import { ActionsField } from '../fields/actionField.mjs';
import FormulaField from '../fields/formulaField.mjs';
@ -124,6 +124,33 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
return [source, page ? `pg ${page}.` : null].filter(x => x).join('. ');
}
/**
* Augments the description for the item with type specific info to display. Implemented in applicable item subtypes.
* @param {object} [options] - Options that modify the styling of the rendered template. { headerStyle: undefined|'none'|'large' }
* @returns {string}
*/
async getDescriptionData(_options) {
return { prefix: null, value: this.description, suffix: null };
}
/**
* Gets the enriched and augmented description for the item.
* @param {object} [options] - Options that modify the styling of the rendered template. { headerStyle: undefined|'none'|'large' }
* @returns {string}
*/
async getEnrichedDescription() {
if (!this.metadata.hasDescription) return '';
const { prefix, value, suffix } = await this.getDescriptionData();
const fullDescription = [prefix, value, suffix].filter(p => !!p).join('\n<hr>\n');
return await foundry.applications.ux.TextEditor.implementation.enrichHTML(fullDescription, {
relativeTo: this,
rollData: this.getRollData(),
secrets: this.isOwner
});
}
/**
* Obtain a data object used to evaluate any dice rolls associated with this Item Type
* @param {object} [options] - Options which modify the getRollData method.
@ -135,6 +162,30 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
return data;
}
prepareBaseData() {
super.prepareBaseData();
for (const action of this.actions ?? []) {
if (!action.actor) continue;
const actionsToRegister = [];
for (let i = 0; i < action.triggers.length; i++) {
const trigger = action.triggers[i];
const { args } = CONFIG.DH.TRIGGER.triggers[trigger.trigger];
const fn = new foundry.utils.AsyncFunction(...args, `{${trigger.command}\n}`);
actionsToRegister.push(fn.bind(action));
if (i === action.triggers.length - 1)
game.system.registeredTriggers.registerTriggers(
trigger.trigger,
action.actor?.uuid,
trigger.triggeringActorType,
this.parent.uuid,
actionsToRegister
);
}
}
}
async _preCreate(data, options, user) {
// Skip if no initial action is required or actions already exist
if (this.metadata.hasInitialAction && foundry.utils.isEmpty(this.actions)) {

View file

@ -43,6 +43,13 @@ 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
}),
scale: new fields.NumberField({ nullable: false, min: 0.2, max: 3, step: 0.05, initial: 1 }),
height: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }),
width: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true })
}),
@ -178,6 +185,7 @@ export default class DHBeastform extends BaseDataItem {
tokenImg: this.parent.parent.prototypeToken.texture.src,
tokenRingImg: this.parent.parent.prototypeToken.ring.subject.texture,
tokenSize: {
scale: this.parent.parent.prototypeToken.texture.scaleX,
height: this.parent.parent.prototypeToken.height,
width: this.parent.parent.prototypeToken.width
}
@ -190,11 +198,22 @@ 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
src: this.tokenImg,
scaleX: this.tokenSize.scale,
scaleY: this.tokenSize.scale
},
ring: {
subject: {
@ -202,16 +221,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 => ({
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

@ -29,7 +29,21 @@ export default class DHDomainCard extends BaseDataItem {
required: true,
initial: CONFIG.DH.DOMAIN.cardTypes.ability.id
}),
inVault: new fields.BooleanField({ initial: false })
inVault: new fields.BooleanField({ initial: false }),
vaultActive: new fields.BooleanField({
required: true,
nullable: false,
initial: false
}),
loadoutIgnore: new fields.BooleanField({
required: true,
nullable: false,
initial: false
}),
domainTouched: new fields.NumberField({
nullable: true,
initial: null
})
};
}
@ -38,6 +52,19 @@ export default class DHDomainCard extends BaseDataItem {
return game.i18n.localize(allDomainData[this.domain].label);
}
get isVaultSupressed() {
return this.inVault && !this.vaultActive;
}
get isDomainTouchedSuppressed() {
if (!this.parent.system.domainTouched || this.parent.parent?.type !== 'character') return false;
const matchingDomainCards = this.parent.parent.items.filter(
item => !item.system.inVault && item.system.domain === this.parent.system.domain
).length;
return matchingDomainCards < this.parent.system.domainTouched;
}
/* -------------------------------------------- */
/**@override */
@ -66,6 +93,10 @@ export default class DHDomainCard extends BaseDataItem {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.duplicateDomainCard'));
return false;
}
if (!this.actor.system.loadoutSlot.available) {
data.system.inVault = true;
}
}
}

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

@ -110,6 +110,21 @@ export default class DHWeapon extends AttachableItem {
);
}
/**@inheritdoc */
async getDescriptionData() {
const baseDescription = this.description;
const allFeatures = CONFIG.DH.ITEM.allWeaponFeatures();
const features = this.weaponFeatures.map(x => allFeatures[x.value]);
if (!features.length) return { prefix: null, value: baseDescription, suffix: null };
const prefix = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/items/weapon/description.hbs',
{ features }
);
return { prefix, value: baseDescription, suffix: null };
}
prepareDerivedData() {
this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait;
}

View file

@ -1,3 +1,8 @@
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
/* Foundry does not add any system data for subtyped Scenes. The data model is therefore used by instantiating a new instance of it for sceneConfigSettings.mjs.
Needed dataprep and lifetime hooks are handled in documents/scene.
*/
export default class DHScene extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
@ -13,7 +18,8 @@ export default class DHScene extends foundry.abstract.DataModel {
veryClose: new fields.NumberField({ integer: true, label: 'DAGGERHEART.CONFIG.Range.veryClose.name' }),
close: new fields.NumberField({ integer: true, label: 'DAGGERHEART.CONFIG.Range.close.name' }),
far: new fields.NumberField({ integer: true, label: 'DAGGERHEART.CONFIG.Range.far.name' })
})
}),
sceneEnvironments: new ForeignDocumentUUIDArrayField({ type: 'Actor', prune: true })
};
}
}

View file

@ -173,6 +173,13 @@ export default class DhAutomation extends foundry.abstract.DataModel {
label: 'DAGGERHEART.GENERAL.player.plurial'
})
})
}),
triggers: new fields.SchemaField({
enabled: new fields.BooleanField({
nullable: false,
initial: true,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.triggers.enabled.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

@ -35,7 +35,9 @@ export default class D20Roll extends DHRoll {
get isCritical() {
if (!this.d20._evaluated) return;
return this.d20.total >= this.data.system.criticalThreshold;
const criticalThreshold = this.options.actionType === 'reaction' ? 20 : this.data.system.criticalThreshold;
return this.d20.total >= criticalThreshold;
}
get hasAdvantage() {
@ -98,10 +100,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
});
});
@ -127,15 +129,55 @@ export default class D20Roll extends DHRoll {
const modifiers = foundry.utils.deepClone(this.options.roll.baseModifiers) ?? [];
modifiers.push(
...this.getBonus(`roll.${this.options.actionType}`, `${this.options.actionType?.capitalize()} Bonus`)
);
modifiers.push(
...this.getBonus(`roll.${this.options.roll.type}`, `${this.options.roll.type?.capitalize()} Bonus`)
...this.getBonus(
`system.bonuses.roll.${this.options.actionType}`,
`${this.options.actionType?.capitalize()} Bonus`
)
);
if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.attack.id) {
modifiers.push(
...this.getBonus(
`system.bonuses.roll.${this.options.roll.type}`,
`${this.options.roll.type?.capitalize()} Bonus`
)
);
}
if (
this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.attack.id ||
(this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id && this.options.hasDamage)
) {
modifiers.push(
...this.getBonus(`system.bonuses.roll.attack`, `${this.options.roll.type?.capitalize()} Bonus`)
);
}
return modifiers;
}
getActionChangeKeys() {
const changeKeys = new Set([`system.bonuses.roll.${this.options.actionType}`]);
if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.attack.id) {
changeKeys.add(`system.bonuses.roll.${this.options.roll.type}`);
}
if (
this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.attack.id ||
(this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id && this.options.hasDamage)
) {
changeKeys.add(`system.bonuses.roll.attack`);
}
if (this.options.roll.trait && this.data.traits?.[this.options.roll.trait]) {
if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.spellcast.id)
changeKeys.add('system.bonuses.roll.trait');
}
return changeKeys;
}
static postEvaluate(roll, config = {}) {
const data = super.postEvaluate(roll, config);
data.type = config.actionType;

View file

@ -93,7 +93,6 @@ export default class DamageRoll extends DHRoll {
type = this.options.messageType ?? (this.options.hasHealing ? 'healing' : 'damage'),
options = part ?? this.options;
modifiers.push(...this.getBonus(`${type}`, `${type.capitalize()} Bonus`));
if (!this.options.hasHealing) {
options.damageTypes?.forEach(t => {
modifiers.push(...this.getBonus(`${type}.${t}`, `${t.capitalize()} ${type.capitalize()} Bonus`));
@ -108,6 +107,31 @@ export default class DamageRoll extends DHRoll {
return modifiers;
}
getActionChangeKeys() {
const type = this.options.messageType ?? (this.options.hasHealing ? 'healing' : 'damage');
const changeKeys = [];
for (const roll of this.options.roll) {
for (const damageType of roll.damageTypes?.values?.() ?? []) {
changeKeys.push(`system.bonuses.${type}.${damageType}`);
}
}
const item = this.data.parent?.items?.get(this.options.source.item);
if (item) {
switch (item.type) {
case 'weapon':
if (!this.options.hasHealing)
['primaryWeapon', 'secondaryWeapon'].forEach(w =>
changeKeys.push(`system.bonuses.damage.${w}`)
);
break;
}
}
return changeKeys;
}
constructFormula(config) {
this.options.roll.forEach((part, index) => {
part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data));

View file

@ -4,6 +4,7 @@ export default class DHRoll extends Roll {
baseTerms = [];
constructor(formula, data = {}, options = {}) {
super(formula, data, options);
options.bonusEffects = this.bonusEffectBuilder();
if (!this.data || !Object.keys(this.data).length) this.data = options.data;
}
@ -164,12 +165,18 @@ export default class DHRoll extends Roll {
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
...this.constructor.parse(modifier.join(' + '), this.options.data)
];
} else {
} else if (Number.isNumeric(modifier)) {
const numTerm = modifier < 0 ? '-' : '+';
return [
new foundry.dice.terms.OperatorTerm({ operator: numTerm }),
new foundry.dice.terms.NumericTerm({ number: Math.abs(modifier) })
];
} else {
const numTerm = modifier < 0 ? '-' : '+';
return [
new foundry.dice.terms.OperatorTerm({ operator: numTerm }),
...this.constructor.parse(modifier, this.options.data)
];
}
}
@ -185,18 +192,20 @@ export default class DHRoll extends Roll {
}
getBonus(path, label) {
const bonus = foundry.utils.getProperty(this.data.bonuses, path),
modifiers = [];
if (bonus?.bonus)
modifiers.push({
label: label,
value: bonus?.bonus
});
if (bonus?.dice?.length)
modifiers.push({
label: label,
value: bonus?.dice
});
const modifiers = [];
for (const effect of Object.values(this.options.bonusEffects)) {
if (!effect.selected) continue;
for (const change of effect.changes) {
if (!change.key.includes(path)) continue;
const changeValue = game.system.api.documents.DhActiveEffect.getChangeValue(
this.data,
change,
effect.origEffect
);
modifiers.push({ label: label, value: changeValue });
}
}
return modifiers;
}
@ -235,68 +244,28 @@ export default class DHRoll extends Roll {
static temporaryModifierBuilder(config) {
return {};
}
bonusEffectBuilder() {
const changeKeys = this.getActionChangeKeys();
return (
this.options.effects?.reduce((acc, effect) => {
if (effect.changes.some(x => changeKeys.some(key => x.key.includes(key)))) {
acc[effect.id] = {
id: effect.id,
name: effect.name,
description: effect.description,
changes: effect.changes,
origEffect: effect,
selected: !effect.disabled
};
}
return acc;
}, {}) ?? []
);
}
getActionChangeKeys() {
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'}`
);
}
@ -129,9 +130,14 @@ export default class DualityRoll extends D20Roll {
this.terms = [this.terms[0], this.terms[1], this.terms[2]];
return;
}
this.terms[0] = new foundry.dice.terms.Die({ faces: 12 });
this.terms[0] = new foundry.dice.terms.Die({
faces: this.data.rules.dualityRoll?.defaultHopeDice ?? 12
});
this.terms[1] = new foundry.dice.terms.OperatorTerm({ operator: '+' });
this.terms[2] = new foundry.dice.terms.Die({ faces: 12 });
this.terms[2] = new foundry.dice.terms.Die({
faces: this.data.rules.dualityRoll?.defaultFearDice ?? 12
});
}
applyAdvantage() {
@ -172,6 +178,34 @@ export default class DualityRoll extends D20Roll {
return modifiers;
}
getActionChangeKeys() {
const changeKeys = new Set([`system.bonuses.roll.${this.options.actionType}`]);
if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.attack.id) {
changeKeys.add(`system.bonuses.roll.${this.options.roll.type}`);
}
if (
this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.attack.id ||
(this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id && this.options.hasDamage)
) {
changeKeys.add(`system.bonuses.roll.attack`);
}
if (this.options.roll.trait && this.data.traits?.[this.options.roll.trait]) {
if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.spellcast.id)
changeKeys.add('system.bonuses.roll.trait');
}
const weapons = ['primaryWeapon', 'secondaryWeapon'];
weapons.forEach(w => {
if (this.options.source.item && this.options.source.item === this.data[w]?.id)
changeKeys.add(`system.bonuses.roll.${w}`);
});
return changeKeys;
}
static async buildEvaluate(roll, config = {}, message = {}) {
await super.buildEvaluate(roll, config, message);
@ -219,6 +253,114 @@ export default class DualityRoll extends D20Roll {
return data;
}
static async buildPost(roll, config, message) {
await super.buildPost(roll, config, message);
await DualityRoll.dualityUpdate(config);
await DualityRoll.handleTriggers(roll, config);
}
static async handleTriggers(roll, config) {
if (!config.source?.actor) return;
const updates = [];
const dualityUpdates = await game.system.registeredTriggers.runTrigger(
CONFIG.DH.TRIGGER.triggers.dualityRoll.id,
roll.data?.parent,
roll,
roll.data?.parent
);
if (dualityUpdates?.length) updates.push(...dualityUpdates);
if (config.roll.result.duality === -1) {
const fearUpdates = await game.system.registeredTriggers.runTrigger(
CONFIG.DH.TRIGGER.triggers.fearRoll.id,
roll.data?.parent,
roll,
roll.data?.parent
);
if (fearUpdates?.length) updates.push(...fearUpdates);
}
config.resourceUpdates.addResources(updates);
}
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 +399,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

@ -1,8 +1,11 @@
export { default as DhpActor } from './actor.mjs';
export { default as DHItem } from './item.mjs';
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';
export { default as DhTokenManager } from './tokenManager.mjs';

View file

@ -1,4 +1,5 @@
import { itemAbleRollParse } from '../helpers/utils.mjs';
import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs';
export default class DhActiveEffect extends foundry.documents.ActiveEffect {
/* -------------------------------------------- */
@ -19,7 +20,10 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
}
if (this.parent?.type === 'domainCard') {
return this.parent.system.inVault;
const isVaultSupressed = this.parent.system.isVaultSupressed;
const domainTouchedSupressed = this.parent.system.isDomainTouchedSuppressed;
return isVaultSupressed || domainTouchedSupressed;
}
return super.isSuppressed;
@ -85,29 +89,49 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
await super._preCreate(data, options, user);
}
/** @inheritdoc */
_onCreate(data, options, userId) {
super._onCreate(data, options, userId);
Hooks.callAll(RefreshType.EffectsDisplay);
}
/** @inheritdoc */
_onDelete(data, options, userId) {
super._onDelete(data, options, userId);
Hooks.callAll(RefreshType.EffectsDisplay);
}
/* -------------------------------------------- */
/* Methods */
/* -------------------------------------------- */
/**@inheritdoc*/
static applyField(model, change, field) {
const isOriginTarget = change.value.toLowerCase().includes('origin.@');
change.value = DhActiveEffect.getChangeValue(model, change, change.effect);
super.applyField(model, change, field);
}
/** */
static getChangeValue(model, change, effect) {
let value = change.value;
const isOriginTarget = value.toLowerCase().includes('origin.@');
let parseModel = model;
if (isOriginTarget && change.effect.origin) {
change.value = change.value.replaceAll(/origin\.@/gi, '@');
if (isOriginTarget && effect.origin) {
value = change.value.replaceAll(/origin\.@/gi, '@');
try {
const effect = foundry.utils.fromUuidSync(change.effect.origin);
const originEffect = foundry.utils.fromUuidSync(effect.origin);
const doc =
effect.parent?.parent instanceof game.system.api.documents.DhpActor
? effect.parent
: effect.parent.parent;
originEffect.parent?.parent instanceof game.system.api.documents.DhpActor
? originEffect.parent
: originEffect.parent.parent;
if (doc) parseModel = doc;
} catch (_) {}
}
const evalValue = this.effectSafeEval(itemAbleRollParse(change.value, parseModel, change.effect.parent));
change.value = evalValue ?? change.value;
super.applyField(model, change, field);
const evalValue = this.effectSafeEval(itemAbleRollParse(value, parseModel, effect.parent));
return evalValue ?? value;
}
/**
@ -179,28 +203,11 @@ 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)
) {
/* 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, {
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,32 @@ 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;
}
}
}
const results = await game.system.registeredTriggers.runTrigger(
CONFIG.DH.TRIGGER.triggers.postDamageReduction.id,
this,
updates,
this
);
if (results?.length) {
const resourceMap = new ResourceUpdateMap(results[0].originActor);
for (var result of results) resourceMap.addResources(result.updates);
resourceMap.updateResources();
}
updates.forEach(
@ -679,6 +722,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 +808,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;
}
@ -844,4 +896,65 @@ export default class DhpActor extends Actor {
if (this.system._getTags) tags.push(...this.system._getTags());
return tags;
}
/** Get active effects */
getActiveEffects() {
const statusMap = new Map(foundry.CONFIG.statusEffects.map(status => [status.id, status]));
return this.effects
.filter(x => !x.disabled)
.reduce((acc, effect) => {
acc.push(effect);
const currentStatusActiveEffects = acc.filter(
x => x.statuses.size === 1 && x.name === game.i18n.localize(statusMap.get(x.statuses.first())?.name)
);
for (var status of effect.statuses) {
if (!currentStatusActiveEffects.find(x => x.statuses.has(status))) {
const statusData = statusMap.get(status);
if (statusData) {
acc.push({
condition: status,
appliedBy: game.i18n.localize(effect.name),
name: game.i18n.localize(statusData.name),
statuses: new Set([status]),
img: statusData.icon ?? statusData.img,
description: game.i18n.localize(statusData.description),
tint: effect.tint
});
}
}
}
return acc;
}, []);
}
/* Temporarily copying the foundry method to add a fix to a bug with scenes
https://discord.com/channels/170995199584108546/1296292044011995136/1446693077443149856
*/
getDependentTokens({ scenes, linked = false } = {}) {
if (this.isToken && !scenes) return [this.token];
if (scenes) scenes = Array.isArray(scenes) ? scenes : [scenes];
else scenes = Array.from(this._dependentTokens.keys());
/* Code to filter out nonexistant scenes */
scenes = scenes.filter(scene => game.scenes.some(x => x.id === scene.id));
if (this.isToken) {
const parent = this.token.parent;
return scenes.includes(parent) ? [this.token] : [];
}
const allTokens = [];
for (const scene of scenes) {
if (!scene) continue;
const tokens = this._dependentTokens.get(scene);
for (const token of tokens ?? []) {
if (!linked || token.actorLink) allTokens.push(token);
}
}
return allTokens;
}
}

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