Compare commits

...

270 commits
1.1.2 ... main

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

* Updated Consumables RollTable

* Placed the rolltable compendium in the SRD folder

* updated Journal with fixed links

* Re-added descriptions in rolltable

---------

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

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

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

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

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

* Adversaries

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

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

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

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

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

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

* Fixed so tokenPreview works with tokenSize

* Correction for prototypeToken

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

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

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

---------

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

* .

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

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

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

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

* feat: style armor hbs tooltip

* feat: style consumable hbs tooltip

* feat: style loot hbs tooltip

* feat: style feature hbs tooltip

* bugfix: prevent style conflicts between tooltips

* feat: style action hbs tooltip

* feat: style attack hbs tooltip

* feat: style effect hbs tooltip

* feat: increase tooltip width

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

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

* bugfix: requested changes

* bugfix: fix typo

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

* .

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

* Added small line-height to the experience name

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

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

* Moving the draggable spots to internal divs and images

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

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

* Fixing other uses of dragDrop to handle the layout change

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

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

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

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

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

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

---------

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

* Prefixed sheet labels with 'Default DH'

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

* .

* .

* Finished token implementation

* Fixed token-config

* Updated SRD adversaries

* .

* Added size to Beastform tokenData

* Fixed sizing for evolved beastforms

* Beastform compendium update

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

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

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

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

* .

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

* Newline at end of file

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

* Adjust sizing and spacing

* Restore ItemTransferDialog subclass for module use

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

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

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

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

* Removing spaces

* Removing a needless return line

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

* Action buttons have icons. Pretty Iconic

* Features tweaked to support only on limited actors

* Duplicate ActionTypes+references are removed.

* Adversary and Environment SRD entries updated.

* Updated name field workflow in character actor.

* Adversary name fields are improved as well.

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

This reverts commit 66924c530f.

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

This reverts commit f60e8cffda.

* Fixed prototype token in Abandoned Grove

* Label change

---------

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

* Create fake placeholder

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

* Remove unused plain style

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

---------

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

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

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

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

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

* Remove unnecessary second render
2025-12-07 13:39:40 +01:00
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
Chris Ryan
b4c2034789
Add expanded descriptions/features to Classes (#1336)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-11-26 09:30:26 +01:00
WBHarry
cd5b8e9c75
[Fix] Levelup/Delevel fixes (#1342)
* .

* .
2025-11-26 02:42:42 +01:00
WBHarry
44b805d0df
[Fix] Party And Effect Minifixes (#1337)
* .

* Added migration to remove ghosts from the TagTeamDialog
2025-11-25 23:20:07 +01:00
WBHarry
d616ddc113
Fixed the translation (#1341) 2025-11-25 17:18:25 +01:00
Carlos Fernandez
4510deae96
Fix marking companion damage as direct damage (#1333) 2025-11-25 08:23:25 +01:00
WBHarry
d137e33c3d
[Feature] Countdown Rols And Randomzie (#1329)
* .

* .

* .

* Raised system version
2025-11-25 00:36:43 +01:00
WBHarry
9e0bc3cff1
Fixed so that the token hud for party tokens works again (#1326) 2025-11-25 00:35:05 +01:00
WBHarry
f78f8e32b6
Added critical threshold for Adversaries, determining at what number they crit (#1331) 2025-11-25 00:26:27 +01:00
Carlos Fernandez
d5b501cb98
Move config assignments to initial load phase (#1330) 2025-11-25 00:25:59 +01:00
WBHarry
4b76223e45 Raised version 2025-11-23 15:24:24 +01:00
WBHarry
b9508e19e8
[Feature] Become Unstoppable (#1321)
* Added implementation of unstoppable

* Forgot to add the updated Unstoppable Feature itself

* Added immunity for hidden and SRD additions for immunity
2025-11-23 15:23:52 +01:00
WBHarry
e6a242ba43
. (#1322) 2025-11-23 15:20:04 +01:00
WBHarry
81e7f24d22
Fixed the css selector so that you can drag out potential adversaries again (#1319) 2025-11-23 15:19:47 +01:00
WBHarry
6398ba15f2
Ancestry and community is now reset when dragging in a new one (#1315) 2025-11-23 15:19:30 +01:00
WBHarry
ebf98a6ae5
Added a confirmation window when deleting a feature (#1324) 2025-11-23 15:12:25 +01:00
WBHarry
2920ead81a
[Fix] Inventory Item Transfer (#1316)
* Fixed so items from the inventory tab of the Party sheet can be dragged out

* Added transfer logic

* Added translation

* Improved item transfer dialog title

* Simplified title

* Updated image

* Simplified the handlebars templates for itemTransfer

* Improved item-identicial check

* Slight improved rendering

* .
2025-11-23 15:04:42 +01:00
WBHarry
9f7554cdff
[Feature] DiceSoNice Fonts (#1318)
* Added a font choice to system diceSoNice settings

* .
2025-11-23 02:19:57 +01:00
WBHarry
87643dc662
Fixed so that effects applied to self will work again (#1320) 2025-11-22 15:24:11 +01:00
WBHarry
e77f538ab7
Fixed so that using the loop button actually increases/decreases max on looping instead of cocatenating to string (#1314) 2025-11-20 18:46:44 +01:00
WBHarry
207220ff7b
[Feature] Countdown Actions (#1302)
* Added countdown actions

* Added a CountdownAutomation setting to enable/disable countdown automation

* Added Looping

* Added characterSpotlight automation

* Countdown max as formula to enable random countdowns

* Updated compendiums with countdowns

* .

* Fixed lightmode colouration

* Raised system version

* Added automation for ActionRolls on countdowns

* Added automation on fear to countdowns

* Corrected attackAction countdown automation

* Added initial countdown upon creating a CountdownAction

* Improved ActionCountdown initial name to be 'Start Countdown'
2025-11-20 11:46:00 +01:00
WBHarry
0233979a9f
[Fix] RCT Countdown Positioning (#1306)
* Fixed so that the countdown Ui positions itself well when RCT video is enabled

* Removed hooks and just placing the Ui Countdowns in the UI-Right-column-1 placement
2025-11-20 11:10:58 +01:00
Carlos Fernandez
844c683c7d
Show loot actions in sheet (#1312) 2025-11-20 09:59:59 +01:00
Carlos Fernandez
72944698c5
Fix enriching multiple instances of the same inline (#1309) 2025-11-20 09:57:46 +01:00
Carlos Fernandez
28401989d0
Fallback action description to item description when posted in chat (#1310) 2025-11-20 09:56:32 +01:00
WBHarry
d1cbaa7809
[Fix] PouncingPredator Beastform (#1307)
* .

* .
2025-11-19 21:47:28 +10:00
Chris Ryan
a02015df57
Add passive effects for each trait for use in the Druid Evolution feature. (#1299)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-11-19 11:59:14 +01:00
WBHarry
8e785a5890
. (#1305) 2025-11-19 20:46:27 +10:00
WBHarry
336995b748
Changed from .loader to .daggerheart-loader to avoid conflicts with modules applying broad classes (#1303) 2025-11-19 20:26:04 +10:00
WBHarry
a146132171
Beastfom Effect Unstuck (#1298) 2025-11-17 16:54:52 +01:00
Carlos Fernandez
5c52a33496
Fix styling of used action tokens for players (#1297) 2025-11-17 11:05:26 +01:00
WBHarry
b9d67e44da
[Fix] Downtime Actions (#1295)
* Fixed so downtime actiosn can be used again

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

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

* .

---------

Co-authored-by: Carlos Fernandez <CarlosFdez@users.noreply.github.com>
2025-11-17 10:17:22 +01:00
WBHarry
fe8e98ef35
Fixed so that status effects are shown on ActiveEffectConfig again when useGeneric is not active (#1293) 2025-11-17 09:53:03 +01:00
Carlos Fernandez
c3660ffa34
Fix party sheet tab height when expanded (#1296) 2025-11-17 09:52:48 +01:00
WBHarry
97f408c185
[Fix] Beastform Actor Wildcard Image (#1292)
* .

* .
2025-11-17 09:22:24 +01:00
WBHarry
023e17d47d Made it so you enable each currency denomination separately incase you don't want to have all 4 shown. 2025-11-16 15:30:47 +01:00
WBHarry
481ce46edf
[Fix] Party Fixes (#1284)
* Fixed deletion of characters in the world locking up the party actor

* .

* Fixed so leader in group roll gains resourcse

* Fixed so party.inventory has the right controls

* Corrected for added character purning

* .
2025-11-16 01:52:19 +01:00
WBHarry
7df43d71e0
[Fix] Item Browser Corrections (#1290)
* Fixed so lists do not fail to show items when search is empty

* Fixed so that the clear button loads in the items

* .

* .
2025-11-16 01:14:01 +01:00
WBHarry
fcb9c032ef
[Fix] Beastform Fixes (#1288)
* Fixed Beastforms getting stuck

* Made the wildcard image select dialog scrollable

* Beastform button shifts to cancelBeastform
2025-11-15 11:51:01 -05:00
WBHarry
91b1b92d19
Corrected vairable (#1289) 2025-11-15 16:54:20 +01:00
Carlos Fernandez
369559f935
Do minor styling fixes (#1283) 2025-11-15 15:09:50 +01:00
Carlos Fernandez
97ddf651a0
Prune missing party members (#1285) 2025-11-15 14:51:34 +01:00
Carlos Fernandez
bdbca0d35a
Hide countdown panel for players when empty (#1286) 2025-11-15 14:36:50 +01:00
Chris Ryan
ef0290ae8a
Update the Ancestry descriptions to include the Ancestry Features text from the SRD (#1275)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-11-14 23:37:08 +01:00
Chris Ryan
f730bb762c
Update the Community descriptions to include the Community Feature te… (#1280)
* Update the Community descriptions to include the Community Feature text from the SRD

* Corrected newline

---------

Co-authored-by: Chris Ryan <chrisr@blackhole>
Co-authored-by: WBHarry <williambjrklund@gmail.com>
2025-11-14 23:29:47 +01:00
Chris Ryan
fbf1ee2046
Fixed the Retract feature to be applied to the Galapa character (#1281)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-11-14 22:57:31 +01:00
Chris Ryan
249f27ed5b
Remove the HP cost on the feature (#1282)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-11-14 22:56:33 +01:00
Chris Ryan
63e66bd0d3
Fixed the resource setup to remove error (#1272)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-11-12 08:41:00 +01:00
Chris Ryan
977715d22a
Protect from error when no actor (e.g. enricher) (#1273)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-11-12 08:29:33 +01:00
WBHarry
72bf0171b3
Fixed so it uses the currently viewed scene instead of the active one (#1261) 2025-11-11 22:14:56 +01:00
WBHarry
d1caded970
Fixed so that the Homebrew settings screen for homebrew weapon/armor features gets a correct title instead of 'Downtime Moves' (#1263) 2025-11-11 22:14:17 +01:00
WBHarry
54109bf655
Fixed so countdown migration doesn't get stuck because of countdowns set to 'Limited' ownership (#1264) 2025-11-11 22:14:06 +01:00
WBHarry
c4b227de41
Added a system setting (appearance) to remove the Countdown UI (#1265) 2025-11-11 22:13:53 +01:00
WBHarry
6408528319
The delete button wasn't working for them with broken links (#1266) 2025-11-11 22:13:37 +01:00
WBHarry
7c4200b431
[Fix] Countdown UI Initial Position (#1267)
* .

* Fixed so countdown ui respects foundry UiScale

* Raised system version
2025-11-11 22:13:24 +01:00
WBHarry
84ef1063d8
[Fix] Party NonCombat Type (#1260)
* Set Party actor as a non-combat type

* Improved initial potrait view on limited sheets
2025-11-11 18:03:55 +01:00
WBHarry
074a9486ae
Fixed so that undefined flag doesn't cause an error (#1259) 2025-11-11 16:23:16 +01:00
WBHarry
261a3a68b0
[PR] [Feature] Party Sheet (#1230)
* start development

* finish party members tab

* start resources tab

* finish resources tab

* finish inventory tab and add inital template to projects tab

* add resource buttons actions methods

* add group roll dialog

* Main implementation

* Fixed costs

* Minor fixes and tweaks for the party sheet (#1239)

* Minor fixes and tweaks for the party sheet

* Fix scroll restoration for party sheet tabs

* Finished GroupRoll

* Removed/commented-out not yet implemented things

* Commented out Difficulty since it's not used yet

* Re-render party when members update (#1242)

* Fixed so style applies in preview chat message

* Added the clown car

* Fixed so items can be dropped into the Party sheet

* Added delete icon to inventory

* Fixed TokenHUD token property useage. Fixed skipping roll message

* Added visible modifier to GroupRoll leader result

* Leader roll displays the large result display right away after rolling

* Corrected tokenHUD for non-player-tokens

* Fixed clowncar tokenData

* Fixed TagTeam roll message and sound

* Removed final TagTeamRoll roll sound

* [PR] [Party Sheets] Sidebar character sheet changes (#1249)

* Something experimenting

* I am silly (wearning Dunce hat)

* Stressful task

* Armor functional to be hit

* CSS Changes to accomadate pip boy

* last minute change to resource section for better visual feeling

* restoring old css for toggle

* Added setting to toggle pip/number display

* toggle functionality added

* Fixed light-mode in characterSheet

* Fixed multi-row resource pips display for character

* Fixed separators

* Added pip-display to Adversary and Companion. Some fixing on armor display

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>

* Fixed party height and resource armor update

* Fixed deletebutton padding

* Only showing expand-me icon on InventoryItem if there is a description to show

* .

* Fixed menu icon to be beige instead of white in dark mode

---------

Co-authored-by: moliloo <dev.murilobrito@gmail.com>
Co-authored-by: Carlos Fernandez <CarlosFdez@users.noreply.github.com>
Co-authored-by: Nikhil Nagarajan <potter.nikhil@gmail.com>
2025-11-11 16:02:45 +01:00
WBHarry
f53252a369
. (#1254) 2025-11-10 18:30:15 -05:00
WBHarry
052b6baefe
[Feature] Extended Scene Range Measurements (#1255)
* Extended the capabilities of scene rangemeasurement settings

* Corrected darkmode tab title
2025-11-10 18:25:31 -05:00
WBHarry
7055591a76
[PR] [Feature] Beastform Wildcards (#1247)
* Added support for wildcard paths in beastform token paths

* Fixed browse dataSource
2025-11-10 16:30:04 +01:00
Nikhil Nagarajan
2d6390248f
Updated SRD and changed default images (#1253) 2025-11-10 16:29:45 +01:00
WBHarry
66961d7fea
. (#1252) 2025-11-10 08:02:44 -05:00
WBHarry
321b7c2da3
[Fix] Resource Input Alignment (#1251)
* Removed padding

* Fixed it the better way
2025-11-09 13:51:32 +01:00
WBHarry
f52c3e840e
. (#1248) 2025-11-09 09:52:32 +10:00
Chris Ryan
a7bc8db55f
Fix broken TemplateEnricher - CONFIG.MeasuredTemplate.types has been … (#1246)
* Fix broken TemplateEnricher - CONFIG.MeasuredTemplate.types has been deprecated and the error was breaking the code

* Fix for shortHand of ranges

---------

Co-authored-by: Chris Ryan <chrisr@blackhole>
Co-authored-by: WBHarry <williambjrklund@gmail.com>
2025-11-08 20:28:49 +01:00
WBHarry
a7d035bcdb Fixed infinite loop errors 2025-11-08 14:30:15 +01:00
WBHarry
3af90dc0f1 Merged main to development 2025-11-08 12:59:27 +01:00
Chris Ryan
bccedffbca
merge main to development (#1244)
* Fixed typo of defi ant to defiant (#1175)

Co-authored-by: Chris Ryan <chrisr@blackhole>

* Add dice value support to Advesaries and Environments (#1186)

Co-authored-by: Chris Ryan <chrisr@blackhole>

* Update Powerful Beast for Errata changes (#1185)

Co-authored-by: Chris Ryan <chrisr@blackhole>

* Remove unnecessary chatDisplay (#1171)

* Remove dupe weapon (#1167)

Co-authored-by: Chris Ryan <chrisr@blackhole>

* Rune Ward reduces damage with flat 1d8 (#1191)

Co-authored-by: Chris Ryan <chrisr@blackhole>

* Fix adv hand crossbow to d6 damage, not d4 (#1208)

Co-authored-by: Chris Ryan <chrisr@blackhole>

* Fix advantages for Armored Sentry (#1210)

Co-authored-by: Chris Ryan <chrisr@blackhole>

* Change the damage to use d8 instead of d6 (#1211)

Co-authored-by: Chris Ryan <chrisr@blackhole>

* [PR] Compendium fixes and Typo checks - 26 Oct (#1233)

* PR fixes and Typo checks - 26 Oct

* removed dwarf tough skin action as it was unecessary

* Fix the damage output of the Takedown Beastform feature (#1240)

Co-authored-by: Chris Ryan <chrisr@blackhole>

---------

Co-authored-by: Chris Ryan <chrisr@blackhole>
Co-authored-by: UsernameIsInUse <49582925+UsernameIsInUse@users.noreply.github.com>
Co-authored-by: Nikhil Nagarajan <potter.nikhil@gmail.com>
2025-11-08 12:56:04 +01:00
Chris Ryan
3aaae26ba1
Fix typo in AttributionDialog class name (#1245)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-11-08 12:55:08 +01:00
WBHarry
ba84a75bd0
[Feature] 942 - Limited Sheet Views (#1090)
* Added stumps for all limited actor views

* Added description to adversary and environment

* style limited sheets

* Limited views are no longer resizable

* .

* Update styles/less/sheets/actors/actor-sheet-shared.less

Co-authored-by: Nikhil Nagarajan <potter.nikhil@gmail.com>

---------

Co-authored-by: moliloo <dev.murilobrito@gmail.com>
Co-authored-by: Nikhil Nagarajan <potter.nikhil@gmail.com>
2025-11-07 09:04:34 -05:00
Chris Ryan
3c1f646b61
Fix the damage output of the Takedown Beastform feature (#1240)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-11-06 10:05:41 -05:00
Carlos Fernandez
ce3e2a804c
Improve sidebar appearance when labels are long (#1238) 2025-11-06 10:02:22 -05:00
WBHarry
659670c403
custom defeated icons (#1241) 2025-11-06 12:01:19 -03:00
WBHarry
ff79dd19bf
[Feature] 946 - Damage/Effect ChatMessages (#1089)
* Initial damage message

* Added hover functionality to adversary damage cards

* Added effect message

* enhance chat messages styles

* .

* Fixed promise lockup

* .

* Fixed token path for message

---------

Co-authored-by: moliloo <dev.murilobrito@gmail.com>
2025-10-29 13:56:37 -04:00
Nikhil Nagarajan
e2508501a5
[PR] Compendium fixes and Typo checks - 26 Oct (#1233)
* PR fixes and Typo checks - 26 Oct

* removed dwarf tough skin action as it was unecessary
2025-10-29 18:17:09 +01:00
WBHarry
906c7ac853
[Feature] 613 - Countdown Improvements (#1184)
* Added CountdownEdit view

* Added countdowns UI element

* .

* Fixed migration of countdowns

* .

* .

* style countdown interface, application and ownership dialog

* fix buttons height in ownsership selection

* .

* Added coloured pips to UI cooldowns to signify player visibility if not every player has it

* .

* Added max-height and overflow

* Sync countdown current with max when equal (#1221)

* Update module/applications/ui/countdownEdit.mjs

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

* .

---------

Co-authored-by: moliloo <dev.murilobrito@gmail.com>
Co-authored-by: Carlos Fernandez <CarlosFdez@users.noreply.github.com>
2025-10-27 18:24:38 -03:00
WBHarry
07cdcf2d78
Improved order of steps (#1232) 2025-10-26 21:57:46 +10:00
IrkTheImp
d4f80b6fa1
[Feature] 789 - spotlight order (#1222)
* order spotlight requests

* add spotlight requests section to combat tracker
2025-10-25 18:59:25 +02:00
Carlos Fernandez
45b9b52314
Allow viewing domain card item sheet by clicking the name in grid view (#1215) 2025-10-15 23:01:41 +10:00
Carlos Fernandez
dc073aa9cc
Clean up loadout tab card name visual styling (#1216) 2025-10-14 12:01:54 -03:00
Carlos Fernandez
25fb2ee570
[Community PR] Remove item description fieldset (#1214)
* Remove item description fieldset

* Move styling to tab description and reduce tab specific overrides

* Move artist attribution style back to tab description

* Roll back editor menu changes
2025-10-14 12:00:16 -03:00
Chris Ryan
a80789f1f1
Change the damage to use d8 instead of d6 (#1211)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-10-08 14:14:23 -04:00
Chris Ryan
e15d210e20
Fix advantages for Armored Sentry (#1210)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-10-08 14:12:53 -04:00
Chris Ryan
6f2ffaf2f8
Fix adv hand crossbow to d6 damage, not d4 (#1208)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-10-08 13:24:09 -04:00
Carlos Fernandez
b3a72d6b1d
[Community PR] Use 2d12 as hover icon for PCs and omit when feature has no uses (#1202)
* Use 2d12 as hover icon for player rolls

* SVG test files added

Try to manipulate the fills and stroke of the SVG files as needed. I have made hope and fear based vectors to easily identify between them.

* Updated SVG path

Fear path fixed

* Use b&w duality and only show roll img if there are usable actions

* Switch to SVG and change colors

---------

Co-authored-by: Nikhil Nagarajan <potter.nikhil@gmail.com>
2025-10-08 12:18:19 -04:00
Carlos Fernandez
86eeba0648
Implement @Lookup enricher (#1196) 2025-10-04 21:10:39 +10:00
Chris Ryan
952779000d
[PR] [Feature] Remember the position and size of the compendium browser (#1123)
* Remember the position and size of the compendium browser

* Save position, size details for the 3 types of compendium browser

---------

Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-09-29 14:15:02 -04:00
Carlos Fernandez
cc6ef0b7bf
Anchor chat message image crop to top (#1193) 2025-09-29 11:10:13 -03:00
Chris Ryan
b406ec6c96
Rune Ward reduces damage with flat 1d8 (#1191)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-09-20 17:04:20 +02:00
Carlos Fernandez
e655954890
Fix item summary animation replaying on every rerender (#1176) 2025-09-19 11:33:56 +10:00
Chris Ryan
f1b5c80a53
Null check metadata (#1145)
* Null check metadata

* Fix d20RollDialog action find

---------

Co-authored-by: Chris Ryan <chrisr@blackhole>
Co-authored-by: Dapoolp <elcatnet@gmail.com>
2025-09-18 22:28:25 +02:00
Chris Ryan
4379741681
Remove dupe weapon (#1167)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-09-18 22:27:26 +02:00
Chris Ryan
55586c93c8
Move Foundryborne menu location; fix localisation; fix deprecation (#1169)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-09-18 22:27:04 +02:00
UsernameIsInUse
f25387e964
Remove unnecessary chatDisplay (#1171) 2025-09-18 22:26:08 +02:00
Chris Ryan
a477c9b852
Fixed a merge? error in the itemBrowserConfig (#1174)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-09-18 22:25:08 +02:00
Chris Ryan
2748e91aa6
Update Powerful Beast for Errata changes (#1185)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-09-18 22:08:12 +02:00
Chris Ryan
a76486a86c
Add dice value support to Advesaries and Environments (#1186)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-09-18 22:07:35 +02:00
Chris Ryan
513464b01b
Fixed typo of defi ant to defiant (#1175)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-09-10 10:11:24 -04:00
Carlos Fernandez
a57d154d45
Add space between paragraphs (#1172) 2025-09-09 23:03:13 -03:00
Chris Ryan
58f039ce96
Add extra features to the Temple Enricher and fix the mousewheel issues with the Template Manager (#1147)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-09-07 01:27:46 +02:00
WBHarry
2c6aabb97a Merged with main 2025-09-07 01:01:53 +02:00
WBHarry
2176038ec6
Added homebrew for armor and weapon fatures (#1166)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-09-07 08:47:21 +10:00
WBHarry
f1b6d3851d
[PR] [Feature] 590 - Daggerheart Menu (#1007)
* Added menu with refresh tools

* Replaced menu icon
2025-09-07 08:30:29 +10:00
Chris Ryan
fd92540792
Add keys, update usage for translations (#1156)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-09-06 22:06:17 +02:00
Chris Ryan
e258d9c5f6
Fixed typo in button class (#1159)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-09-06 22:05:24 +02:00
Chris Ryan
d4a98d3f66
Add motives and tactics for Oak Treant (#1158)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-09-06 19:53:21 +02:00
Chris Ryan
ec54da4e23
Add and fix Tier names in level up (#1142)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-09-05 09:47:12 -04:00
joaquinpereyra98
eefb28c312
[REFACTOR] Simplify the DHAppearanceSettings (#1057)
Co-authored-by: Joaquin Pereyra <joaquinpereyra98@users.noreply.github.com>
2025-09-05 11:09:47 +10:00
Dapoulp
2d92576121
Fix/1149 fix targets apply effects (#1151)
* Temp ActionField attack type missing

* Move missing attack type to getModel

* Fix targets apply effects
2025-09-05 10:56:38 +10:00
Dapoulp
f04619f73b
Fix/1144 fix experiences roll use costs (#1150)
* Temp ActionField attack type missing

* Move missing attack type to getModel

* Fix costs object being updated during getRealCosts
2025-09-05 10:45:05 +10:00
Dapoulp
0bd423ef52
fix (#1143) 2025-09-03 21:42:18 +10:00
Chris Ryan
3f08741a34
Use less ambiguous sort direction symbols (#1108)
* Use less ambiguous sort direction symbols

* Up arrow for ascending, small bar at top, large bar at bottom; Down arrow for descending, large bar at top, small bar at bottom

* Using matching icons

* Switch the order

---------

Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-09-03 13:54:41 +10:00
Chris Ryan
3c893df175
Fix the missing translation keys (#1133)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-09-02 09:26:32 -04:00
Chris Ryan
1b9defe4ad
The uuid is expected, not id (#1132)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-09-02 09:00:46 -04:00
Chris Ryan
179dd3e6c3
Fix the typos related to the Protective feature on Round Shields (#1131)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-09-01 20:50:48 -04:00
Dapoulp
31238113c9
Fix/1116 fix action nullable fields (#1117)
* Temp ActionField attack type missing

* Move missing attack type to getModel

* Damage/Healing fields nullable fix

* Fix TargetField null value

* Other fixes

* Fix Action type to be not nullable
2025-08-30 11:44:16 -04:00
Chris Ryan
ef4d37d725
typo fixes (#1121)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-08-29 21:47:08 -04:00
Chris Ryan
3f1e7f4f4a
Added some protection for no data supplied (#1115)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-08-29 09:49:33 -04:00
WBHarry
c6741b1c7a
Added background and connection questions (#1109) 2025-08-29 09:26:22 +10:00
WBHarry
8fd63d5963
[Feature] 1033 - Consume Quantity On Use (#1106)
* Initial migration

* Updated compendium YML

* Added Quantity as a possible cost

* Added quantity consumption to all Compendium Consumables

* .

* Added DestroyOnEmpty property
2025-08-28 12:29:17 -04:00
Dapoulp
9dd773001d
Compendium browser per type (#1103)
* Compendium Browser per type

* Sort number column

* Re-add subclass config

* Sidebar buttons

* Add Characters folder

* Css

* Done
2025-08-28 11:29:40 +10:00
WBHarry
9f2c2f1bed
Changed so the default domaincard view is list (#1102) 2025-08-27 09:22:29 -04:00
Chris Ryan
18687b6131
Fix Avalanche typo (#1098)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-08-27 09:01:15 +02:00
Dapoulp
1eb3ff11c0
Task/415 action refactor part 2 (#1094) 2025-08-27 09:00:18 +02:00
WBHarry
8e5dd22370
[Fix] 900 - Compendium Self Targetting (#1077)
* Fixed classes and subclasses

* Went over DomainCards

* Everything except de-facto-items

* Fixed items

* Fixed merge conflicts
2025-08-27 10:12:48 +10:00
WBHarry
7a764e39ae Merge branch 'main' into development 2025-08-26 21:38:27 +02:00
Dapoulp
aaf6c689fc
Add hint css (#1059)
* Add hint css

* Move .hint css class to global
2025-08-26 14:22:40 +02:00
WBHarry
3386a9d61d
[Feature] 1031 - Action Names In Chat (#1039)
* Fixed so that actions don't print the actor name in chat along with its own name

* .
2025-08-26 14:13:31 +02:00
WBHarry
990c73987e Merged with main 2025-08-24 21:31:57 +02:00
Chris Ryan
afdffb672a
[PR] [Fix] Homebrew Settings, Initial Trait Modifiers improvements and clean up (#1035)
* Remove old homebrew settings template file

* Improve the Initial Trait Modifiers config display

* Remove font setting

---------

Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-08-23 22:38:09 -03:00
Murilo Brito
2a0d748b5e
[Bugfix] Fix light theme to roll messages (#1066)
* fix light theme to roll messages

* fix light theme issues

* finally fix issues

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>
2025-08-24 02:52:18 +02:00
WBHarry
661808b5f1
Fixed so observer permission doesn't allow using or editing anything (#1073) 2025-08-24 10:33:54 +10:00
WBHarry
16173363d4
[Feature] 687 - Companion Improvements (#1049)
* Companions can't be put into CombatState anymore. Companions now have a Action Roll button

* Added handling for multiselect toggleCombat
2025-08-24 10:24:57 +10:00
Dapoulp
a72d4583cd
[PR]Add custom formula to weapon base attack (#964)
* Add custom formula to weapon base attack

* Remove log

* Update weapon custom damage formula label + update font-size in px
2025-08-23 14:54:57 +02:00
Chris Ryan
471cbd55df
Fixes some measurement issues with templates (#1065)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-08-23 13:24:28 +02:00
WBHarry
d5f7e17339
Added Custom Adversary Types (#1048) 2025-08-23 10:18:25 +10:00
Dapoulp
ee786544c7
Fix double damageKeyToNumber (#1044) 2025-08-22 12:22:47 +02:00
Luiz HD Costa
888cf9172b
[Community PR] Localize hardcoded text (#1002)
* Localize remaining hardcoded user-facing strings

* Introduce pluralize helper for localizing strings

* Localize missing strings from ItemBrowser
2025-08-22 11:35:36 +10:00
WBHarry
e9f7c0c16b Merged with main 2025-08-22 01:57:54 +02:00
Chris Ryan
60b55619e1
[PR] [Feature] 652 Allow override range measurement settings (#1030)
* Look for rangeMeasurementSettingsOverride on the scene to switch off DH global range measurement settings.

* Part progress on adding config tab to scene config

* Hard coded template; no value applied/saved

* Flag fix

* Use the flags setting

* Clean up

* Remove import

* Better initialisation of PARTS and TABS

* Fix localisation

---------

Co-authored-by: Chris Ryan <chrisr@blackhole>
Co-authored-by: WBHarry <williambjrklund@gmail.com>
2025-08-21 11:05:09 -04:00
Murilo Brito
7a6bbe3488
add light theme for chat messages (#1016) 2025-08-20 18:14:46 +10:00
joaquinpereyra98
af250d7a61
[BUG] - Deleting items in the scrollable window causes scrollbar to reset to top (#1025)
Fixes #977

Co-authored-by: Joaquin Pereyra <joaquinpereyra98@users.noreply.github.com>
2025-08-20 11:07:35 +10:00
WBHarry
774b6dbdcc
Fixed so character dice rolls await the DiceSoNice animation before consuming resources (#1024) 2025-08-20 10:56:00 +10:00
WBHarry
85111648aa
Fixed Spear/HallowedAxe and type in SRD (#1023) 2025-08-20 10:42:26 +10:00
WBHarry
fa8bd63614 Merge branch 'main' into development 2025-08-19 20:58:13 +02:00
WBHarry
4038c44f9a
[Fix] 987 - Auto Expand Bottom Chat (#1010)
* If the last message in the chatlog is updated, scroll to bottom of chatlog

* Cleaned up with ui.chat methods
2025-08-19 21:03:16 +10:00
Carlos Fernandez
f19548ef4f
Make sidebar hover more visually stable (#999) 2025-08-18 00:40:58 -03:00
Carlos Fernandez
bc5b01bdcf
Use more opaque color for sheet background (#974) 2025-08-18 00:39:17 -03:00
WBHarry
f69e5704e4
Added a simple ViewMode for a character's levelup progression (#997) 2025-08-18 11:59:57 +10:00
WBHarry
5cd5de31aa
Scrolling Texts are now queued with a 600ms delay (#989) 2025-08-18 11:52:20 +10:00
WBHarry
8c84edddad
[PR][Feature] 798 - Attribution (#986)
* Added attribution for items, adversary and environment

* Added Attribution to all adversaries

* Added attribution to environments

* Added attribution for class and subclass

* Added Attribution to Ancestry/Community

* Added Attribution for Beastforms

* Added Attribution to DomainCards

* Added Attribution for wepaons

* Attribution for Armor/Loot/Consumables

* Added a setting to hide attribution
2025-08-18 11:44:59 +10:00
Murilo Brito
495575fba4
[Support] Update Development Branch (#1001)
* add typo report template (#971)

Co-authored-by: Psitacus <walther.johnson@ucalgary.ca>

* Fix/fix weapon damage datas in sheet (#988)

* Temp ActionField attack type missing

* Move missing attack type to getModel

* Fix weapon base attack reseting on update

* [PR]Fix/allow deal damage button use owner (#985)

* Temp ActionField attack type missing

* Move missing attack type to getModel

* Fix player not allowed to use Deal Damage button if Actor not assigned

* Fix/add translation key for Unarmed Attack (#973)

Co-authored-by: Chris Ryan <chrisr@blackhole>

* Add setup script for development (#981)

* [PR] testing something (#995)

* testing something

* added template for PR test

* Initial Commit (#992)

---------

Co-authored-by: Psitacus <59754077+Psitacus@users.noreply.github.com>
Co-authored-by: Psitacus <walther.johnson@ucalgary.ca>
Co-authored-by: Dapoulp <74197441+Dapoulp@users.noreply.github.com>
Co-authored-by: Chris Ryan <73275196+chrisryan10@users.noreply.github.com>
Co-authored-by: Chris Ryan <chrisr@blackhole>
Co-authored-by: Luiz HD Costa <luiz.costa@hey.com>
Co-authored-by: Nikhil Nagarajan <potter.nikhil@gmail.com>
2025-08-18 11:19:22 +10:00
WBHarry
577ed5f491
Added tooltips for experience description (#975) 2025-08-18 11:00:30 +10:00
joaquinpereyra98
18b6194afe
[Feature] Improves ForeignDocumentUUIDArrayField for can use documents to update it. (#963)
Fixes #389

Co-authored-by: Joaquin Pereyra <joaquinpereyra98@users.noreply.github.com>
2025-08-17 18:55:33 +02:00
Dapoulp
16591cd327
Feature/761 direct damage (#961)
* Add Direct Damage to Adversary base attack

* Add direct damage to weapon
2025-08-17 15:58:34 +02:00
Dapoulp
03e6570d68
Feature/519 auto expand roll message (#959)
* Add settings to auto expand roll message sections

* Change Settings labels to match roll message ones

* Add setting to auto expand chat card desc
2025-08-17 14:42:55 +02:00
Murilo Brito
96d26a1e5b
refactor font-family styles to avoid unecessary diplicities (#983) 2025-08-17 14:37:39 +02:00
WBHarry
2820c96259 Merge branch 'main' into development 2025-08-16 02:22:27 +02:00
WBHarry
b3c0344b91 Merged in main 2025-08-14 20:39:53 +02:00
Dapoulp
47754e78cd
Fix roll dialog default rollmode value (#938) 2025-08-14 13:08:59 -04:00
WBHarry
ecd33caa7b Merged with main 2025-08-14 00:51:43 +02:00
WBHarry
24f5cb5a5c
Hotfix 1.0.2 (#916) (#919)
* Updated the background image for the system

* Fixed so Weapon/Armor features are added again

* Fixed so fear is available as a resource to be deducted by actions (#757)

* Changed to use the config labels and src

* Updated Weapons

* Fixed so the decrease button of simple fear tracker is not visible when not hovered

* Fixed so armor preUpdate doesn't fail if no system changes are made

* Updated .gitignore and author details (#777)

* Add author details and name mapping for chrisryan10 (#773)



* Add build to ignore for my linux dev (#775)



---------



* Corrected sneak attack active effect (#780)

* Fixed a spelling error (#779)

* Fix bardic rally showing in damage dialog when it should not (#783)

* update spelling (#786)

* Translating inventory descriptions (#782)

* updated credits for 1.0.1 release (#797)

* updated credits for 1.0.1 release

* further updated artwork credits

* Chagned handlebarhelper rollparsed to be more defensive (#794)

* Added missing scene refreshType (#790)

* Remove ability use buttons for not owned abilities (#795)

* [Fix] PrayerDice Fixed (#799)

* Fixed prayer dice, and wheelchair images

* Fixed -settings data sources

* Dragging features from one adversary to another (#788)

* [Fix] Levelup Fixes (#787)

* Fixed crash on experience selection. Fixed subclass error on multiclassing

* Fixed so multiclasses do not gain the hope feature for the class

* Fixed so Class/Subclass features are properly deleted on delevel

* Removed automatic deletion of features on delevel when not using levelup auto

* Fixed so custom domains can be selected in levelup when multiclassing

* Changed so encounter countdowns is a button (#804)

* Fixed so that dropping on class/subclass...creates the item on the character (#803)

* [BUG] - Importing All Adversaries/Environments (#814)

Fixes #774



* Bug/671 reaction roll chat title (#809)

* Update Reaction Roll Chat Message Title

* Removed console log

---------



* Improve Trait tooltip display (#817)

Fixes #806



* [BUG] - Combat Tracker d12 logo not found (#812)

Fixes #764



* Compendium Browser (#821)

* Corrected timbending description localization (#816)

* [Fix] Compendium Item (#810)

* Corrected Emberwoven Armor

* Fixed subclass regression

* Fixed so character's with wildcard images don't break beastform (#815)

* Fix roll result based duality damage (#822)

* Impproved Adversary Sheet Data Display (#751)

* Impproved Adversary Sheet Data Display
Fixes #604

* FIX: formula lables for attacks and weapons

---------



* Fixed so exp increases can be selected normally (#835)

* Update localization of rollSelection.hbs (#841)

The Fear label lacked a localization path

* renamed .md files (#834)

* Removed the unintended icons that came from merge conflicts (#838)

* Sheet image position (#861)

* Remove extra result text from reaction chat message (#860)

* Remove extra result text from reaction chat message

* Remove log

* [Fix] 850 - Downtime Refreshes (#859)

* Fixed the filtering of refreshable features

* Raised version

* [Fix] 691 - CharacterCreation Improvement  (#863)

* Removed main creation tab. Added equipment to remaining tab

* Fixed MixedAncestry name setting

* Fixed tab after first creation

* Fix/857 generic roll buttons (#866)

* Action Macro working again

* Remove buttons from generic roll for non-gm

* Update sidebar.less Armor Slots label padding, for translations (#872)

Equalizes the vertical and horizontal padding on the Armor Slots status label, to allow for more (much needed) room for the upcoming French translation (and others in the future).

Doesn't change anything for the display in English.

* Hid item attachments for now (#876)

* Fixed so effects supposed to use item data use the model directly, since items have no rolldata (#883)

* Made sure the beastform user isn't moved onto other duplicate actors. Fixed scrolling text duplication (#882)

* Change the critical damage max calculation (#890)

* Change the critical damage max calculation to avoid setting the rolled portion to maximum all the time.

* Change to the more neater code

* Restored package.json

---------




* Fix movement (#885)

* Changed so companion can level up on its own (#879)

* Fix the missing ancestries in the SRD (#888)



* Removed the old, now unused companion levelup button (#893)

* Fixed so custom downtime moves will display their descriptions in the tooltip (#894)

* Made coinflip icons in chat round (#895)

* Fixed undefined case (#898)

* Fix/877 hope update order (#896)

* Action Macro working again

* k

* Fix Hope gain on Duality Roll

* [Feature] Descriptions enhancements (#887)

* add style to hover items and add start setting to features be expanded by default

* REFACTOR:
now prepare description onRender and simply the other methods

* add setting to extend description from items and add molilo contacts in system.json

---------



* Fixed traits container styling in light-mode (#899)

* [BUG] - Movement Button selector not showing current mode (#912)

Fixes #910



* Fix/864 chat targeting (#911)

* Fix targeting

* Fix chat target selection

* Uncomment chatDisplay

* Removed unessecary arg

* [Feature] 648 - Mark Defeated Actors (#914)

* Improved death marking styling

* Added automation for defeated status

* Fixed so the tracker recognises and sets the correct defeated statuses depending on type

* Fixed so missing statuses doesn't cause crashes

* Increased companion sheet width by 40 pixels

* Added missing inheritDoc

* Removed fas

* FIX: use img instead of icons con statusEffectConfigns (#917)

Authored-by: Joaquin Pereyra <joaquinpereyra98@users.noreply.github.com>

---------

Co-authored-by: Chris Ryan <73275196+chrisryan10@users.noreply.github.com>
Co-authored-by: Chris Ryan <chrisr@blackhole>
Co-authored-by: Dapoulp <74197441+Dapoulp@users.noreply.github.com>
Co-authored-by: IrkTheImp <41175833+IrkTheImp@users.noreply.github.com>
Co-authored-by: CPTN_Cosmo <cptncosmo@gmail.com>
Co-authored-by: Josh Q. <jshqntnr13@gmail.com>
Co-authored-by: joaquinpereyra98 <24190917+joaquinpereyra98@users.noreply.github.com>
Co-authored-by: Joaquin Pereyra <joaquinpereyra98@users.noreply.github.com>
Co-authored-by: SunnySunSun <snysun@pm.me>
Co-authored-by: Murilo Brito <91566541+moliloo@users.noreply.github.com>
2025-08-13 22:14:09 +02:00
Dapoulp
06184773b9
direct damage (#873) 2025-08-12 18:37:16 +02:00
WBHarry
6e747e67ee Merge branch 'main' into development 2025-08-11 18:00:21 +02:00
WBHarry
4ffa690aec
Hotfix 1.0.1 (#825)
* Updated the background image for the system

* Fixed so Weapon/Armor features are added again

* Fixed so fear is available as a resource to be deducted by actions (#757)

* Changed to use the config labels and src

* Updated Weapons

* Fixed so the decrease button of simple fear tracker is not visible when not hovered

* Fixed so armor preUpdate doesn't fail if no system changes are made

* Updated .gitignore and author details (#777)

* Add author details and name mapping for chrisryan10 (#773)

Co-authored-by: Chris Ryan <chrisr@blackhole>

* Add build to ignore for my linux dev (#775)

Co-authored-by: Chris Ryan <chrisr@blackhole>

---------

Co-authored-by: Chris Ryan <chrisr@blackhole>

* Corrected sneak attack active effect (#780)

* Fixed a spelling error (#779)

* Fix bardic rally showing in damage dialog when it should not (#783)

* update spelling (#786)

* Translating inventory descriptions (#782)

* updated credits for 1.0.1 release (#797)

* updated credits for 1.0.1 release

* further updated artwork credits

* Chagned handlebarhelper rollparsed to be more defensive (#794)

* Added missing scene refreshType (#790)

* Remove ability use buttons for not owned abilities (#795)

* [Fix] PrayerDice Fixed (#799)

* Fixed prayer dice, and wheelchair images

* Fixed -settings data sources

* Dragging features from one adversary to another (#788)

* [Fix] Levelup Fixes (#787)

* Fixed crash on experience selection. Fixed subclass error on multiclassing

* Fixed so multiclasses do not gain the hope feature for the class

* Fixed so Class/Subclass features are properly deleted on delevel

* Removed automatic deletion of features on delevel when not using levelup auto

* Fixed so custom domains can be selected in levelup when multiclassing

* Changed so encounter countdowns is a button (#804)

* Fixed so that dropping on class/subclass...creates the item on the character (#803)

* [BUG] - Importing All Adversaries/Environments (#814)

Fixes #774

Co-authored-by: Joaquin Pereyra <joaquinpereyra98@users.noreply.github.com>

* Bug/671 reaction roll chat title (#809)

* Update Reaction Roll Chat Message Title

* Removed console log

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>

* Improve Trait tooltip display (#817)

Fixes #806

Co-authored-by: Joaquin Pereyra <joaquinpereyra98@users.noreply.github.com>

* [BUG] - Combat Tracker d12 logo not found (#812)

Fixes #764

Co-authored-by: Joaquin Pereyra <joaquinpereyra98@users.noreply.github.com>

* Compendium Browser (#821)

* Corrected timbending description localization (#816)

* [Fix] Compendium Item (#810)

* Corrected Emberwoven Armor

* Fixed subclass regression

* Fixed so character's with wildcard images don't break beastform (#815)

* Fix roll result based duality damage (#822)

---------

Co-authored-by: Chris Ryan <73275196+chrisryan10@users.noreply.github.com>
Co-authored-by: Chris Ryan <chrisr@blackhole>
Co-authored-by: Dapoulp <74197441+Dapoulp@users.noreply.github.com>
Co-authored-by: IrkTheImp <41175833+IrkTheImp@users.noreply.github.com>
Co-authored-by: CPTN_Cosmo <cptncosmo@gmail.com>
Co-authored-by: Josh Q. <jshqntnr13@gmail.com>
Co-authored-by: joaquinpereyra98 <24190917+joaquinpereyra98@users.noreply.github.com>
Co-authored-by: Joaquin Pereyra <joaquinpereyra98@users.noreply.github.com>
2025-08-11 10:02:06 +10:00
1615 changed files with 36151 additions and 29671 deletions

View file

@ -44,12 +44,14 @@ We encourage contributors to leave comments or open Discussions when proposing s
## 🧾 Issue & PR Best Practices ## 🧾 Issue & PR Best Practices
**For Issues:** **For Issues:**
- Use clear, descriptive titles - Use clear, descriptive titles
- Provide a concise explanation of the problem or idea - Provide a concise explanation of the problem or idea
- Include reproduction steps or example scenarios if it's a bug - Include reproduction steps or example scenarios if it's a bug
- Add screenshots or logs if helpful - Add screenshots or logs if helpful
**For Pull Requests:** **For Pull Requests:**
- Use a clear title summarizing the change - Use a clear title summarizing the change
- Provide a brief description of what your code does and why - Provide a brief description of what your code does and why
- Link to any related Issues - Link to any related Issues
@ -71,6 +73,6 @@ Discussions are currently happening on GitHub — in Issues, PRs, and [GitHub Di
## 🤗 Thank You! ## 🤗 Thank You!
Whether you're fixing a typo or designing entire mechanics — every contribution matters. Thank you for helping bring *Daggerheart* to life in FoundryVTT through **Foundryborne**! Whether you're fixing a typo or designing entire mechanics — every contribution matters. Thank you for helping bring _Daggerheart_ to life in FoundryVTT through **Foundryborne**!
🐸🛠️ 🐸🛠️

BIN
assets/icons/arrow-dunk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View file

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg"
width="42.0746mm" height="50.8071mm"
viewBox="0 0 159 192">
<path id="Fear"
fill="#808080" stroke="black" stroke-width="1"
d="M 107.14,62.84
C 107.14,62.84 52.70,137.55 52.70,137.55
52.70,137.55 110.33,137.55 110.33,137.55
110.33,137.55 127.99,79.63 127.99,79.63
127.99,79.63 107.14,62.84 107.14,62.84 Z
M 48.07,144.21
C 48.07,144.21 13.61,192.28 13.61,192.28
14.01,191.90 30.68,177.55 47.81,176.06
47.81,176.06 64.48,176.30 64.48,176.30
64.69,176.22 79.31,180.77 79.31,180.77
79.31,180.77 126.04,165.15 126.04,165.15
126.04,165.15 110.91,143.92 110.91,143.92
110.91,143.92 48.07,144.21 48.07,144.21 Z
M 134.24,81.37
C 134.24,81.37 115.74,140.33 115.74,140.33
115.74,140.33 131.41,161.00 131.41,161.00
131.41,161.00 158.98,122.82 158.98,122.82
158.98,122.82 158.91,72.83 158.91,72.83
158.91,72.83 134.24,81.37 134.24,81.37 Z
M 130.43,30.45
C 130.43,30.45 110.41,59.27 110.41,59.27
110.41,59.27 131.77,75.21 131.77,75.21
131.77,75.21 157.51,67.23 157.51,67.23
157.25,67.32 130.42,30.45 130.43,30.45 Z" />
<path id="Hope"
fill="white" stroke="black" stroke-width="1"
d="M 143.84,1.80
C 143.84,1.80 105.59,54.92 105.59,54.92
105.59,54.92 83.04,39.72 83.04,39.72
83.04,39.72 82.87,13.08 82.87,13.08
82.23,14.32 111.40,18.59 112.13,17.16
113.68,18.32 145.11,2.74 143.84,1.80 Z
M 76.01,13.40
C 76.01,13.40 76.01,40.05 76.01,40.05
76.01,40.05 25.99,75.35 25.99,75.35
25.99,75.35 1.96,68.16 1.96,68.16
1.96,68.16 30.08,27.62 30.08,27.62
30.08,27.62 76.01,13.40 76.01,13.40 Z
M 79.11,44.62
C 79.11,44.62 101.34,60.81 101.34,60.81
101.34,60.81 48.38,133.54 48.38,133.54
48.38,133.54 30.57,80.09 30.57,80.09
30.57,80.09 79.11,44.62 79.11,44.62 Z
M 24.36,81.07
C 24.36,81.07 43.64,139.59 43.64,139.59
43.64,139.59 28.11,162.31 28.11,162.31
28.11,162.31 0.33,123.90 0.33,123.90
0.33,123.90 0.33,74.05 0.33,74.05
0.33,74.05 24.36,81.07 24.36,81.07 Z" />
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="height: 512px; width: 512px;"><path d="M0 0h512v512H0z" fill="#000" fill-opacity="1"></path><g class="" transform="translate(0,0)" style=""><path d="M369.1 21.22c-19.2 0-36.2 10.63-47.9 26.47-11.7 15.84-18.6 37.03-18.6 60.31 0 21.1 5.7 40.5 15.5 55.7-5.7 1.6-11 3.9-15.9 6.6-10.2-8.5-22.6-13.6-35.9-13.6-19.3 0-36.3 10.6-48 26.4-4.7 6.4-8.6 13.6-11.6 21.5-4.8-2.4-9.9-4.3-15.5-5.6 9.4-15.1 14.8-34.1 14.8-54.7 0-23.2-6.9-44.43-18.6-60.27-11.7-15.84-28.7-26.5-47.9-26.5s-36.2 10.66-47.94 26.5C79.87 99.87 73 121.1 73 144.3c0 21.1 5.69 40.5 15.47 55.8-32.07 9.1-50.29 37.1-59.44 70-9.79 35.2-10.87 77.3-10.87 115.6v9.4h45.5l6.78 99.3h18.75l-7.28-106.5-4.1-80-18.65 1 3.47 67.5H36.97c.24-35.2 1.97-72.1 10.09-101.2 8.78-31.6 23.32-52.8 51.25-58.2l4.69-.1c10.3 8.8 22.9 14.2 36.5 14.2 14.1 0 26.9-5.7 37.4-15h4.6c7.8 1.2 14.4 3.5 20.1 6.7-1.2 6.6-1.9 13.5-1.9 20.6 0 21.1 5.7 40.5 15.5 55.8-32.1 9.1-50.3 37.2-59.4 70-9.8 35.2-10.9 77.3-10.9 115.6v9.4c21.7-.3 42.8.2 64.3.2l-.5-7.3-4.1-80-18.7.9 3.4 67.5h-25.6c.3-35.2 2-72.1 10.1-101.2 8.7-31.6 23.3-52.7 51.1-58.2l4.9-.1c10.3 8.8 22.8 14.2 36.4 14.2 14.1 0 27-5.7 37.5-15h4.4c15.4 2.4 26.1 8.9 34.5 18.6 8.5 9.7 14.5 23.2 18.5 39.2 7.3 29.5 7.7 66.9 7.7 102.5h-23.4l3.5-67.5-18.7-.9-4.2 82-.3 5.3c20.8 0 43.3-.3 61.9-.2v-9.4c0-38.1.5-80.6-8.4-116.3-4.4-17.8-11.3-34.1-22.4-47-9.7-11.1-22.7-19.4-38.8-23.4 9.4-15.1 14.7-34.1 14.7-54.7 0-22.5-6.4-43.2-17.5-58.8 3.9-1.8 8.1-3.1 12.7-4h4.7c10.3 8.8 22.9 14.2 36.5 14.2 14.1 0 27-5.8 37.4-15l4.6-.1c15.4 2.5 26 8.9 34.4 18.6 8.5 9.8 14.5 23.3 18.5 39.3 7.3 29.4 7.7 66.8 7.7 102.4h-23.4l3.5-67.4-18.7-1-4.1 79.7-8.6 143.1h18.7l8.2-135.7h43.1v-9.3c0-38.2.6-80.7-8.3-116.3-4.5-17.9-11.4-34.2-22.5-47-9.6-11.2-22.6-19.5-38.8-23.5 9.4-15.1 14.8-34 14.8-54.6 0-23.28-6.9-44.47-18.6-60.31-11.6-15.29-31.5-26.13-47.9-26.47zm0 18.69c12.4 0 23.9 6.69 32.9 18.87 9 12.19 14.9 29.67 14.9 49.22 0 19.5-5.9 37-14.9 49.2-9 12.2-20.5 18.9-32.9 18.9-12.3 0-23.9-6.7-32.9-18.9s-14.9-29.7-14.9-49.2c0-19.55 5.9-37.03 14.9-49.22 9-12.18 20.6-18.87 32.9-18.87zM139.5 76.22c12.4 0 23.9 6.72 32.9 18.9s14.9 29.68 14.9 49.18-5.9 37-14.9 49.2c-9 12.2-20.5 18.9-32.9 18.9-12.4 0-23.9-6.7-32.9-18.9-8.97-12.2-14.91-29.7-14.91-49.2 0-19.5 5.94-37 14.91-49.17 9-12.19 20.5-18.91 32.9-18.91zm197.8 22.34v18.64h22.5V98.56h-22.5zm41.1 0v18.64h22.5V98.56h-22.5zM107.7 134.9v18.7h22.5v-18.7h-22.5zm41.1 0v18.7h22.5v-18.7h-22.5zm117.5 40.4c12.3 0 23.8 6.7 32.8 18.9 9 12.2 15 29.7 15 49.2 0 19.6-6 37-15 49.2-9 12.2-20.5 18.9-32.8 18.9-12.4 0-24-6.7-33-18.9-8.9-12.2-14.9-29.6-14.9-49.2 0-19.5 6-37 14.9-49.2 9-12.2 20.6-18.9 33-18.9zM234.5 234v18.7h22.4V234zm41.1 0v18.7H298V234h-22.4z" fill="#fff" fill-opacity="1"></path></g></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -1,5 +1,6 @@
import { SYSTEM } from './module/config/system.mjs'; import { SYSTEM } from './module/config/system.mjs';
import * as applications from './module/applications/_module.mjs'; import * as applications from './module/applications/_module.mjs';
import * as data from './module/data/_module.mjs';
import * as models from './module/data/_module.mjs'; import * as models from './module/data/_module.mjs';
import * as documents from './module/documents/_module.mjs'; import * as documents from './module/documents/_module.mjs';
import * as dice from './module/dice/_module.mjs'; import * as dice from './module/dice/_module.mjs';
@ -7,10 +8,8 @@ import * as fields from './module/data/fields/_module.mjs';
import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs'; import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs';
import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs'; import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs';
import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs'; import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs';
import { NarrativeCountdowns } from './module/applications/ui/countdowns.mjs';
import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll } from './module/dice/_module.mjs'; import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll } from './module/dice/_module.mjs';
import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs'; import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs';
import { registerCountdownHooks } from './module/data/countdowns.mjs';
import { import {
handlebarsRegistration, handlebarsRegistration,
runMigrations, runMigrations,
@ -18,85 +17,168 @@ import {
socketRegistration socketRegistration
} from './module/systemRegistration/_module.mjs'; } from './module/systemRegistration/_module.mjs';
import { placeables } from './module/canvas/_module.mjs'; import { placeables } from './module/canvas/_module.mjs';
import { registerRollDiceHooks } from './module/dice/dhRoll.mjs';
import './node_modules/@yaireo/tagify/dist/tagify.css'; import './node_modules/@yaireo/tagify/dist/tagify.css';
import TemplateManager from './module/documents/templateManager.mjs'; import TemplateManager from './module/documents/templateManager.mjs';
CONFIG.DH = SYSTEM;
CONFIG.TextEditor.enrichers.push(...enricherConfig);
CONFIG.Dice.rolls = [BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll];
CONFIG.Dice.daggerheart = {
DHRoll: DHRoll,
DualityRoll: DualityRoll,
D20Roll: D20Roll,
DamageRoll: DamageRoll
};
CONFIG.Actor.documentClass = documents.DhpActor;
CONFIG.Actor.dataModels = models.actors.config;
CONFIG.Item.documentClass = documents.DHItem;
CONFIG.Item.dataModels = models.items.config;
CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect;
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;
CONFIG.ChatMessage.documentClass = documents.DhChatMessage;
CONFIG.ChatMessage.template = 'systems/daggerheart/templates/ui/chat/chat-message.hbs';
CONFIG.Canvas.rulerClass = placeables.DhRuler;
CONFIG.Canvas.layers.templates.layerClass = placeables.DhTemplateLayer;
CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate;
CONFIG.Scene.documentClass = documents.DhScene;
CONFIG.Token.documentClass = documents.DhToken;
CONFIG.Token.prototypeSheetClass = applications.sheetConfigs.DhPrototypeTokenConfig;
CONFIG.Token.objectClass = placeables.DhTokenPlaceable;
CONFIG.Token.rulerClass = placeables.DhTokenRuler;
CONFIG.Token.hudClass = applications.hud.DHTokenHUD;
CONFIG.ui.combat = applications.ui.DhCombatTracker;
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();
Hooks.once('init', () => { Hooks.once('init', () => {
CONFIG.DH = SYSTEM;
game.system.api = { game.system.api = {
applications, applications,
data,
models, models,
documents, documents,
dice, dice,
fields fields
}; };
CONFIG.TextEditor.enrichers.push(...enricherConfig);
CONFIG.statusEffects = [
...CONFIG.statusEffects.filter(x => !['dead', 'unconscious'].includes(x.id)),
...Object.values(SYSTEM.GENERAL.conditions).map(x => ({
...x,
name: game.i18n.localize(x.name),
systemEffect: true
}))
];
CONFIG.Dice.daggerheart = {
DHRoll: DHRoll,
DualityRoll: DualityRoll,
D20Roll: D20Roll,
DamageRoll: DamageRoll
};
CONFIG.Dice.rolls = [BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll];
CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate;
const { DocumentSheetConfig } = foundry.applications.apps; const { DocumentSheetConfig } = foundry.applications.apps;
CONFIG.Token.documentClass = documents.DhToken;
CONFIG.Token.prototypeSheetClass = applications.sheetConfigs.DhPrototypeTokenConfig;
DocumentSheetConfig.unregisterSheet(TokenDocument, 'core', foundry.applications.sheets.TokenConfig); DocumentSheetConfig.unregisterSheet(TokenDocument, 'core', foundry.applications.sheets.TokenConfig);
DocumentSheetConfig.registerSheet(TokenDocument, SYSTEM.id, applications.sheetConfigs.DhTokenConfig, { DocumentSheetConfig.registerSheet(TokenDocument, SYSTEM.id, applications.sheetConfigs.DhTokenConfig, {
makeDefault: true makeDefault: true
}); });
CONFIG.Item.documentClass = documents.DHItem; const sheetLabel = typePath => () =>
game.i18n.format('DAGGERHEART.GENERAL.typeSheet', {
//Registering the Item DataModel type: game.i18n.localize(typePath)
CONFIG.Item.dataModels = models.items.config; });
const { Items, Actors } = foundry.documents.collections; const { Items, Actors } = foundry.documents.collections;
Items.unregisterSheet('core', foundry.applications.sheets.ItemSheetV2); 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.Ancestry, {
Items.registerSheet(SYSTEM.id, applications.sheets.items.Community, { types: ['community'], makeDefault: true }); types: ['ancestry'],
Items.registerSheet(SYSTEM.id, applications.sheets.items.Class, { types: ['class'], makeDefault: true }); makeDefault: true,
Items.registerSheet(SYSTEM.id, applications.sheets.items.Subclass, { types: ['subclass'], makeDefault: true }); label: sheetLabel('TYPES.Item.ancestry')
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.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, { Items.registerSheet(SYSTEM.id, applications.sheets.items.Loot, {
types: ['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 });
CONFIG.Actor.documentClass = documents.DhpActor;
CONFIG.Actor.dataModels = models.actors.config;
Actors.unregisterSheet('core', foundry.applications.sheets.ActorSheetV2); 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.Character, {
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Companion, { types: ['companion'], makeDefault: true }); types: ['character'],
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Adversary, { types: ['adversary'], makeDefault: true }); 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, { Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Environment, {
types: ['environment'], types: ['environment'],
makeDefault: true makeDefault: true,
label: sheetLabel('TYPES.Actor.environment')
});
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Party, {
types: ['party'],
makeDefault: true,
label: sheetLabel('TYPES.Actor.party')
}); });
CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect;
CONFIG.ActiveEffect.dataModels = models.activeEffects.config;
DocumentSheetConfig.unregisterSheet( DocumentSheetConfig.unregisterSheet(
CONFIG.ActiveEffect.documentClass, CONFIG.ActiveEffect.documentClass,
@ -108,58 +190,56 @@ Hooks.once('init', () => {
SYSTEM.id, SYSTEM.id,
applications.sheetConfigs.ActiveEffectConfig, applications.sheetConfigs.ActiveEffectConfig,
{ {
makeDefault: true makeDefault: true,
label: sheetLabel('DOCUMENT.ActiveEffect')
} }
); );
CONFIG.Token.hudClass = applications.hud.DHTokenHUD;
CONFIG.Combat.dataModels = {
base: models.DhCombat
};
CONFIG.Combatant.dataModels = {
base: models.DhCombatant
};
CONFIG.ChatMessage.dataModels = models.chatMessages.config;
CONFIG.ChatMessage.documentClass = documents.DhChatMessage;
CONFIG.ChatMessage.template = 'systems/daggerheart/templates/ui/chat/chat-message.hbs';
CONFIG.Canvas.rulerClass = placeables.DhRuler;
CONFIG.Canvas.layers.templates.layerClass = placeables.DhTemplateLayer;
CONFIG.Token.objectClass = placeables.DhTokenPlaceable;
CONFIG.Combat.documentClass = documents.DhpCombat;
CONFIG.ui.combat = applications.ui.DhCombatTracker;
CONFIG.ui.chat = applications.ui.DhChatLog;
CONFIG.ui.hotbar = applications.ui.DhHotbar;
CONFIG.Token.rulerClass = placeables.DhTokenRuler;
CONFIG.ui.resources = applications.ui.DhFearTracker;
CONFIG.ux.ContextMenu = applications.ux.DHContextMenu;
CONFIG.ux.TooltipManager = documents.DhTooltipManager;
CONFIG.ux.TemplateManager = new TemplateManager();
game.socket.on(`system.${SYSTEM.id}`, socketRegistration.handleSocketEvent); game.socket.on(`system.${SYSTEM.id}`, socketRegistration.handleSocketEvent);
// Make Compendium Dialog resizable // Make Compendium Dialog resizable
foundry.applications.sidebar.apps.Compendium.DEFAULT_OPTIONS.window.resizable = true; 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: sheetLabel('DOCUMENT.Scene')
});
settingsRegistration.registerDHSettings(); settingsRegistration.registerDHSettings();
RegisterHandlebarsHelpers.registerHelpers(); RegisterHandlebarsHelpers.registerHelpers();
return handlebarsRegistration(); return handlebarsRegistration();
}); });
Hooks.on('ready', async () => { Hooks.on('setup', () => {
ui.resources = new CONFIG.ui.resources(); CONFIG.statusEffects = [
if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).displayFear !== 'hide') ...CONFIG.statusEffects.filter(x => !['dead', 'unconscious'].includes(x.id)),
ui.resources.render({ force: true }); ...Object.values(SYSTEM.GENERAL.conditions()).map(x => ({
...x,
name: game.i18n.localize(x.name),
systemEffect: true
}))
];
});
Hooks.on('ready', async () => {
const appearanceSettings = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance);
ui.resources = new CONFIG.ui.resources();
if (appearanceSettings.displayFear !== 'hide') ui.resources.render({ force: true });
if (appearanceSettings.displayCountdownUI) {
ui.countdowns = new CONFIG.ui.countdowns();
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();
registerCountdownHooks();
socketRegistration.registerSocketHooks(); socketRegistration.registerSocketHooks();
registerRollDiceHooks();
socketRegistration.registerUserQueries(); socketRegistration.registerUserQueries();
if (!game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.welcomeMessage)) { if (!game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.welcomeMessage)) {
@ -175,9 +255,9 @@ Hooks.on('ready', async () => {
Hooks.once('dicesoniceready', () => {}); Hooks.once('dicesoniceready', () => {});
Hooks.on('renderChatMessageHTML', (_, element, message) => { Hooks.on('renderChatMessageHTML', (document, element) => {
enricherRenderSetup(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)); if (cssClass) cssClass.split(' ').forEach(cls => element.classList.add(cls));
}); });
@ -230,73 +310,72 @@ Hooks.on('chatMessage', (_, message) => {
} }
}); });
Hooks.on('renderJournalDirectory', async (tab, html, _, options) => { const updateActorsRangeDependentEffects = async token => {
if (tab.id === 'journal') {
if (options.parts && !options.parts.includes('footer')) return;
const buttons = tab.element.querySelector('.directory-footer.action-buttons');
const title = game.i18n.format('DAGGERHEART.APPLICATIONS.Countdown.title', {
type: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.types.narrative')
});
buttons.insertAdjacentHTML(
'afterbegin',
`
<button id="narrative-countdown-button">
<i class="fa-solid fa-stopwatch"></i>
<span style="font-weight: 400; font-family: var(--font-sans);">${title}</span>
</button>`
);
buttons.querySelector('#narrative-countdown-button').onclick = async () => {
new NarrativeCountdowns().open();
};
}
});
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 rangeMeasurement = game.settings.get( const rangeMeasurement = game.settings.get(
CONFIG.DH.id, CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.variantRules CONFIG.DH.SETTINGS.gameSettings.variantRules
).rangeMeasurement; ).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; const { target, range, type } = effect.system.rangeDependence;
if ((target === 'friendly' && disposition !== 1) || (target === 'hostile' && disposition !== -1))
return false;
const distanceBetween = canvas.grid.measurePath([ // If there are no targets, assume false. Otherwise, start with the effect enabled.
{ ...movedToken.toObject(), x: data.destination.x, y: data.destination.y }, let enabledEffect = game.user.targets.size !== 0;
token // Expect all targets to meet the rangeDependence requirements
]).distance; for (let userTarget of game.user.targets) {
const distance = rangeMeasurement[range]; 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 reverse = type === CONFIG.DH.GENERAL.rangeInclusion.outsideRange.id;
const newDisabled = reverse ? distanceBetween <= distance : distanceBetween > distance; const inRange =
const oldDisabled = effectUpdates[effect.uuid] ? effectUpdates[effect.uuid].disabled : newDisabled; required === 5
effectUpdates[effect.uuid] = { ? userTarget.isAdjacentWith(token.object)
disabled: oldDisabled || newDisabled, : userTarget.distanceTo(token.object) <= required;
value: effect 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 updateAllRangeDependentEffects = async () => {
const effect = effectUpdates[key]; const effectsAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).effects;
await effect.value.update({ disabled: effect.disabled }); if (!effectsAutomation.rangeDependent) return;
const tokens = canvas.scene.tokens;
if (game.user.character) {
// The character updates their character's token. There can be only one token.
const characterToken = tokens.find(x => x.actor === game.user.character);
updateActorsRangeDependentEffects(characterToken);
} else if (game.user.isActiveGM) {
// The GM is responsible for all other tokens.
const playerCharacters = game.users.players.filter(x => x.active).map(x => x.character);
for (const token of tokens.filter(x => !playerCharacters.includes(x.actor))) {
updateActorsRangeDependentEffects(token);
}
}
};
const 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));

View file

@ -20,13 +20,23 @@
"character": "Character", "character": "Character",
"companion": "Companion", "companion": "Companion",
"adversary": "Adversary", "adversary": "Adversary",
"environment": "Environment" "environment": "Environment",
"party": "Party"
} }
}, },
"CONTROLS": { "CONTROLS": {
"inFront": "In Front" "inFront": "In Front"
}, },
"SCENE": {
"TABS": {
"SHEET": {
"dh": "Daggerheart"
}
}
},
"DAGGERHEART": { "DAGGERHEART": {
"CharacterSheet": "Character Sheet",
"ACTIONS": { "ACTIONS": {
"TYPES": { "TYPES": {
"attack": { "attack": {
@ -37,6 +47,10 @@
"name": "Beastform", "name": "Beastform",
"tooltip": "Shapeshift the user into another form." "tooltip": "Shapeshift the user into another form."
}, },
"countdown": {
"name": "Countdown",
"tooltip": "Start a countdown"
},
"damage": { "damage": {
"name": "Damage", "name": "Damage",
"tooltip": "Direct damage without a roll." "tooltip": "Direct damage without a roll."
@ -64,6 +78,18 @@
"exactHint": "The Character's Tier is used if empty", "exactHint": "The Character's Tier is used if empty",
"label": "Beastform" "label": "Beastform"
}, },
"countdown": {
"defaultOwnership": "Default Ownership",
"startCountdown": "Start Countdown"
},
"damage": {
"multiplier": "Multiplier",
"flatMultiplier": "Flat Multiplier"
},
"general": {
"customFormula": "Custom Formula",
"formula": "Formula"
},
"displayInChat": "Display in chat" "displayInChat": "Display in chat"
}, },
"RollField": { "RollField": {
@ -78,8 +104,10 @@
"Settings": { "Settings": {
"attackBonus": "Attack Bonus", "attackBonus": "Attack Bonus",
"attackName": "Attack Name", "attackName": "Attack Name",
"criticalThreshold": "Critical Threshold",
"includeBase": { "label": "Include Item Damage" }, "includeBase": { "label": "Include Item Damage" },
"multiplier": "Multiplier", "multiplier": "Multiplier",
"saveHint": "Set a default Trait to enable Reaction Roll. It can be changed later in Reaction Roll Dialog.",
"resultBased": { "resultBased": {
"label": "Formula based on Hope/Fear result." "label": "Formula based on Hope/Fear result."
}, },
@ -104,7 +132,8 @@
"RangeDependance": { "RangeDependance": {
"hint": "Settings for an optional distance at which this effect should activate", "hint": "Settings for an optional distance at which this effect should activate",
"title": "Range Dependant" "title": "Range Dependant"
} },
"immuneStatusText": "Immunity: {status}"
}, },
"ACTORS": { "ACTORS": {
"Adversary": { "Adversary": {
@ -153,7 +182,9 @@
"hint": "Add single words or short text as reminders and hints of what a character has advantage on." "hint": "Add single words or short text as reminders and hints of what a character has advantage on."
}, },
"age": "Age", "age": "Age",
"backgroundQuestions": "Backgrounds",
"companionFeatures": "Companion Features", "companionFeatures": "Companion Features",
"connections": "Connections",
"contextMenu": { "contextMenu": {
"consume": "Consume Item", "consume": "Consume Item",
"equip": "Equip", "equip": "Equip",
@ -194,8 +225,11 @@
"confirmTitle": "Companion Levelup", "confirmTitle": "Companion Levelup",
"confirmText": "Would you like to level up your companion {name} by {levelChange} levels at this time? (You can do it manually later)" "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", "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?" "InvalidOldCharacterImportText": "Character data exported prior to system version 1.1 will not generate a complete character. Do you wish to continue?",
"cancelBeastform": "Cancel Beastform"
}, },
"Companion": { "Companion": {
"FIELDS": { "FIELDS": {
@ -238,6 +272,9 @@
} }
}, },
"APPLICATIONS": { "APPLICATIONS": {
"Attribution": {
"title": "Attribution"
},
"CharacterCreation": { "CharacterCreation": {
"tabs": { "tabs": {
"ancestry": "Ancestry", "ancestry": "Ancestry",
@ -246,7 +283,8 @@
"experience": "Experience", "experience": "Experience",
"traits": "Traits", "traits": "Traits",
"domainCards": "Domain Cards", "domainCards": "Domain Cards",
"equipment": "Equipment" "equipment": "Equipment",
"story": "Story"
}, },
"ancestryNamePlaceholder": "Your ancestry's name", "ancestryNamePlaceholder": "Your ancestry's name",
"buttonTitle": "Character Setup", "buttonTitle": "Character Setup",
@ -267,6 +305,7 @@
"selectSubclass": "Select Subclass", "selectSubclass": "Select Subclass",
"startingItems": "Starting Items", "startingItems": "Starting Items",
"story": "Story", "story": "Story",
"storyExplanation": "Select which background and connection prompts you want to copy into your character's background.",
"suggestedArmor": "Suggested Armor", "suggestedArmor": "Suggested Armor",
"suggestedPrimaryWeapon": "Suggested Primary Weapon", "suggestedPrimaryWeapon": "Suggested Primary Weapon",
"suggestedSecondaryWeapon": "Suggested Secondary Weapon", "suggestedSecondaryWeapon": "Suggested Secondary Weapon",
@ -288,24 +327,34 @@
"equip": "Equip", "equip": "Equip",
"sendToChat": "Send To Chat", "sendToChat": "Send To Chat",
"toLoadout": "Send to Loadout", "toLoadout": "Send to Loadout",
"recall": "Recall",
"toVault": "Send to Vault", "toVault": "Send to Vault",
"unequip": "Unequip", "unequip": "Unequip",
"useItem": "Use Item" "useItem": "Use Item"
}, },
"Countdown": { "Countdown": {
"addCountdown": "Add Countdown", "addCountdown": "Add Countdown",
"loopingTypes": {
"noLooping": "No Looping",
"looping": "Looping",
"increasing": "Increasing",
"decreasing": "Decreasing"
},
"FIELDS": { "FIELDS": {
"countdowns": { "countdowns": {
"element": { "element": {
"name": { "label": "Name" }, "name": { "label": "Name" },
"progress": { "progress": {
"current": { "label": "Current" }, "current": { "label": "Current" },
"max": { "label": "Max" }, "looping": { "label": "Looping" },
"start": { "label": "Start" },
"startFormula": { "label": "Start Formula" },
"type": { "type": {
"label": { "label": "Label", "hint": "Used for custom" }, "label": { "label": "Label", "hint": "Used for custom" },
"value": { "label": "Value" } "value": { "label": "Value" }
} }
} },
"type": { "label": "Progression Type" }
} }
} }
}, },
@ -320,6 +369,29 @@
"encounter": "Encounter" "encounter": "Encounter"
} }
}, },
"CountdownEdit": {
"title": "Countdown Edit",
"viewTitle": "Countdowns",
"editTitle": "Edit Countdowns",
"newCountdown": "New Countdown",
"removeCountdownTitle": "Remove Countdown",
"removeCountdownText": "Are you sure you want to remove the countdown: {name}?",
"current": "Current Value",
"start": "Start Value",
"startFormula": "Randomized Start Value Formula",
"currentCountdownCurrent": "Current: {value}",
"currentCountdownStart": "Start: {value}",
"category": "Category",
"progressionType": "Progression",
"decreasing": "Decreasing",
"looping": "Looping",
"defaultOwnershipTooltip": "The default player ownership of countdowns",
"hideNewCountdowns": "Hide New Countdowns"
},
"DaggerheartMenu": {
"title": "GM Tools",
"refreshFeatures": "Refresh Features"
},
"DeleteConfirmation": { "DeleteConfirmation": {
"title": "Delete {type} - {name}", "title": "Delete {type} - {name}",
"text": "Are you sure you want to delete {name}?" "text": "Are you sure you want to delete {name}?"
@ -332,7 +404,8 @@
"stressReduction": "Reduce By Stress", "stressReduction": "Reduce By Stress",
"title": "Damage Reduction", "title": "Damage Reduction",
"unncessaryStress": "You don't need to expend stress", "unncessaryStress": "You don't need to expend stress",
"usedMarks": "Used Marks" "usedMarks": "Used Marks",
"reduceSeverity": "Severity Reduced By {nr}"
}, },
"DeathMove": { "DeathMove": {
"selectMove": "Select Move", "selectMove": "Select Move",
@ -391,9 +464,18 @@
}, },
"HUD": { "HUD": {
"tokenHUD": { "tokenHUD": {
"genericEffects": "Foundry Effects" "genericEffects": "Foundry Effects",
"depositPartyTokens": "Deposit Party Tokens",
"retrievePartyTokens": "Retrieve Party Tokens"
} }
}, },
"ImageSelect": {
"title": "Select Image",
"selectImage": "Select Image"
},
"ItemTransfer": {
"transfer": "Transfer"
},
"Levelup": { "Levelup": {
"actions": { "actions": {
"creatureComfort": { "creatureComfort": {
@ -426,6 +508,7 @@
}, },
"navigateLevel": "To Level {level}", "navigateLevel": "To Level {level}",
"navigateToLevelup": "Return To Levelup", "navigateToLevelup": "Return To Levelup",
"finishLevelup": "Finish Levelup",
"navigateToSummary": "To Summary", "navigateToSummary": "To Summary",
"options": { "options": {
"trait": "Gain a +1 bonus to two unmarked character traits and mark them.", "trait": "Gain a +1 bonus to two unmarked character traits and mark them.",
@ -475,24 +558,28 @@
}, },
"takeLevelUp": "Finish Level Up", "takeLevelUp": "Finish Level Up",
"tier2": { "tier2": {
"name": "Tier 2",
"label": "Levels 2-4", "label": "Levels 2-4",
"infoLabel": "At Level 2, gain an additional Experience at +2 and gain a +1 bonus to your Proficiency.", "infoLabel": "At Level 2, gain an additional Experience at +2 and gain a +1 bonus to your Proficiency.",
"pretext": "Choose two options from the list below", "pretext": "Choose two options from the list below",
"posttext": "Take an additional domain card of your level or lower from a domain you have access to." "posttext": "Take an additional domain card of your level or lower from a domain you have access to."
}, },
"tier3": { "tier3": {
"name": "Tier 3",
"label": "Levels 5-7", "label": "Levels 5-7",
"infoLabel": "At Level 5, take an additional Experience and clear all marks on Character Traits.", "infoLabel": "At Level 5, take an additional Experience and clear all marks on Character Traits.",
"pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.", "pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.",
"posttext": "Take an additional domain card of your level or lower from a domain you have access to." "posttext": "Take an additional domain card of your level or lower from a domain you have access to."
}, },
"tier4": { "tier4": {
"name": "Tier 4",
"label": "Levels 8-10", "label": "Levels 8-10",
"infoLabel": "At Level 8, take an additional Experience and clear all marks on Character Traits.", "infoLabel": "At Level 8, take an additional Experience and clear all marks on Character Traits.",
"pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.", "pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.",
"posttext": "Take an additional domain card of your level or lower from a domain you have access to." "posttext": "Take an additional domain card of your level or lower from a domain you have access to."
}, },
"title": "{actor} Level Up" "title": "{actor} Level Up",
"viewModeTitle": "{actor} Level Up (View Mode)"
}, },
"MulticlassChoice": { "MulticlassChoice": {
"title": "Multiclassing - {actor}", "title": "Multiclassing - {actor}",
@ -501,6 +588,7 @@
}, },
"OwnershipSelection": { "OwnershipSelection": {
"title": "Ownership Selection - {name}", "title": "Ownership Selection - {name}",
"noPlayers": "No players to assign ownership to",
"default": "Default Ownership" "default": "Default Ownership"
}, },
"ReactionRoll": { "ReactionRoll": {
@ -514,6 +602,22 @@
"ResourceDice": { "ResourceDice": {
"title": "{name} Resource", "title": "{name} Resource",
"rerollDice": "Reroll Dice" "rerollDice": "Reroll Dice"
},
"TagTeamSelect": {
"title": "Tag Team Roll",
"leaderTitle": "Initiating Character",
"membersTitle": "Participants",
"partyTeam": "Party Team",
"hopeCost": "Hope Cost",
"initiatingCharacter": "Initiating Character",
"linkMessageHint": "Make a roll from your character sheet to link it to the Tag Team Roll",
"damageNotRolled": "Damage not rolled in chat message yet",
"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": { "CLASS": {
@ -523,11 +627,6 @@
} }
}, },
"CONFIG": { "CONFIG": {
"ActionType": {
"passive": "Passive",
"action": "Action",
"reaction": "Reaction"
},
"AdversaryTrait": { "AdversaryTrait": {
"relentless": { "relentless": {
"name": "Relentless", "name": "Relentless",
@ -587,6 +686,14 @@
"description": "Enemies that enhance their allies and/or disrupt their opponents." "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": { "ArmorFeature": {
"burning": { "burning": {
"name": "Burning", "name": "Burning",
@ -810,6 +917,30 @@
"evolved": "Evolved", "evolved": "Evolved",
"hybrid": "Hybrid" "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": { "Burden": {
"oneHanded": "One-Handed", "oneHanded": "One-Handed",
"twoHanded": "Two-Handed" "twoHanded": "Two-Handed"
@ -845,9 +976,12 @@
} }
}, },
"CountdownType": { "CountdownType": {
"spotlight": "Spotlight", "actionRoll": "Action Roll",
"characterAttack": "Character Attack",
"characterSpotlight": "Character Spotlight",
"custom": "Custom", "custom": "Custom",
"characterAttack": "Character Attack" "fear": "Fear",
"spotlight": "Spotlight"
}, },
"DamageType": { "DamageType": {
"physical": { "physical": {
@ -857,6 +991,11 @@
"magical": { "magical": {
"name": "Magical", "name": "Magical",
"abbreviation": "Mag" "abbreviation": "Mag"
},
"direct": {
"name": "Direct Damage",
"short": "Direct",
"abbreviation": "Dir"
} }
}, },
"DeathMoves": { "DeathMoves": {
@ -896,6 +1035,12 @@
"description": "" "description": ""
} }
}, },
"FeatureForm": {
"label": "Feature Form",
"passive": "Passive",
"action": "Action",
"reaction": "Reaction"
},
"Gold": { "Gold": {
"title": "Gold", "title": "Gold",
"coins": "Coins", "coins": "Coins",
@ -906,23 +1051,28 @@
"HealingType": { "HealingType": {
"hitPoints": { "hitPoints": {
"name": "Hit Points", "name": "Hit Points",
"abbreviation": "HP" "abbreviation": "HP",
"inChatRoll": "Damage"
}, },
"stress": { "stress": {
"name": "Stress", "name": "Stress",
"abbreviation": "STR" "abbreviation": "STR",
"inChatRoll": "Stress"
}, },
"hope": { "hope": {
"name": "Hope", "name": "Hope",
"abbreviation": "HO" "abbreviation": "HO",
"inChatRoll": "Hope"
}, },
"armor": { "armor": {
"name": "Armor Slot", "name": "Armor Slot",
"abbreviation": "AS" "abbreviation": "AS",
"inChatRoll": "Armor Slot"
}, },
"fear": { "fear": {
"name": "Fear", "name": "Fear",
"abbreviation": "FR" "abbreviation": "FR",
"inChatRoll": "Fear"
} }
}, },
"ItemResourceProgression": { "ItemResourceProgression": {
@ -931,7 +1081,8 @@
}, },
"ItemResourceType": { "ItemResourceType": {
"simple": "Simple", "simple": "Simple",
"diceValue": "Dice Value" "diceValue": "Dice Value",
"die": "Die"
}, },
"Range": { "Range": {
"self": { "self": {
@ -989,6 +1140,12 @@
"selectType": "Select Action Type", "selectType": "Select Action Type",
"selectAction": "Action Selection" "selectAction": "Action Selection"
}, },
"TargetTypes": {
"any": "Any",
"friendly": "Friendly",
"hostile": "Hostile",
"self": "Self"
},
"TemplateTypes": { "TemplateTypes": {
"circle": "Circle", "circle": "Circle",
"cone": "Cone", "cone": "Cone",
@ -997,6 +1154,14 @@
"rect": "Rectangle", "rect": "Rectangle",
"ray": "Ray" "ray": "Ray"
}, },
"TokenSize": {
"tiny": "Tiny",
"small": "Small",
"medium": "Medium",
"large": "Large",
"huge": "Huge",
"gargantuan": "Gargantuan"
},
"Traits": { "Traits": {
"agility": { "agility": {
"name": "Agility", "name": "Agility",
@ -1106,7 +1271,7 @@
}, },
"burning": { "burning": {
"name": "Burning", "name": "Burning",
"description": "When you roll the maximum value on a damage die, roll an additional damage die.", "description": "When you roll a 6 on a damage die, the target must mark a Stress.",
"actions": { "actions": {
"burn": { "burn": {
"name": "Burn", "name": "Burn",
@ -1637,7 +1802,9 @@
"label": "Long Rest: Bonus Long Rest Moves", "label": "Long Rest: Bonus Long Rest Moves",
"hint": "The number of extra Long Rest Moves the character can take during a Long Rest." "hint": "The number of extra Long Rest Moves the character can take during a Long Rest."
} }
} },
"target": "Target",
"targetSelf": "Self"
}, },
"maxLoadout": { "maxLoadout": {
"label": "Max Loadout Cards Bonus" "label": "Max Loadout Cards Bonus"
@ -1652,6 +1819,7 @@
"plural": "Costs" "plural": "Costs"
}, },
"Damage": { "Damage": {
"massive": "Massive",
"severe": "Severe", "severe": "Severe",
"major": "Major", "major": "Major",
"minor": "Minor", "minor": "Minor",
@ -1838,6 +2006,10 @@
} }
} }
}, },
"SpotlightRequests": {
"singular": "Spotlight Request",
"plural": "Spotlight Requests"
},
"Tabs": { "Tabs": {
"details": "Details", "details": "Details",
"attack": "Attack", "attack": "Attack",
@ -1862,6 +2034,7 @@
"story": "Story", "story": "Story",
"biography": "Biography", "biography": "Biography",
"general": "General", "general": "General",
"resources": "Resources",
"foundation": "Foundation", "foundation": "Foundation",
"specialization": "Specialization", "specialization": "Specialization",
"mastery": "Mastery", "mastery": "Mastery",
@ -1877,7 +2050,15 @@
"tier4": "tier 4", "tier4": "tier 4",
"domains": "Domains", "domains": "Domains",
"downtime": "Downtime", "downtime": "Downtime",
"rules": "Rules" "roll": "Roll",
"rules": "Rules",
"partyMembers": "Party Members",
"projects": "Projects",
"types": "Types",
"itemFeatures": "Item Features",
"questions": "Questions",
"configuration": "Configuration",
"base": "Base"
}, },
"Tiers": { "Tiers": {
"singular": "Tier", "singular": "Tier",
@ -1894,17 +2075,21 @@
"amount": "Amount", "amount": "Amount",
"any": "Any", "any": "Any",
"armor": "Armor", "armor": "Armor",
"armorFeatures": "Armor Features",
"armors": "Armors", "armors": "Armors",
"armorScore": "Armor Score", "armorScore": "Armor Score",
"activeEffects": "Active Effects", "activeEffects": "Active Effects",
"armorSlots": "Armor Slots", "armorSlots": "Armor Slots",
"artistAttribution": "Artwork By: {artist}",
"attack": "Attack", "attack": "Attack",
"basics": "Basics", "basics": "Basics",
"bonus": "Bonus", "bonus": "Bonus",
"burden": "Burden", "burden": "Burden",
"condition": "Condition",
"continue": "Continue", "continue": "Continue",
"criticalSuccess": "Critical Success", "criticalSuccess": "Critical Success",
"criticalShort": "Critical", "criticalShort": "Critical",
"custom": "Custom",
"d20Roll": "D20 Roll", "d20Roll": "D20 Roll",
"damage": "Damage", "damage": "Damage",
"damageRoll": "Damage Roll", "damageRoll": "Damage Roll",
@ -1927,6 +2112,8 @@
"fear": "Fear", "fear": "Fear",
"features": "Features", "features": "Features",
"formula": "Formula", "formula": "Formula",
"general": "General",
"gm": "GM",
"healing": "Healing", "healing": "Healing",
"healingRoll": "Healing Roll", "healingRoll": "Healing Roll",
"hit": { "hit": {
@ -1940,11 +2127,13 @@
}, },
"hope": "Hope", "hope": "Hope",
"hordeHp": "Horde HP", "hordeHp": "Horde HP",
"icon": "Icon",
"identify": "Identity", "identify": "Identity",
"imagePath": "Image Path", "imagePath": "Image Path",
"inactiveEffects": "Inactive Effects", "inactiveEffects": "Inactive Effects",
"inventory": "Inventory", "inventory": "Inventory",
"itemResource": "Item Resource", "itemResource": "Item Resource",
"itemQuantity": "Item Quantity",
"items": "Items", "items": "Items",
"label": "Label", "label": "Label",
"level": "Level", "level": "Level",
@ -1960,15 +2149,22 @@
"missingDragDropThing": "Drop {thing} here", "missingDragDropThing": "Drop {thing} here",
"multiclass": "Multiclass", "multiclass": "Multiclass",
"newCategory": "New Category", "newCategory": "New Category",
"newThing": "New {thing}",
"none": "None", "none": "None",
"noTarget": "No current target", "noTarget": "No current target",
"partner": "Partner", "partner": "Partner",
"player": {
"single": "Player",
"plurial": "Players"
},
"proficiency": "Proficiency", "proficiency": "Proficiency",
"quantity": "Quantity", "quantity": "Quantity",
"range": "Range", "range": "Range",
"reactionRoll": "Reaction Roll", "reactionRoll": "Reaction Roll",
"recovery": "Recovery", "recovery": "Recovery",
"refresh": "Refresh",
"reroll": "Reroll", "reroll": "Reroll",
"rerolled": "Rerolled",
"rerollThing": "Reroll {thing}", "rerollThing": "Reroll {thing}",
"resource": "Resource", "resource": "Resource",
"roll": "Roll", "roll": "Roll",
@ -1978,6 +2174,8 @@
"save": "Save", "save": "Save",
"scalable": "Scalable", "scalable": "Scalable",
"situationalBonus": "Situational Bonus", "situationalBonus": "Situational Bonus",
"spent": "Spent",
"step": "Step",
"stress": "Stress", "stress": "Stress",
"subclasses": "Subclasses", "subclasses": "Subclasses",
"success": "Success", "success": "Success",
@ -1987,9 +2185,12 @@
"plural": "Targets" "plural": "Targets"
}, },
"title": "Title", "title": "Title",
"tokenSize": "Token Size",
"total": "Total", "total": "Total",
"traitModifier": "Trait Modifier",
"true": "True", "true": "True",
"type": "Type", "type": "Type",
"typeSheet": "System {type} Sheet",
"unarmed": "Unarmed", "unarmed": "Unarmed",
"unarmedAttack": "Unarmed Attack", "unarmedAttack": "Unarmed Attack",
"unarmored": "Unarmored", "unarmored": "Unarmored",
@ -1997,11 +2198,17 @@
"used": "Used", "used": "Used",
"uses": "Uses", "uses": "Uses",
"value": "Value", "value": "Value",
"weaponFeatures": "Weapon Features",
"weapons": "Weapons", "weapons": "Weapons",
"withThing": "With {thing}" "withThing": "With {thing}"
}, },
"ITEMS": { "ITEMS": {
"FIELDS": { "FIELDS": {
"attribution": {
"source": { "label": "Source" },
"page": { "label": "Page" },
"artist": { "label": "Artist" }
},
"resource": { "resource": {
"amount": { "label": "Amount" }, "amount": { "label": "Amount" },
"dieFaces": { "label": "Die Faces" }, "dieFaces": { "label": "Die Faces" },
@ -2036,6 +2243,7 @@
"tokenRingImg": { "label": "Subject Texture" }, "tokenRingImg": { "label": "Subject Texture" },
"tokenSize": { "tokenSize": {
"placeholder": "Using character dimensions", "placeholder": "Using character dimensions",
"disabledPlaceholder": "Set by character size",
"height": { "label": "Height" }, "height": { "label": "Height" },
"width": { "label": "Width" } "width": { "label": "Width" }
}, },
@ -2059,7 +2267,11 @@
"evolvedDrag": "Drag a form here to evolve it.", "evolvedDrag": "Drag a form here to evolve it.",
"hybridize": "Hybridize", "hybridize": "Hybridize",
"hybridizeFeatureTitle": "Hybrid Features", "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": { "Class": {
"hopeFeatures": "Hope Features", "hopeFeatures": "Hope Features",
@ -2076,7 +2288,8 @@
} }
}, },
"Consumable": { "Consumable": {
"consumeOnUse": "Consume On Use" "consumeOnUse": "Consume On Use",
"destroyOnEmpty": "Destroy On Empty"
}, },
"DomainCard": { "DomainCard": {
"type": "Type", "type": "Type",
@ -2097,14 +2310,47 @@
"SETTINGS": { "SETTINGS": {
"Appearance": { "Appearance": {
"FIELDS": { "FIELDS": {
"displayFear": { "label": "Fear Display" }, "displayFear": {
"dualityColorScheme": { "label": "Chat Style" }, "label": "Display Fear"
"showGenericStatusEffects": { "label": "Show Foundry Status Effects" }, },
"displayCountdownUI": {
"label": "Display Countdown UI"
},
"showGenericStatusEffects": {
"label": "Show Foundry Status Effects"
},
"hideAttribution": {
"label": "Hide Attribution"
},
"expandedTitle": "Auto-expand Descriptions", "expandedTitle": "Auto-expand Descriptions",
"extendCharacterDescriptions": { "label": "Characters" }, "extendCharacterDescriptions": {
"extendAdversaryDescriptions": { "label": "Adversaries" }, "label": "Characters"
"extendEnvironmentDescriptions": { "label": "Environments" }, },
"extendItemDescriptions": { "label": "Items" } "extendAdversaryDescriptions": {
"label": "Adversaries"
},
"extendEnvironmentDescriptions": {
"label": "Environments"
},
"extendItemDescriptions": {
"label": "Items"
},
"expandRollMessage": {
"title": "Auto-expand Message Sections",
"desc": {
"label": "Description"
},
"roll": {
"label": "Formula"
},
"damage": {
"label": "Damage/Healing"
},
"target": {
"label": "Target"
}
},
"useResourcePips": { "label": "Pip Display For Resources" }
}, },
"fearDisplay": { "fearDisplay": {
"token": "Tokens", "token": "Tokens",
@ -2134,6 +2380,10 @@
"gm": { "label": "GM" }, "gm": { "label": "GM" },
"players": { "label": "Players" } "players": { "label": "Players" }
}, },
"countdownAutomation": {
"label": "Countdown Automation",
"hint": "Automatically progress countdowns based on their progression settings"
},
"levelupAuto": { "levelupAuto": {
"label": "Levelup Automation", "label": "Levelup Automation",
"hint": "When you've made your choices and finish levelup, the numerical changes are automatically applied to your character." "hint": "When you've made your choices and finish levelup, the numerical changes are automatically applied to your character."
@ -2159,15 +2409,48 @@
"playerCanEditSheet": { "playerCanEditSheet": {
"label": "Players Can Manually Edit Character Settings", "label": "Players Can Manually Edit Character Settings",
"hint": "Players are allowed to access the manual Character Settings and change their statistics beyond the rules." "hint": "Players are allowed to access the manual Character Settings and change their statistics beyond the rules."
},
"roll": {
"roll": {
"label": "Roll",
"hint": "Auto behavior for rolls like Attack, Spellcast, etc."
},
"damage": {
"label": "Damage/Healing Roll",
"hint": "Auto behavior for Damage & Healing rolls after the Attack/Spellcast."
},
"save": {
"label": "Reaction Roll",
"hint": "Auto behavior if a Reaction Roll is needed. Targets must be selected before the action is made"
},
"damageApply": {
"label": "Apply Damage/Healing",
"hint": "Automatically apply damages & healings. Targets must be selected before the action is made and Reaction Roll Automation must be different than Never. Bypass users permissions."
},
"effect": {
"label": "Apply Effects",
"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."
}
},
"summaryMessages": {
"label": "Summary Messages"
} }
}, },
"defeated": { "defeated": {
"title": "Defeated Handling" "title": "Defeated Handling"
},
"roll": {
"title": "Actions"
} }
}, },
"Homebrew": { "Homebrew": {
"newDowntimeMove": "Downtime Move", "newDowntimeMove": "Downtime Move",
"downtimeMove": "Downtime Move",
"armorFeature": "Armor Feature",
"weaponFeature": "Weapon Feature",
"newFeature": "New Item Feature",
"downtimeMoves": "Downtime Moves", "downtimeMoves": "Downtime Moves",
"itemFeatures": "Item Features",
"nrChoices": "# Moves Per Rest", "nrChoices": "# Moves Per Rest",
"resetMovesTitle": "Reset {type} Downtime Moves", "resetMovesTitle": "Reset {type} Downtime Moves",
"resetMovesText": "Are you sure you want to reset?", "resetMovesText": "Are you sure you want to reset?",
@ -2181,11 +2464,13 @@
"maxDomains": { "label": "Max Class Domains", "hint": "Max domains you can set on a class" } "maxDomains": { "label": "Max Class Domains", "hint": "Max domains you can set on a class" }
}, },
"currency": { "currency": {
"enabled": "Enable Overrides",
"title": "Currency Overrides", "title": "Currency Overrides",
"changeIcon": "Change Currency Icon",
"currencyName": "Currency Name", "currencyName": "Currency Name",
"coinName": "Coin Name", "coinName": "Coin Name",
"handfulName": "Handful Name", "handfulName": "Handful Name",
"iconName": "Icon Name",
"iconNameHint": "Icons are from fontawesome",
"bagName": "Bag Name", "bagName": "Bag Name",
"chestName": "Chest Name" "chestName": "Chest Name"
}, },
@ -2200,6 +2485,10 @@
"deleteDomain": "Delete Domain", "deleteDomain": "Delete Domain",
"deleteDomainText": "Are you sure you want to delete the {name} domain? It will be immediately removed from all Actors in this world where it's currently used. Compendiums are not cleared.", "deleteDomainText": "Are you sure you want to delete the {name} domain? It will be immediately removed from all Actors in this world where it's currently used. Compendiums are not cleared.",
"duplicateDomain": "There is already a domain with this identification." "duplicateDomain": "There is already a domain with this identification."
},
"adversaryType": {
"title": "Custom Adversary Types",
"newType": "Adversary Type"
} }
}, },
"Menu": { "Menu": {
@ -2220,10 +2509,8 @@
"hint": "System ruler setup for displaying ranges in Daggerheart" "hint": "System ruler setup for displaying ranges in Daggerheart"
}, },
"appearance": { "appearance": {
"title": "Appearance Settings",
"label": "Appearance Settings", "label": "Appearance Settings",
"hint": "Modify the look of various parts of the system", "hint": "Modify the look of various parts of the system",
"name": "Appearance Settings",
"duality": "Duality Rolls", "duality": "Duality Rolls",
"diceSoNice": { "diceSoNice": {
"title": "Dice So Nice", "title": "Dice So Nice",
@ -2235,7 +2522,8 @@
"texture": "Texture", "texture": "Texture",
"colorset": "Theme", "colorset": "Theme",
"material": "Material", "material": "Material",
"system": "Dice Preset" "system": "Dice Preset",
"font": "Font"
} }
}, },
"variantRules": { "variantRules": {
@ -2244,6 +2532,11 @@
"hint": "Apply variant rules from the Daggerheart system", "hint": "Apply variant rules from the Daggerheart system",
"name": "Variant Rules", "name": "Variant Rules",
"actionTokens": "Action Tokens" "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": { "Resources": {
@ -2257,12 +2550,25 @@
"actionTokens": { "actionTokens": {
"enabled": { "label": "Enabled" }, "enabled": { "label": "Enabled" },
"tokens": { "label": "Tokens" } "tokens": { "label": "Tokens" }
},
"massiveDamage": {
"title": "Massive Damage",
"enabled": { "label": "Enabled" }
} }
} }
}, },
"ResetSettings": { "ResetSettings": {
"resetConfirmationTitle": "Reset Settings", "resetConfirmationTitle": "Reset Settings",
"resetConfirmationText": "Are you sure you want to reset the {settings}?" "resetConfirmationText": "Are you sure you want to reset the {settings}?"
},
"Scene": {
"FIELDS": {
"rangeMeasurement": {
"setting": { "label": "Setting" }
}
},
"disabledText": "Daggerheart Measurements are disabled in System Settings - Variant Rules",
"rangeMeasurement": "Range Measurement"
} }
}, },
"UI": { "UI": {
@ -2270,6 +2576,7 @@
"action": { "action": {
"title": "Action" "title": "Action"
}, },
"appliedTo": "Applied To",
"applyEffect": { "applyEffect": {
"title": "Apply Effects - {name}" "title": "Apply Effects - {name}"
}, },
@ -2279,6 +2586,11 @@
"rollHealing": "Roll Healing", "rollHealing": "Roll Healing",
"applyEffect": "Apply Effects" "applyEffect": "Apply Effects"
}, },
"clearResource": "Clear {quantity} {resource}",
"damageSummary": {
"title": "Damage Applied",
"healingTitle": "Healing Applied"
},
"damageRoll": { "damageRoll": {
"title": "Damage - {damage}", "title": "Damage - {damage}",
"dealDamageToTargets": "Damage Hit Targets", "dealDamageToTargets": "Damage Hit Targets",
@ -2300,18 +2612,101 @@
"dualityRoll": { "dualityRoll": {
"abilityCheckTitle": "{ability} Check" "abilityCheckTitle": "{ability} Check"
}, },
"effectSummary": {
"title": "Effects Applied",
"immunityTo": "Immunity: {immunities}"
},
"featureTitle": "Class Feature", "featureTitle": "Class Feature",
"groupRoll": {
"title": "Group Roll",
"leader": "Leader",
"partyTeam": "Party Team",
"team": "Team",
"selectLeader": "Select a Leader",
"selectMember": "Select a Member",
"rerollTitle": "Reroll Group Roll",
"rerollContent": "Are you sure you want to reroll your {trait} check?",
"rerollTooltip": "Reroll",
"wholePartySelected": "The whole party is selected"
},
"healingRoll": { "healingRoll": {
"title": "Heal - {damage}", "title": "Heal - {damage}",
"heal": "Heal", "heal": "Heal",
"applyHealing": "Apply Healing" "applyHealing": "Apply Healing"
}, },
"markResource": "Mark {quantity} {resource}",
"refreshMessage": {
"title": "Feature Refresh",
"header": "Refreshed"
},
"reroll": { "reroll": {
"confirmTitle": "Reroll Dice", "confirmTitle": "Reroll Dice",
"confirmText": "Are you sure you want to reroll?" "confirmText": "Are you sure you want to reroll?"
}, },
"resourceRoll": { "resourceRoll": {
"playerMessage": "{user} rerolled their {name}" "playerMessage": "{user} rerolled their {name}"
},
"tagTeam": {
"title": "Tag Team",
"membersTitle": "Members"
}
},
"ChatLog": {
"rerollDamage": "Reroll Damage",
"assignTagRoll": "Assign as Tag Roll"
},
"Countdowns": {
"title": "Countdowns",
"toggleIconMode": "Toggle Icon Only",
"noPlayerAccess": "This countdown isn't visible to any players",
"loop": "Looping",
"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",
"searchPlaceholder": "Search...",
"columnName": "Name",
"tooltipFilters": "Filters",
"tooltipErase": "Erase",
"difficultyMin": "Difficulty (Min)",
"difficultyMax": "Difficulty (Max)",
"hitPointsMin": "Hit Points (Min)",
"hitPointsMax": "Hit Points (Max)",
"stressMin": "Stress (Min)",
"stressMax": "Stress (Max)",
"armorScoreMin": "Armor Score (Min)",
"armorScoreMax": "Armor Score (Max)",
"levelMin": "Level (Min)",
"levelMax": "Level (Max)",
"recallCostMin": "Recall Cost (Min)",
"recallCostMax": "Recall Cost (Max)",
"evasionMin": "Evasion (Min)",
"evasionMax": "Evasion (Max)",
"subtype": "Subtype",
"missing": "<i>Missing</i>",
"folders": {
"characters": "Characters",
"adversaries": "Adversaries",
"ancestries": "Ancestries",
"equipment": "Equipment",
"classes": "Classes",
"subclasses": "Subclasses",
"domainCards": "Domain Cards",
"communities": "Communities",
"environments": "Environments",
"beastforms": "Beastforms",
"features": "Features",
"items": "Items",
"weapons": "Weapons",
"armors": "Armors",
"consumables": "Consumables",
"loots": "Loots"
} }
}, },
"Notifications": { "Notifications": {
@ -2319,6 +2714,7 @@
"beastformInapplicable": "A beastform can only be applied to a Character.", "beastformInapplicable": "A beastform can only be applied to a Character.",
"beastformAlreadyApplied": "The character already has a beastform applied!", "beastformAlreadyApplied": "The character already has a beastform applied!",
"noTargetsSelected": "No targets are selected.", "noTargetsSelected": "No targets are selected.",
"noTargetsSelectedOrPerm": "No targets are selected or with the update permission.",
"attackTargetDoesNotExist": "The target token no longer exists", "attackTargetDoesNotExist": "The target token no longer exists",
"insufficentAdvancements": "You don't have enough advancements left.", "insufficentAdvancements": "You don't have enough advancements left.",
"noAssignedPlayerCharacter": "You have no assigned character.", "noAssignedPlayerCharacter": "You have no assigned character.",
@ -2346,6 +2742,8 @@
"wrongDomain": "The card isn't from one of your class domains.", "wrongDomain": "The card isn't from one of your class domains.",
"cardTooHighLevel": "The card is too high level!", "cardTooHighLevel": "The card is too high level!",
"duplicateCard": "You cannot select the same card more than once.", "duplicateCard": "You cannot select the same card more than once.",
"duplicateCharacter": "This actor is already registered in the party members list.",
"onlyCharactersInPartySheet": "You can only drag characters, companions and adversaries to the party sheet.",
"notPrimary": "The weapon is not a primary weapon!", "notPrimary": "The weapon is not a primary weapon!",
"notSecondary": "The weapon is not a secondary weapon!", "notSecondary": "The weapon is not a secondary weapon!",
"itemTooHighTier": "The item must be from Tier1", "itemTooHighTier": "The item must be from Tier1",
@ -2373,11 +2771,32 @@
"beastformEquipWeapon": "You cannot use weapons while in a Beastform.", "beastformEquipWeapon": "You cannot use weapons while in a Beastform.",
"loadoutMaxReached": "You've reached maximum loadout. Move atleast one domain card to the vault, or increase the limit in homebrew settings if desired.", "loadoutMaxReached": "You've reached maximum loadout. Move atleast one domain card to the vault, or increase the limit in homebrew settings if desired.",
"domainMaxReached": "You've reached the maximum domains for the class. Increase the limit in homebrew settings if desired.", "domainMaxReached": "You've reached the maximum domains for the class. Increase the limit in homebrew settings if desired.",
"insufficientResources": "You have insufficient resources", "insufficientResources": "You don't have enough resources to use that action.",
"actionNoUsesRemaining": "That action doesn't have remaining uses.",
"multiclassAlreadyPresent": "You already have a class and multiclass", "multiclassAlreadyPresent": "You already have a class and multiclass",
"subclassesAlreadyPresent": "You already have a class and multiclass subclass", "subclassesAlreadyPresent": "You already have a class and multiclass subclass",
"noDiceSystem": "Your selected dice {system} does not have a {faces} dice", "noDiceSystem": "Your selected dice {system} does not have a {faces} dice",
"subclassAlreadyLinked": "{name} is already a subclass in the class {class}. Remove it from there if you want it to be a subclass to this class." "gmMenuRefresh": "You refreshed all actions and resources {types}",
"subclassAlreadyLinked": "{name} is already a subclass in the class {class}. Remove it from there if you want it to be a subclass to this class.",
"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.",
"tokenActorMissing": "{name} is missing an Actor",
"tokenActorsMissing": "[{names}] missing Actors"
},
"Sidebar": {
"actorDirectory": {
"tier": "Tier {tier} {type}",
"character": "Level {level} Character",
"companion": "Level {level} - {partner}",
"companionNoPartner": "No Partner"
},
"daggerheartMenu": {
"title": "Daggerheart Menu",
"startSession": "Start Session",
"startScene": "Start Scene"
}
}, },
"Tooltip": { "Tooltip": {
"disableEffect": "Disable Effect", "disableEffect": "Disable Effect",
@ -2404,7 +2823,12 @@
"rulesOff": "Rules Off", "rulesOff": "Rules Off",
"remainingUses": "Uses refresh on {type}", "remainingUses": "Uses refresh on {type}",
"rightClickExtand": "Right-Click to extand", "rightClickExtand": "Right-Click to extand",
"companionPartnerLevelBlock": "The companion needs an assigned partner to level up." "companionPartnerLevelBlock": "The companion needs an assigned partner to level up.",
"configureAttribution": "Configure Attribution",
"deleteItem": "Delete Item",
"immune": "Immune",
"middleClick": "[Middle Click] Keep tooltip view",
"tokenSize": "The token size used on the canvas"
} }
} }
} }

View file

@ -2,8 +2,10 @@ export * as characterCreation from './characterCreation/_module.mjs';
export * as dialogs from './dialogs/_module.mjs'; export * as dialogs from './dialogs/_module.mjs';
export * as hud from './hud/_module.mjs'; export * as hud from './hud/_module.mjs';
export * as levelup from './levelup/_module.mjs'; export * as levelup from './levelup/_module.mjs';
export * as scene from './scene/_module.mjs';
export * as settings from './settings/_module.mjs'; export * as settings from './settings/_module.mjs';
export * as sheets from './sheets/_module.mjs'; export * as sheets from './sheets/_module.mjs';
export * as sheetConfigs from './sheets-configs/_module.mjs'; export * as sheetConfigs from './sheets-configs/_module.mjs';
export * as sidebar from './sidebar/_module.mjs';
export * as ui from './ui/_module.mjs'; export * as ui from './ui/_module.mjs';
export * as ux from './ux/_module.mjs'; export * as ux from './ux/_module.mjs';

View file

@ -1,6 +1,5 @@
import { abilities } from '../../config/actorConfig.mjs'; import { abilities } from '../../config/actorConfig.mjs';
import { burden } from '../../config/generalConfig.mjs'; import { burden } from '../../config/generalConfig.mjs';
import { ItemBrowser } from '../ui/itemBrowser.mjs';
import { createEmbeddedItemsWithEffects, createEmbeddedItemWithEffects } from '../../helpers/utils.mjs'; import { createEmbeddedItemsWithEffects, createEmbeddedItemWithEffects } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -46,8 +45,6 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
}; };
this._dragDrop = this._createDragDropHandlers(); this._dragDrop = this._createDragDropHandlers();
this.itemBrowser = null;
} }
get title() { get title() {
@ -86,9 +83,9 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
static PARTS = { static PARTS = {
tabs: { template: 'systems/daggerheart/templates/characterCreation/tabs.hbs' }, tabs: { template: 'systems/daggerheart/templates/characterCreation/tabs.hbs' },
class: { template: 'systems/daggerheart/templates/characterCreation/tabs/class.hbs' },
ancestry: { template: 'systems/daggerheart/templates/characterCreation/tabs/ancestry.hbs' }, ancestry: { template: 'systems/daggerheart/templates/characterCreation/tabs/ancestry.hbs' },
community: { template: 'systems/daggerheart/templates/characterCreation/tabs/community.hbs' }, community: { template: 'systems/daggerheart/templates/characterCreation/tabs/community.hbs' },
class: { template: 'systems/daggerheart/templates/characterCreation/tabs/class.hbs' },
traits: { template: 'systems/daggerheart/templates/characterCreation/tabs/traits.hbs' }, traits: { template: 'systems/daggerheart/templates/characterCreation/tabs/traits.hbs' },
experience: { template: 'systems/daggerheart/templates/characterCreation/tabs/experience.hbs' }, experience: { template: 'systems/daggerheart/templates/characterCreation/tabs/experience.hbs' },
domainCards: { template: 'systems/daggerheart/templates/characterCreation/tabs/domainCards.hbs' }, domainCards: { template: 'systems/daggerheart/templates/characterCreation/tabs/domainCards.hbs' },
@ -98,6 +95,13 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
}; };
static TABS = { static TABS = {
class: {
active: false,
cssClass: '',
group: 'setup',
id: 'class',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.class'
},
ancestry: { ancestry: {
active: true, active: true,
cssClass: '', cssClass: '',
@ -112,13 +116,6 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
id: 'community', id: 'community',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.community' label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.community'
}, },
class: {
active: false,
cssClass: '',
group: 'setup',
id: 'class',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.class'
},
traits: { traits: {
active: false, active: false,
cssClass: '', cssClass: '',
@ -159,10 +156,10 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
v.cssClass = v.active ? 'active' : ''; v.cssClass = v.active ? 'active' : '';
switch (v.id) { switch (v.id) {
case 'community': case 'ancestry':
v.disabled = this.setup.visibility < 2; v.disabled = this.setup.visibility < 2;
break; break;
case 'class': case 'community':
v.disabled = this.setup.visibility < 3; v.disabled = this.setup.visibility < 3;
break; break;
case 'traits': case 'traits':
@ -195,7 +192,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
} }
async _prepareContext(_options) { async _prepareContext(_options) {
this.tabGroups.setup = this.tabGroups.setup ?? 'ancestry'; this.tabGroups.setup = this.tabGroups.setup ?? 'class';
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.tabs = this._getTabs(this.constructor.TABS); context.tabs = this._getTabs(this.constructor.TABS);
@ -269,13 +266,13 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
context.isLastTab = this.tabGroups.setup === 'equipment'; context.isLastTab = this.tabGroups.setup === 'equipment';
switch (this.tabGroups.setup) { switch (this.tabGroups.setup) {
case null: case null:
case 'ancestry': case 'class':
context.nextDisabled = this.setup.visibility === 1; context.nextDisabled = this.setup.visibility === 1;
break; break;
case 'community': case 'ancestry':
context.nextDisabled = this.setup.visibility === 2; context.nextDisabled = this.setup.visibility === 2;
break; break;
case 'class': case 'community':
context.nextDisabled = this.setup.visibility === 3; context.nextDisabled = this.setup.visibility === 3;
break; break;
case 'traits': case 'traits':
@ -366,11 +363,11 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
case 4: case 4:
return this.getNrSelectedTrait() === 6 ? 5 : 4; return this.getNrSelectedTrait() === 6 ? 5 : 4;
case 3: case 3:
return this.setup.class.uuid && this.setup.subclass.uuid ? 4 : 3; return this.setup.community.uuid ? 4 : 3;
case 2: case 2:
return this.setup.community.uuid ? 3 : 2; return this.setup.primaryAncestry.uuid ? 3 : 2;
case 1: case 1:
return this.setup.primaryAncestry.uuid ? 2 : 1; return this.setup.class.uuid && this.setup.subclass.uuid ? 2 : 1;
} }
} }
@ -425,8 +422,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
equipment = ['armor', 'weapon']; equipment = ['armor', 'weapon'];
const presets = { const presets = {
compendium: 'daggerheart', folder: equipment.includes(type) ? `equipments.folders.${type}s` : type,
folder: equipment.includes(type) ? 'equipments' : type,
render: { render: {
noFolder: true noFolder: true
} }
@ -449,7 +445,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
'type': { key: 'type', value: type } 'type': { key: 'type', value: type }
}; };
return (this.itemBrowser = await new ItemBrowser({ presets }).render({ force: true })); ui.compendiumBrowser.open(presets);
} }
static async viewItem(_, target) { static async viewItem(_, target) {
@ -477,10 +473,10 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
static setupGoNext() { static setupGoNext() {
switch (this.setup.visibility) { switch (this.setup.visibility) {
case 2: case 2:
this.tabGroups.setup = 'community'; this.tabGroups.setup = 'ancestry';
break; break;
case 3: case 3:
this.tabGroups.setup = 'class'; this.tabGroups.setup = 'community';
break; break;
case 4: case 4:
this.tabGroups.setup = 'traits'; this.tabGroups.setup = 'traits';
@ -567,7 +563,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
{ overwrite: true } { overwrite: true }
); );
if (this.itemBrowser) this.itemBrowser.close(); if (ui.compendiumBrowser) ui.compendiumBrowser.close();
this.close(); this.close();
} }

View file

@ -1,11 +1,16 @@
export { default as AttributionDialog } from './attributionDialog.mjs';
export { default as BeastformDialog } from './beastformDialog.mjs'; export { default as BeastformDialog } from './beastformDialog.mjs';
export { default as d20RollDialog } from './d20RollDialog.mjs'; export { default as d20RollDialog } from './d20RollDialog.mjs';
export { default as DamageDialog } from './damageDialog.mjs'; export { default as DamageDialog } from './damageDialog.mjs';
export { default as DamageReductionDialog } from './damageReductionDialog.mjs'; export { default as DamageReductionDialog } from './damageReductionDialog.mjs';
export { default as DeathMove } from './deathMove.mjs'; export { default as DeathMove } from './deathMove.mjs';
export { default as Downtime } from './downtime.mjs'; export { default as Downtime } from './downtime.mjs';
export { default as ImageSelectDialog } from './imageSelectDialog.mjs';
export { default as ItemTransferDialog } from './itemTransfer.mjs';
export { default as MulticlassChoiceDialog } from './multiclassChoiceDialog.mjs'; export { default as MulticlassChoiceDialog } from './multiclassChoiceDialog.mjs';
export { default as OwnershipSelection } from './ownershipSelection.mjs'; export { default as OwnershipSelection } from './ownershipSelection.mjs';
export { default as RerollDamageDialog } from './rerollDamageDialog.mjs'; export { default as RerollDamageDialog } from './rerollDamageDialog.mjs';
export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs'; export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs';
export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs'; export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs';
export { default as GroupRollDialog } from './group-roll-dialog.mjs';
export { default as TagTeamDialog } from './tagTeamDialog.mjs';

View file

@ -57,7 +57,11 @@ export default class ActionSelectionDialog extends HandlebarsApplicationMixin(Ap
/** @inheritDoc */ /** @inheritDoc */
async _prepareContext(options) { async _prepareContext(options) {
const actions = this.#item.system.actionsList, const actions = this.#item.system.actionsList.map(action => ({
...action.toObject(),
id: action.id,
img: action.baseAction ? action.parent.parent.img : action.img
})),
itemName = this.#item.name; itemName = this.#item.name;
return { return {
...(await super._prepareContext(options)), ...(await super._prepareContext(options)),

View file

@ -0,0 +1,93 @@
import autocomplete from 'autocompleter';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class AttributionDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(item) {
super({});
this.item = item;
this.sources = Object.keys(CONFIG.DH.GENERAL.attributionSources).flatMap(groupKey => {
const group = CONFIG.DH.GENERAL.attributionSources[groupKey];
return group.values.map(x => ({ group: group.label, ...x }));
});
}
get title() {
return game.i18n.localize('DAGGERHEART.APPLICATIONS.Attribution.title');
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'dh-style', 'dialog', 'views', 'attribution'],
position: { width: 'auto', height: 'auto' },
window: { icon: 'fa-solid fa-signature' },
form: { handler: this.updateData, submitOnChange: false, closeOnSubmit: true }
};
static PARTS = {
main: { template: 'systems/daggerheart/templates/dialogs/attribution.hbs' }
};
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
const sources = this.sources;
htmlElement.querySelectorAll('.attribution-input').forEach(element => {
autocomplete({
input: element,
fetch: function (text, update) {
if (!text) {
update(sources);
} else {
text = text.toLowerCase();
var suggestions = sources.filter(n => n.label.toLowerCase().includes(text));
update(suggestions);
}
},
render: function (item, search) {
const label = game.i18n.localize(item.label);
const matchIndex = label.toLowerCase().indexOf(search);
const beforeText = label.slice(0, matchIndex);
const matchText = label.slice(matchIndex, matchIndex + search.length);
const after = label.slice(matchIndex + search.length, label.length);
const element = document.createElement('li');
element.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
if (item.hint) {
element.dataset.tooltip = game.i18n.localize(item.hint);
}
return element;
},
renderGroup: function (label) {
const itemElement = document.createElement('div');
itemElement.textContent = game.i18n.localize(label);
return itemElement;
},
onSelect: function (item) {
element.value = item.label;
},
click: e => e.fetch(),
customize: function (_input, _inputRect, container) {
container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ;
},
minLength: 0
});
});
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.item = this.item;
context.data = this.item.system.attribution;
return context;
}
static async updateData(_event, _element, formData) {
await this.item.update({ 'system.attribution': formData.object });
this.item.sheet.refreshFrame();
}
}

View file

@ -276,7 +276,29 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
const featureItem = item; const featureItem = item;
app.addEventListener( app.addEventListener(
'close', 'close',
() => resolve({ selected: app.selected, evolved: app.evolved, hybrid: app.hybrid, item: featureItem }), 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,
evolved ?? app.selected
);
if (data) {
if (!data.selectedImage) selected = null;
else {
const imageSource = evolved ?? selected;
if (imageSource.usesDynamicToken) imageSource.system.tokenRingImg = data.selectedImage;
else imageSource.system.tokenImg = data.selectedImage;
}
}
resolve({
selected: selected,
evolved: { ...app.evolved, form: evolved },
hybrid: app.hybrid,
item: featureItem
});
},
{ once: true } { once: true }
); );
app.render({ force: true }); app.render({ force: true });

View file

@ -1,3 +1,5 @@
import { abilities } from '../../config/actorConfig.mjs';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class D20RollDialog extends HandlebarsApplicationMixin(ApplicationV2) { export default class D20RollDialog extends HandlebarsApplicationMixin(ApplicationV2) {
@ -7,20 +9,20 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.roll = roll; this.roll = roll;
this.config = config; this.config = config;
this.config.experiences = []; this.config.experiences = [];
this.reactionOverride = config.roll?.type === 'reaction'; this.reactionOverride = config.actionType === 'reaction';
if (config.source?.action) { if (config.source?.action) {
this.item = config.data.parent.items.get(config.source.item) ?? config.data.parent; this.item = config.data.parent.items.get(config.source.item) ?? config.data.parent;
this.action = this.action =
config.data.attack?._id == config.source.action config.data.attack?._id == config.source.action
? config.data.attack ? config.data.attack
: this.item.system.actions.get(config.source.action); : this.item.system.actionsList?.find(a => a.id === config.source.action);
} }
} }
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
id: 'roll-selection', // id: 'roll-selection',
classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'roll-selection'], classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'roll-selection'],
position: { position: {
width: 'auto' width: 'auto'
@ -32,6 +34,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
updateIsAdvantage: this.updateIsAdvantage, updateIsAdvantage: this.updateIsAdvantage,
selectExperience: this.selectExperience, selectExperience: this.selectExperience,
toggleReaction: this.toggleReaction, toggleReaction: this.toggleReaction,
toggleTagTeamRoll: this.toggleTagTeamRoll,
submitRoll: this.submitRoll submitRoll: this.submitRoll
}, },
form: { form: {
@ -42,7 +45,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
}; };
get title() { get title() {
return this.config.title; return `${this.config.title}${this.actor ? `: ${this.actor.name}` : ''}`;
} }
get actor() { get actor() {
@ -66,7 +69,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.rollConfig = this.config; context.rollConfig = this.config;
context.hasRoll = !!this.config.roll; context.hasRoll = !!this.config.roll;
context.canRoll = true; context.canRoll = true;
context.selectedRollMode = this.config.selectedRollMode; context.selectedRollMode = this.config.selectedRollMode ?? game.settings.get('core', 'rollMode');
context.rollModes = Object.entries(CONFIG.Dice.rollModes).map(([action, { label, icon }]) => ({ context.rollModes = Object.entries(CONFIG.Dice.rollModes).map(([action, { label, icon }]) => ({
action, action,
label, label,
@ -81,7 +84,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
); );
context.costs = updatedCosts.map(x => ({ context.costs = updatedCosts.map(x => ({
...x, ...x,
label: x.keyIsID label: x.itemId
? this.action.parent.parent.name ? this.action.parent.parent.name
: game.i18n.localize(CONFIG.DH.GENERAL.abilityCosts[x.key].label) : game.i18n.localize(CONFIG.DH.GENERAL.abilityCosts[x.key].label)
})); }));
@ -101,7 +104,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.roll = this.roll; context.roll = this.roll;
context.rollType = this.roll?.constructor.name; context.rollType = this.roll?.constructor.name;
context.rallyDie = this.roll.rallyChoices; 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 => ({ context.experiences = Object.keys(experiences).map(id => ({
id, id,
...experiences[id] ...experiences[id]
@ -113,15 +116,31 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.isLite = this.config.roll?.lite; context.isLite = this.config.roll?.lite;
context.extraFormula = this.config.extraFormula; context.extraFormula = this.config.extraFormula;
context.formula = this.roll.constructFormula(this.config); context.formula = this.roll.constructFormula(this.config);
if (this.actor?.system?.traits) context.abilities = this.getTraitModifiers();
context.showReaction = !context.rollConfig.type && context.rollType === 'DualityRoll'; context.showReaction = !this.config.roll?.type && context.rollType === 'DualityRoll';
context.reactionOverride = this.reactionOverride; context.reactionOverride = this.reactionOverride;
} }
const tagTeamSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
if (this.actor && tagTeamSetting.members[this.actor.id] && !this.config.skips?.createMessage) {
context.activeTagTeamRoll = true;
context.tagTeamSelected = this.config.tagTeamSelected;
}
return context; return context;
} }
getTraitModifiers() {
return Object.values(abilities).map(a => ({
id: a.id,
label: `${game.i18n.localize(a.label)} (${this.actor.system.traits[a.id]?.value.signedString() ?? 0})`
}));
}
static updateRollConfiguration(event, _, formData) { static updateRollConfiguration(event, _, formData) {
const { ...rest } = foundry.utils.expandObject(formData.object); const { ...rest } = foundry.utils.expandObject(formData.object);
this.config.selectedRollMode = rest.selectedRollMode; this.config.selectedRollMode = rest.selectedRollMode;
if (this.config.costs) { if (this.config.costs) {
@ -133,6 +152,12 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.roll[key] = value; this.roll[key] = value;
}); });
} }
if (rest.hasOwnProperty('trait')) {
this.config.roll.trait = rest.trait;
this.config.title = game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(abilities[this.config.roll.trait]?.label)
});
}
this.config.extraFormula = rest.extraFormula; this.config.extraFormula = rest.extraFormula;
this.render(); this.render();
} }
@ -151,7 +176,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.config.experiences.indexOf(button.dataset.key) > -1 this.config.experiences.indexOf(button.dataset.key) > -1
? this.config.experiences.filter(x => x !== button.dataset.key) ? this.config.experiences.filter(x => x !== button.dataset.key)
: [...this.config.experiences, button.dataset.key]; : [...this.config.experiences, button.dataset.key];
if (this.config?.data?.parent?.type === 'character' || this.config?.data?.parent?.type === 'companion') {
this.config.costs = this.config.costs =
this.config.costs.indexOf(this.config.costs.find(c => c.extKey === button.dataset.key)) > -1 this.config.costs.indexOf(this.config.costs.find(c => c.extKey === button.dataset.key)) > -1
? this.config.costs.filter(x => x.extKey !== button.dataset.key) ? this.config.costs.filter(x => x.extKey !== button.dataset.key)
@ -159,27 +183,31 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
...this.config.costs, ...this.config.costs,
{ {
extKey: button.dataset.key, extKey: button.dataset.key,
key: 'hope', key: this.config?.data?.parent?.isNPC ? 'fear' : 'hope',
value: 1, value: 1,
name: this.config.data?.experiences?.[button.dataset.key]?.name name: this.config.data?.system.experiences?.[button.dataset.key]?.name
} }
]; ];
}
this.render(); this.render();
} }
static toggleReaction() { static toggleReaction() {
if (this.config.roll) { if (this.config.roll) {
this.reactionOverride = !this.reactionOverride; this.reactionOverride = !this.reactionOverride;
this.config.roll.type = this.reactionOverride this.config.actionType = this.reactionOverride
? CONFIG.DH.ITEM.actionTypes.reaction.id ? 'reaction'
: this.config.roll.type === CONFIG.DH.ITEM.actionTypes.reaction.id : this.config.actionType === 'reaction'
? null ? 'action'
: this.config.roll.type; : this.config.actionType;
this.render(); this.render();
} }
} }
static toggleTagTeamRoll() {
this.config.tagTeamSelected = !this.config.tagTeamSelected;
this.render();
}
static async submitRoll() { static async submitRoll() {
await this.close({ submitted: true }); await this.close({ submitted: true });
} }

View file

@ -10,6 +10,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.reject = reject; this.reject = reject;
this.actor = actor; this.actor = actor;
this.damage = damage; this.damage = damage;
this.damageType = damageType;
this.rulesDefault = game.settings.get( this.rulesDefault = game.settings.get(
CONFIG.DH.id, CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Automation CONFIG.DH.SETTINGS.gameSettings.Automation
@ -57,6 +58,11 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
null null
); );
this.reduceSeverity = this.damageType.reduce((value, curr) => {
return Math.max(this.actor.system.rules.damageReduction.reduceSeverity[curr], value);
}, 0);
this.actor.system.rules.damageReduction.reduceSeverity[this.damageType];
this.thresholdImmunities = Object.keys(actor.system.rules.damageReduction.thresholdImmunities).reduce( this.thresholdImmunities = Object.keys(actor.system.rules.damageReduction.thresholdImmunities).reduce(
(acc, key) => { (acc, key) => {
if (actor.system.rules.damageReduction.thresholdImmunities[key]) if (actor.system.rules.damageReduction.thresholdImmunities[key])
@ -111,7 +117,9 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
CONFIG.DH.GENERAL.ruleChoice.onWithToggle.id, CONFIG.DH.GENERAL.ruleChoice.onWithToggle.id,
CONFIG.DH.GENERAL.ruleChoice.offWithToggle.id CONFIG.DH.GENERAL.ruleChoice.offWithToggle.id
].includes(this.rulesDefault); ].includes(this.rulesDefault);
context.thresholdImmunities = this.thresholdImmunities; context.reduceSeverity = this.reduceSeverity;
context.thresholdImmunities =
Object.keys(this.thresholdImmunities).length > 0 ? this.thresholdImmunities : null;
const { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage } = const { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage } =
this.getDamageInfo(); this.getDamageInfo();
@ -173,6 +181,9 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.damage - armorMarkReduction - selectedStressMarks.length - stressReductions.length, this.damage - armorMarkReduction - selectedStressMarks.length - stressReductions.length,
0 0
); );
if (this.reduceSeverity) {
currentDamage = Math.max(currentDamage - this.reduceSeverity, 0);
}
if (this.thresholdImmunities[currentDamage]) currentDamage = 0; if (this.thresholdImmunities[currentDamage]) currentDamage = 0;

View file

@ -1,3 +1,5 @@
import { refreshIsAllowed } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV2) { export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV2) {
@ -91,14 +93,10 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
} }
getRefreshables() { 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) { if (x.system.actions) {
const recoverable = x.system.actions.reduce((acc, action) => { const recoverable = x.system.actions.reduce((acc, action) => {
if ( if (refreshIsAllowed([this.shortrest ? 'shortRest' : 'longRest'], action.uses.recovery)) {
action.uses.recovery &&
((action.uses.recovery === 'longRest' && !this.shortrest) ||
action.uses.recovery === 'shortRest')
) {
acc.push({ acc.push({
title: x.name, title: x.name,
name: action.name, name: action.name,
@ -120,8 +118,7 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
if ( if (
x.system.resource && x.system.resource &&
x.system.resource.type && x.system.resource.type &&
((x.system.resource.recovery === 'longRest') === !this.shortrest || refreshIsAllowed([this.shortrest ? 'shortRest' : 'longRest'], x.system.resource.recovery)
x.system.resource.recovery === 'shortRest')
) { ) {
acc.push({ acc.push({
title: game.i18n.localize(`TYPES.Item.${x.type}`), title: game.i18n.localize(`TYPES.Item.${x.type}`),
@ -178,11 +175,23 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
} }
static async takeDowntime() { static async takeDowntime() {
const moves = Object.values(this.moveData).flatMap(category => { const moves = Object.keys(this.moveData).flatMap(categoryKey => {
return Object.values(category.moves) const category = this.moveData[categoryKey];
.filter(x => x.selected) return Object.keys(category.moves)
.flatMap(move => [...Array(move.selected).keys()].map(_ => move)); .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}`,
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 cls = getDocumentClass('ChatMessage');
const msg = { const msg = {
@ -202,7 +211,9 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
`DAGGERHEART.APPLICATIONS.Downtime.${this.shortrest ? 'shortRest' : 'longRest'}.title` `DAGGERHEART.APPLICATIONS.Downtime.${this.shortrest ? 'shortRest' : 'longRest'}.title`
), ),
actor: { name: this.actor.name, img: this.actor.img }, actor: { name: this.actor.name, img: this.actor.img },
moves: moves moves: moves,
characters: characters,
selfId: this.actor.uuid
} }
), ),
flags: { flags: {

View file

@ -0,0 +1,196 @@
import autocomplete from 'autocompleter';
import { abilities } from '../../config/actorConfig.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class GroupRollDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(actors) {
super();
this.actors = actors;
this.actorLeader = {};
this.actorsMembers = [];
}
get title() {
return 'Group Roll';
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'group-roll'],
position: { width: 'auto', height: 'auto' },
window: {
title: 'DAGGERHEART.UI.Chat.groupRoll.title'
},
actions: {
roll: GroupRollDialog.#roll,
removeLeader: GroupRollDialog.#removeLeader,
removeMember: GroupRollDialog.#removeMember
},
form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false }
};
static PARTS = {
application: {
id: 'group-roll',
template: 'systems/daggerheart/templates/dialogs/group-roll/group-roll.hbs'
}
};
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
const leaderChoices = this.actors.filter(x => this.actorsMembers.every(member => member.actor?.id !== x.id));
const memberChoices = this.actors.filter(
x => this.actorLeader?.actor?.id !== x.id && this.actorsMembers.every(member => member.actor?.id !== x.id)
);
htmlElement.querySelectorAll('.leader-change-input').forEach(element => {
autocomplete({
input: element,
fetch: function (text, update) {
if (!text) {
update(leaderChoices);
} else {
text = text.toLowerCase();
var suggestions = leaderChoices.filter(n => n.name.toLowerCase().includes(text));
update(suggestions);
}
},
render: function (actor, search) {
const actorName = game.i18n.localize(actor.name);
const matchIndex = actorName.toLowerCase().indexOf(search);
const beforeText = actorName.slice(0, matchIndex);
const matchText = actorName.slice(matchIndex, matchIndex + search.length);
const after = actorName.slice(matchIndex + search.length, actorName.length);
const img = document.createElement('img');
img.src = actor.img;
const element = document.createElement('li');
element.appendChild(img);
const label = document.createElement('span');
label.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
element.appendChild(label);
return element;
},
renderGroup: function (label) {
const itemElement = document.createElement('div');
itemElement.textContent = game.i18n.localize(label);
return itemElement;
},
onSelect: actor => {
element.value = actor.uuid;
this.actorLeader = { actor: actor, trait: 'agility', difficulty: 0 };
this.render();
},
click: e => e.fetch(),
customize: function (_input, _inputRect, container) {
container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ;
},
minLength: 0
});
});
htmlElement.querySelectorAll('.team-push-input').forEach(element => {
autocomplete({
input: element,
fetch: function (text, update) {
if (!text) {
update(memberChoices);
} else {
text = text.toLowerCase();
var suggestions = memberChoices.filter(n => n.name.toLowerCase().includes(text));
update(suggestions);
}
},
render: function (actor, search) {
const actorName = game.i18n.localize(actor.name);
const matchIndex = actorName.toLowerCase().indexOf(search);
const beforeText = actorName.slice(0, matchIndex);
const matchText = actorName.slice(matchIndex, matchIndex + search.length);
const after = actorName.slice(matchIndex + search.length, actorName.length);
const img = document.createElement('img');
img.src = actor.img;
const element = document.createElement('li');
element.appendChild(img);
const label = document.createElement('span');
label.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
element.appendChild(label);
return element;
},
renderGroup: function (label) {
const itemElement = document.createElement('div');
itemElement.textContent = game.i18n.localize(label);
return itemElement;
},
onSelect: actor => {
element.value = actor.uuid;
this.actorsMembers.push({ actor: actor, trait: 'agility', difficulty: 0 });
this.render({ force: true });
},
click: e => e.fetch(),
customize: function (_input, _inputRect, container) {
container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ;
},
minLength: 0
});
});
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.leader = this.actorLeader;
context.members = this.actorsMembers;
context.traitList = abilities;
context.allSelected = this.actorsMembers.length + (this.actorLeader?.actor ? 1 : 0) === this.actors.length;
context.rollDisabled = context.members.length === 0 || !this.actorLeader?.actor;
return context;
}
static updateData(event, _, formData) {
const { actorLeader, actorsMembers } = foundry.utils.expandObject(formData.object);
this.actorLeader = foundry.utils.mergeObject(this.actorLeader, actorLeader);
this.actorsMembers = foundry.utils.mergeObject(this.actorsMembers, actorsMembers);
this.render(true);
}
static async #removeLeader(_, button) {
this.actorLeader = null;
this.render();
}
static async #removeMember(_, button) {
this.actorsMembers = this.actorsMembers.filter(m => m.actor.uuid !== button.dataset.memberUuid);
this.render();
}
static async #roll() {
const cls = getDocumentClass('ChatMessage');
const systemData = {
leader: this.actorLeader,
members: this.actorsMembers
};
const msg = {
type: 'groupRoll',
user: game.user.id,
speaker: cls.getSpeaker(),
title: game.i18n.localize('DAGGERHEART.UI.Chat.groupRoll.title'),
system: systemData,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/groupRoll.hbs',
{ system: systemData }
)
};
cls.create(msg);
this.close();
}
}

View file

@ -0,0 +1,70 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class ImageSelectDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(titleName, images) {
super();
this.titleName = titleName;
this.images = images;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'dialog', 'dh-style', 'image-select'],
position: {
width: 612,
height: 'auto'
},
window: {
icon: 'fa-solid fa-paw'
},
actions: {
selectImage: ImageSelectDialog.#selectImage,
finishSelection: ImageSelectDialog.#finishSelection
}
};
get title() {
return this.titleName;
}
/** @override */
static PARTS = {
main: {
template: 'systems/daggerheart/templates/dialogs/image-select/main.hbs',
scrollable: ['.images-container']
},
footer: { template: 'systems/daggerheart/templates/dialogs/image-select/footer.hbs' }
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.images = this.images;
context.selectedImage = this.selectedImage;
return context;
}
static #selectImage(_event, button) {
this.selectedImage = button.dataset.image ?? button.querySelector('img').dataset.image;
this.render();
}
static #finishSelection() {
this.close({ submitted: true });
}
async close(options = {}) {
if (!options.submitted) this.selectedImage = null;
await super.close();
}
static async configure(title, images) {
return new Promise(resolve => {
const app = new this(title, images);
app.addEventListener('close', () => resolve(app.selectedImage), { once: true });
app.render({ force: true });
});
}
}

View file

@ -0,0 +1,62 @@
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class ItemTransferDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(data) {
super({});
this.data = data;
}
get title() {
return this.data.title;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'dh-style', 'dialog', 'item-transfer'],
position: { width: 400, height: 'auto' },
window: { icon: 'fa-solid fa-hand-holding-hand' },
actions: {
finish: ItemTransferDialog.#finish
}
};
static PARTS = {
main: { template: 'systems/daggerheart/templates/dialogs/item-transfer.hbs', root: true }
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
return foundry.utils.mergeObject(context, this.data);
}
static async #finish() {
this.selected = this.form.elements.quantity.valueAsNumber || null;
this.close();
}
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;
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(options) {
return new Promise(resolve => {
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

@ -1,18 +1,20 @@
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class OwnershipSelection extends HandlebarsApplicationMixin(ApplicationV2) { export default class OwnershipSelection extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(resolve, reject, name, ownership) { constructor(name, ownership, defaultOwnership) {
super({}); super({});
this.resolve = resolve;
this.reject = reject;
this.name = name; this.name = name;
this.ownership = ownership; this.ownership = foundry.utils.deepClone(ownership);
this.defaultOwnership = defaultOwnership;
} }
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
classes: ['daggerheart', 'views', 'ownership-selection'], classes: ['daggerheart', 'views', 'dialog', 'dh-style', 'ownership-selection'],
window: {
icon: 'fa-solid fa-users'
},
position: { position: {
width: 600, width: 600,
height: 'auto' height: 'auto'
@ -30,43 +32,48 @@ export default class OwnershipSelection extends HandlebarsApplicationMixin(Appli
return game.i18n.format('DAGGERHEART.APPLICATIONS.OwnershipSelection.title', { name: this.name }); return game.i18n.format('DAGGERHEART.APPLICATIONS.OwnershipSelection.title', { name: this.name });
} }
getOwnershipData(id) {
return this.ownership[id] ?? CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT;
}
async _prepareContext(_options) { async _prepareContext(_options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.ownershipOptions = Object.keys(CONST.DOCUMENT_OWNERSHIP_LEVELS).map(level => ({ context.ownershipOptions = CONFIG.DH.GENERAL.simpleOwnershiplevels;
value: CONST.DOCUMENT_OWNERSHIP_LEVELS[level], context.defaultOwnership = this.defaultOwnership;
label: game.i18n.localize(`OWNERSHIP.${level}`) context.ownership = game.users.reduce((acc, user) => {
}));
context.ownership = {
default: this.ownership.default,
players: Object.keys(this.ownership.players).reduce((acc, x) => {
const user = game.users.get(x);
if (!user.isGM) { if (!user.isGM) {
acc[x] = { acc[user.id] = {
...user,
img: user.character?.img ?? 'icons/svg/cowled.svg', img: user.character?.img ?? 'icons/svg/cowled.svg',
name: user.name, ownership: this.getOwnershipData(user.id)
ownership: this.ownership.players[x].value
}; };
} }
return acc; return acc;
}, {}) }, {});
}; context.showOwnership = Boolean(Object.keys(context.ownership).length);
return context; return context;
} }
static async updateData(event, _, formData) { static async updateData(event, _, formData) {
const { ownership } = foundry.utils.expandObject(formData.object); const data = foundry.utils.expandObject(formData.object);
this.close(data);
this.resolve(ownership);
this.close(true);
} }
async close(fromSave) { async close(data) {
if (!fromSave) { if (data) {
this.reject(); this.saveData = data;
} }
await super.close(); await super.close();
} }
static async configure(name, ownership, defaultOwnership) {
return new Promise(resolve => {
const app = new this(name, ownership, defaultOwnership);
app.addEventListener('close', () => resolve(app.saveData), { once: true });
app.render({ force: true });
});
}
} }

View file

@ -1,3 +1,5 @@
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) { export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) {
@ -122,6 +124,15 @@ export default class RerollDamageDialog extends HandlebarsApplicationMixin(Appli
}, {}) }, {})
}; };
await this.message.update(update); await this.message.update(update);
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
});
await this.close(); await this.close();
} }

View file

@ -0,0 +1,347 @@
import { getCritDamageBonus } from '../../helpers/utils.mjs';
import { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class TagTeamDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(party) {
super();
this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
this.party = party;
this.setupHooks = Hooks.on(socketEvent.Refresh, ({ refreshType }) => {
if (refreshType === RefreshType.TagTeamRoll) {
this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
this.render();
}
});
}
get title() {
return game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.title');
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'tag-team-dialog'],
position: { width: 550, height: 'auto' },
actions: {
removeMember: TagTeamDialog.#removeMember,
unlinkMessage: TagTeamDialog.#unlinkMessage,
selectMessage: TagTeamDialog.#selectMessage,
createTagTeam: TagTeamDialog.#createTagTeam
},
form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false }
};
static PARTS = {
application: {
id: 'tag-team-dialog',
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog.hbs'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.hopeCost = this.hopeCost;
context.data = this.data;
context.memberOptions = this.party.filter(c => !this.data.members[c.id]);
context.selectedCharacterOptions = this.party.filter(c => this.data.members[c.id]);
context.members = Object.keys(this.data.members).map(id => {
const roll = this.data.members[id].messageId ? game.messages.get(this.data.members[id].messageId) : null;
context.usesDamage =
context.usesDamage === undefined
? roll?.system.hasDamage
: context.usesDamage && roll?.system.hasDamage;
return {
character: this.party.find(x => x.id === id),
selected: this.data.members[id].selected,
roll: roll,
damageValues: roll
? Object.keys(roll.system.damage).map(key => ({
key: key,
name: game.i18n.localize(CONFIG.DH.GENERAL.healingTypes[key].label),
total: roll.system.damage[key].total
}))
: null
};
});
const initiatorChar = this.party.find(x => x.id === this.data.initiator.id);
context.initiator = {
character: initiatorChar,
cost: this.data.initiator.cost
};
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 (!context.selectedData.damageValues) context.selectedData.damageValues = {};
for (let damage of member.damageValues) {
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 {
context.selectedData.damageValues[damage.key] = {
...foundry.utils.deepClone(damage),
total: damageTotal
};
}
}
}
}
context.showResult = Object.values(context.members).reduce((enabled, member) => {
if (!member.roll) return enabled;
if (context.usesDamage) {
enabled = enabled === null ? member.damageValues.length > 0 : enabled && member.damageValues.length > 0;
} else {
enabled = enabled === null ? Boolean(member.roll) : enabled && Boolean(member.roll);
}
return enabled;
}, null);
context.createDisabled =
!context.selectedData.result ||
!this.data.initiator.id ||
Object.keys(this.data.members).length === 0 ||
Object.values(context.members).some(x =>
context.usesDamage ? !x.damageValues || x.damageValues.length === 0 : !x.roll
);
return context;
}
async updateSource(update) {
await this.data.updateSource(update);
if (game.user.isGM) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, this.data.toObject());
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
});
} else {
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateSetting,
uuid: CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll,
update: this.data.toObject(),
refresh: { refreshType: RefreshType.TagTeamRoll }
}
});
}
}
static async updateData(_event, _element, formData) {
const { selectedAddMember, initiator } = foundry.utils.expandObject(formData.object);
const update = { initiator: initiator };
if (selectedAddMember) {
const member = await foundry.utils.fromUuid(selectedAddMember);
update[`members.${member.id}`] = { messageId: null };
}
await this.updateSource(update);
this.render();
}
static async #removeMember(_, button) {
const update = { [`members.-=${button.dataset.characterId}`]: null };
if (this.data.initiator.id === button.dataset.characterId) {
update.iniator = { id: null };
}
await this.updateSource(update);
}
static async #unlinkMessage(_, button) {
await this.updateSource({ [`members.${button.id}.messageId`]: null });
}
static async #selectMessage(_, button) {
const member = this.data.members[button.id];
const currentSelected = Object.keys(this.data.members).find(key => this.data.members[key].selected);
const curretSelectedUpdate =
currentSelected && currentSelected !== button.id ? { [`${currentSelected}`]: { selected: false } } : {};
await this.updateSource({
members: {
[`${button.id}`]: { selected: !member.selected },
...curretSelectedUpdate
}
});
}
static async #createTagTeam() {
const mainRollId = Object.keys(this.data.members).find(key => this.data.members[key].selected);
const mainRoll = game.messages.get(this.data.members[mainRollId].messageId);
if (this.data.initiator.cost) {
const initiator = this.party.find(x => x.id === this.data.initiator.id);
if (initiator.system.resources.hope.value < this.data.initiator.cost) {
return ui.notifications.warn(
game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.insufficientHope')
);
}
}
const secondaryRolls = Object.keys(this.data.members)
.filter(key => key !== mainRollId)
.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]) {
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.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',
user: game.user.id,
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.title'),
speaker: cls.getSpeaker({ actor: this.party.find(x => x.id === mainRollId) }),
system: systemData,
rolls: mainRoll.rolls,
sound: null,
flags: { core: { RollTable: true } }
};
await cls.create(msgData);
const fearUpdate = { key: 'fear', value: null, total: null, enabled: true };
for (let memberId of Object.keys(this.data.members)) {
const resourceUpdates = [];
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) {
fearUpdate.value = fearUpdate.value === null ? 1 : fearUpdate.value + 1;
fearUpdate.total = fearUpdate.total === null ? -1 : fearUpdate.total - 1;
}
this.party.find(x => x.id === memberId).modifyResource(resourceUpdates);
}
if (fearUpdate.value) {
this.party.find(x => x.id === mainRollId).modifyResource([fearUpdate]);
}
/* Improve by fetching default from schema */
const update = { members: [], initiator: { id: null, cost: 3 } };
if (game.user.isGM) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, update);
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
});
} else {
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateSetting,
uuid: CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll,
update: update,
refresh: { refreshType: RefreshType.TagTeamRoll }
}
});
}
}
static async assignRoll(char, message) {
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
const character = settings.members[char.id];
if (!character) return;
await settings.updateSource({ [`members.${char.id}.messageId`]: message.id });
if (game.user.isGM) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, settings);
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
});
} else {
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateSetting,
uuid: CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll,
update: settings,
refresh: { refreshType: RefreshType.TagTeamRoll }
}
});
}
}
async close(options = {}) {
Hooks.off(socketEvent.Refresh, this.setupHooks);
await super.close(options);
}
}

View file

@ -1,6 +1,12 @@
import { shuffleArray } from '../../helpers/utils.mjs';
export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['daggerheart'] classes: ['daggerheart'],
actions: {
combat: DHTokenHUD.#onToggleCombat,
togglePartyTokens: DHTokenHUD.#togglePartyTokens
}
}; };
/** @override */ /** @override */
@ -11,11 +17,28 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
} }
}; };
static #nonCombatTypes = ['environment', 'companion', 'party'];
async _prepareContext(options) { async _prepareContext(options) {
const context = await super._prepareContext(options); const context = await super._prepareContext(options);
if (!this.actor) return context;
context.partyOnCanvas =
this.actor.type === 'party' &&
this.actor.system.partyMembers.some(member => member.getActiveTokens().length > 0);
context.icons.toggleParty = 'systems/daggerheart/assets/icons/arrow-dunk.png';
context.actorType = this.actor.type;
context.usesEffects = this.actor.type !== 'party';
context.canToggleCombat = DHTokenHUD.#nonCombatTypes.includes(this.actor.type)
? false
: context.canToggleCombat;
context.systemStatusEffects = Object.keys(context.statusEffects).reduce((acc, key) => { context.systemStatusEffects = Object.keys(context.statusEffects).reduce((acc, key) => {
const effect = context.statusEffects[key]; const effect = context.statusEffects[key];
if (effect.systemEffect) acc[key] = effect; if (effect.systemEffect) {
const disabled = !effect.isActive && this.actor.system.rules?.conditionImmunities?.[key];
acc[key] = { ...effect, disabled };
}
return acc; return acc;
}, {}); }, {});
@ -36,6 +59,138 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
return context; return context;
} }
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))
.map(t => t.document);
if (!this.object.controlled && this.document.actor) tokens.push(this.document);
try {
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);
}
}
static async #togglePartyTokens(_, button) {
const icon = button.querySelector('img');
icon.classList.toggle('flipped');
button.dataset.tooltip = game.i18n.localize(
icon.classList.contains('flipped')
? 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.retrievePartyTokens'
: 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.depositPartyTokens'
);
const animationDuration = 500;
const activeTokens = this.actor.system.partyMembers.flatMap(member => member.getActiveTokens());
const { x: actorX, y: actorY } = this.document;
if (activeTokens.length > 0) {
for (let token of activeTokens) {
await token.document.update(
{ x: actorX, y: actorY, alpha: 0 },
{ animation: { duration: animationDuration } }
);
setTimeout(() => token.document.delete(), animationDuration);
}
} else {
const activeScene = game.scenes.find(x => x.id === game.user.viewedScene);
const partyTokenData = [];
for (let member of this.actor.system.partyMembers) {
const data = await member.getTokenDocument();
partyTokenData.push(data.toObject());
}
const newTokens = await activeScene.createEmbeddedDocuments(
'Token',
partyTokenData.map(tokenData => ({
...tokenData,
alpha: 0,
x: actorX,
y: actorY
}))
);
const { sizeX, sizeY } = activeScene.grid;
const nrRandomPositions = Math.ceil(newTokens.length / 8) * 8;
/* This is an overcomplicated mess, but I'm stupid */
const positions = shuffleArray(
[...Array(nrRandomPositions).keys()].map((_, index) => {
const nonZeroIndex = index + 1;
const indexFloor = Math.floor(index / 8);
const distanceCoefficient = indexFloor + 1;
const side = 3 + indexFloor * 2;
const sideMiddle = Math.ceil(side / 2);
const inbetween = 1 + indexFloor * 2;
const inbetweenMiddle = Math.ceil(inbetween / 2);
if (index < side) {
const distance =
nonZeroIndex === sideMiddle
? 0
: nonZeroIndex < sideMiddle
? -nonZeroIndex
: nonZeroIndex - sideMiddle;
return { x: actorX - sizeX * distance, y: actorY - sizeY * distanceCoefficient };
} else if (index < side + inbetween) {
const inbetweenIndex = nonZeroIndex - side;
const distance =
inbetweenIndex === inbetweenMiddle
? 0
: inbetweenIndex < inbetweenMiddle
? -inbetweenIndex
: inbetweenIndex - inbetweenMiddle;
return { x: actorX + sizeX * distanceCoefficient, y: actorY + sizeY * distance };
} else if (index < 2 * side + inbetween) {
const sideIndex = nonZeroIndex - side - inbetween;
const distance =
sideIndex === sideMiddle
? 0
: sideIndex < sideMiddle
? sideIndex
: -(sideIndex - sideMiddle);
return { x: actorX + sizeX * distance, y: actorY + sizeY * distanceCoefficient };
} else {
const inbetweenIndex = nonZeroIndex - 2 * side - inbetween;
const distance =
inbetweenIndex === inbetweenMiddle
? 0
: inbetweenIndex < inbetweenMiddle
? inbetweenIndex
: -(inbetweenIndex - inbetweenMiddle);
return { x: actorX - sizeX * distanceCoefficient, y: actorY + sizeY * distance };
}
})
);
for (let token of newTokens) {
const position = positions.pop();
token.update(
{ x: position.x, y: position.y, alpha: 1 },
{ animation: { duration: animationDuration } }
);
}
}
}
_getStatusEffectChoices() { _getStatusEffectChoices() {
// Include all HUD-enabled status effects // Include all HUD-enabled status effects
const choices = {}; const choices = {};
@ -59,16 +214,20 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
} }
// Update the status of effects which are active for the token actor // 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 effect of activeEffects) {
for (const statusId of effect.statuses) { for (const statusId of effect.statuses) {
const status = choices[statusId]; const status = choices[statusId];
if (!status) continue; 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) {
if (status._id !== effect.id) continue; if (status._id !== effect.id) continue;
} }
status.isActive = true; status.isActive = true;
if (effect.getFlag('core', 'overlay')) status.isOverlay = true; if (effect.getFlag?.('core', 'overlay')) status.isOverlay = true;
} }
} }

View file

@ -1,3 +1,4 @@
export { default as CharacterLevelup } from './characterLevelup.mjs'; export { default as CharacterLevelup } from './characterLevelup.mjs';
export { default as CompanionLevelup } from './companionLevelup.mjs'; export { default as CompanionLevelup } from './companionLevelup.mjs';
export { default as Levelup } from './levelup.mjs'; export { default as Levelup } from './levelup.mjs';
export { default as LevelupViewMode } from './levelupViewMode.mjs';

View file

@ -280,11 +280,19 @@ export default class DhCharacterLevelUp extends LevelUpBase {
break; break;
case 'experience': case 'experience':
if (!advancement[choiceKey]) advancement[choiceKey] = []; 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 data = checkbox.data.map(data => {
const experience = Object.keys(this.actor.system.experiences).find( const experience = Object.keys(allExperiences).find(x => x === data);
x => x === data return allExperiences[experience]?.name ?? '';
);
return this.actor.system.experiences[experience]?.name ?? '';
}); });
advancement[choiceKey].push({ data: data, value: checkbox.value }); advancement[choiceKey].push({ data: data, value: checkbox.value });
break; break;

View file

@ -1,6 +1,5 @@
import { abilities, subclassFeatureLabels } from '../../config/actorConfig.mjs'; import { abilities, subclassFeatureLabels } from '../../config/actorConfig.mjs';
import { getDeleteKeys, tagifyElement } from '../../helpers/utils.mjs'; import { getDeleteKeys, tagifyElement } from '../../helpers/utils.mjs';
import { ItemBrowser } from '../ui/itemBrowser.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -12,8 +11,6 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
this._dragDrop = this._createDragDropHandlers(); this._dragDrop = this._createDragDropHandlers();
this.tabGroups.primary = 'advancements'; this.tabGroups.primary = 'advancements';
this.itemBrowser = null;
} }
get title() { get title() {
@ -360,11 +357,23 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const experienceIncreaseTagify = htmlElement.querySelector('.levelup-experience-increases'); const experienceIncreaseTagify = htmlElement.querySelector('.levelup-experience-increases');
if (experienceIncreaseTagify) { 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( tagifyElement(
experienceIncreaseTagify, experienceIncreaseTagify,
Object.keys(this.actor.system.experiences).reduce((acc, id) => { Object.keys(allExperiences).reduce((acc, id) => {
const experience = this.actor.system.experiences[id]; const experience = allExperiences[id];
if (experience.name) {
acc.push({ id: id, label: experience.name }); acc.push({ id: id, label: experience.name });
}
return acc; return acc;
}, []), }, []),
@ -540,7 +549,6 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const type = target.dataset.compendium ?? target.dataset.type; const type = target.dataset.compendium ?? target.dataset.type;
const presets = { const presets = {
compendium: 'daggerheart',
folder: type, folder: type,
render: { render: {
noFolder: true noFolder: true
@ -559,7 +567,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
}; };
} }
return (this.itemBrowser = await new ItemBrowser({ presets }).render({ force: true })); ui.compendiumBrowser.open(presets);
} }
static async selectPreview(_, button) { static async selectPreview(_, button) {
@ -662,7 +670,8 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
}, {}); }, {});
await this.actor.levelUp(levelupData); await this.actor.levelUp(levelupData);
if (this.itemBrowser) this.itemBrowser.close();
if (ui.compendiumBrowser) ui.compendiumBrowser.close();
this.close(); this.close();
} }
} }

View file

@ -0,0 +1,95 @@
import { chunkify } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhlevelUpViewMode extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(actor) {
super({});
this.actor = actor;
}
get title() {
return game.i18n.format('DAGGERHEART.APPLICATIONS.Levelup.viewModeTitle', { actor: this.actor.name });
}
static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'dialog', 'dh-style', 'levelup'],
position: { width: 1000, height: 'auto' },
window: {
resizable: true,
icon: 'fa-solid fa-arrow-turn-up'
}
};
static PARTS = {
main: { template: 'systems/daggerheart/templates/levelup/tabs/viewMode.hbs' }
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
const { tiers } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers);
const tierKeys = Object.keys(tiers);
const selections = Object.keys(this.actor.system.levelData.levelups).reduce(
(acc, key) => {
const level = this.actor.system.levelData.levelups[key];
Object.keys(level.selections).forEach(optionKey => {
const choice = level.selections[optionKey];
if (!acc[choice.tier][choice.optionKey]) acc[choice.tier][choice.optionKey] = {};
acc[choice.tier][choice.optionKey][choice.checkboxNr] = choice;
});
return acc;
},
tierKeys.reduce((acc, key) => {
acc[key] = {};
return acc;
}, {})
);
context.tiers = tierKeys.map((tierKey, tierIndex) => {
const tier = tiers[tierKey];
return {
name: tier.name,
active: true,
groups: Object.keys(tier.options).map(optionKey => {
const option = tier.options[optionKey];
const checkboxes = [...Array(option.checkboxSelections).keys()].flatMap(index => {
const checkboxNr = index + 1;
const checkboxData = selections[tierKey]?.[optionKey]?.[checkboxNr];
const checkbox = { ...option, checkboxNr, tier: tierKey, disabled: true };
if (checkboxData) {
checkbox.level = checkboxData.level;
checkbox.selected = true;
}
return checkbox;
});
let label = game.i18n.localize(option.label);
return {
label: label,
checkboxGroups: chunkify(checkboxes, option.minCost, chunkedBoxes => {
const anySelected = chunkedBoxes.some(x => x.selected);
const anyDisabled = chunkedBoxes.some(x => x.disabled);
return {
multi: option.minCost > 1,
checkboxes: chunkedBoxes.map(x => ({
...x,
selected: anySelected,
disabled: anyDisabled
}))
};
})
};
})
};
});
return context;
}
}

View file

@ -0,0 +1 @@
export { default as DhSceneConfigSettings } from './sceneConfigSettings.mjs';

View file

@ -0,0 +1,63 @@
export default class DhSceneConfigSettings extends foundry.applications.sheets.SceneConfig {
// static DEFAULT_OPTIONS = {
// ...super.DEFAULT_OPTIONS,
// form: {
// handler: this.updateData,
// closeOnSubmit: true
// }
// };
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' },
footer
};
return tmpParts;
}
static PARTS = DhSceneConfigSettings.buildParts();
static buildTabs() {
super.TABS.sheet.tabs.push({ id: 'dh', src: 'systems/daggerheart/assets/logos/FoundryBorneLogoWhite.svg' });
return super.TABS;
}
static TABS = DhSceneConfigSettings.buildTabs();
_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();
});
break;
}
}
/** @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.variantRules = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules);
break;
}
return context;
}
// static async updateData(event, _, formData) {
// const data = foundry.utils.expandObject(formData.object);
// this.close(data);
// }
}

View file

@ -3,43 +3,48 @@ import { getDiceSoNicePreset } from '../../config/generalConfig.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
/**
* @import {ApplicationClickAction} from "@client/applications/_types.mjs"
*/
export default class DHAppearanceSettings extends HandlebarsApplicationMixin(ApplicationV2) { export default class DHAppearanceSettings extends HandlebarsApplicationMixin(ApplicationV2) {
constructor() { /**@inheritdoc */
super({});
this.settings = new DhAppearance(
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance).toObject()
);
}
get title() {
return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.title');
}
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
id: 'daggerheart-appearance-settings', id: 'daggerheart-appearance-settings',
classes: ['daggerheart', 'dialog', 'dh-style', 'setting'], classes: ['daggerheart', 'dialog', 'dh-style', 'setting'],
position: { width: '600', height: 'auto' }, position: { width: '600', height: 'auto' },
window: { window: {
title: 'DAGGERHEART.SETTINGS.Menu.title',
icon: 'fa-solid fa-gears' icon: 'fa-solid fa-gears'
}, },
actions: { actions: {
reset: this.reset, reset: DHAppearanceSettings.#onReset,
save: this.save, preview: DHAppearanceSettings.#onPreview
preview: this.preview
}, },
form: { handler: this.updateData, submitOnChange: true } form: {
closeOnSubmit: true,
handler: DHAppearanceSettings.#onSubmit
}
}; };
static PARTS = { static PARTS = {
main: { header: { template: 'systems/daggerheart/templates/settings/appearance-settings/header.hbs' },
template: 'systems/daggerheart/templates/settings/appearance-settings.hbs' tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
} main: { template: 'systems/daggerheart/templates/settings/appearance-settings/main.hbs' },
diceSoNice: { template: 'systems/daggerheart/templates/settings/appearance-settings/diceSoNice.hbs' },
footer: { template: 'templates/generic/form-footer.hbs' }
}; };
/** @inheritdoc */ /** @inheritdoc */
static TABS = { static TABS = {
general: {
tabs: [
{ id: 'main', label: 'DAGGERHEART.GENERAL.Tabs.general' },
{ id: 'diceSoNice', label: 'DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.title' }
],
initial: 'main'
},
diceSoNice: { diceSoNice: {
tabs: [ tabs: [
{ id: 'hope', label: 'DAGGERHEART.GENERAL.hope' }, { id: 'hope', label: 'DAGGERHEART.GENERAL.hope' },
@ -51,79 +56,150 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
} }
}; };
changeTab(tab, group, options) { /**@type {DhAppearance}*/
super.changeTab(tab, group, options); setting;
this.render(); static #localized = false;
}
async _prepareContext(_options) { /** @inheritDoc */
const context = await super._prepareContext(_options); async _preFirstRender(_context, _options) {
context.settingFields = this.settings; await super._preFirstRender(_context, _options);
if (!DHAppearanceSettings.#localized) {
context.showDiceSoNice = game.modules.get('dice-so-nice')?.active; foundry.helpers.Localization.localizeDataModel(this.setting.constructor);
if (game.dice3d) { DHAppearanceSettings.#localized = true;
context.diceSoNiceTextures = game.dice3d.exports.TEXTURELIST;
context.diceSoNiceColorsets = game.dice3d.exports.COLORSETS;
context.diceSoNiceMaterials = Object.keys(game.dice3d.DiceFactory.material_options).map(key => ({
key: key,
name: `DICESONICE.Material${key.capitalize()}`
}));
context.diceSoNiceSystems = [];
for (const [key, system] of game.dice3d.DiceFactory.systems.entries()) {
context.diceSoNiceSystems.push({ key, name: system.name });
} }
} }
context.diceTab = { /** @inheritdoc */
key: this.tabGroups.diceSoNice, _configureRenderParts(options) {
source: this.settings._source.diceSoNice[this.tabGroups.diceSoNice], const parts = super._configureRenderParts(options);
fields: this.settings.schema.fields.diceSoNice.fields[this.tabGroups.diceSoNice].fields if (!game.modules.get('dice-so-nice')?.active) {
}; delete parts.diceSoNice;
delete parts.tabs;
}
return parts;
}
/**@inheritdoc */
async _prepareContext(options) {
const context = await super._prepareContext(options);
if (options.isFirstRender)
this.setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
context.setting = this.setting;
context.fields = this.setting.schema.fields;
context.tabs = this._prepareTabs('general');
context.dsnTabs = this._prepareTabs('diceSoNice');
return context; return context;
} }
static async updateData(event, element, formData) { /**@inheritdoc */
const updatedSettings = foundry.utils.expandObject(formData.object); async _preparePartContext(partId, context, options) {
const partContext = await super._preparePartContext(partId, context, options);
await this.settings.updateSource(updatedSettings); if (partId in context.tabs) partContext.tab = partContext.tabs[partId];
this.render(); switch (partId) {
case 'diceSoNice':
await this.prepareDiceSoNiceContext(partContext);
break;
case 'footer':
partContext.buttons = [
{ type: 'button', action: 'reset', icon: 'fa-solid fa-arrow-rotate-left', label: 'Reset' },
{ type: 'submit', icon: 'fa-solid fa-floppy-disk', label: 'Save Changes' }
];
break;
}
return partContext;
} }
static async preview() { /**
const source = this.settings._source.diceSoNice[this.tabGroups.diceSoNice]; * Prepare render context for the DSN part.
let faces = 'd12'; * @param {ApplicationRenderContext} context
switch (this.tabGroups.diceSoNice) { * @returns {Promise<void>}
case 'advantage': * @protected
case 'disadvantage': */
faces = 'd6'; async prepareDiceSoNiceContext(context) {
context.diceSoNiceTextures = Object.entries(game.dice3d.exports.TEXTURELIST).reduce(
(acc, [k, v]) => ({
...acc,
[k]: v.name
}),
{}
);
context.diceSoNiceColorsets = Object.values(game.dice3d.exports.COLORSETS).reduce(
(acc, v) => ({
...acc,
[v.id]: v.description
}),
{}
);
context.diceSoNiceMaterials = Object.keys(game.dice3d.DiceFactory.material_options).reduce(
(acc, key) => ({
...acc,
[key]: `DICESONICE.Material${key.capitalize()}`
}),
{}
);
context.diceSoNiceSystems = Object.fromEntries(
[...game.dice3d.DiceFactory.systems].map(([k, v]) => [k, v.name])
);
context.diceSoNiceFonts = game.dice3d.exports.Utils.prepareFontList();
foundry.utils.mergeObject(
context.dsnTabs,
['hope', 'fear', 'advantage', 'disadvantage'].reduce(
(acc, key) => ({
...acc,
[key]: {
values: this.setting.diceSoNice[key],
fields: this.setting.schema.getField(`diceSoNice.${key}`).fields
} }
const preset = await getDiceSoNicePreset(source, faces); }),
const diceSoNiceRoll = await new Roll(`1${faces}`).evaluate(); {}
)
);
}
/**
* Submit the configuration form.
* @this {DHAppearanceSettings}
* @param {SubmitEvent} event
* @param {HTMLFormElement} form
* @param {foundry.applications.ux.FormDataExtended} formData
* @returns {Promise<void>}
*/
static async #onSubmit(event, form, formData) {
const data = this.setting.schema.clean(foundry.utils.expandObject(formData.object));
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance, data);
}
/* -------------------------------------------- */
/**
* Submit the configuration form.
* @this {DHAppearanceSettings}
* @type {ApplicationClickAction}
*/
static async #onPreview(_, target) {
const formData = new foundry.applications.ux.FormDataExtended(target.closest('form'));
const { diceSoNice } = foundry.utils.expandObject(formData.object);
const { key } = target.dataset;
const faces = ['advantage', 'disadvantage'].includes(key) ? 'd6' : 'd12';
const preset = await getDiceSoNicePreset(diceSoNice[key], faces);
const diceSoNiceRoll = await new foundry.dice.Roll(`1${faces}`).evaluate();
diceSoNiceRoll.dice[0].options.appearance = preset.appearance; diceSoNiceRoll.dice[0].options.appearance = preset.appearance;
diceSoNiceRoll.dice[0].options.modelFile = preset.modelFile; diceSoNiceRoll.dice[0].options.modelFile = preset.modelFile;
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, false); await game.dice3d.showForRoll(diceSoNiceRoll, game.user, false);
} }
static async reset() { /**
this.settings = new DhAppearance(); * Reset the form back to default values.
this.render(); * @this {DHAppearanceSettings}
} * @type {ApplicationClickAction}
*/
static async save() { static async #onReset() {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance, this.settings.toObject()); this.setting = new this.setting.constructor();
this.render({ force: false });
this.close();
}
_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;
} }
} }

View file

@ -35,13 +35,14 @@ export default class DhAutomationSettings extends HandlebarsApplicationMixin(App
header: { template: 'systems/daggerheart/templates/settings/automation-settings/header.hbs' }, header: { template: 'systems/daggerheart/templates/settings/automation-settings/header.hbs' },
general: { template: 'systems/daggerheart/templates/settings/automation-settings/general.hbs' }, general: { template: 'systems/daggerheart/templates/settings/automation-settings/general.hbs' },
rules: { template: 'systems/daggerheart/templates/settings/automation-settings/rules.hbs' }, rules: { template: 'systems/daggerheart/templates/settings/automation-settings/rules.hbs' },
roll: { template: 'systems/daggerheart/templates/settings/automation-settings/roll.hbs' },
footer: { template: 'systems/daggerheart/templates/settings/automation-settings/footer.hbs' } footer: { template: 'systems/daggerheart/templates/settings/automation-settings/footer.hbs' }
}; };
/** @inheritdoc */ /** @inheritdoc */
static TABS = { static TABS = {
main: { main: {
tabs: [{ id: 'general' }, { id: 'rules' }], tabs: [{ id: 'general' }, { id: 'rules' }, { id: 'roll' }],
initial: 'general', initial: 'general',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
} }

View file

@ -1,5 +1,6 @@
import { DhHomebrew } from '../../data/settings/_module.mjs'; import { DhHomebrew } from '../../data/settings/_module.mjs';
import { slugify } from '../../helpers/utils.mjs'; import { slugify } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhHomebrewSettings extends HandlebarsApplicationMixin(ApplicationV2) { export default class DhHomebrewSettings extends HandlebarsApplicationMixin(ApplicationV2) {
@ -10,11 +11,14 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).toObject() game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).toObject()
); );
this.selected = { this.selected = this.#getDefaultAdversaryType();
domain: null
};
} }
#getDefaultAdversaryType = () => ({
domain: null,
adversaryType: null
});
get title() { get title() {
return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.title'); return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.title');
} }
@ -28,6 +32,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
icon: 'fa-solid fa-gears' icon: 'fa-solid fa-gears'
}, },
actions: { actions: {
editCurrencyIcon: this.changeCurrencyIcon,
addItem: this.addItem, addItem: this.addItem,
editItem: this.editItem, editItem: this.editItem,
removeItem: this.removeItem, removeItem: this.removeItem,
@ -35,7 +40,11 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
addDomain: this.addDomain, addDomain: this.addDomain,
toggleSelectedDomain: this.toggleSelectedDomain, toggleSelectedDomain: this.toggleSelectedDomain,
deleteDomain: this.deleteDomain, deleteDomain: this.deleteDomain,
addAdversaryType: this.addAdversaryType,
deleteAdversaryType: this.deleteAdversaryType,
selectAdversaryType: this.selectAdversaryType,
save: this.save, save: this.save,
resetTokenSizes: this.resetTokenSizes,
reset: this.reset reset: this.reset
}, },
form: { handler: this.updateData, submitOnChange: true } form: { handler: this.updateData, submitOnChange: true }
@ -45,6 +54,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
settings: { template: 'systems/daggerheart/templates/settings/homebrew-settings/settings.hbs' }, settings: { template: 'systems/daggerheart/templates/settings/homebrew-settings/settings.hbs' },
domains: { template: 'systems/daggerheart/templates/settings/homebrew-settings/domains.hbs' }, domains: { template: 'systems/daggerheart/templates/settings/homebrew-settings/domains.hbs' },
types: { template: 'systems/daggerheart/templates/settings/homebrew-settings/types.hbs' },
itemTypes: { template: 'systems/daggerheart/templates/settings/homebrew-settings/itemFeatures.hbs' },
downtime: { template: 'systems/daggerheart/templates/settings/homebrew-settings/downtime.hbs' }, downtime: { template: 'systems/daggerheart/templates/settings/homebrew-settings/downtime.hbs' },
footer: { template: 'systems/daggerheart/templates/settings/homebrew-settings/footer.hbs' } footer: { template: 'systems/daggerheart/templates/settings/homebrew-settings/footer.hbs' }
}; };
@ -52,12 +63,19 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
/** @inheritdoc */ /** @inheritdoc */
static TABS = { static TABS = {
main: { main: {
tabs: [{ id: 'settings' }, { id: 'domains' }, { id: 'downtime' }], tabs: [{ id: 'settings' }, { id: 'domains' }, { id: 'types' }, { id: 'itemFeatures' }, { id: 'downtime' }],
initial: 'settings', initial: 'settings',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
} }
}; };
changeTab(tab, group, options) {
super.changeTab(tab, group, options);
this.selected = this.#getDefaultAdversaryType();
this.render();
}
async _prepareContext(_options) { async _prepareContext(_options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.settingFields = this.settings; context.settingFields = this.settings;
@ -79,6 +97,11 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
context.configDomains = CONFIG.DH.DOMAIN.domains; context.configDomains = CONFIG.DH.DOMAIN.domains;
context.homebrewDomains = this.settings.domains; context.homebrewDomains = this.settings.domains;
break; break;
case 'types':
context.selectedAdversaryType = this.selected.adversaryType
? { id: this.selected.adversaryType, ...this.settings.adversaryTypes[this.selected.adversaryType] }
: null;
break;
} }
return context; return context;
@ -94,34 +117,100 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
this.render(); this.render();
} }
static async addItem(_, target) { 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({ await this.settings.updateSource({
[`restMoves.${target.dataset.type}.moves.${foundry.utils.randomID()}`]: { [`currency.${type}.icon`]: icon
});
this.render();
}
}
static async addItem(_, target) {
const { type } = target.dataset;
if (['shortRest', 'longRest'].includes(type)) {
await this.settings.updateSource({
[`restMoves.${type}.moves.${foundry.utils.randomID()}`]: {
name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newDowntimeMove'), name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newDowntimeMove'),
img: 'icons/magic/life/cross-worn-green.webp', img: 'icons/magic/life/cross-worn-green.webp',
description: '', description: '',
actions: [] actions: []
} }
}); });
} else if (['armorFeatures', 'weaponFeatures'].includes(type)) {
await this.settings.updateSource({
[`itemFeatures.${type}.${foundry.utils.randomID()}`]: {
name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newFeature'),
img: 'icons/magic/life/cross-worn-green.webp',
description: '',
actions: [],
effects: []
}
});
}
this.render(); this.render();
} }
static async editItem(_, target) { static async editItem(_, target) {
const move = this.settings.restMoves[target.dataset.type].moves[target.dataset.id]; const { type, id } = target.dataset;
const path = `restMoves.${target.dataset.type}.moves.${target.dataset.id}`; const isDowntime = ['shortRest', 'longRest'].includes(type);
const editedMove = await game.system.api.applications.sheetConfigs.DowntimeConfig.configure( const path = isDowntime ? `restMoves.${type}.moves.${id}` : `itemFeatures.${type}.${id}`;
move, const featureBase = isDowntime ? this.settings.restMoves[type].moves[id] : this.settings.itemFeatures[type][id];
path,
this.settings
);
if (!editedMove) return;
await this.updateAction.bind(this)(editedMove, target.dataset.type, target.dataset.id); const configTitle = isDowntime
? game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.downtimeMove')
: type === 'armorFeatures'
? game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.armorFeature')
: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.weaponFeature');
const editedBase = await game.system.api.applications.sheetConfigs.SettingFeatureConfig.configure(
configTitle,
featureBase,
path,
this.settings,
{ hasIcon: isDowntime, hasEffects: !isDowntime }
);
if (!editedBase) return;
await this.updateAction.bind(this)(editedBase, target.dataset.type, target.dataset.id);
} }
async updateAction(data, type, id) { async updateAction(data, type, id) {
const isDowntime = ['shortRest', 'longRest'].includes(type);
const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`;
await this.settings.updateSource({ await this.settings.updateSource({
[`restMoves.${type}.moves.${id}`]: { [`${path}.${id}`]: {
actions: data.actions, actions: data.actions,
name: data.name, name: data.name,
icon: data.icon, icon: data.icon,
@ -129,12 +218,16 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
description: data.description description: data.description
} }
}); });
this.render(); this.render();
} }
static async removeItem(_, target) { static async removeItem(_, target) {
const { type, id } = target.dataset;
const isDowntime = ['shortRest', 'longRest'].includes(type);
const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`;
await this.settings.updateSource({ await this.settings.updateSource({
[`restMoves.${target.dataset.type}.moves.-=${target.dataset.id}`]: null [`${path}.-=${id}`]: null
}); });
this.render(); this.render();
} }
@ -301,11 +394,45 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
this.render(); this.render();
} }
static async addAdversaryType(_, target) {
const newId = foundry.utils.randomID();
await this.settings.updateSource({
[`adversaryTypes.${newId}`]: {
id: newId,
label: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.adversaryType.newType')
}
});
this.selected.adversaryType = newId;
this.render();
}
static async deleteAdversaryType(_, target) {
const { key } = target.dataset;
await this.settings.updateSource({ [`adversaryTypes.-=${key}`]: null });
this.selected.adversaryType = this.selected.adversaryType === key ? null : this.selected.adversaryType;
this.render();
}
static async selectAdversaryType(_, target) {
this.selected.adversaryType = this.selected.adversaryType === target.dataset.type ? null : target.dataset.type;
this.render();
}
static async save() { static async save() {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject()); await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
this.close(); this.close();
} }
static async resetTokenSizes() {
await this.settings.updateSource({
tokenSizes: this.settings.schema.fields.tokenSizes.initial
});
this.render();
}
static async reset() { static async reset() {
const confirmed = await foundry.applications.api.DialogV2.confirm({ const confirmed = await foundry.applications.api.DialogV2.confirm({
window: { window: {

View file

@ -1,8 +1,10 @@
export { default as ActionConfig } from './action-config.mjs'; 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 CharacterSettings } from './character-settings.mjs';
export { default as AdversarySettings } from './adversary-settings.mjs'; export { default as AdversarySettings } from './adversary-settings.mjs';
export { default as CompanionSettings } from './companion-settings.mjs'; export { default as CompanionSettings } from './companion-settings.mjs';
export { default as DowntimeConfig } from './downtimeConfig.mjs'; export { default as SettingActiveEffectConfig } from './setting-active-effect-config.mjs';
export { default as SettingFeatureConfig } from './setting-feature-config.mjs';
export { default as EnvironmentSettings } from './environment-settings.mjs'; export { default as EnvironmentSettings } from './environment-settings.mjs';
export { default as ActiveEffectConfig } from './activeEffectConfig.mjs'; export { default as ActiveEffectConfig } from './activeEffectConfig.mjs';
export { default as DhTokenConfig } from './token-config.mjs'; export { default as DhTokenConfig } from './token-config.mjs';

View file

@ -0,0 +1,236 @@
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;
}
get title() {
return `${game.i18n.localize('DAGGERHEART.GENERAL.Tabs.settings')}: ${this.action.name}`;
}
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' },
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
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
}
};
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(true);
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;
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),
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) });
}
/** 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);
}
}

View file

@ -1,232 +1,32 @@
import DaggerheartSheet from '../sheets/daggerheart-sheet.mjs'; import DHActionBaseConfig from './action-base-config.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}`;
}
export default class DHActionConfig extends DHActionBaseConfig {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', ...DHActionBaseConfig.DEFAULT_OPTIONS,
classes: ['daggerheart', 'dh-style', 'dialog', 'max-800'],
window: {
icon: 'fa-solid fa-wrench',
resizable: false
},
position: { width: 600, height: 'auto' },
actions: { actions: {
toggleSection: this.toggleSection, ...DHActionBaseConfig.DEFAULT_OPTIONS.actions,
addEffect: this.addEffect, addEffect: this.addEffect,
removeEffect: this.removeEffect, removeEffect: this.removeEffect,
addElement: this.addElement, editEffect: this.editEffect
removeElement: this.removeElement,
editEffect: this.editEffect,
addDamage: this.addDamage,
removeDamage: this.removeDamage
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
} }
}; };
static PARTS = { async _prepareContext(options) {
header: { const context = await super._prepareContext(options);
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: 'Base'
},
config: {
active: false,
cssClass: '',
group: 'primary',
id: 'config',
icon: null,
label: 'Configuration'
},
effect: {
active: false,
cssClass: '',
group: 'primary',
id: 'effect',
icon: null,
label: 'Effect'
}
};
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;
if (!!this.action.effects) context.effects = this.action.effects.map(e => this.action.item.effects.get(e._id)); 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.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; return context;
} }
static toggleSection(_, button) { static async addEffect(_event) {
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[this.action.parent.parent.id] = {
label: 'DAGGERHEART.GENERAL.itemResource',
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);
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) {
const item = this.action.parent.parent.id === value.key;
value.keyIsID = Boolean(item);
}
}
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.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) {
if (!this.action.effects) return; if (!this.action.effects) return;
const effectData = this._addEffectData.bind(this)(), const effectData = this._addEffectData.bind(this)();
[created] = await this.action.item.createEmbeddedDocuments('ActiveEffect', [effectData], { render: false }), const data = this.action.toObject();
data = this.action.toObject();
const [created] = await this.action.item.createEmbeddedDocuments('ActiveEffect', [effectData], {
render: false
});
data.effects.push({ _id: created._id }); data.effects.push({ _id: created._id });
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
this.action.item.effects.get(created._id).sheet.render(true); this.action.item.effects.get(created._id).sheet.render(true);
@ -246,6 +46,10 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
}; };
} }
getEffectDetails(id) {
return this.action.item.effects.get(id);
}
static removeEffect(event, button) { static removeEffect(event, button) {
if (!this.action.effects) return; if (!this.action.effects) return;
const index = button.dataset.index, const index = button.dataset.index,

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)) { if (!ignoredActorKeys.includes(key)) {
const model = game.system.api.models.actors[key]; const model = game.system.api.models.actors[key];
const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model); 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 group = game.i18n.localize(model.metadata.label);
const choices = CONFIG.Token.documentClass const choices = CONFIG.Token.documentClass
.getTrackedAttributeChoices(attributes, model) .getTrackedAttributeChoices(attributes, model)
@ -96,6 +99,13 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
}); });
} }
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.systemFields = context.document.system.schema.fields;
return context;
}
async _preparePartContext(partId, context) { async _preparePartContext(partId, context) {
const partContext = await super._preparePartContext(partId, context); const partContext = await super._preparePartContext(partId, context);
switch (partId) { switch (partId) {
@ -105,7 +115,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
CONFIG.DH.SETTINGS.gameSettings.appearance CONFIG.DH.SETTINGS.gameSettings.appearance
).showGenericStatusEffects; ).showGenericStatusEffects;
if (!useGeneric) { if (!useGeneric) {
partContext.statuses = Object.values(CONFIG.DH.GENERAL.conditions).map(status => ({ partContext.statuses = Object.values(CONFIG.DH.GENERAL.conditions()).map(status => ({
value: status.id, value: status.id,
label: game.i18n.localize(status.name) label: game.i18n.localize(status.name)
})); }));

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. * Adds a new category entry to the actor.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
@ -109,9 +122,9 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
async _onDrop(event) { async _onDrop(event) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (data.fromInternal) return;
const item = await fromUuid(data.uuid); 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')) { if (item.type === 'adversary' && event.target.closest('.category-container')) {
const target = event.target.closest('.category-container'); const target = event.target.closest('.category-container');
const path = `system.potentialAdversaries.${target.dataset.potentialAdversary}.adversaries`; 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 */ /** @inheritDoc */
async _prepareResourcesTab() { static DEFAULT_OPTIONS = {
const token = this.token; ...super.DEFAULT_OPTIONS,
const usesTrackableAttributes = !foundry.utils.isEmpty(CONFIG.Actor.trackableAttributes); form: { handler: DhPrototypeTokenConfig.#onSubmit }
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
}; };
/**
* Process form submission for the sheet
* @this {PrototypeTokenConfig}
* @type {ApplicationFormSubmission}
*/
static async #onSubmit(event, form, formData) {
const submitData = this._processFormData(event, form, formData);
submitData.detectionModes ??= []; // Clear detection modes array
this._processChanges(submitData);
const changes = { prototypeToken: submitData };
const changedTokenSizeValue = getActorSizeFromForm(this.element, this.actor);
if (changedTokenSizeValue) changes.system = { size: changedTokenSizeValue };
this.actor.validate({ changes, clean: true, fallback: false });
await this.actor.update(changes);
} }
} }

View file

@ -0,0 +1,231 @@
import autocomplete from 'autocompleter';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class SettingActiveEffectConfig extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(effect) {
super({});
this.effect = foundry.utils.deepClone(effect);
const ignoredActorKeys = ['config', 'DhEnvironment'];
this.changeChoices = Object.keys(game.system.api.models.actors).reduce((acc, key) => {
if (!ignoredActorKeys.includes(key)) {
const model = game.system.api.models.actors[key];
const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model);
const group = game.i18n.localize(model.metadata.label);
const choices = CONFIG.Token.documentClass
.getTrackedAttributeChoices(attributes, model)
.map(x => ({ ...x, group: group }));
acc.push(...choices);
}
return acc;
}, []);
}
static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'sheet', 'dh-style', 'active-effect-config', 'standard-form'],
tag: 'form',
position: {
width: 560
},
form: {
submitOnChange: false,
closeOnSubmit: false,
handler: SettingActiveEffectConfig.#onSubmit
},
actions: {
editImage: SettingActiveEffectConfig.#editImage,
addChange: SettingActiveEffectConfig.#addChange,
deleteChange: SettingActiveEffectConfig.#deleteChange
}
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/activeEffect/header.hbs' },
tabs: { template: 'templates/generic/tab-navigation.hbs' },
details: { template: 'systems/daggerheart/templates/sheets/activeEffect/details.hbs', scrollable: [''] },
settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' },
changes: {
template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs',
scrollable: ['ol[data-changes]']
},
footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' }
};
static TABS = {
sheet: {
tabs: [
{ id: 'details', icon: 'fa-solid fa-book' },
{ id: 'settings', icon: 'fa-solid fa-bars', label: 'DAGGERHEART.GENERAL.Tabs.settings' },
{ id: 'changes', icon: 'fa-solid fa-gears' }
],
initial: 'details',
labelPrefix: 'EFFECT.TABS'
}
};
/**@inheritdoc */
async _onFirstRender(context, options) {
await super._onFirstRender(context, options);
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.source = this.effect;
context.fields = game.system.api.documents.DhActiveEffect.schema.fields;
context.systemFields = game.system.api.data.activeEffects.BaseEffect._schema.fields;
return context;
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
const changeChoices = this.changeChoices;
htmlElement.querySelectorAll('.effect-change-input').forEach(element => {
autocomplete({
input: element,
fetch: function (text, update) {
if (!text) {
update(changeChoices);
} else {
text = text.toLowerCase();
var suggestions = changeChoices.filter(n => n.label.toLowerCase().includes(text));
update(suggestions);
}
},
render: function (item, search) {
const label = game.i18n.localize(item.label);
const matchIndex = label.toLowerCase().indexOf(search);
const beforeText = label.slice(0, matchIndex);
const matchText = label.slice(matchIndex, matchIndex + search.length);
const after = label.slice(matchIndex + search.length, label.length);
const element = document.createElement('li');
element.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
if (item.hint) {
element.dataset.tooltip = game.i18n.localize(item.hint);
}
return element;
},
renderGroup: function (label) {
const itemElement = document.createElement('div');
itemElement.textContent = game.i18n.localize(label);
return itemElement;
},
onSelect: function (item) {
element.value = `system.${item.value}`;
},
click: e => e.fetch(),
customize: function (_input, _inputRect, container) {
container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ;
},
minLength: 0
});
});
}
async _preparePartContext(partId, context) {
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(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.appearance
).showGenericStatusEffects;
if (!useGeneric) {
context.statuses = [
...context.statuses,
Object.values(CONFIG.DH.GENERAL.conditions).map(status => ({
value: status.id,
label: game.i18n.localize(status.name)
}))
];
}
break;
case 'changes':
context.modes = Object.entries(CONST.ACTIVE_EFFECT_MODES).reduce((modes, [key, value]) => {
modes[value] = game.i18n.localize(`EFFECT.MODE_${key}`);
return modes;
}, {});
context.priorities = ActiveEffectConfig.DEFAULT_PRIORITIES;
break;
}
return context;
}
static async #onSubmit(_event, _form, formData) {
this.data = foundry.utils.expandObject(formData.object);
this.close();
}
/**
* Edit a Document image.
* @this {DocumentSheetV2}
* @type {ApplicationClickAction}
*/
static async #editImage(_event, target) {
if (target.nodeName !== 'IMG') {
throw new Error('The editImage action is available only for IMG elements.');
}
const attr = target.dataset.edit;
const current = foundry.utils.getProperty(this.effect, attr);
const fp = new FilePicker.implementation({
current,
type: 'image',
callback: path => (target.src = path),
position: {
top: this.position.top + 40,
left: this.position.left + 10
}
});
await fp.browse();
}
/**
* Add a new change to the effect's changes array.
* @this {ActiveEffectConfig}
* @type {ApplicationClickAction}
*/
static async #addChange() {
const { changes, ...rest } = foundry.utils.expandObject(new FormDataExtended(this.form).object);
const updatedChanges = Object.values(changes ?? {});
updatedChanges.push({});
this.effect = { ...rest, changes: updatedChanges };
this.render();
}
/**
* Delete a change from the effect's changes array.
* @this {ActiveEffectConfig}
* @type {ApplicationClickAction}
*/
static async #deleteChange(event) {
const submitData = foundry.utils.expandObject(new FormDataExtended(this.form).object);
const updatedChanges = Object.values(submitData.changes);
const row = event.target.closest('li');
const index = Number(row.dataset.index) || 0;
updatedChanges.splice(index, 1);
this.effect = { ...submitData, changes: updatedChanges };
this.render();
}
static async configure(effect, options = {}) {
return new Promise(resolve => {
const app = new this(effect, options);
app.addEventListener('close', () => resolve(app.data), { once: true });
app.render({ force: true });
});
}
}

View file

@ -1,21 +1,26 @@
import { actionsTypes } from '../../data/action/_module.mjs'; 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; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DowntimeConfig extends HandlebarsApplicationMixin(ApplicationV2) { export default class SettingFeatureConfig extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(move, movePath, settings, options) { constructor(configTitle, move, movePath, settings, optionalParts, options) {
super(options); super(options);
this.configTitle = configTitle;
this.move = move; this.move = move;
this.movePath = movePath; this.movePath = movePath;
this.actionsPath = `${movePath}.actions`; this.actionsPath = `${movePath}.actions`;
this.settings = settings; this.settings = settings;
const { hasIcon, hasEffects } = optionalParts;
this.hasIcon = hasIcon;
this.hasEffects = hasEffects;
} }
get title() { get title() {
return game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.downtimeMoves'); return this.configTitle;
} }
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
@ -30,6 +35,7 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati
addItem: this.addItem, addItem: this.addItem,
editItem: this.editItem, editItem: this.editItem,
removeItem: this.removeItem, removeItem: this.removeItem,
addEffect: this.addEffect,
resetMoves: this.resetMoves, resetMoves: this.resetMoves,
saveForm: this.saveForm saveForm: this.saveForm
}, },
@ -41,13 +47,14 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
main: { template: 'systems/daggerheart/templates/settings/downtime-config/main.hbs' }, main: { template: 'systems/daggerheart/templates/settings/downtime-config/main.hbs' },
actions: { template: 'systems/daggerheart/templates/settings/downtime-config/actions.hbs' }, actions: { template: 'systems/daggerheart/templates/settings/downtime-config/actions.hbs' },
effects: { template: 'systems/daggerheart/templates/settings/downtime-config/effects.hbs' },
footer: { template: 'systems/daggerheart/templates/settings/downtime-config/footer.hbs' } footer: { template: 'systems/daggerheart/templates/settings/downtime-config/footer.hbs' }
}; };
/** @inheritdoc */ /** @inheritdoc */
static TABS = { static TABS = {
primary: { primary: {
tabs: [{ id: 'main' }, { id: 'actions' }], tabs: [{ id: 'main' }, { id: 'actions' }, { id: 'effects' }],
initial: 'main', initial: 'main',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
} }
@ -55,6 +62,9 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati
async _prepareContext(_options) { async _prepareContext(_options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.tabs = this._filterTabs(context.tabs);
context.hasIcon = this.hasIcon;
context.hasEffects = this.hasEffects;
context.move = this.move; context.move = this.move;
context.move.enrichedDescription = await foundry.applications.ux.TextEditor.enrichHTML( context.move.enrichedDescription = await foundry.applications.ux.TextEditor.enrichHTML(
context.move.description context.move.description
@ -92,6 +102,8 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati
return ( return (
(await foundry.applications.api.DialogV2.input({ (await foundry.applications.api.DialogV2.input({
window: { title: game.i18n.localize('DAGGERHEART.CONFIG.SelectAction.selectType') }, window: { title: game.i18n.localize('DAGGERHEART.CONFIG.SelectAction.selectType') },
position: { width: 300 },
classes: ['daggerheart', 'dh-style'],
content: await foundry.applications.handlebars.renderTemplate( content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/actionTypes/actionType.hbs', 'systems/daggerheart/templates/actionTypes/actionType.hbs',
{ types: CONFIG.DH.ACTIONS.actionTypes } { types: CONFIG.DH.ACTIONS.actionTypes }
@ -130,31 +142,109 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati
} }
static async editItem(_, target) { static async editItem(_, target) {
const actionId = target.dataset.id; const { type, id } = target.dataset;
const action = this.move.actions.get(actionId); if (type === 'effect') {
await new DHActionConfig(action, async updatedMove => { const effectIndex = this.move.effects.findIndex(x => x.id === id);
await this.settings.updateSource({ [`${this.actionsPath}.${actionId}`]: updatedMove }); const effect = this.move.effects[effectIndex];
const updatedEffect =
await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect);
if (!updatedEffect) return;
await this.settings.updateSource({
[`${this.movePath}.effects`]: this.move.effects.reduce((acc, effect, index) => {
acc.push(index === effectIndex ? { ...updatedEffect, id: effect.id } : effect);
return acc;
}, [])
});
this.move = foundry.utils.getProperty(this.settings, this.movePath); this.move = foundry.utils.getProperty(this.settings, this.movePath);
this.render(); this.render();
} else {
const action = this.move.actions.get(id);
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); }).render(true);
} }
}
static async removeItem(_, target) { 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 }); await this.settings.updateSource({ [`${this.actionsPath}.-=${target.dataset.id}`]: null });
}
this.move = foundry.utils.getProperty(this.settings, this.movePath);
this.render();
}
static async addEffect(_, target) {
const currentEffects = foundry.utils.getProperty(this.settings, `${this.movePath}.effects`);
await this.settings.updateSource({
[`${this.movePath}.effects`]: [
...currentEffects,
game.system.api.data.activeEffects.BaseEffect.getDefaultObject()
]
});
this.move = foundry.utils.getProperty(this.settings, this.movePath); this.move = foundry.utils.getProperty(this.settings, this.movePath);
this.render(); this.render();
} }
static resetMoves() {} static resetMoves() {}
_filterTabs(tabs) {
return this.hasEffects
? tabs
: Object.keys(tabs).reduce((acc, key) => {
if (key !== 'effects') acc[key] = tabs[key];
return acc;
}, {});
}
/** @override */ /** @override */
_onClose(options = {}) { _onClose(options = {}) {
if (!options.submitted) this.move = null; if (!options.submitted) this.move = null;
} }
static async configure(move, movePath, settings, options = {}) { static async configure(configTitle, move, movePath, settings, optionalParts, options = {}) {
return new Promise(resolve => { return new Promise(resolve => {
const app = new this(move, movePath, settings, options); const app = new this(configTitle, move, movePath, settings, optionalParts, options);
app.addEventListener('close', () => resolve(app.move), { once: true }); app.addEventListener('close', () => resolve(app.move), { once: true });
app.render({ force: true }); app.render({ force: true });
}); });

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

View file

@ -2,3 +2,4 @@ export { default as Adversary } from './adversary.mjs';
export { default as Character } from './character.mjs'; export { default as Character } from './character.mjs';
export { default as Companion } from './companion.mjs'; export { default as Companion } from './companion.mjs';
export { default as Environment } from './environment.mjs'; export { default as Environment } from './environment.mjs';
export { default as Party } from './party.mjs';

View file

@ -4,24 +4,57 @@ import DHBaseActorSheet from '../api/base-actor.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
export default class AdversarySheet extends DHBaseActorSheet { export default class AdversarySheet extends DHBaseActorSheet {
/** @inheritDoc */
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['adversary'], classes: ['adversary'],
position: { width: 660, height: 766 }, position: { width: 660, height: 766 },
window: { resizable: true }, window: { resizable: true },
actions: { actions: {
reactionRoll: AdversarySheet.#reactionRoll toggleHitPoints: AdversarySheet.#toggleHitPoints,
toggleStress: AdversarySheet.#toggleStress,
reactionRoll: AdversarySheet.#reactionRoll,
toggleResourceDice: AdversarySheet.#toggleResourceDice,
handleResourceDice: AdversarySheet.#handleResourceDice
}, },
window: { window: {
resizable: true resizable: true,
controls: [
{
icon: 'fa-solid fa-signature',
label: 'DAGGERHEART.UI.Tooltip.configureAttribution',
action: 'editAttribution'
} }
]
},
dragDrop: [
{
dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
dropSelector: null
}
],
}; };
static PARTS = { static PARTS = {
sidebar: { template: 'systems/daggerheart/templates/sheets/actors/adversary/sidebar.hbs' }, limited: {
template: 'systems/daggerheart/templates/sheets/actors/adversary/limited.hbs',
scrollable: ['.limited-container']
},
sidebar: {
template: 'systems/daggerheart/templates/sheets/actors/adversary/sidebar.hbs',
scrollable: ['.shortcut-items-section']
},
header: { template: 'systems/daggerheart/templates/sheets/actors/adversary/header.hbs' }, header: { template: 'systems/daggerheart/templates/sheets/actors/adversary/header.hbs' },
features: { template: 'systems/daggerheart/templates/sheets/actors/adversary/features.hbs' }, features: {
notes: { template: 'systems/daggerheart/templates/sheets/actors/adversary/notes.hbs' }, template: 'systems/daggerheart/templates/sheets/actors/adversary/features.hbs',
effects: { template: 'systems/daggerheart/templates/sheets/actors/adversary/effects.hbs' } scrollable: ['.feature-section']
},
notes: {
template: 'systems/daggerheart/templates/sheets/actors/adversary/notes.hbs'
},
effects: {
template: 'systems/daggerheart/templates/sheets/actors/adversary/effects.hbs',
scrollable: ['.effects-sections']
}
}; };
/** @inheritdoc */ /** @inheritdoc */
@ -33,10 +66,40 @@ export default class AdversarySheet extends DHBaseActorSheet {
} }
}; };
/** @inheritdoc */
_initializeApplicationOptions(options) {
const applicationOptions = super._initializeApplicationOptions(options);
if (applicationOptions.document.testUserPermission(game.user, 'LIMITED', { exact: true })) {
applicationOptions.position.width = 360;
applicationOptions.position.height = 'auto';
}
return applicationOptions;
}
/**@inheritdoc */ /**@inheritdoc */
async _prepareContext(options) { async _prepareContext(options) {
const context = await super._prepareContext(options); const context = await super._prepareContext(options);
context.systemFields.attack.fields = this.document.system.attack.schema.fields; context.systemFields.attack.fields = this.document.system.attack.schema.fields;
context.resources = Object.keys(this.document.system.resources).reduce((acc, key) => {
acc[key] = this.document.system.resources[key];
return acc;
}, {});
const maxResource = Math.max(context.resources.hitPoints.max, context.resources.stress.max);
context.resources.hitPoints.emptyPips =
context.resources.hitPoints.max < maxResource ? maxResource - context.resources.hitPoints.max : 0;
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; return context;
} }
@ -45,7 +108,11 @@ export default class AdversarySheet extends DHBaseActorSheet {
context = await super._preparePartContext(partId, context, options); context = await super._preparePartContext(partId, context, options);
switch (partId) { switch (partId) {
case 'header': case 'header':
case 'limited':
await this._prepareHeaderContext(context, options); await this._prepareHeaderContext(context, options);
const adversaryTypes = CONFIG.DH.ACTOR.allAdversaryTypes();
context.adversaryType = game.i18n.localize(adversaryTypes[this.document.system.type].label);
break; break;
case 'notes': case 'notes':
await this._prepareNotesContext(context, options); await this._prepareNotesContext(context, options);
@ -109,10 +176,41 @@ 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 */ /* Application Clicks Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */
/**
* Toggles hitpoint resource value.
* @type {ApplicationClickAction}
*/
static async #toggleHitPoints(_, button) {
const hitPointsValue = Number.parseInt(button.dataset.value);
const newValue =
this.document.system.resources.hitPoints.value >= hitPointsValue ? hitPointsValue - 1 : hitPointsValue;
await this.document.update({ 'system.resources.hitPoints.value': newValue });
}
/**
* Toggles stress resource value.
* @type {ApplicationClickAction}
*/
static async #toggleStress(_, button) {
const StressValue = Number.parseInt(button.dataset.value);
const newValue = this.document.system.resources.stress.value >= StressValue ? StressValue - 1 : StressValue;
await this.document.update({ 'system.resources.stress.value': newValue });
}
/** /**
* Performs a reaction roll for an Adversary. * Performs a reaction roll for an Adversary.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
@ -123,9 +221,9 @@ export default class AdversarySheet extends DHBaseActorSheet {
title: `Reaction Roll: ${this.actor.name}`, title: `Reaction Roll: ${this.actor.name}`,
headerTitle: 'Adversary Reaction Roll', headerTitle: 'Adversary Reaction Roll',
roll: { roll: {
type: 'reaction' type: 'trait'
}, },
type: 'trait', actionType: 'reaction',
hasRoll: true, hasRoll: true,
data: this.actor.getRollData() data: this.actor.getRollData()
}; };
@ -133,6 +231,40 @@ export default class AdversarySheet extends DHBaseActorSheet {
this.actor.diceRoll(config); this.actor.diceRoll(config);
} }
/**
* Toggle the used state of a resource dice.
* @type {ApplicationClickAction}
*/
static async #toggleResourceDice(event, target) {
const item = await getDocFromElement(target);
const { dice } = event.target.closest('.item-resource').dataset;
const diceState = item.system.resource.diceStates[dice];
await item.update({
[`system.resource.diceStates.${dice}.used`]: diceState ? !diceState.used : true
});
}
/**
* Handle the roll values of resource dice.
* @type {ApplicationClickAction}
*/
static async #handleResourceDice(_, target) {
const item = await getDocFromElement(target);
if (!item) return;
const rollValues = await game.system.api.applications.dialogs.ResourceDiceDialog.create(item, this.document);
if (!rollValues) return;
await item.update({
'system.resource.diceStates': rollValues.reduce((acc, state, index) => {
acc[index] = { value: state.value, used: state.used };
return acc;
}, {})
});
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Application Listener Actions */ /* Application Listener Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */

View file

@ -1,40 +1,53 @@
import DHBaseActorSheet from '../api/base-actor.mjs'; import DHBaseActorSheet from '../api/base-actor.mjs';
import DhpDeathMove from '../../dialogs/deathMove.mjs'; import DhpDeathMove from '../../dialogs/deathMove.mjs';
import { abilities } from '../../../config/actorConfig.mjs'; import { abilities } from '../../../config/actorConfig.mjs';
import DhCharacterlevelUp from '../../levelup/characterLevelup.mjs'; import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs'; import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
import FilterMenu from '../../ux/filter-menu.mjs'; import FilterMenu from '../../ux/filter-menu.mjs';
import { getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs'; import { getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs';
import { ItemBrowser } from '../../ui/itemBrowser.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
const { TextEditor } = foundry.applications.ux;
export default class CharacterSheet extends DHBaseActorSheet { export default class CharacterSheet extends DHBaseActorSheet {
/**@inheritdoc */ /**@inheritdoc */
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['character'], classes: ['character'],
position: { width: 850, height: 800 }, position: { width: 850, height: 800 },
/* Foundry adds disabled to all buttons and inputs if editPermission is missing. This is not desired. */
editPermission: CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER,
actions: { actions: {
toggleVault: CharacterSheet.#toggleVault, toggleVault: CharacterSheet.#toggleVault,
rollAttribute: CharacterSheet.#rollAttribute, rollAttribute: CharacterSheet.#rollAttribute,
toggleHitPoints: CharacterSheet.#toggleHitPoints,
toggleStress: CharacterSheet.#toggleStress,
toggleArmor: CharacterSheet.#toggleArmor,
toggleHope: CharacterSheet.#toggleHope, toggleHope: CharacterSheet.#toggleHope,
toggleLoadoutView: CharacterSheet.#toggleLoadoutView, toggleLoadoutView: CharacterSheet.#toggleLoadoutView,
openPack: CharacterSheet.#openPack, openPack: CharacterSheet.#openPack,
makeDeathMove: CharacterSheet.#makeDeathMove, makeDeathMove: CharacterSheet.#makeDeathMove,
levelManagement: CharacterSheet.#levelManagement, levelManagement: CharacterSheet.#levelManagement,
viewLevelups: CharacterSheet.#viewLevelups,
toggleEquipItem: CharacterSheet.#toggleEquipItem, toggleEquipItem: CharacterSheet.#toggleEquipItem,
toggleResourceDice: CharacterSheet.#toggleResourceDice, toggleResourceDice: CharacterSheet.#toggleResourceDice,
handleResourceDice: CharacterSheet.#handleResourceDice, handleResourceDice: CharacterSheet.#handleResourceDice,
advanceResourceDie: CharacterSheet.#advanceResourceDie,
cancelBeastform: CharacterSheet.#cancelBeastform,
useDowntime: this.useDowntime, useDowntime: this.useDowntime,
tempBrowser: CharacterSheet.#tempBrowser viewParty: CharacterSheet.#viewParty,
}, },
window: { window: {
resizable: true resizable: true,
controls: [
{
icon: 'fa-solid fa-angles-up',
label: 'DAGGERHEART.ACTORS.Character.viewLevelups',
action: 'viewLevelups'
}
]
}, },
dragDrop: [ dragDrop: [
{ {
dragSelector: '[data-item-id][draggable="true"]', dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
dropSelector: null dropSelector: null
} }
], ],
@ -68,8 +81,14 @@ export default class CharacterSheet extends DHBaseActorSheet {
/**@override */ /**@override */
static PARTS = { static PARTS = {
limited: {
id: 'limited',
scrollable: ['.limited-container'],
template: 'systems/daggerheart/templates/sheets/actors/character/limited.hbs'
},
sidebar: { sidebar: {
id: 'sidebar', id: 'sidebar',
scrollable: ['.shortcut-items-section'],
template: 'systems/daggerheart/templates/sheets/actors/character/sidebar.hbs' template: 'systems/daggerheart/templates/sheets/actors/character/sidebar.hbs'
}, },
header: { header: {
@ -78,22 +97,27 @@ export default class CharacterSheet extends DHBaseActorSheet {
}, },
features: { features: {
id: 'features', id: 'features',
scrollable: ['.features-sections'],
template: 'systems/daggerheart/templates/sheets/actors/character/features.hbs' template: 'systems/daggerheart/templates/sheets/actors/character/features.hbs'
}, },
loadout: { loadout: {
id: 'loadout', id: 'loadout',
scrollable: ['.items-section'],
template: 'systems/daggerheart/templates/sheets/actors/character/loadout.hbs' template: 'systems/daggerheart/templates/sheets/actors/character/loadout.hbs'
}, },
inventory: { inventory: {
id: 'inventory', id: 'inventory',
scrollable: ['.items-section'],
template: 'systems/daggerheart/templates/sheets/actors/character/inventory.hbs' template: 'systems/daggerheart/templates/sheets/actors/character/inventory.hbs'
}, },
biography: { biography: {
id: 'biography', id: 'biography',
scrollable: ['.items-section'],
template: 'systems/daggerheart/templates/sheets/actors/character/biography.hbs' template: 'systems/daggerheart/templates/sheets/actors/character/biography.hbs'
}, },
effects: { effects: {
id: 'effects', id: 'effects',
scrollable: ['.effects-sections'],
template: 'systems/daggerheart/templates/sheets/actors/character/effects.hbs' template: 'systems/daggerheart/templates/sheets/actors/character/effects.hbs'
} }
}; };
@ -116,27 +140,49 @@ export default class CharacterSheet extends DHBaseActorSheet {
element.addEventListener('change', this.updateItemResource.bind(this)); element.addEventListener('change', this.updateItemResource.bind(this));
element.addEventListener('click', e => e.stopPropagation()); element.addEventListener('click', e => e.stopPropagation());
}); });
htmlElement.querySelectorAll('.inventory-item-quantity').forEach(element => {
element.addEventListener('change', this.updateItemQuantity.bind(this));
});
// Add listener for armor marks input // Add listener for armor marks input
htmlElement.querySelectorAll('.armor-marks-input').forEach(element => { htmlElement.querySelectorAll('.armor-marks-input').forEach(element => {
element.addEventListener('change', this.updateArmorMarks.bind(this)); element.addEventListener('change', this.updateArmorMarks.bind(this));
}); });
htmlElement.querySelectorAll('.item-resource.die').forEach(element => {
element.addEventListener('contextmenu', this.lowerResourceDie.bind(this));
});
}
/** @inheritdoc */
_initializeApplicationOptions(options) {
const applicationOptions = super._initializeApplicationOptions(options);
if (applicationOptions.document.testUserPermission(game.user, 'LIMITED', { exact: true })) {
applicationOptions.position.width = 360;
applicationOptions.position.height = 'auto';
}
return applicationOptions;
} }
/** @inheritDoc */ /** @inheritDoc */
async _onRender(context, options) { async _onRender(context, options) {
await super._onRender(context, options); await super._onRender(context, options);
if (!this.document.testUserPermission(game.user, 'LIMITED', { exact: true })) {
this.element this.element
.querySelector('.level-value') .querySelector('.level-value')
?.addEventListener('change', event => this.document.updateLevel(Number(event.currentTarget.value))); ?.addEventListener('change', event => this.document.updateLevel(Number(event.currentTarget.value)));
const observer = this.document.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER, {
exact: true
});
if (observer) {
this.element.querySelector('.window-content').classList.add('viewMode');
}
this._createFilterMenus(); this._createFilterMenus();
this._createSearchFilter(); this._createSearchFilter();
} }
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Prepare Context */ /* Prepare Context */
@ -155,24 +201,17 @@ export default class CharacterSheet extends DHBaseActorSheet {
return acc; return acc;
}, {}); }, {});
context.inventory = { context.resources = Object.keys(this.document.system.resources).reduce((acc, key) => {
currency: { acc[key] = this.document.system.resources[key];
title: game.i18n.localize('DAGGERHEART.CONFIG.Gold.title'), return acc;
coins: game.i18n.localize('DAGGERHEART.CONFIG.Gold.coins'), }, {});
handfuls: game.i18n.localize('DAGGERHEART.CONFIG.Gold.handfuls'), const maxResource = Math.max(context.resources.hitPoints.max, context.resources.stress.max);
bags: game.i18n.localize('DAGGERHEART.CONFIG.Gold.bags'), context.resources.hitPoints.emptyPips =
chests: game.i18n.localize('DAGGERHEART.CONFIG.Gold.chests') context.resources.hitPoints.max < maxResource ? maxResource - context.resources.hitPoints.max : 0;
} context.resources.stress.emptyPips =
}; context.resources.stress.max < maxResource ? maxResource - context.resources.stress.max : 0;
const homebrewCurrency = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).currency; context.beastformActive = this.document.effects.find(x => x.type === 'beastform');
if (homebrewCurrency.enabled) {
context.inventory.currency = homebrewCurrency;
}
if (context.inventory.length === 0) {
context.inventory = Array(1).fill(Array(5).fill([]));
}
return context; return context;
} }
@ -210,7 +249,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
* @protected * @protected
*/ */
async _prepareLoadoutContext(context, _options) { async _prepareLoadoutContext(context, _options) {
context.cardView = !game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsList); context.cardView = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsCard);
} }
/** /**
@ -280,6 +319,40 @@ export default class CharacterSheet extends DHBaseActorSheet {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.loadoutMaxReached')); 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', name: 'toVault',
icon: 'fa-solid fa-arrow-down', icon: 'fa-solid fa-arrow-down',
@ -551,14 +624,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
this.render(); 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) { async updateArmorMarks(event) {
const armor = this.document.system.armor; const armor = this.document.system.armor;
if (!armor) return; if (!armor) return;
@ -586,7 +651,14 @@ export default class CharacterSheet extends DHBaseActorSheet {
if (!value || !subclass) if (!value || !subclass)
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.missingClassOrSubclass')); return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.missingClassOrSubclass'));
new DhCharacterlevelUp(this.document).render({ force: true }); new CharacterLevelup(this.document).render({ force: true });
}
/**
* Opens the charater level management window in viewMode.
*/
static #viewLevelups() {
new LevelupViewMode(this.document).render({ force: true });
} }
/** /**
@ -605,7 +677,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
const { key } = button.dataset; const { key } = button.dataset;
const presets = { const presets = {
compendium: 'daggerheart',
folder: key, folder: key,
filter: filter:
key === 'subclasses' key === 'subclasses'
@ -621,7 +692,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
} }
}; };
return new ItemBrowser({ presets }).render({ force: true }); ui.compendiumBrowser.open(presets);
} }
/** /**
@ -639,41 +710,21 @@ export default class CharacterSheet extends DHBaseActorSheet {
roll: { roll: {
trait: button.dataset.attribute trait: button.dataset.attribute
}, },
hasRoll: true hasRoll: true,
}; actionType: 'action',
const result = await this.document.diceRoll({
...config,
headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`, headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`,
title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: abilityLabel ability: abilityLabel
}) })
});
this.consumeResource(result?.costs);
}
// Remove when Action Refactor part #2 done
async consumeResource(costs) {
if (!costs?.length) return;
const usefulResources = {
...foundry.utils.deepClone(this.actor.system.resources),
fear: {
value: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
max: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear,
reversed: false
}
}; };
const resources = game.system.api.fields.ActionFields.CostField.getRealCosts(costs).map(c => { const result = await this.document.diceRoll(config);
const resource = usefulResources[c.key];
return {
key: c.key,
value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1),
target: resource.target,
keyIsID: resource.keyIsID
};
});
await this.actor.modifyResource(resources); /* 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 //TODO: redo toggleEquipItem method
@ -718,11 +769,42 @@ export default class CharacterSheet extends DHBaseActorSheet {
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #toggleLoadoutView(_, button) { static async #toggleLoadoutView(_, button) {
const newAbilityView = button.dataset.value !== 'true'; const newAbilityView = button.dataset.value === 'true';
await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsList, newAbilityView); await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsCard, newAbilityView);
this.render(); this.render();
} }
/**
* Toggles hitpoint resource value.
* @type {ApplicationClickAction}
*/
static async #toggleHitPoints(_, button) {
const hitPointsValue = Number.parseInt(button.dataset.value);
const newValue =
this.document.system.resources.hitPoints.value >= hitPointsValue ? hitPointsValue - 1 : hitPointsValue;
await this.document.update({ 'system.resources.hitPoints.value': newValue });
}
/**
* Toggles stress resource value.
* @type {ApplicationClickAction}
*/
static async #toggleStress(_, button) {
const StressValue = Number.parseInt(button.dataset.value);
const newValue = this.document.system.resources.stress.value >= StressValue ? StressValue - 1 : StressValue;
await this.document.update({ 'system.resources.stress.value': newValue });
}
/**
* Toggles ArmorScore resource value.
* @type {ApplicationClickAction}
*/
static async #toggleArmor(_, button, element) {
const ArmorValue = Number.parseInt(button.dataset.value);
const newValue = this.document.system.armor.system.marks.value >= ArmorValue ? ArmorValue - 1 : ArmorValue;
await this.document.system.armor.update({ 'system.marks.value': newValue });
}
/** /**
* Toggles a hope resource value. * Toggles a hope resource value.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
@ -762,13 +844,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
}); });
} }
/**
* Temp
*/
static async #tempBrowser(_, target) {
new ItemBrowser().render({ force: true });
}
/** /**
* Handle the roll values of resource dice. * Handle the roll values of resource dice.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
@ -788,6 +863,71 @@ export default class CharacterSheet extends DHBaseActorSheet {
}); });
} }
/** */
static #advanceResourceDie(_, target) {
this.updateResourceDie(target, true);
}
lowerResourceDie(event) {
event.preventDefault();
event.stopPropagation();
this.updateResourceDie(event.target, false);
}
async updateResourceDie(target, advance) {
const item = await getDocFromElement(target);
if (!item) return;
const advancedValue = item.system.resource.value + (advance ? 1 : -1);
await item.update({
'system.resource.value': Math.min(advancedValue, Number(item.system.resource.dieFaces.split('d')[1]))
});
}
/**
*
*/
static async #cancelBeastform(_, target) {
const item = await getDocFromElement(target);
if (!item) return;
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. * Open the downtime application.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
@ -798,40 +938,48 @@ export default class CharacterSheet extends DHBaseActorSheet {
}); });
} }
/** @inheritdoc */
async _onDragStart(event) { async _onDragStart(event) {
const item = await getDocFromElement(event.target); const inventoryItem = event.currentTarget.closest('.inventory-item');
if (inventoryItem) {
const dragData = { event.dataTransfer.setDragImage(inventoryItem.querySelector('img'), 60, 0);
type: item.documentName, }
uuid: item.uuid
};
event.dataTransfer.setData('text/plain', JSON.stringify(dragData));
super._onDragStart(event); super._onDragStart(event);
} }
async _onDrop(event) { async _onDropItem(event, item) {
// Prevent event bubbling to avoid duplicate handling if (this.document.uuid === item.parent?.uuid) {
event.preventDefault(); return super._onDropItem(event, item);
event.stopPropagation(); }
super._onDrop(event); if (item.type === 'beastform') {
this._onDropItem(event, TextEditor.getDragEventData(event)); if (this.document.effects.find(x => x.type === 'beastform')) {
return ui.notifications.warn(
game.i18n.localize('DAGGERHEART.UI.Notifications.beastformAlreadyApplied')
);
} }
async _onDropItem(event, data) {
const item = await Item.implementation.fromDropData(data);
const itemData = item.toObject(); const itemData = item.toObject();
const data = await game.system.api.data.items.DHBeastform.getWildcardImage(this.document, itemData);
if (item.type === 'domainCard' && !this.document.system.loadoutSlot.available) { if (!data?.selectedImage) {
itemData.system.inVault = true; 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); // If this is a type that gets deleted, delete it first (but still defer to super)
const createdItem = await this._onDropItemCreate(itemData); 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) { async _onDropItemCreate(itemData, event) {

View file

@ -8,14 +8,23 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
classes: ['actor', 'companion'], classes: ['actor', 'companion'],
position: { width: 340 }, position: { width: 340 },
actions: { actions: {
toggleStress: DhCompanionSheet.#toggleStress,
actionRoll: DhCompanionSheet.#actionRoll,
levelManagement: DhCompanionSheet.#levelManagement levelManagement: DhCompanionSheet.#levelManagement
} }
}; };
static PARTS = { static PARTS = {
limited: {
template: 'systems/daggerheart/templates/sheets/actors/companion/limited.hbs',
scrollable: ['.limited-container']
},
header: { template: 'systems/daggerheart/templates/sheets/actors/companion/header.hbs' }, header: { template: 'systems/daggerheart/templates/sheets/actors/companion/header.hbs' },
details: { template: 'systems/daggerheart/templates/sheets/actors/companion/details.hbs' }, details: { template: 'systems/daggerheart/templates/sheets/actors/companion/details.hbs' },
effects: { template: 'systems/daggerheart/templates/sheets/actors/companion/effects.hbs' } effects: {
template: 'systems/daggerheart/templates/sheets/actors/companion/effects.hbs',
scrollable: ['.effects-sections']
}
}; };
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -42,6 +51,61 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
/* Application Clicks Actions */ /* Application Clicks Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */
/**
* Toggles stress resource value.
* @type {ApplicationClickAction}
*/
static async #toggleStress(_, button) {
const StressValue = Number.parseInt(button.dataset.value);
const newValue = this.document.system.resources.stress.value >= StressValue ? StressValue - 1 : StressValue;
await this.document.update({ 'system.resources.stress.value': newValue });
}
/**
*
*/
static async #actionRoll(event) {
const partner = this.actor.system.partner;
const config = {
event,
title: `${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}: ${this.actor.name}`,
headerTitle: `Companion ${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}`,
roll: {
trait: partner.system.spellcastModifierTrait?.key
},
hasRoll: true,
data: partner.getRollData()
};
const result = await partner.diceRoll(config);
this.consumeResource(result?.costs);
}
// Remove when Action Refactor part #2 done
async consumeResource(costs) {
if (!costs?.length) return;
const partner = this.actor.system.partner;
const usefulResources = {
...foundry.utils.deepClone(partner.system.resources),
fear: {
value: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
max: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear,
reversed: false
}
};
const resources = game.system.api.fields.ActionFields.CostField.getRealCosts(costs).map(c => {
const resource = usefulResources[c.key];
return {
key: c.key,
value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1),
target: resource.target
};
});
await partner.modifyResource(resources);
}
/** /**
* Opens the companions level management window. * Opens the companions level management window.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}

View file

@ -1,3 +1,4 @@
import { getDocFromElement } from '../../../helpers/utils.mjs';
import DHBaseActorSheet from '../api/base-actor.mjs'; import DHBaseActorSheet from '../api/base-actor.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
@ -8,21 +9,44 @@ export default class DhpEnvironment extends DHBaseActorSheet {
classes: ['environment'], classes: ['environment'],
position: { position: {
width: 500, width: 500,
height: 725 height: 740
}, },
window: { window: {
resizable: true resizable: true,
controls: [
{
icon: 'fa-solid fa-signature',
label: 'DAGGERHEART.UI.Tooltip.configureAttribution',
action: 'editAttribution'
}
]
}, },
actions: {}, actions: {
dragDrop: [{ dragSelector: '.action-section .inventory-item', dropSelector: null }] toggleResourceDice: DhpEnvironment.#toggleResourceDice,
handleResourceDice: DhpEnvironment.#handleResourceDice
},
dragDrop: [
{
dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
dropSelector: null
}
]
}; };
/**@override */ /**@override */
static PARTS = { static PARTS = {
limited: {
template: 'systems/daggerheart/templates/sheets/actors/environment/limited.hbs',
scrollable: ['.limited-container']
},
header: { template: 'systems/daggerheart/templates/sheets/actors/environment/header.hbs' }, header: { template: 'systems/daggerheart/templates/sheets/actors/environment/header.hbs' },
features: { template: 'systems/daggerheart/templates/sheets/actors/environment/features.hbs' }, features: {
template: 'systems/daggerheart/templates/sheets/actors/environment/features.hbs',
scrollable: ['.feature-section']
},
potentialAdversaries: { potentialAdversaries: {
template: 'systems/daggerheart/templates/sheets/actors/environment/potentialAdversaries.hbs' template: 'systems/daggerheart/templates/sheets/actors/environment/potentialAdversaries.hbs',
scrollable: ['.items-section']
}, },
notes: { template: 'systems/daggerheart/templates/sheets/actors/environment/notes.hbs' } notes: { template: 'systems/daggerheart/templates/sheets/actors/environment/notes.hbs' }
}; };
@ -36,12 +60,28 @@ export default class DhpEnvironment extends DHBaseActorSheet {
} }
}; };
/** @inheritdoc */
_initializeApplicationOptions(options) {
const applicationOptions = super._initializeApplicationOptions(options);
if (applicationOptions.document.testUserPermission(game.user, 'LIMITED', { exact: true })) {
applicationOptions.position.width = 360;
applicationOptions.position.height = 'auto';
}
return applicationOptions;
}
/**@inheritdoc */ /**@inheritdoc */
async _preparePartContext(partId, context, options) { async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options); context = await super._preparePartContext(partId, context, options);
switch (partId) { switch (partId) {
case 'header': case 'header':
await this._prepareHeaderContext(context, options); await this._prepareHeaderContext(context, options);
break;
case 'features':
await this._prepareFeaturesContext(context, options);
break; break;
case 'notes': case 'notes':
await this._prepareNotesContext(context, options); await this._prepareNotesContext(context, options);
@ -78,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. * Prepare render context for the Header part.
* @param {ApplicationRenderContext} context * @param {ApplicationRenderContext} context
@ -98,12 +154,51 @@ export default class DhpEnvironment extends DHBaseActorSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
async _onDragStart(event) { async _onDragStart(event) {
const item = event.currentTarget.closest('.inventory-item'); const item = event.currentTarget.closest('.inventory-item[data-type=adversary]');
if (item) { if (item) {
const adversaryData = { type: 'Actor', uuid: item.dataset.itemUuid }; const adversaryData = { type: 'Actor', uuid: item.dataset.itemUuid };
event.dataTransfer.setData('text/plain', JSON.stringify(adversaryData)); event.dataTransfer.setData('text/plain', JSON.stringify(adversaryData));
event.dataTransfer.setDragImage(item, 60, 0); event.dataTransfer.setDragImage(item, 60, 0);
} else {
return super._onDragStart(event);
} }
} }
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
/**
* Toggle the used state of a resource dice.
* @type {ApplicationClickAction}
*/
static async #toggleResourceDice(event, target) {
const item = await getDocFromElement(target);
const { dice } = event.target.closest('.item-resource').dataset;
const diceState = item.system.resource.diceStates[dice];
await item.update({
[`system.resource.diceStates.${dice}.used`]: diceState ? !diceState.used : true
});
}
/**
* Handle the roll values of resource dice.
* @type {ApplicationClickAction}
*/
static async #handleResourceDice(_, target) {
const item = await getDocFromElement(target);
if (!item) return;
const rollValues = await game.system.api.applications.dialogs.ResourceDiceDialog.create(item, this.document);
if (!rollValues) return;
await item.update({
'system.resource.diceStates': rollValues.reduce((acc, state, index) => {
acc[index] = { value: state.value, used: state.used };
return acc;
}, {})
});
}
} }

View file

@ -0,0 +1,479 @@
import DHBaseActorSheet from '../api/base-actor.mjs';
import { getDocFromElement } from '../../../helpers/utils.mjs';
import { ItemBrowser } from '../../ui/itemBrowser.mjs';
import FilterMenu from '../../ux/filter-menu.mjs';
import DaggerheartMenu from '../../sidebar/tabs/daggerheartMenu.mjs';
import { socketEvent } from '../../../systemRegistration/socket.mjs';
import GroupRollDialog from '../../dialogs/group-roll-dialog.mjs';
import DhpActor from '../../../documents/actor.mjs';
import DHItem from '../../../documents/item.mjs';
export default class Party extends DHBaseActorSheet {
constructor(options) {
super(options);
this.refreshSelections = DaggerheartMenu.defaultRefreshSelections();
}
/**@inheritdoc */
static DEFAULT_OPTIONS = {
classes: ['party'],
position: {
width: 550,
height: 900
},
window: {
resizable: true
},
actions: {
deletePartyMember: Party.#deletePartyMember,
deleteItem: Party.#deleteItem,
toggleHope: Party.#toggleHope,
toggleHitPoints: Party.#toggleHitPoints,
toggleStress: Party.#toggleStress,
toggleArmorSlot: Party.#toggleArmorSlot,
tempBrowser: Party.#tempBrowser,
refeshActions: Party.#refeshActions,
triggerRest: Party.#triggerRest,
tagTeamRoll: Party.#tagTeamRoll,
groupRoll: Party.#groupRoll,
selectRefreshable: DaggerheartMenu.selectRefreshable,
refreshActors: DaggerheartMenu.refreshActors
},
dragDrop: [{ dragSelector: '[data-item-id]', dropSelector: null }]
};
/**@override */
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/actors/party/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
partyMembers: { template: 'systems/daggerheart/templates/sheets/actors/party/party-members.hbs' },
resources: {
template: 'systems/daggerheart/templates/sheets/actors/party/resources.hbs',
scrollable: ['']
},
/* NOT YET IMPLEMENTED */
// projects: {
// template: 'systems/daggerheart/templates/sheets/actors/party/projects.hbs',
// scrollable: ['']
// },
inventory: {
template: 'systems/daggerheart/templates/sheets/actors/party/inventory.hbs',
scrollable: ['.tab.inventory .items-section']
},
notes: { template: 'systems/daggerheart/templates/sheets/actors/party/notes.hbs' }
};
/** @inheritdoc */
static TABS = {
primary: {
tabs: [
{ id: 'partyMembers' },
{ id: 'resources' },
/* NOT YET IMPLEMENTED */
// { id: 'projects' },
{ id: 'inventory' },
{ id: 'notes' }
],
initial: 'partyMembers',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}
};
static ALLOWED_ACTOR_TYPES = ['character', 'companion', 'adversary'];
static DICE_ROLL_ACTOR_TYPES = ['character'];
async _onRender(context, options) {
await super._onRender(context, options);
this._createFilterMenus();
this._createSearchFilter();
}
/* -------------------------------------------- */
/* Prepare Context */
/* -------------------------------------------- */
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
switch (partId) {
case 'header':
await this._prepareHeaderContext(context, options);
break;
case 'notes':
await this._prepareNotesContext(context, options);
break;
}
return context;
}
/**
* Prepare render context for the Header part.
* @param {ApplicationRenderContext} context
* @param {ApplicationRenderOptions} options
* @returns {Promise<void>}
* @protected
*/
async _prepareHeaderContext(context, _options) {
const { system } = this.document;
const { TextEditor } = foundry.applications.ux;
context.description = await TextEditor.implementation.enrichHTML(system.description, {
secrets: this.document.isOwner,
relativeTo: this.document
});
}
/**
* Prepare render context for the Biography part.
* @param {ApplicationRenderContext} context
* @param {ApplicationRenderOptions} options
* @returns {Promise<void>}
* @protected
*/
async _prepareNotesContext(context, _options) {
const { system } = this.document;
const { TextEditor } = foundry.applications.ux;
const paths = {
notes: 'notes'
};
for (const [key, path] of Object.entries(paths)) {
const value = foundry.utils.getProperty(system, path);
context[key] = {
field: system.schema.getField(path),
value,
enriched: await TextEditor.implementation.enrichHTML(value, {
secrets: this.document.isOwner,
relativeTo: this.document
})
};
}
}
/**
* Toggles a hope resource value.
* @type {ApplicationClickAction}
*/
static async #toggleHope(_, target) {
const hopeValue = Number.parseInt(target.dataset.value);
const actor = await foundry.utils.fromUuid(target.dataset.actorId);
const newValue = actor.system.resources.hope.value >= hopeValue ? hopeValue - 1 : hopeValue;
await actor.update({ 'system.resources.hope.value': newValue });
this.render();
}
/**
* Toggles a hp resource value.
* @type {ApplicationClickAction}
*/
static async #toggleHitPoints(_, target) {
const hitPointsValue = Number.parseInt(target.dataset.value);
const actor = await foundry.utils.fromUuid(target.dataset.actorId);
const newValue = actor.system.resources.hitPoints.value >= hitPointsValue ? hitPointsValue - 1 : hitPointsValue;
await actor.update({ 'system.resources.hitPoints.value': newValue });
this.render();
}
/**
* Toggles a stress resource value.
* @type {ApplicationClickAction}
*/
static async #toggleStress(_, target) {
const stressValue = Number.parseInt(target.dataset.value);
const actor = await foundry.utils.fromUuid(target.dataset.actorId);
const newValue = actor.system.resources.stress.value >= stressValue ? stressValue - 1 : stressValue;
await actor.update({ 'system.resources.stress.value': newValue });
this.render();
}
/**
* Toggles a armor slot resource value.
* @type {ApplicationClickAction}
*/
static async #toggleArmorSlot(_, target, element) {
const armorItem = await foundry.utils.fromUuid(target.dataset.itemUuid);
const armorValue = Number.parseInt(target.dataset.value);
const newValue = armorItem.system.marks.value >= armorValue ? armorValue - 1 : armorValue;
await armorItem.update({ 'system.marks.value': newValue });
this.render();
}
/**
* Opens Compedium Browser
*/
static async #tempBrowser(_, target) {
new ItemBrowser().render({ force: true });
}
static async #refeshActions() {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: 'New Section',
icon: 'fa-solid fa-campground'
},
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sidebar/daggerheart-menu/main.hbs',
{
refreshables: DaggerheartMenu.defaultRefreshSelections()
}
),
classes: ['daggerheart', 'dialog', 'dh-style', 'tab', 'sidebar-tab', 'daggerheartMenu-sidebar']
});
if (!confirmed) return;
}
static async #triggerRest(_, button) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize(`DAGGERHEART.APPLICATIONS.Downtime.${button.dataset.type}.title`),
icon: button.dataset.type === 'shortRest' ? 'fa-solid fa-utensils' : 'fa-solid fa-bed'
},
content: 'This will trigger a dialog to players make their downtime moves, are you sure?',
classes: ['daggerheart', 'dialog', 'dh-style']
});
if (!confirmed) return;
this.document.system.partyMembers.forEach(actor => {
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.DowntimeTrigger,
data: {
actorId: actor.uuid,
downtimeType: button.dataset.type
}
});
});
}
static async downtimeMoveQuery({ actorId, downtimeType }) {
const actor = await foundry.utils.fromUuid(actorId);
if (!actor || !actor?.isOwner) reject();
new game.system.api.applications.dialogs.Downtime(actor, downtimeType === 'shortRest').render({
force: true
});
}
static async #tagTeamRoll() {
new game.system.api.applications.dialogs.TagTeamDialog(
this.document.system.partyMembers.filter(x => Party.DICE_ROLL_ACTOR_TYPES.includes(x.type))
).render({
force: true
});
}
static async #groupRoll(_params) {
new GroupRollDialog(
this.document.system.partyMembers.filter(x => Party.DICE_ROLL_ACTOR_TYPES.includes(x.type))
).render({ force: true });
}
/**
* Get the set of ContextMenu options for Consumable and Loot.
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
* @this {CharacterSheet}
* @protected
*/
static #getItemContextOptions() {
return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true });
}
/* -------------------------------------------- */
/* Filter Tracking */
/* -------------------------------------------- */
/**
* The currently active search filter.
* @type {foundry.applications.ux.SearchFilter}
*/
#search = {};
/**
* The currently active search filter.
* @type {FilterMenu}
*/
#menu = {};
/**
* Tracks which item IDs are currently displayed, organized by filter type and section.
* @type {{
* inventory: {
* search: Set<string>,
* menu: Set<string>
* },
* loadout: {
* search: Set<string>,
* menu: Set<string>
* },
* }}
*/
#filteredItems = {
inventory: {
search: new Set(),
menu: new Set()
},
loadout: {
search: new Set(),
menu: new Set()
}
};
/* -------------------------------------------- */
/* Search Inputs */
/* -------------------------------------------- */
/**
* Create and initialize search filter instances for the inventory and loadout sections.
*
* Sets up two {@link foundry.applications.ux.SearchFilter} instances:
* - One for the inventory, which filters items in the inventory grid.
* - One for the loadout, which filters items in the loadout/card grid.
* @private
*/
_createSearchFilter() {
//Filters could be a application option if needed
const filters = [
{
key: 'inventory',
input: 'input[type="search"].search-inventory',
content: '[data-application-part="inventory"] .items-section',
callback: this._onSearchFilterInventory.bind(this)
}
];
for (const { key, input, content, callback } of filters) {
const filter = new foundry.applications.ux.SearchFilter({
inputSelector: input,
contentSelector: content,
callback
});
filter.bind(this.element);
this.#search[key] = filter;
}
}
/**
* Handle invetory items search and filtering.
* @param {KeyboardEvent} event The keyboard input event.
* @param {string} query The input search string.
* @param {RegExp} rgx The regular expression query that should be matched against.
* @param {HTMLElement} html The container to filter items from.
* @protected
*/
async _onSearchFilterInventory(_event, query, rgx, html) {
this.#filteredItems.inventory.search.clear();
for (const li of html.querySelectorAll('.inventory-item')) {
const item = await getDocFromElement(li);
const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name);
if (matchesSearch) this.#filteredItems.inventory.search.add(item.id);
const { menu } = this.#filteredItems.inventory;
li.hidden = !(menu.has(item.id) && matchesSearch);
}
}
/* -------------------------------------------- */
/* Filter Menus */
/* -------------------------------------------- */
_createFilterMenus() {
//Menus could be a application option if needed
const menus = [
{
key: 'inventory',
container: '[data-application-part="inventory"]',
content: '.items-section',
callback: this._onMenuFilterInventory.bind(this),
target: '.filter-button',
filters: FilterMenu.invetoryFilters
}
];
menus.forEach(m => {
const container = this.element.querySelector(m.container);
this.#menu[m.key] = new FilterMenu(container, m.target, m.filters, m.callback, {
contentSelector: m.content
});
});
}
/**
* Callback when filters change
* @param {PointerEvent} event
* @param {HTMLElement} html
* @param {import('../ux/filter-menu.mjs').FilterItem[]} filters
*/
async _onMenuFilterInventory(_event, html, filters) {
this.#filteredItems.inventory.menu.clear();
for (const li of html.querySelectorAll('.inventory-item')) {
const item = await getDocFromElement(li);
const matchesMenu =
filters.length === 0 || filters.some(f => foundry.applications.ux.SearchFilter.evaluateFilter(item, f));
if (matchesMenu) this.#filteredItems.inventory.menu.add(item.id);
const { search } = this.#filteredItems.inventory;
li.hidden = !(search.has(item.id) && matchesMenu);
}
}
/* -------------------------------------------- */
async _onDropActor(event, document) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
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)) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.duplicateCharacter'));
}
await this.document.update({ 'system.partyMembers': [...currentMembers, document.uuid] });
} else {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.onlyCharactersInPartySheet'));
}
return null;
}
static async #deletePartyMember(event, target) {
const doc = await getDocFromElement(target.closest('.inventory-item'));
if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize('TYPES.Actor.adversary'),
name: doc.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: doc.name })
});
if (!confirmed) return;
}
const currentMembers = this.document.system.partyMembers.map(x => x.uuid);
const newMemberdList = currentMembers.filter(uuid => uuid !== doc.uuid);
await this.document.update({ 'system.partyMembers': newMemberdList });
}
static async #deleteItem(event, target) {
const doc = await getDocFromElement(target.closest('.inventory-item'));
if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize('TYPES.Actor.party'),
name: doc.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: doc.name })
});
if (!confirmed) return;
}
this.document.deleteEmbeddedDocuments('Item', [doc.id]);
}
}

View file

@ -44,9 +44,8 @@ export default class DHBaseActorSettings extends DHApplicationMixin(DocumentShee
const context = await super._prepareContext(options); const context = await super._prepareContext(options);
context.isNPC = this.actor.isNPC; context.isNPC = this.actor.isNPC;
if (context.systemFields.attack) { if (context.systemFields.attack)
context.systemFields.attack.fields = this.actor.system.attack.schema.fields; context.systemFields.attack.fields = this.actor.system.attack.schema.fields;
}
return context; return context;
} }

View file

@ -1,6 +1,5 @@
const { HandlebarsApplicationMixin } = foundry.applications.api; const { HandlebarsApplicationMixin } = foundry.applications.api;
import { getDocFromElement, getDocFromElementSync, tagifyElement } from '../../../helpers/utils.mjs'; import { getDocFromElement, getDocFromElementSync, tagifyElement } from '../../../helpers/utils.mjs';
import { ItemBrowser } from '../../ui/itemBrowser.mjs';
const typeSettingsMap = { const typeSettingsMap = {
character: 'extendCharacterDescriptions', character: 'extendCharacterDescriptions',
@ -85,6 +84,8 @@ export default function DHApplicationMixin(Base) {
this._dragDrop = this._createDragDropHandlers(); this._dragDrop = this._createDragDropHandlers();
} }
#nonHeaderAttribution = ['environment', 'ancestry', 'community', 'domainCard'];
/** /**
* The default options for the sheet. * The default options for the sheet.
* @type {DHSheetV2Configuration} * @type {DHSheetV2Configuration}
@ -98,10 +99,12 @@ export default function DHApplicationMixin(Base) {
deleteDoc: DHSheetV2.#deleteDoc, deleteDoc: DHSheetV2.#deleteDoc,
toChat: DHSheetV2.#toChat, toChat: DHSheetV2.#toChat,
useItem: DHSheetV2.#useItem, useItem: DHSheetV2.#useItem,
viewItem: DHSheetV2.#viewItem,
toggleEffect: DHSheetV2.#toggleEffect, toggleEffect: DHSheetV2.#toggleEffect,
toggleExtended: DHSheetV2.#toggleExtended, toggleExtended: DHSheetV2.#toggleExtended,
addNewItem: DHSheetV2.#addNewItem, addNewItem: DHSheetV2.#addNewItem,
browseItem: DHSheetV2.#browseItem browseItem: DHSheetV2.#browseItem,
editAttribution: DHSheetV2.#editAttribution
}, },
contextMenus: [ contextMenus: [
{ {
@ -125,6 +128,43 @@ export default function DHApplicationMixin(Base) {
tagifyConfigs: [] tagifyConfigs: []
}; };
/**@inheritdoc */
async _renderFrame(options) {
const frame = await super._renderFrame(options);
const hideAttribution = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.appearance
).hideAttribution;
const headerAttribution = !this.#nonHeaderAttribution.includes(this.document.type);
if (!hideAttribution && this.document.system.metadata.hasAttribution && headerAttribution) {
const { source, page } = this.document.system.attribution;
const attribution = [source, page ? `pg ${page}.` : null].filter(x => x).join('. ');
const element = `<label class="attribution-header-label">${attribution}</label>`;
this.window.controls.insertAdjacentHTML('beforebegin', element);
}
return frame;
}
/**
* Refresh the custom parts of the application frame
*/
refreshFrame() {
const hideAttribution = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.appearance
).hideAttribution;
const headerAttribution = !this.#nonHeaderAttribution.includes(this.document.type);
if (!hideAttribution && this.document.system.metadata.hasAttribution && headerAttribution) {
const { source, page } = this.document.system.attribution;
const attribution = [source, page ? `pg ${page}.` : null].filter(x => x).join('. ');
const label = this.window.header.querySelector('.attribution-header-label');
label.innerHTML = attribution;
}
}
/** /**
* Related documents that should cause a rerender of this application when updated. * Related documents that should cause a rerender of this application when updated.
*/ */
@ -138,6 +178,79 @@ export default function DHApplicationMixin(Base) {
_attachPartListeners(partId, htmlElement, options) { _attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options); super._attachPartListeners(partId, htmlElement, options);
this._dragDrop.forEach(d => d.bind(htmlElement)); 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 */ /**@inheritdoc */
@ -164,11 +277,19 @@ export default function DHApplicationMixin(Base) {
this.relatedDocs.filter(doc => doc).map(doc => delete doc.apps[this.id]); this.relatedDocs.filter(doc => doc).map(doc => delete doc.apps[this.id]);
} }
/** @inheritdoc */
async _renderHTML(context, options) {
const rendered = await super._renderHTML(context, options);
for (const result of Object.values(rendered)) {
await this.#prepareInventoryDescription(result);
}
return rendered;
}
/**@inheritdoc */ /**@inheritdoc */
async _onRender(context, options) { async _onRender(context, options) {
await super._onRender(context, options); await super._onRender(context, options);
this._createTagifyElements(this.options.tagifyConfigs); this._createTagifyElements(this.options.tagifyConfigs);
await this.#prepareInventoryDescription(context);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -176,8 +297,8 @@ export default function DHApplicationMixin(Base) {
/* -------------------------------------------- */ /* -------------------------------------------- */
/**@inheritdoc */ /**@inheritdoc */
_syncPartState(partId, newElement, priorElement, state) { _preSyncPartState(partId, newElement, priorElement, state) {
super._syncPartState(partId, newElement, priorElement, state); super._preSyncPartState(partId, newElement, priorElement, state);
for (const el of priorElement.querySelectorAll('.extensible.extended')) { for (const el of priorElement.querySelectorAll('.extensible.extended')) {
const { actionId, itemUuid } = el.parentElement.dataset; const { actionId, itemUuid } = el.parentElement.dataset;
const selector = `${actionId ? `[data-action-id="${actionId}"]` : `[data-item-uuid="${itemUuid}"]`} .extensible`; const selector = `${actionId ? `[data-action-id="${actionId}"]` : `[data-item-uuid="${itemUuid}"]`} .extensible`;
@ -274,10 +395,11 @@ export default function DHApplicationMixin(Base) {
_onDrop(event) { _onDrop(event) {
event.stopPropagation(); event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (data.fromInternal === this.document.uuid) return; if (data.type === 'ActiveEffect' && data.fromInternal !== this.document.uuid) {
if (data.type === 'ActiveEffect') {
this.document.createEmbeddedDocuments('ActiveEffect', [data.data]); this.document.createEmbeddedDocuments('ActiveEffect', [data.data]);
} else {
// Fallback to super, but note that item sheets do not have this function
return super._onDrop?.(event);
} }
} }
@ -382,7 +504,9 @@ export default function DHApplicationMixin(Base) {
callback: async (target, event) => { callback: async (target, event) => {
const doc = await getDocFromElement(target), const doc = await getDocFromElement(target),
action = doc?.system?.attack ?? doc; action = doc?.system?.attack ?? doc;
return action && action.use(event, { byPassRoll: true }); const config = action.prepareConfig(event);
config.hasRoll = false;
return action && action.workflow.get('damage').execute(config, null, true);
} }
}); });
@ -401,7 +525,7 @@ export default function DHApplicationMixin(Base) {
options.push({ options.push({
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat', name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
icon: 'fa-solid fa-message', icon: 'fa-solid fa-message',
callback: async target => (await getDocFromElement(target)).toChat(this.document.id) callback: async target => (await getDocFromElement(target)).toChat(this.document.uuid)
}); });
if (deletable) if (deletable)
@ -442,11 +566,12 @@ export default function DHApplicationMixin(Base) {
/** /**
* Prepares and enriches an inventory item or action description for display. * Prepares and enriches an inventory item or action description for display.
* @param {HTMLElement} element the element to enrich the inventory items of
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async #prepareInventoryDescription(context) { async #prepareInventoryDescription(element) {
// Get all inventory item elements with a data-item-uuid attribute // Get all inventory item elements with a data-item-uuid attribute
const inventoryItems = this.element.querySelectorAll('.inventory-item[data-item-uuid]'); const inventoryItems = element.querySelectorAll('.inventory-item[data-item-uuid]');
for (const el of inventoryItems) { for (const el of inventoryItems) {
// Get the doc uuid from the element // Get the doc uuid from the element
const { itemUuid } = el?.dataset || {}; const { itemUuid } = el?.dataset || {};
@ -537,28 +662,27 @@ export default function DHApplicationMixin(Base) {
static async #browseItem(event, target) { static async #browseItem(event, target) {
const type = target.dataset.compendium ?? target.dataset.type; const type = target.dataset.compendium ?? target.dataset.type;
const presets = {}; const presets = {
render: {
noFolder: true
}
};
switch (type) { switch (type) {
case 'loot': case 'loot':
presets.folder = 'equipments.folders.loots';
break;
case 'consumable': case 'consumable':
presets.folder = 'equipments.folders.consumables';
break;
case 'armor': case 'armor':
presets.folder = 'equipments.folders.armors';
break;
case 'weapon': case 'weapon':
presets.compendium = 'daggerheart'; presets.folder = 'equipments.folders.weapons';
presets.folder = 'equipments';
presets.render = {
noFolder: true
};
presets.filter = {
type: { key: 'type', value: type, forced: true }
};
break; break;
case 'domainCard': case 'domainCard':
presets.compendium = 'daggerheart';
presets.folder = 'domains'; presets.folder = 'domains';
presets.render = {
noFolder: true
};
presets.filter = { presets.filter = {
'level.max': { key: 'level.max', value: this.document.system.levelData.level.current }, 'level.max': { key: 'level.max', value: this.document.system.levelData.level.current },
'system.domain': { key: 'system.domain', value: this.document.system.domains } 'system.domain': { key: 'system.domain', value: this.document.system.domains }
@ -568,7 +692,15 @@ export default function DHApplicationMixin(Base) {
return; return;
} }
return new ItemBrowser({ presets }).render({ force: true }); ui.compendiumBrowser.open(presets);
}
/**
* Open the attribution dialog
* @type {ApplicationClickAction}
*/
static async #editAttribution() {
new game.system.api.applications.dialogs.AttributionDialog(this.document).render({ force: true });
} }
/** /**
@ -604,6 +736,9 @@ export default function DHApplicationMixin(Base) {
}; };
if (inVault) data['system.inVault'] = true; if (inVault) data['system.inVault'] = true;
if (disabled) data.disabled = 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 }); const doc = await cls.create(data, { parent, renderSheet: !event.shiftKey });
if (parentIsItem && type === 'feature') { if (parentIsItem && type === 'feature') {
@ -640,7 +775,7 @@ export default function DHApplicationMixin(Base) {
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #toChat(_event, target) { static async #toChat(_event, target) {
let doc = await getDocFromElement(target); const doc = await getDocFromElement(target);
return doc.toChat(doc.uuid); return doc.toChat(doc.uuid);
} }
@ -649,10 +784,19 @@ export default function DHApplicationMixin(Base) {
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #useItem(event, target) { static async #useItem(event, target) {
let doc = await getDocFromElement(target); const doc = await getDocFromElement(target);
await doc.use(event); await doc.use(event);
} }
/**
* View an item by opening its sheet
* @type {ApplicationClickAction}
*/
static async #viewItem(_, target) {
const doc = await getDocFromElement(target);
await doc.sheet.render({ force: true });
}
/** /**
* Toggle a ActiveEffect * Toggle a ActiveEffect
* @type {ApplicationClickAction} * @type {ApplicationClickAction}

View file

@ -1,3 +1,4 @@
import { getDocFromElement, itemIsIdentical } from '../../../helpers/utils.mjs';
import DHBaseActorSettings from './actor-setting.mjs'; import DHBaseActorSettings from './actor-setting.mjs';
import DHApplicationMixin from './application-mixin.mjs'; import DHApplicationMixin from './application-mixin.mjs';
@ -33,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 }
]
}; };
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -47,6 +51,12 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
return (this.#settingSheet ??= SheetClass ? new SheetClass({ document: this.document }) : null); return (this.#settingSheet ??= SheetClass ? new SheetClass({ document: this.document }) : null);
} }
get isVisible() {
const viewPermission = this.document.testUserPermission(game.user, this.options.viewPermission);
const limitedOnly = this.document.testUserPermission(game.user, this.options.viewPermission, { exact: true });
return limitedOnly ? this.document.system.metadata.hasLimitedView : viewPermission;
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Prepare Context */ /* Prepare Context */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -55,6 +65,36 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
async _prepareContext(_options) { async _prepareContext(_options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.isNPC = this.document.isNPC; context.isNPC = this.document.isNPC;
context.useResourcePips = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.appearance
).useResourcePips;
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; return context;
} }
@ -69,10 +109,39 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
return context; return context;
} }
_configureRenderParts(options) {
const parts = super._configureRenderParts(options);
if (!this.document.system.metadata.hasLimitedView) return parts;
if (this.document.testUserPermission(game.user, 'LIMITED', { exact: true })) return { limited: parts.limited };
return Object.keys(parts).reduce((acc, key) => {
if (key !== 'limited') acc[key] = parts[key];
return acc;
}, {});
}
/** @inheritDoc */
async _onRender(context, options) {
await super._onRender(context, options);
if (
this.document.system.metadata.hasLimitedView &&
this.document.testUserPermission(game.user, 'LIMITED', { exact: true })
) {
this.element.classList = `${this.element.classList} limited`;
}
}
/**@inheritdoc */ /**@inheritdoc */
_attachPartListeners(partId, htmlElement, options) { _attachPartListeners(partId, htmlElement, options) {
super._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 => { htmlElement.querySelectorAll('.item-button .action-uses-button').forEach(element => {
element.addEventListener('contextmenu', DHBaseActorSheet.#modifyActionUses); element.addEventListener('contextmenu', DHBaseActorSheet.#modifyActionUses);
}); });
@ -111,6 +180,15 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true }); 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 */ /* Application Clicks Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -179,13 +257,98 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
/* Application Drag/Drop */ /* Application Drag/Drop */
/* -------------------------------------------- */ /* -------------------------------------------- */
async _onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (data.type === 'Currency' && ['character', 'party'].includes(this.document.type)) {
const originActor = await foundry.utils.fromUuid(data.originActor);
if (!originActor || originActor.uuid === this.document.uuid) return;
const currency = data.currency;
const quantity = await game.system.api.applications.dialogs.ItemTransferDialog.configure({
originActor,
targetActor: this.document,
currency
});
if (quantity) {
originActor.update({ [`system.gold.${currency}`]: Math.max(0, originActor.system.gold[currency] - quantity) });
this.document.update({ [`system.gold.${currency}`]: this.document.system.gold[currency] + quantity });
}
return;
}
return super._onDrop(event);
}
async _onDropItem(event, item) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const 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 */
if (item.system.metadata.isInventoryItem) {
if (item.system.metadata.isQuantifiable) {
const actorItem = originActor.items.get(data.originId);
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 {
await actorItem.update({
'system.quantity': actorItem.system.quantity - quantityTransfered
});
}
const existingItem = this.document.items.find(x => itemIsIdentical(x, item));
if (existingItem) {
await existingItem.update({
'system.quantity': existingItem.system.quantity + quantityTransfered
});
} else {
const createData = item.toObject();
await this.document.createEmbeddedDocuments('Item', [
{
...createData,
system: {
...createData.system,
quantity: quantityTransfered
}
}
]);
}
}
} else {
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
await this.document.createEmbeddedDocuments('Item', [item.toObject()]);
}
}
}
/** /**
* On dragStart on the item. * On dragStart on the item.
* @param {DragEvent} event - The drag event * @param {DragEvent} event - The drag event
*/ */
async _onDragStart(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) { if (attackItem) {
const attackData = { const attackData = {
type: 'Attack', type: 'Attack',
@ -195,8 +358,20 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
}; };
event.dataTransfer.setData('text/plain', JSON.stringify(attackData)); event.dataTransfer.setData('text/plain', JSON.stringify(attackData));
event.dataTransfer.setDragImage(attackItem.querySelector('img'), 60, 0); 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); super._onDragStart(event);
} }
}
} }

View file

@ -13,7 +13,16 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['item'], classes: ['item'],
position: { width: 600 }, position: { width: 600 },
window: { resizable: true }, window: {
resizable: true,
controls: [
{
icon: 'fa-solid fa-signature',
label: 'DAGGERHEART.UI.Tooltip.configureAttribution',
action: 'editAttribution'
}
]
},
form: { form: {
submitOnChange: true submitOnChange: true
}, },
@ -24,9 +33,9 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
removeResource: DHBaseItemSheet.#removeResource removeResource: DHBaseItemSheet.#removeResource
}, },
dragDrop: [ dragDrop: [
{ dragSelector: null, dropSelector: '.tab.features .drop-section' }, { dragSelector: null, dropSelector: '.drop-section' },
{ dragSelector: '.feature-item', dropSelector: null }, { dragSelector: '.feature-item', dropSelector: null },
{ dragSelector: '.action-item', dropSelector: null } { dragSelector: '.inventory-item', dropSelector: null }
], ],
contextMenus: [ contextMenus: [
{ {
@ -55,6 +64,15 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
/* Prepare Context */ /* Prepare Context */
/* -------------------------------------------- */ /* -------------------------------------------- */
/**@inheritdoc */
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.showAttribution = !game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
.hideAttribution;
return context;
}
/**@inheritdoc */ /**@inheritdoc */
async _preparePartContext(partId, context, options) { async _preparePartContext(partId, context, options) {
await super._preparePartContext(partId, context, options); await super._preparePartContext(partId, context, options);
@ -181,19 +199,33 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
static async #deleteFeature(_, element) { static async #deleteFeature(_, element) {
const target = element.closest('[data-item-uuid]'); const target = element.closest('[data-item-uuid]');
const feature = await getDocFromElement(target); const feature = await getDocFromElement(target);
if (!feature) { if (!feature) {
await this.document.update({ await this.document.update({
'system.features': this.document.system.features 'system.features': this.document.system.features
.filter(x => x.item) .filter(x => x.item)
.map(x => ({ ...x, item: x.item.uuid })) .map(x => ({ ...x, item: x.item.uuid }))
}); });
} else } else {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize('TYPES.Item.feature'),
name: feature.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: feature.name })
});
if (!confirmed) return;
await this.document.update({ await this.document.update({
'system.features': this.document.system.features 'system.features': this.document.system.features
.filter(x => target.dataset.type !== x.type || x.item.uuid !== feature.uuid) .filter(x => target.dataset.type !== x.type || x.item.uuid !== feature.uuid)
.map(x => ({ ...x, item: x.item.uuid })) .map(x => ({ ...x, item: x.item.uuid }))
}); });
} }
}
/** /**
* Add a resource to the item. * Add a resource to the item.
@ -224,37 +256,30 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
* @param {DragEvent} event - The drag event * @param {DragEvent} event - The drag event
*/ */
async _onDragStart(event) { async _onDragStart(event) {
/* Can prolly be improved a lot, but I don't wanna >_< */
const featureItem = event.currentTarget.closest('.feature-item'); const featureItem = event.currentTarget.closest('.feature-item');
const inventoryItem = event.currentTarget.closest('.inventory-item');
const lineItem = event.currentTarget.closest('.item-line');
const dragItemData = featureItem ?? inventoryItem ?? lineItem;
if (featureItem) { const dragItem = await foundry.utils.fromUuid(dragItemData.dataset.itemUuid);
const feature = this.document.system.features.find(x => x?.id === featureItem.id); if (dragItem) {
if (!feature) { if (!dragItem) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing')); ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing'));
return; return;
} }
const featureData = { type: 'Item', data: { ...feature.toObject(), _id: null }, fromInternal: true }; let dragData = {};
event.dataTransfer.setData('text/plain', JSON.stringify(featureData)); if (dragItemData.dataset.type === 'effect')
event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0); dragData = {
} else { type: 'ActiveEffect',
const actionItem = event.currentTarget.closest('.action-item'); fromInternal: this.document.uuid,
if (actionItem) { data: { ...dragItem, uuid: dragItem.uuid, id: dragItem.id }
const action = this.document.system.actions[actionItem.dataset.index];
if (!action) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.actionIsMissing'));
return;
}
const actionData = {
type: 'Action',
data: { ...action.toObject(), id: action.id, itemUuid: this.document.uuid },
fromInternal: true
}; };
event.dataTransfer.setData('text/plain', JSON.stringify(actionData)); else dragData = { type: 'Item', uuid: dragItem.uuid, id: dragItem.id, fromInternal: this.document.id };
event.dataTransfer.setDragImage(actionItem.querySelector('img'), 60, 0);
} else { event.dataTransfer.setData('text/plain', JSON.stringify(dragData));
super._onDragStart(event); event.dataTransfer.setDragImage(dragItemData.querySelector('img'), 60, 0);
}
} }
} }
@ -266,7 +291,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
super._onDrop(event); super._onDrop(event);
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (data.fromInternal) return; if (data.fromInternal === this.document.id) return;
const target = event.target.closest('fieldset.drop-section'); const target = event.target.closest('fieldset.drop-section');
let item = await fromUuid(data.uuid); let item = await fromUuid(data.uuid);

View file

@ -8,7 +8,7 @@ export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
tagifyConfigs: [ tagifyConfigs: [
{ {
selector: '.features-input', selector: '.features-input',
options: () => CONFIG.DH.ITEM.armorFeatures, options: () => CONFIG.DH.ITEM.orderedArmorFeatures(),
callback: ArmorSheet.#onFeatureSelect callback: ArmorSheet.#onFeatureSelect
} }
] ]

View file

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

View file

@ -46,6 +46,10 @@ export default class ClassSheet extends DHBaseItemSheet {
template: 'systems/daggerheart/templates/sheets/items/class/settings.hbs', template: 'systems/daggerheart/templates/sheets/items/class/settings.hbs',
scrollable: ['.settings'] scrollable: ['.settings']
}, },
questions: {
template: 'systems/daggerheart/templates/sheets/items/class/questions.hbs',
scrollable: ['.questions']
},
effects: { effects: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs', template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs',
scrollable: ['.effects'] scrollable: ['.effects']
@ -55,7 +59,13 @@ export default class ClassSheet extends DHBaseItemSheet {
/** @inheritdoc */ /** @inheritdoc */
static TABS = { static TABS = {
primary: { primary: {
tabs: [{ id: 'description' }, { id: 'features' }, { id: 'settings' }, { id: 'effects' }], tabs: [
{ id: 'description' },
{ id: 'features' },
{ id: 'settings' },
{ id: 'questions' },
{ id: 'effects' }
],
initial: 'description', initial: 'description',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
} }
@ -115,8 +125,8 @@ export default class ClassSheet extends DHBaseItemSheet {
async _onDrop(event) { async _onDrop(event) {
event.stopPropagation(); event.stopPropagation();
const data = TextEditor.getDragEventData(event); const data = TextEditor.getDragEventData(event);
const item = data.data ?? (await fromUuid(data.uuid)); const item = await fromUuid(data.uuid);
const itemType = data.data ? data.type : item.type; const itemType = data.type === 'ActiveEffect' ? data.type : item.type;
const target = event.target.closest('fieldset.drop-section'); const target = event.target.closest('fieldset.drop-section');
if (itemType === 'subclass') { if (itemType === 'subclass') {
if (item.system.linkedClass) { if (item.system.linkedClass) {
@ -193,10 +203,10 @@ export default class ClassSheet extends DHBaseItemSheet {
if (target === 'subclasses') { if (target === 'subclasses') {
const subclass = await foundry.utils.fromUuid(uuid); const subclass = await foundry.utils.fromUuid(uuid);
await subclass.update({ 'system.linkedClass': null }); await subclass?.update({ 'system.linkedClass': null });
} }
await this.document.update({ [`system.${target}`]: prop.filter(i => i.uuid !== uuid).map(x => x.uuid) }); await this.document.update({ [`system.${target}`]: prop.filter(i => i && i.uuid !== uuid).map(x => x.uuid) });
} }
/** /**

View file

@ -31,4 +31,11 @@ export default class FeatureSheet extends DHBaseItemSheet {
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' 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

@ -8,7 +8,7 @@ export default class WeaponSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
tagifyConfigs: [ tagifyConfigs: [
{ {
selector: '.features-input', selector: '.features-input',
options: () => CONFIG.DH.ITEM.weaponFeatures, options: () => CONFIG.DH.ITEM.orderedWeaponFeatures(),
callback: WeaponSheet.#onFeatureSelect callback: WeaponSheet.#onFeatureSelect
} }
] ]

View file

@ -0,0 +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,73 @@
export default class DhSidebar extends foundry.applications.sidebar.Sidebar {
/** @override */
static TABS = {
chat: {
documentName: 'ChatMessage'
},
combat: {
documentName: 'Combat'
},
scenes: {
documentName: 'Scene',
gmOnly: true
},
actors: {
documentName: 'Actor'
},
items: {
documentName: 'Item'
},
journal: {
documentName: 'JournalEntry',
tooltip: 'SIDEBAR.TabJournal'
},
tables: {
documentName: 'RollTable'
},
cards: {
documentName: 'Cards'
},
macros: {
documentName: 'Macro'
},
playlists: {
documentName: 'Playlist'
},
compendium: {
tooltip: 'SIDEBAR.TabCompendium',
icon: 'fa-solid fa-book-atlas'
},
daggerheartMenu: {
tooltip: 'DAGGERHEART.UI.Sidebar.daggerheartMenu.title',
img: 'systems/daggerheart/assets/logos/FoundryBorneLogoWhite.svg',
gmOnly: true
},
settings: {
tooltip: 'SIDEBAR.TabSettings',
icon: 'fa-solid fa-gears'
}
};
/** @override */
static PARTS = {
tabs: {
id: 'tabs',
template: 'systems/daggerheart/templates/sidebar/tabs.hbs'
}
};
/** @override */
async _prepareTabContext(context, options) {
context.tabs = Object.entries(this.constructor.TABS).reduce((obj, [k, v]) => {
let { documentName, gmOnly, tooltip, icon, img } = v;
if (gmOnly && !game.user.isGM) return obj;
if (documentName) {
tooltip ??= getDocumentClass(documentName).metadata.labelPlural;
icon ??= CONFIG[documentName]?.sidebarIcon;
}
obj[k] = { tooltip, icon, img };
obj[k].active = this.tabGroups.primary === k;
return obj;
}, {});
}
}

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

@ -0,0 +1,162 @@
import { refreshIsAllowed } from '../../../helpers/utils.mjs';
const { HandlebarsApplicationMixin } = foundry.applications.api;
const { AbstractSidebarTab } = foundry.applications.sidebar;
/**
* The daggerheart menu tab.
* @extends {AbstractSidebarTab}
* @mixes HandlebarsApplication
*/
export default class DaggerheartMenu extends HandlebarsApplicationMixin(AbstractSidebarTab) {
constructor(options) {
super(options);
this.refreshSelections = DaggerheartMenu.defaultRefreshSelections();
}
static defaultRefreshSelections() {
return {
session: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.session') },
scene: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.scene') },
longRest: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.longrest') },
shortRest: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.shortrest') }
};
}
/** @override */
static DEFAULT_OPTIONS = {
classes: ['dh-style'],
window: {
title: 'SIDEBAR.TabSettings'
},
actions: {
selectRefreshable: DaggerheartMenu.#selectRefreshable,
refreshActors: DaggerheartMenu.#refreshActors
}
};
/** @override */
static tabName = 'daggerheartMenu';
/** @override */
static PARTS = {
main: { template: 'systems/daggerheart/templates/sidebar/daggerheart-menu/main.hbs' }
};
/* -------------------------------------------- */
/** @inheritDoc */
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.refreshables = this.refreshSelections;
context.disableRefresh = Object.values(this.refreshSelections).every(x => !x.selected);
return context;
}
async getRefreshables(types) {
const refreshedActors = {};
for (let actor of game.actors) {
if (['character', 'adversary'].includes(actor.type) && actor.prototypeToken.actorLink) {
const updates = {};
for (let item of actor.items) {
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(
game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[item.system.resource.recovery].label)
);
if (!updates[item.id]?.system) updates[item.id] = { system: {} };
const increasing =
item.system.resource.progression === CONFIG.DH.ITEM.itemResourceProgression.increasing.id;
updates[item.id].system = {
...updates[item.id].system,
'resource.value': increasing
? 0
: Roll.replaceFormulaData(item.system.resource.max, actor.getRollData())
};
}
if (item.system.metadata?.hasActions) {
const refreshTypes = new Set();
const actions = item.system.actions.filter(action => {
if (refreshIsAllowed(types, action.uses.recovery)) {
refreshTypes.add(action.uses.recovery);
return true;
}
return false;
});
if (actions.length === 0) continue;
if (!refreshedActors[actor.id])
refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() };
refreshedActors[actor.id].refreshed.add(
...refreshTypes.map(type => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[type].label))
);
if (!updates[item.id]?.system) updates[item.id] = { system: {} };
updates[item.id].system = {
...updates[item.id].system,
...actions.reduce(
(acc, action) => {
acc.actions[action.id] = { 'uses.value': 0 };
return acc;
},
{ actions: updates[item.id].system.actions ?? {} }
)
};
}
}
for (let key in updates) {
const update = updates[key];
await actor.items.get(key).update(update);
}
}
}
return refreshedActors;
}
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
static async #selectRefreshable(_event, button) {
const { type } = button.dataset;
this.refreshSelections[type].selected = !this.refreshSelections[type].selected;
this.render();
}
static async #refreshActors() {
const refreshKeys = Object.keys(this.refreshSelections).filter(key => this.refreshSelections[key].selected);
await this.getRefreshables(refreshKeys);
const types = refreshKeys.map(x => this.refreshSelections[x].label).join(', ');
ui.notifications.info(
game.i18n.format('DAGGERHEART.UI.Notifications.gmMenuRefresh', {
types: `[${types}]`
})
);
this.refreshSelections = DaggerheartMenu.defaultRefreshSelections();
const cls = getDocumentClass('ChatMessage');
const msg = {
user: game.user.id,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/refreshMessage.hbs',
{
types: types
}
),
title: game.i18n.localize('DAGGERHEART.UI.Chat.refreshMessage.title'),
speaker: cls.getSpeaker()
};
cls.create(msg);
this.render();
}
}

View file

@ -1,5 +1,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 DhChatLog } from './chatLog.mjs';
export { default as DhCombatTracker } from './combatTracker.mjs'; export { default as DhCombatTracker } from './combatTracker.mjs';
export * as DhCountdowns from './countdowns.mjs'; export { default as DhEffectsDisplay } from './effectsDisplay.mjs';
export { default as DhFearTracker } from './fearTracker.mjs'; export { default as DhFearTracker } from './fearTracker.mjs';
export { default as DhHotbar } from './hotbar.mjs'; export { default as DhHotbar } from './hotbar.mjs';
export { ItemBrowser } from './itemBrowser.mjs';

View file

@ -1,4 +1,5 @@
import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs'; import { abilities } from '../../config/actorConfig.mjs';
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog { export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog {
constructor(options) { constructor(options) {
@ -37,7 +38,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
// } // }
// }, // },
{ {
name: 'Reroll Damage', name: game.i18n.localize('DAGGERHEART.UI.ChatLog.rerollDamage'),
icon: '<i class="fa-solid fa-dice"></i>', icon: '<i class="fa-solid fa-dice"></i>',
condition: li => { condition: li => {
const message = game.messages.get(li.dataset.messageId); const message = game.messages.get(li.dataset.messageId);
@ -54,30 +55,31 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
]; ];
} }
addChatListeners = async (app, html, data) => { addChatListeners = async (document, html, data) => {
html.querySelectorAll('.duality-action-damage').forEach(element => const message = data?.message ?? document.toObject(false);
element.addEventListener('click', event => this.onRollDamage(event, data.message))
);
html.querySelectorAll('.target-save').forEach(element =>
element.addEventListener('click', event => this.onRollSave(event, data.message))
);
html.querySelectorAll('.roll-all-save-button').forEach(element =>
element.addEventListener('click', event => this.onRollAllSave(event, data.message))
);
html.querySelectorAll('.simple-roll-button').forEach(element => 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('.healing-button').forEach(element =>
element.addEventListener('click', event => this.onHealing(event, data.message))
); );
html.querySelectorAll('.ability-use-button').forEach(element => 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 => 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 => 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, message))
);
html.querySelectorAll('.group-roll-reroll').forEach(element =>
element.addEventListener('click', event => this.groupRollReroll(event, message))
);
html.querySelectorAll('.group-roll-success').forEach(element =>
element.addEventListener('click', event => this.groupRollSuccessEvent(event, message))
);
html.querySelectorAll('.group-roll-header-expand-section').forEach(element =>
element.addEventListener('click', this.groupRollExpandSection)
); );
}; };
@ -90,80 +92,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
super.close(options); super.close(options);
} }
async getActor(uuid) {
return await foundry.utils.fromUuid(uuid);
}
getAction(actor, itemId, actionId) {
const item = actor.items.get(itemId),
action =
actor.system.attack?._id === actionId
? actor.system.attack
: item.system.attack?._id === actionId
? item.system.attack
: item?.system?.actions?.get(actionId);
return action;
}
async onRollDamage(event, message) {
event.stopPropagation();
const actor = await this.getActor(message.system.source.actor);
if(!actor.isOwner) return true;
if (message.system.source.item && message.system.source.action) {
const action = this.getAction(actor, message.system.source.item, message.system.source.action);
if (!action || !action?.rollDamage) return;
await action.rollDamage(event, message);
}
}
async onRollSave(event, message) {
event.stopPropagation();
const actor = await this.getActor(message.system.source.actor),
tokenId = event.target.closest('[data-token]')?.dataset.token,
token = game.canvas.tokens.get(tokenId);
if (!token?.actor || !token.isOwner) return true;
if (message.system.source.item && message.system.source.action) {
const action = this.getAction(actor, message.system.source.item, message.system.source.action);
if (!action || !action?.hasSave) return;
action.rollSave(token.actor, event, message).then(result =>
emitAsGM(
GMUpdateEvent.UpdateSaveMessage,
action.updateSaveMessage.bind(action, result, message, token.id),
{
action: action.uuid,
message: message._id,
token: token.id,
result
}
)
);
}
}
async onRollAllSave(event, message) {
event.stopPropagation();
if (!game.user.isGM) return;
const targets = event.target.parentElement.querySelectorAll('[data-token] .target-save');
const actor = await this.getActor(message.system.source.actor),
action = this.getAction(actor, message.system.source.item, message.system.source.action);
targets.forEach(async el => {
const tokenId = el.closest('[data-token]')?.dataset.token,
token = game.canvas.tokens.get(tokenId);
if (!token.actor) return;
if (game.user === token.actor.owner) el.dispatchEvent(new PointerEvent('click', { shiftKey: true }));
else {
token.actor.owner
.query('reactionRoll', {
actionId: action.uuid,
actorId: token.actor.uuid,
event,
message
})
.then(result => action.updateSaveMessage(result, message, token.id));
}
});
}
async onRollSimple(event, message) { async onRollSimple(event, message) {
const buttonType = event.target.dataset.type ?? 'damage', const buttonType = event.target.dataset.type ?? 'damage',
total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0), total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0),
@ -197,17 +125,32 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
item.system.attack?.id === event.currentTarget.id item.system.attack?.id === event.currentTarget.id
? item.system.attack ? item.system.attack
: item.system.actions.get(event.currentTarget.id); : item.system.actions.get(event.currentTarget.id);
if (event.currentTarget.dataset.directDamage) action.use(event, { byPassRoll: true }); if (event.currentTarget.dataset.directDamage) {
else action.use(event); const config = action.prepareConfig(event);
config.hasRoll = false;
action.workflow.get('damage').execute(config, null, true);
} else action.use(event);
} }
async actionUseButton(event, message) { async actionUseButton(event, message) {
const { moveIndex, actionIndex } = event.currentTarget.dataset; 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 actionType = message.system.moves[moveIndex].actions[actionIndex];
const cls = game.system.api.models.actions.actionsTypes[actionType.type]; const cls = game.system.api.models.actions.actionsTypes[actionType.type];
const action = new cls( const action = new cls(
{ ...actionType, _id: foundry.utils.randomID(), name: game.i18n.localize(actionType.name) }, {
...actionType,
_id: foundry.utils.randomID(),
name: game.i18n.localize(actionType.name),
originItem: {
type: CONFIG.DH.ITEM.originItemType.restMove,
itemPath: movePath,
actionIndex: actionIndex
},
targetUuid: targetUuid
},
{ parent: parent.system } { parent: parent.system }
); );
@ -249,6 +192,182 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
'system.roll': newRoll, 'system.roll': newRoll,
'rolls': [parsedRoll] 'rolls': [parsedRoll]
}); });
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
} }
});
}
}
async groupRollButton(event, message) {
const path = event.currentTarget.dataset.path;
const isLeader = path === 'leader';
const { actor: actorData, trait } = foundry.utils.getProperty(message.system, path);
const actor = game.actors.get(actorData._id);
if (!actor) {
return ui.notifications.error(
game.i18n.format('DAGGERHEART.UI.Notifications.documentIsMissing', {
documentType: game.i18n.localize('TYPES.Actor.character')
})
);
}
if (!actor.testUserPermission(game.user, 'OWNER')) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.noActorOwnership'));
}
const traitLabel = game.i18n.localize(abilities[trait].label);
const config = {
event: event,
title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`,
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: traitLabel
}),
roll: {
trait: trait,
advantage: 0,
modifiers: [{ label: traitLabel, value: actor.system.traits[trait].value }]
},
hasRoll: true,
skips: {
createMessage: true,
resources: !isLeader,
updateCountdowns: !isLeader
}
};
const result = await actor.diceRoll({
...config,
headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`,
title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: traitLabel
})
});
if (!result) return;
const newMessageData = foundry.utils.deepClone(message.system);
foundry.utils.setProperty(newMessageData, `${path}.result`, result.roll);
const renderData = { system: new game.system.api.models.chatMessages.config.groupRoll(newMessageData) };
const updatedContent = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/groupRoll.hbs',
{ ...renderData, user: game.user }
);
const mess = game.messages.get(message._id);
await emitAsGM(
GMUpdateEvent.UpdateDocument,
mess.update.bind(mess),
{
...renderData,
content: updatedContent
},
mess.uuid
);
}
async groupRollReroll(event, message) {
const path = event.currentTarget.dataset.path;
const { actor: actorData, trait } = foundry.utils.getProperty(message.system, path);
const actor = game.actors.get(actorData._id);
if (!actor.testUserPermission(game.user, 'OWNER')) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.noActorOwnership'));
}
const traitLabel = game.i18n.localize(abilities[trait].label);
const config = {
event: event,
title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`,
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: traitLabel
}),
roll: {
trait: trait,
advantage: 0,
modifiers: [{ label: traitLabel, value: actor.system.traits[trait].value }]
},
hasRoll: true,
skips: {
createMessage: true,
updateCountdowns: true
}
};
const result = await actor.diceRoll({
...config,
headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`,
title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: traitLabel
})
});
const newMessageData = foundry.utils.deepClone(message.system);
foundry.utils.setProperty(newMessageData, `${path}.result`, { ...result.roll, rerolled: true });
const renderData = { system: new game.system.api.models.chatMessages.config.groupRoll(newMessageData) };
const updatedContent = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/groupRoll.hbs',
{ ...renderData, user: game.user }
);
const mess = game.messages.get(message._id);
await emitAsGM(
GMUpdateEvent.UpdateDocument,
mess.update.bind(mess),
{
...renderData,
content: updatedContent
},
mess.uuid
);
}
async groupRollSuccessEvent(event, message) {
if (!game.user.isGM) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmOnly'));
}
const { path, success } = event.currentTarget.dataset;
const { actor: actorData } = foundry.utils.getProperty(message.system, path);
const actor = game.actors.get(actorData._id);
if (!actor.testUserPermission(game.user, 'OWNER')) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.noActorOwnership'));
}
const newMessageData = foundry.utils.deepClone(message.system);
foundry.utils.setProperty(newMessageData, `${path}.manualSuccess`, Boolean(success));
const renderData = { system: new game.system.api.models.chatMessages.config.groupRoll(newMessageData) };
const updatedContent = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/groupRoll.hbs',
{ ...renderData, user: game.user }
);
const mess = game.messages.get(message._id);
await emitAsGM(
GMUpdateEvent.UpdateDocument,
mess.update.bind(mess),
{
...renderData,
content: updatedContent
},
mess.uuid
);
}
async groupRollExpandSection(event) {
event.target
.closest('.group-roll-header-expand-section')
.querySelectorAll('i')
.forEach(element => {
element.classList.toggle('fa-angle-up');
element.classList.toggle('fa-angle-down');
});
event.target.closest('.group-roll-section').querySelector('.group-roll-content').classList.toggle('closed');
} }
} }

View file

@ -1,12 +1,11 @@
import { EncounterCountdowns } from '../ui/countdowns.mjs'; import { AdversaryBPPerEncounter } from '../../config/encounterConfig.mjs';
export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker { export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
actions: { actions: {
requestSpotlight: this.requestSpotlight, requestSpotlight: this.requestSpotlight,
toggleSpotlight: this.toggleSpotlight, toggleSpotlight: this.toggleSpotlight,
setActionTokens: this.setActionTokens, setActionTokens: this.setActionTokens
openCountdowns: this.openCountdowns
} }
}; };
@ -22,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) { async _prepareCombatContext(context, options) {
await super._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, { 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 }
}); });
} }
@ -35,11 +56,27 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
const adversaries = context.turns?.filter(x => x.isNPC) ?? []; const adversaries = context.turns?.filter(x => x.isNPC) ?? [];
const characters = 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 && 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, { Object.assign(context, {
actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens, actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens,
adversaries, adversaries,
characters characters: characters
?.filter(x => !x.isNPC)
.filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0),
spotlightRequests
}); });
} }
@ -90,6 +127,7 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
resource, resource,
active: index === combat.turn, active: index === combat.turn,
canPing: combatant.sceneId === canvas.scene?.id && game.user.hasPermission('PING_CANVAS'), canPing: combatant.sceneId === canvas.scene?.id && game.user.hasPermission('PING_CANVAS'),
type: combatant.actor?.system?.type,
img: await this._getCombatantThumbnail(combatant) img: await this._getCombatantThumbnail(combatant)
}; };
@ -114,7 +152,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
async setCombatantSpotlight(combatantId) { async setCombatantSpotlight(combatantId) {
const update = { const update = {
system: { system: {
'spotlight.requesting': false 'spotlight.requesting': false,
'spotlight.requestOrderIndex': 0
} }
}; };
const combatant = this.viewed.combatants.get(combatantId); const combatant = this.viewed.combatants.get(combatantId);
@ -126,7 +165,14 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
if (this.viewed.turn !== toggleTurn) { if (this.viewed.turn !== toggleTurn) {
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns; const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
await updateCountdowns(CONFIG.DH.GENERAL.countdownTypes.spotlight.id); if (combatant.actor?.type === 'character') {
await updateCountdowns(
CONFIG.DH.GENERAL.countdownProgressionTypes.spotlight.id,
CONFIG.DH.GENERAL.countdownProgressionTypes.characterSpotlight.id
);
} else {
await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.spotlight.id);
}
const autoPoints = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).actionPoints; const autoPoints = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).actionPoints;
if (autoPoints) { if (autoPoints) {
@ -142,11 +188,15 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
} }
static async requestSpotlight(_, target) { static async requestSpotlight(_, target) {
const characters = this.viewed.turns?.filter(x => !x.isNPC) ?? [];
const orderValues = characters.map(character => character.system.spotlight.requestOrderIndex);
const maxRequestIndex = Math.max(...orderValues);
const { combatantId } = target.closest('[data-combatant-id]')?.dataset ?? {}; const { combatantId } = target.closest('[data-combatant-id]')?.dataset ?? {};
const combatant = this.viewed.combatants.get(combatantId); const combatant = this.viewed.combatants.get(combatantId);
await combatant.update({ await combatant.update({
'system.spotlight': { 'system.spotlight': {
requesting: !combatant.system.spotlight.requesting requesting: !combatant.system.spotlight.requesting,
requestOrderIndex: !combatant.system.spotlight.requesting ? maxRequestIndex + 1 : 0
} }
}); });
@ -168,8 +218,4 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
await combatant.update({ 'system.actionTokens': newIndex }); await combatant.update({ 'system.actionTokens': newIndex });
this.render(); this.render();
} }
static openCountdowns() {
new EncounterCountdowns().open();
}
} }

View file

@ -0,0 +1,238 @@
import { DhCountdown } from '../../data/countdowns.mjs';
import { waitForDiceSoNice } from '../../helpers/utils.mjs';
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class CountdownEdit extends HandlebarsApplicationMixin(ApplicationV2) {
constructor() {
super();
this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
this.editingCountdowns = new Set();
this.currentEditCountdown = null;
this.hideNewCountdowns = false;
}
static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'dialog', 'dh-style', 'countdown-edit'],
tag: 'form',
position: { width: 600 },
window: {
title: 'DAGGERHEART.APPLICATIONS.CountdownEdit.title',
icon: 'fa-solid fa-clock-rotate-left'
},
actions: {
addCountdown: CountdownEdit.#addCountdown,
toggleCountdownEdit: CountdownEdit.#toggleCountdownEdit,
editCountdownImage: CountdownEdit.#editCountdownImage,
editCountdownOwnership: CountdownEdit.#editCountdownOwnership,
randomiseCountdownStart: CountdownEdit.#randomiseCountdownStart,
removeCountdown: CountdownEdit.#removeCountdown
},
form: { handler: this.updateData, submitOnChange: true }
};
static PARTS = {
countdowns: {
template: 'systems/daggerheart/templates/ui/countdown-edit.hbs',
scrollable: ['.expanded-view', '.edit-content']
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.isGM = game.user.isGM;
context.ownershipDefaultOptions = CONFIG.DH.GENERAL.basicOwnershiplevels;
context.defaultOwnership = this.data.defaultOwnership;
context.countdownBaseTypes = CONFIG.DH.GENERAL.countdownBaseTypes;
context.countdownProgressionTypes = CONFIG.DH.GENERAL.countdownProgressionTypes;
context.countdownLoopingTypes = CONFIG.DH.GENERAL.countdownLoopingTypes;
context.hideNewCountdowns = this.hideNewCountdowns;
context.countdowns = Object.keys(this.data.countdowns).reduce((acc, key) => {
const countdown = this.data.countdowns[key];
const isLooping = countdown.progress.looping !== CONFIG.DH.GENERAL.countdownLoopingTypes.noLooping;
const loopTooltip = isLooping
? countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id
? 'DAGGERHEART.UI.Countdowns.increasingLoop'
: countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id
? 'DAGGERHEART.UI.Countdowns.decreasingLoop'
: 'DAGGERHEART.UI.Countdowns.loop'
: null;
const randomizeValid = !new Roll(countdown.progress.startFormula ?? '').isDeterministic;
acc[key] = {
...countdown,
typeName: game.i18n.localize(CONFIG.DH.GENERAL.countdownBaseTypes[countdown.type].label),
progress: {
...countdown.progress,
typeName: game.i18n.localize(
CONFIG.DH.GENERAL.countdownProgressionTypes[countdown.progress.type].label
)
},
editing: this.editingCountdowns.has(key),
randomizeValid,
loopTooltip
};
return acc;
}, {});
return context;
}
/** @override */
async _postRender(_context, _options) {
if (this.currentEditCountdown) {
setTimeout(() => {
const input = this.element.querySelector(
`.countdown-edit-container[data-id="${this.currentEditCountdown}"] input`
);
if (input) {
input.select();
this.currentEditCountdown = null;
}
}, 100);
}
}
canPerformEdit() {
if (game.user.isGM) return true;
if (!game.users.activeGM) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmRequired'));
return false;
}
return true;
}
async updateSetting(update) {
const noGM = !game.users.find(x => x.isGM && x.active);
if (noGM) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmRequired'));
return;
}
await this.data.updateSource(update);
await emitAsGM(GMUpdateEvent.UpdateCountdowns, this.gmSetSetting.bind(this.data), this.data, null, {
refreshType: RefreshType.Countdown
});
this.render();
}
static async updateData(_event, _, formData) {
const { hideNewCountdowns, ...settingsData } = foundry.utils.expandObject(formData.object);
// Sync current and max if max is changing and they were equal before
for (const [id, countdown] of Object.entries(settingsData.countdowns ?? {})) {
const existing = this.data.countdowns[id];
countdown.progress.current = this.getMatchingCurrentValue(
existing,
countdown.progress.start,
countdown.progress.current
);
}
this.hideNewCountdowns = hideNewCountdowns;
this.updateSetting(settingsData);
}
getMatchingCurrentValue(oldCount, newStart, newCurrent) {
const wasEqual = oldCount && oldCount.progress.current === oldCount.progress.start;
if (wasEqual && newStart !== oldCount.progress.start) {
return newStart;
} else {
return Math.min(newCurrent, newStart);
}
}
async gmSetSetting(data) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data),
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: { refreshType: RefreshType.Countdown }
});
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
}
static #addCountdown() {
const id = foundry.utils.randomID();
this.editingCountdowns.add(id);
this.currentEditCountdown = id;
this.updateSetting({
[`countdowns.${id}`]: DhCountdown.defaultCountdown(null, this.hideNewCountdowns)
});
}
static #editCountdownImage(_, target) {
const countdown = this.data.countdowns[target.id];
const fp = new foundry.applications.apps.FilePicker.implementation({
current: countdown.img,
type: 'image',
callback: async path => this.updateSetting({ [`countdowns.${target.id}.img`]: path }),
top: this.position.top + 40,
left: this.position.left + 10
});
return fp.browse();
}
static #toggleCountdownEdit(_, button) {
const { countdownId } = button.dataset;
const isEditing = this.editingCountdowns.has(countdownId);
if (isEditing) this.editingCountdowns.delete(countdownId);
else {
this.editingCountdowns.add(countdownId);
this.currentEditCountdown = countdownId;
}
this.render();
}
static async #editCountdownOwnership(_, button) {
const countdown = this.data.countdowns[button.dataset.countdownId];
const data = await game.system.api.applications.dialogs.OwnershipSelection.configure(
countdown.name,
countdown.ownership,
this.data.defaultOwnership
);
if (!data) return;
this.updateSetting({ [`countdowns.${button.dataset.countdownId}`]: data });
}
static async #randomiseCountdownStart(_, button) {
const countdown = this.data.countdowns[button.dataset.countdownId];
const roll = await new Roll(countdown.progress.startFormula).roll();
const message = await roll.toMessage({ title: 'Countdown' });
await waitForDiceSoNice(message);
await this.updateSetting({
[`countdowns.${button.dataset.countdownId}.progress`]: {
start: roll.total,
current: this.getMatchingCurrentValue(countdown, roll.total, countdown.progress.current)
}
});
this.render();
}
static async #removeCountdown(event, button) {
const { countdownId } = button.dataset;
if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.CountdownEdit.removeCountdownTitle')
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.CountdownEdit.removeCountdownText', {
name: this.data.countdowns[countdownId].name
})
});
if (!confirmed) return;
}
if (this.editingCountdowns.has(countdownId)) this.editingCountdowns.delete(countdownId);
this.updateSetting({ [`countdowns.-=${countdownId}`]: null });
}
}

View file

@ -1,355 +1,295 @@
import { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import { waitForDiceSoNice } from '../../helpers/utils.mjs';
import constructHTMLButton from '../../helpers/utils.mjs'; import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import OwnershipSelection from '../dialogs/ownershipSelection.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) { /**
constructor(basePath) { * A UI element which displays the countdowns in this world.
super({}); *
* @extends ApplicationV2
* @mixes HandlebarsApplication
*/
this.basePath = basePath; export default class DhCountdowns extends HandlebarsApplicationMixin(ApplicationV2) {
} constructor(options = {}) {
super(options);
get title() {
return game.i18n.format('DAGGERHEART.APPLICATIONS.Countdown.title', { this.setupHooks();
type: game.i18n.localize(`DAGGERHEART.APPLICATIONS.Countdown.types.${this.basePath}`)
});
} }
/** @inheritDoc */
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'dh-style', 'countdown'], id: 'countdowns',
tag: 'form', tag: 'div',
position: { width: 740, height: 700 }, classes: ['daggerheart', 'dh-style', 'countdowns', 'faded-ui'],
window: { window: {
icon: 'fa-solid fa-clock-rotate-left',
frame: true, frame: true,
title: 'Countdowns', title: 'DAGGERHEART.UI.Countdowns.title',
resizable: true, positioned: false,
resizable: false,
minimizable: false minimizable: false
}, },
actions: { actions: {
addCountdown: this.addCountdown, toggleViewMode: DhCountdowns.#toggleViewMode,
removeCountdown: this.removeCountdown, editCountdowns: DhCountdowns.#editCountdowns,
editImage: this.onEditImage, loopCountdown: DhCountdowns.#loopCountdown,
openOwnership: this.openOwnership, decreaseCountdown: (_, target) => this.editCountdown(false, target),
openCountdownOwnership: this.openCountdownOwnership, increaseCountdown: (_, target) => this.editCountdown(true, target)
toggleSimpleView: this.toggleSimpleView
}, },
form: { handler: this.updateData, submitOnChange: true } position: {
width: 400,
height: 222,
top: 50
}
}; };
/** @override */
static PARTS = { static PARTS = {
countdowns: { resources: {
template: 'systems/daggerheart/templates/ui/countdowns.hbs', root: true,
scrollable: ['.expanded-view'] template: 'systems/daggerheart/templates/ui/countdowns.hbs'
} }
}; };
_attachPartListeners(partId, htmlElement, options) { get element() {
super._attachPartListeners(partId, htmlElement, options); return document.body.querySelector('.daggerheart.dh-style.countdowns');
htmlElement.querySelectorAll('.mini-countdown-container').forEach(element => {
element.addEventListener('click', event => this.updateCountdownValue.bind(this)(event, false));
element.addEventListener('contextmenu', event => this.updateCountdownValue.bind(this)(event, true));
});
}
async _preFirstRender(context, options) {
options.position =
game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].position) ??
Countdowns.DEFAULT_OPTIONS.position;
const viewSetting =
game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].simple) ?? !game.user.isGM;
this.simpleView =
game.user.isGM || !this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER) ? viewSetting : true;
context.simple = this.simpleView;
}
_onPosition(position) {
game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].position, position);
} }
/**@inheritdoc */
async _renderFrame(options) { async _renderFrame(options) {
const frame = await super._renderFrame(options); const frame = await super._renderFrame(options);
if (this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER)) { const iconOnly =
const button = constructHTMLButton({ game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode) ===
label: '', CONFIG.DH.GENERAL.countdownAppMode.iconOnly;
classes: ['header-control', 'icon', 'fa-solid', 'fa-wrench'], if (iconOnly) frame.classList.add('icon-only');
dataset: { action: 'toggleSimpleView', tooltip: 'DAGGERHEART.APPLICATIONS.Countdown.toggleSimple' } else frame.classList.remove('icon-only');
});
this.window.controls.after(button); const header = frame.querySelector('.window-header');
header.querySelector('button[data-action="close"]').remove();
if (game.user.isGM) {
const editTooltip = game.i18n.localize('DAGGERHEART.APPLICATIONS.CountdownEdit.editTitle');
const editButton = `<a style="margin-right: 8px;" class="header-control" data-tooltip="${editTooltip}" aria-label="${editTooltip}" data-action="editCountdowns"><i class="fa-solid fa-wrench"></i></a>`;
header.insertAdjacentHTML('beforeEnd', editButton);
} }
const minimizeTooltip = game.i18n.localize('DAGGERHEART.UI.Countdowns.toggleIconMode');
const minimizeButton = `<a class="header-control" data-tooltip="${minimizeTooltip}" aria-label="${minimizeTooltip}" data-action="toggleViewMode"><i class="fa-solid fa-down-left-and-up-right-to-center"></i></a>`;
header.insertAdjacentHTML('beforeEnd', minimizeButton);
return frame; return frame;
} }
testUserPermission(level, exact, altSettings) { /** Returns countdown data filtered by ownership */
if (game.user.isGM) return true; #getCountdowns() {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const settings = const values = Object.entries(setting.countdowns).map(([key, countdown]) => ({
altSettings ?? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath]; key,
const defaultAllowed = exact ? settings.ownership.default === level : settings.ownership.default >= level; countdown,
const userAllowed = exact ownership: DhCountdowns.#getPlayerOwnership(game.user, setting, countdown)
? settings.playerOwnership[game.user.id]?.value === level }));
: settings.playerOwnership[game.user.id]?.value >= level; return values.filter(v => v.ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE);
return defaultAllowed || userAllowed;
} }
async _prepareContext(_options) { /** @override */
const context = await super._prepareContext(_options); async _prepareContext(options) {
const countdownData = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[ const context = await super._prepareContext(options);
this.basePath
];
context.isGM = game.user.isGM; context.isGM = game.user.isGM;
context.base = this.basePath;
context.canCreate = this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER, true); context.iconOnly =
context.source = { game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode) ===
...countdownData, CONFIG.DH.GENERAL.countdownAppMode.iconOnly;
countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => { const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const countdown = countdownData.countdowns[key]; context.countdowns = this.#getCountdowns().reduce((acc, { key, countdown, ownership }) => {
const playersWithAccess = game.users.reduce((acc, user) => {
const ownership = DhCountdowns.#getPlayerOwnership(user, setting, countdown);
if (!user.isGM && ownership && ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) {
acc.push(user);
}
return acc;
}, []);
const nonGmPlayers = game.users.filter(x => !x.isGM);
const countdownEditable = game.user.isGM || ownership === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER;
const isLooping = countdown.progress.looping !== CONFIG.DH.GENERAL.countdownLoopingTypes.noLooping;
const loopTooltip = isLooping
? countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id
? 'DAGGERHEART.UI.Countdowns.increasingLoop'
: countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id
? 'DAGGERHEART.UI.Countdowns.decreasingLoop'
: 'DAGGERHEART.UI.Countdowns.loop'
: null;
const loopDisabled =
!countdownEditable ||
(isLooping && (countdown.progress.current > 0 || countdown.progress.start === '0'));
if (this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.LIMITED, false, countdown)) {
acc[key] = { acc[key] = {
...countdown, ...countdown,
canEdit: this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER, true, countdown) editable: countdownEditable,
noPlayerAccess: nonGmPlayers.length && playersWithAccess.length === 0,
shouldLoop: isLooping && countdown.progress.current === 0 && countdown.progress.start > 0,
loopDisabled: isLooping ? loopDisabled : null,
loopTooltip: isLooping && game.i18n.localize(loopTooltip)
}; };
}
return acc; return acc;
}, {}) }, {});
};
context.systemFields = countdownData.schema.fields;
context.countdownFields = context.systemFields.countdowns.element.fields;
context.simple = this.simpleView;
return context; return context;
} }
static async updateData(event, _, formData) { static #getPlayerOwnership(user, setting, countdown) {
const data = foundry.utils.expandObject(formData.object); const playerOwnership = countdown.ownership[user.id];
const newSetting = foundry.utils.mergeObject( return playerOwnership === undefined || playerOwnership === CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns).toObject(), ? setting.defaultOwnership
data : playerOwnership;
); }
if (game.user.isGM) { cooldownRefresh = ({ refreshType }) => {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, newSetting); if (refreshType === RefreshType.Countdown) this.render();
};
static canPerformEdit() {
if (game.user.isGM) return true;
const noGM = !game.users.find(x => x.isGM && x.active);
if (noGM) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmRequired'));
return false;
}
return true;
}
static async #toggleViewMode() {
const currentMode = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode);
const appMode = CONFIG.DH.GENERAL.countdownAppMode;
const newMode = currentMode === appMode.textIcon ? appMode.iconOnly : appMode.textIcon;
await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode, newMode);
if (newMode === appMode.iconOnly) this.element.classList.add('icon-only');
else this.element.classList.remove('icon-only');
this.render(); this.render();
} else { }
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate, static async #editCountdowns() {
data: { new game.system.api.applications.ui.CountdownEdit().render(true);
action: GMUpdateEvent.UpdateSetting, }
uuid: CONFIG.DH.SETTINGS.gameSettings.Countdowns,
update: newSetting static async #loopCountdown(_, target) {
if (!DhCountdowns.canPerformEdit()) return;
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const countdown = settings.countdowns[target.id];
let progressMax = countdown.progress.start;
let message = null;
if (countdown.progress.startFormula) {
const roll = await new Roll(countdown.progress.startFormula).evaluate();
progressMax = roll.total;
message = await roll.toMessage();
}
const newMax =
countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id
? Number(progressMax) + 1
: countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id
? Math.max(Number(progressMax) - 1, 0)
: progressMax;
await waitForDiceSoNice(message);
await settings.updateSource({
[`countdowns.${target.id}.progress`]: {
current: newMax,
start: newMax
} }
}); });
} await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
refreshType: RefreshType.Countdown
});
} }
async updateSetting(update) { static async editCountdown(increase, target) {
if (game.user.isGM) { if (!DhCountdowns.canPerformEdit()) return;
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, update);
await game.socket.emit(`system.${CONFIG.DH.id}`, { const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const countdown = settings.countdowns[target.id];
const newCurrent = increase
? Math.min(countdown.progress.current + 1, countdown.progress.start)
: Math.max(countdown.progress.current - 1, 0);
await settings.updateSource({ [`countdowns.${target.id}.progress.current`]: newCurrent });
await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
refreshType: RefreshType.Countdown
});
}
static async gmSetSetting(data) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data),
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh, action: socketEvent.Refresh,
data: { data: { refreshType: RefreshType.Countdown }
refreshType: RefreshType.Countdown,
application: `${this.basePath}-countdowns`
}
}); });
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
this.render();
} else {
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateSetting,
uuid: CONFIG.DH.SETTINGS.gameSettings.Countdowns,
update: update,
refresh: { refreshType: RefreshType.Countdown, application: `${this.basePath}-countdowns` }
}
});
}
} }
static onEditImage(_, target) { setupHooks() {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath]; Hooks.on(socketEvent.Refresh, this.cooldownRefresh.bind());
const current = setting.countdowns[target.dataset.countdown].img;
const fp = new foundry.applications.apps.FilePicker.implementation({
current,
type: 'image',
callback: async path => this.updateImage.bind(this)(path, target.dataset.countdown),
top: this.position.top + 40,
left: this.position.left + 10
});
return fp.browse();
} }
async updateImage(path, countdown) { async close(options) {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); /* Opt out of Foundry's standard behavior of closing all application windows marked as UI when Escape is pressed */
await setting.updateSource({ if (options.closeKey) return;
[`${this.basePath}.countdowns.${countdown}.img`]: path
});
await this.updateSetting(setting); Hooks.off(socketEvent.Refresh, this.cooldownRefresh);
return super.close(options);
} }
static openOwnership(_, target) { /**
new Promise((resolve, reject) => { * Sends updates of the countdowns to the GM player. Since this is asynchronous, be sure to
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath]; * update all the countdowns at the same time.
const ownership = { default: setting.ownership.default, players: setting.playerOwnership }; *
new OwnershipSelection(resolve, reject, this.title, ownership).render(true); * @param {...any} progressTypes Countdowns to be updated
}).then(async ownership => { */
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); static async updateCountdowns(...progressTypes) {
await setting.updateSource({ const { countdownAutomation } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
[`${this.basePath}.ownership`]: ownership if (!countdownAutomation) return;
});
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, setting.toObject());
this.render();
});
}
static openCountdownOwnership(_, target) {
const countdownId = target.dataset.countdown;
new Promise((resolve, reject) => {
const countdown = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath]
.countdowns[countdownId];
const ownership = { default: countdown.ownership.default, players: countdown.playerOwnership };
new OwnershipSelection(resolve, reject, countdown.name, ownership).render(true);
}).then(async ownership => {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
await setting.updateSource({
[`${this.basePath}.countdowns.${countdownId}.ownership`]: ownership
});
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, setting);
this.render();
});
}
static async toggleSimpleView() {
this.simpleView = !this.simpleView;
await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].simple, this.simpleView);
this.render();
}
async updateCountdownValue(event, increase) {
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const countdown = countdownSetting[this.basePath].countdowns[event.currentTarget.dataset.countdown]; const updatedCountdowns = Object.keys(countdownSetting.countdowns).reduce((acc, key) => {
const countdown = countdownSetting.countdowns[key];
if (!this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)) { if (progressTypes.indexOf(countdown.progress.type) !== -1 && countdown.progress.current > 0) {
return; acc.push(key);
}
const currentValue = countdown.progress.current;
if (increase && currentValue === countdown.progress.max) return;
if (!increase && currentValue === 0) return;
await countdownSetting.updateSource({
[`${this.basePath}.countdowns.${event.currentTarget.dataset.countdown}.progress.current`]: increase
? currentValue + 1
: currentValue - 1
});
await this.updateSetting(countdownSetting.toObject());
}
static async addCountdown() {
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
await countdownSetting.updateSource({
[`${this.basePath}.countdowns.${foundry.utils.randomID()}`]: {
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.newCountdown'),
ownership: game.user.isGM
? {}
: {
players: {
[game.user.id]: { type: CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER }
}
}
}
});
await this.updateSetting(countdownSetting.toObject());
}
static async removeCountdown(_, target) {
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const countdownName = countdownSetting[this.basePath].countdowns[target.dataset.countdown].name;
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.removeCountdownTitle')
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.Countdown.removeCountdownText', { name: countdownName })
});
if (!confirmed) return;
await countdownSetting.updateSource({ [`${this.basePath}.countdowns.-=${target.dataset.countdown}`]: null });
await this.updateSetting(countdownSetting.toObject());
}
async open() {
await this.render(true);
if (
Object.keys(
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath].countdowns
).length > 0
) {
this.minimize();
}
}
}
export class NarrativeCountdowns extends Countdowns {
constructor() {
super('narrative');
}
static DEFAULT_OPTIONS = {
id: 'narrative-countdowns'
};
}
export class EncounterCountdowns extends Countdowns {
constructor() {
super('encounter');
}
static DEFAULT_OPTIONS = {
id: 'encounter-countdowns'
};
}
export async function updateCountdowns(progressType) {
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const update = Object.keys(countdownSetting).reduce((update, typeKey) => {
return foundry.utils.mergeObject(
update,
Object.keys(countdownSetting[typeKey].countdowns).reduce((acc, countdownKey) => {
const countdown = countdownSetting[typeKey].countdowns[countdownKey];
if (countdown.progress.current > 0 && countdown.progress.type.value === progressType) {
acc[`${typeKey}.countdowns.${countdownKey}.progress.current`] = countdown.progress.current - 1;
} }
return acc;
}, []);
const countdownData = countdownSetting.toObject();
const settings = {
...countdownData,
countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => {
const countdown = foundry.utils.deepClone(countdownData.countdowns[key]);
if (updatedCountdowns.includes(key)) {
countdown.progress.current -= 1;
}
acc[key] = countdown;
return acc; return acc;
}, {}) }, {})
); };
}, {}); await emitAsGM(GMUpdateEvent.UpdateCountdowns,
DhCountdowns.gmSetSetting.bind(settings),
await countdownSetting.updateSource(update); settings, null, {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, countdownSetting); 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) {
await super._onRender(context, options);
this.element.hidden = !game.user.isGM && this.#getCountdowns().length === 0;
if (options?.force) {
document.getElementById('ui-right-column-1')?.appendChild(this.element);
}
}
} }

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

@ -1,4 +1,4 @@
import { emitAsGM, GMUpdateEvent, socketEvent } from "../../systemRegistration/socket.mjs"; import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -78,7 +78,7 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
/** @override */ /** @override */
async _preRender(context, options) { async _preRender(context, options) {
if (this.currentFear > this.maxFear) if (this.currentFear > this.maxFear && game.user.isGM)
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, this.maxFear); await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, this.maxFear);
} }
@ -106,19 +106,10 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
} }
async updateFear(value) { async updateFear(value) {
return emitAsGM(GMUpdateEvent.UpdateFear, game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), value); return emitAsGM(
/* if(!game.user.isGM) GMUpdateEvent.UpdateFear,
await game.socket.emit(`system.${CONFIG.DH.id}`, { game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
action: socketEvent.GMUpdate, value
data: { );
action: GMUpdateEvent.UpdateFear,
update: value
}
});
else
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, value); */
/* if (!game.user.isGM) return;
value = Math.max(0, Math.min(this.maxFear, value));
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, value); */
} }
} }

View file

@ -15,16 +15,14 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
this.fieldFilter = []; this.fieldFilter = [];
this.selectedMenu = { path: [], data: null }; this.selectedMenu = { path: [], data: null };
this.config = CONFIG.DH.ITEMBROWSER.compendiumConfig; this.config = CONFIG.DH.ITEMBROWSER.compendiumConfig;
this.presets = options.presets; this.presets = {};
this.compendiumBrowserTypeKey = 'compendiumBrowserDefault';
if (this.presets?.compendium && this.presets?.folder)
ItemBrowser.selectFolder.call(this, null, null, this.presets.compendium, this.presets.folder);
} }
/** @inheritDoc */ /** @inheritDoc */
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
id: 'itemBrowser', id: 'itemBrowser',
classes: ['daggerheart', 'dh-style', 'dialog', 'compendium-browser'], classes: ['daggerheart', 'dh-style', 'dialog', 'compendium-browser', 'daggerheart-loader'],
tag: 'div', tag: 'div',
window: { window: {
frame: true, frame: true,
@ -84,17 +82,29 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
} }
}; };
/** @inheritDoc */
async _preFirstRender(context, options) {
if (context.presets?.render?.noFolder || context.presets?.render?.lite) options.position.width = 600;
await super._preFirstRender(context, options);
}
/** @inheritDoc */ /** @inheritDoc */
async _preRender(context, options) { async _preRender(context, options) {
if (context.presets?.render?.noFolder || context.presets?.render?.lite) this.presets = options.presets ?? {};
options.parts.splice(options.parts.indexOf('sidebar'), 1); const noFolder = this.presets?.render?.noFolder;
if (noFolder === true) {
this.compendiumBrowserTypeKey = 'compendiumBrowserNoFolder';
}
const lite = this.presets?.render?.lite;
if (lite === true) {
this.compendiumBrowserTypeKey = 'compendiumBrowserLite';
}
const userPresetPosition = game.user.getFlag(
CONFIG.DH.id,
CONFIG.DH.FLAGS[`${this.compendiumBrowserTypeKey}`].position
);
options.position = userPresetPosition ?? ItemBrowser.DEFAULT_OPTIONS.position;
if (!userPresetPosition) {
const width = noFolder === true || lite === true ? 600 : 850;
if (this.rendered) this.setPosition({ width });
else options.position.width = width;
}
await super._preRender(context, options); await super._preRender(context, options);
} }
@ -103,32 +113,35 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
async _onRender(context, options) { async _onRender(context, options) {
await super._onRender(context, options); await super._onRender(context, options);
this._createSearchFilter(); this.element
this._createFilterInputs(); .querySelectorAll('[data-action="selectFolder"]')
this._createDragProcess(); .forEach(element =>
element.classList.toggle('is-selected', element.dataset.folderId === this.selectedMenu.path.join('.'))
if (context.presets?.render?.lite) this.element.classList.add('lite');
if (context.presets?.render?.noFolder) this.element.classList.add('no-folder');
if (context.presets?.render?.noFilter) this.element.classList.add('no-filter');
if (this.presets?.filter) {
Object.entries(this.presets.filter).forEach(
([k, v]) => (this.fieldFilter.find(c => c.name === k).value = v.value)
); );
await this._onInputFilterBrowser();
this._createSearchFilter();
this.element.classList.toggle('lite', this.presets?.render?.lite === true);
this.element.classList.toggle('no-folder', this.presets?.render?.noFolder === true);
this.element.classList.toggle('no-filter', this.presets?.render?.noFilter === true);
this.element.querySelectorAll('.folder-list > [data-action="selectFolder"]').forEach(element => {
element.hidden =
this.presets.render?.folders?.length && !this.presets.render.folders.includes(element.dataset.folderId);
});
} }
_onPosition(position) {
game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.compendiumBrowserTypeKey}`].position, position);
} }
_attachPartListeners(partId, htmlElement, options) { _attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options); super._attachPartListeners(partId, htmlElement, options);
htmlElement htmlElement.querySelectorAll('[data-action="selectFolder"]').forEach(element =>
.querySelectorAll('[data-action="selectFolder"]') element.addEventListener('contextmenu', event => {
.forEach(element => element.addEventListener("contextmenu", (event) => {
event.target.classList.toggle('expanded'); event.target.classList.toggle('expanded');
})) })
);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -139,22 +152,26 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
async _prepareContext(options) { async _prepareContext(options) {
const context = await super._prepareContext(options); const context = await super._prepareContext(options);
context.compendiums = this.getCompendiumFolders(foundry.utils.deepClone(this.config)); context.compendiums = this.getCompendiumFolders(foundry.utils.deepClone(this.config));
// context.pathTitle = this.pathTile;
context.menu = this.selectedMenu; context.menu = this.selectedMenu;
context.formatLabel = this.formatLabel; context.formatLabel = this.formatLabel;
context.formatChoices = this.formatChoices; context.formatChoices = this.formatChoices;
context.fieldFilter = this.fieldFilter = this._createFieldFilter();
context.items = this.items; context.items = this.items;
context.presets = this.presets; context.presets = this.presets;
return context; return context;
} }
open(presets = {}) {
this.presets = presets;
ItemBrowser.selectFolder.call(this);
}
getCompendiumFolders(config, parent = null, depth = 0) { getCompendiumFolders(config, parent = null, depth = 0) {
let folders = []; let folders = [];
Object.values(config).forEach(c => { Object.values(config).forEach(c => {
// if(this.presets.render?.folders?.length && !this.presets.render.folders.includes(c.id)) return;
const folder = { const folder = {
id: c.id, id: c.id,
label: c.label, label: game.i18n.localize(c.label),
selected: (!parent || parent.selected) && this.selectedMenu.path[depth] === c.id selected: (!parent || parent.selected) && this.selectedMenu.path[depth] === c.id
}; };
folder.folders = c.folders folder.folders = c.folders
@ -162,47 +179,108 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
: []; : [];
folders.push(folder); folders.push(folder);
}); });
folders.sort((a, b) => a.label.localeCompare(b.label));
return folders; return folders;
} }
static async selectFolder(_, target, compend, folder) { static async selectFolder(_, target) {
const config = foundry.utils.deepClone(this.config), const folderId = target?.dataset?.folderId ?? this.presets.folder,
compendium = compend ?? target.closest('[data-compendium-id]').dataset.compendiumId, folderData = foundry.utils.getProperty(this.config, folderId) ?? {};
folderId = folder ?? target.dataset.folderId,
folderPath = `${compendium}.folders.${folderId}`, const columns = ItemBrowser.getFolderConfig(folderData).map(col => ({
folderData = foundry.utils.getProperty(config, folderPath); ...col,
label: game.i18n.localize(col.label)
}));
this.selectedMenu = { this.selectedMenu = {
path: folderPath.split('.'), path: folderId?.split('.') ?? [],
data: { data: {
...folderData, ...folderData,
columns: ItemBrowser.getFolderConfig(folderData) columns: columns
} }
}; };
let items = []; await this.render({ force: true, presets: this.presets });
for (const key of folderData.keys) {
const comp = game.packs.get(`${compendium}.${key}`);
if (!comp) return;
items = items.concat(await comp.getDocuments({ type__in: folderData.type }));
}
this.items = ItemBrowser.sortBy(items, 'name'); if (this.selectedMenu?.data?.type?.length) this.loadItems();
if(target) {
target.closest('.compendium-sidebar').querySelectorAll('[data-action="selectFolder"]').forEach(element => element.classList.remove("is-selected"))
target.classList.add('is-selected');
}
this.render({ force: true });
} }
_replaceHTML(result, content, options) { _replaceHTML(result, content, options) {
if(!options.isFirstRender) delete result.sidebar; if (!options.isFirstRender) delete result.sidebar;
super._replaceHTML(result, content, options); super._replaceHTML(result, content, options);
} }
loadItems() {
let loadTimeout = this.toggleLoader(true);
const promises = [];
game.packs.forEach(pack => {
promises.push(
new Promise(async resolve => {
const items = await pack.getDocuments({ type__in: this.selectedMenu?.data?.type });
resolve(items);
})
);
});
Promise.all(promises).then(async result => {
this.items = ItemBrowser.sortBy(
result.flatMap(r => r),
'name'
);
this.fieldFilter = this._createFieldFilter();
if (this.presets?.filter) {
Object.entries(this.presets.filter).forEach(([k, v]) => {
const filter = this.fieldFilter.find(c => c.name === k);
if (filter) filter.value = v.value;
});
// await this._onInputFilterBrowser();
}
const filterList = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/itemBrowser/filterContainer.hbs',
{
fieldFilter: this.fieldFilter,
presets: this.presets,
formatChoices: this.formatChoices
}
);
this.element.querySelector('.filter-content .wrapper').innerHTML = filterList;
const filterContainer = this.element.querySelector('.filter-header > [data-action="expandContent"]');
if (this.fieldFilter.length === 0) filterContainer.setAttribute('disabled', '');
else filterContainer.removeAttribute('disabled');
const itemList = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/itemBrowser/itemContainer.hbs',
{
items: this.items,
menu: this.selectedMenu,
formatLabel: this.formatLabel
}
);
this.element.querySelector('.item-list').innerHTML = itemList;
this._createFilterInputs();
await this._onInputFilterBrowser();
this._createDragProcess();
clearTimeout(loadTimeout);
this.toggleLoader(false);
});
}
toggleLoader(state) {
const container = this.element.querySelector('.item-list');
return setTimeout(() => {
container.classList.toggle('daggerheart-loader', state);
}, 100);
}
static expandContent(_, target) { static expandContent(_, target) {
const parent = target.parentElement; const parent = target.parentElement;
parent.classList.toggle('expanded'); parent.classList.toggle('expanded');
@ -216,7 +294,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
const property = foundry.utils.getProperty(item, field.key); const property = foundry.utils.getProperty(item, field.key);
if (Array.isArray(property)) property.join(', '); if (Array.isArray(property)) property.join(', ');
if (typeof field.format !== 'function') return property ?? '-'; if (typeof field.format !== 'function') return property ?? '-';
return field.format(property); return game.i18n.localize(field.format(property));
} }
formatChoices(data) { formatChoices(data) {
@ -237,6 +315,12 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
else if (typeof f.choices === 'function') { else if (typeof f.choices === 'function') {
f.choices = f.choices(this.items); f.choices = f.choices(this.items);
} }
// Clear field label so template uses our custom label parameter
if (f.field && f.label) {
f.field.label = undefined;
}
f.name ??= f.key; f.name ??= f.key;
f.value = this.presets?.filter?.[f.name]?.value ?? null; f.value = this.presets?.filter?.[f.name]?.value ?? null;
}); });
@ -314,6 +398,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
for (const li of html.querySelectorAll('.item-container')) { for (const li of html.querySelectorAll('.item-container')) {
const itemUUID = li.dataset.itemUuid, const itemUUID = li.dataset.itemUuid,
item = this.items.find(i => i.uuid === itemUUID); item = this.items.find(i => i.uuid === itemUUID);
if (!item) continue;
const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name); const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name);
if (matchesSearch) this.#filteredItems.browser.search.add(item.id); if (matchesSearch) this.#filteredItems.browser.search.add(item.id);
const { input } = this.#filteredItems.browser; const { input } = this.#filteredItems.browser;
@ -345,7 +430,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
if (matchesMenu) this.#filteredItems.browser.input.add(item.id); if (matchesMenu) this.#filteredItems.browser.input.add(item.id);
const { search } = this.#filteredItems.browser; const { search } = this.#filteredItems.browser;
li.hidden = !(search.has(item.id) && matchesMenu); li.hidden = !((this.#search.browser.query.length === 0 || search.has(item.id)) && matchesMenu);
} }
} }
@ -384,6 +469,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
static resetFilters() { static resetFilters() {
this.render({ force: true }); this.render({ force: true });
this.loadItems();
} }
static getFolderConfig(folder, property = 'columns') { static getFolderConfig(folder, property = 'columns') {
@ -405,11 +491,13 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
const newOrder = [...itemList].reverse().sort((a, b) => { const newOrder = [...itemList].reverse().sort((a, b) => {
const aProp = a.querySelector(`[data-item-key="${key}"]`), const aProp = a.querySelector(`[data-item-key="${key}"]`),
bProp = b.querySelector(`[data-item-key="${key}"]`); bProp = b.querySelector(`[data-item-key="${key}"]`),
aValue = isNaN(aProp.innerText) ? aProp.innerText : Number(aProp.innerText),
bValue = isNaN(bProp.innerText) ? bProp.innerText : Number(bProp.innerText);
if (type === 'DESC') { if (type === 'DESC') {
return aProp.innerText < bProp.innerText ? 1 : -1; return aValue < bValue ? 1 : -1;
} else { } else {
return aProp.innerText > bProp.innerText ? 1 : -1; return aValue > bValue ? 1 : -1;
} }
}); });
@ -438,4 +526,41 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
_canDragStart() { _canDragStart() {
return true; return true;
} }
static injectSidebarButton(html) {
if (!game.user.isGM) return;
const sectionId = html.dataset.tab,
menus = {
actors: {
folder: 'adversaries',
render: {
folders: ['adversaries', 'characters', 'environments']
}
},
items: {
folder: 'equipments',
render: {
noFolder: true
}
},
compendium: {}
};
if (Object.keys(menus).includes(sectionId)) {
const headerActions = html.querySelector('.header-actions');
const button = document.createElement('button');
button.type = 'button';
button.classList.add('open-compendium-browser');
button.innerHTML = `
<i class="fa-solid fa-book-atlas"></i>
${game.i18n.localize('DAGGERHEART.UI.Tooltip.compendiumBrowser')}
`;
button.addEventListener('click', event => {
ui.compendiumBrowser.open(menus[sectionId]);
});
headerActions.append(button);
}
}
} }

View file

@ -10,29 +10,48 @@ export default class DhMeasuredTemplate extends foundry.canvas.placeables.Measur
const splitRulerText = this.ruler.text.split(' '); const splitRulerText = this.ruler.text.split(' ');
if (splitRulerText.length > 0) { if (splitRulerText.length > 0) {
const rulerValue = Number(splitRulerText[0]); const rulerValue = Number(splitRulerText[0]);
const vagueLabel = this.constructor.getDistanceLabel(rulerValue, rangeMeasurementSettings); const result = DhMeasuredTemplate.getRangeLabels(rulerValue, rangeMeasurementSettings);
this.ruler.text = vagueLabel; this.ruler.text = result.distance + (result.units ? ' ' + result.units : '');
} }
} }
} }
static getDistanceLabel(distance, settings) { static getRangeLabels(distanceValue, settings) {
if (distance <= settings.melee) { let result = { distance: distanceValue, units: '' };
return game.i18n.localize('DAGGERHEART.CONFIG.Range.melee.name'); const sceneRangeMeasurement = canvas.scene.flags.daggerheart?.rangeMeasurement;
}
if (distance <= settings.veryClose) { const { disable, custom } = CONFIG.DH.GENERAL.sceneRangeMeasurementSetting;
return game.i18n.localize('DAGGERHEART.CONFIG.Range.veryClose.name'); if (sceneRangeMeasurement?.setting === disable.id) {
} result.distance = distanceValue;
if (distance <= settings.close) { result.units = canvas.scene?.grid?.units;
return game.i18n.localize('DAGGERHEART.CONFIG.Range.close.name'); return result;
}
if (distance <= settings.far) {
return game.i18n.localize('DAGGERHEART.CONFIG.Range.far.name');
}
if (distance > settings.far) {
return game.i18n.localize('DAGGERHEART.CONFIG.Range.veryFar.name');
} }
return ''; const melee = sceneRangeMeasurement?.setting === custom.id ? sceneRangeMeasurement.melee : settings.melee;
const veryClose =
sceneRangeMeasurement?.setting === custom.id ? sceneRangeMeasurement.veryClose : settings.veryClose;
const close = sceneRangeMeasurement?.setting === custom.id ? sceneRangeMeasurement.close : settings.close;
const far = sceneRangeMeasurement?.setting === custom.id ? sceneRangeMeasurement.far : settings.far;
if (distanceValue <= melee) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.melee.name');
return result;
}
if (distanceValue <= veryClose) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.veryClose.name');
return result;
}
if (distanceValue <= close) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.close.name');
return result;
}
if (distanceValue <= far) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.far.name');
return result;
}
if (distanceValue > far) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.veryFar.name');
}
return result;
} }
} }

View file

@ -8,9 +8,9 @@ export default class DhpRuler extends foundry.canvas.interaction.Ruler {
const range = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement; const range = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement;
if (range.enabled) { if (range.enabled) {
const distance = DhMeasuredTemplate.getDistanceLabel(waypoint.measurement.distance.toNearest(0.01), range); const result = DhMeasuredTemplate.getRangeLabels(waypoint.measurement.distance.toNearest(0.01), range);
context.cost = { total: distance, units: null }; context.cost = { total: result.distance, units: result.units };
context.distance = { total: distance, units: null }; context.distance = { total: result.distance, units: result.units };
} }
return context; return context;

View file

@ -10,29 +10,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
this.effects.overlay = null; this.effects.overlay = null;
// Categorize effects // Categorize effects
const statusMap = new Map(foundry.CONFIG.statusEffects.map(status => [status.id, status])); const activeEffects = this.actor?.getActiveEffects() ?? [];
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,
tint: effect.tint
});
}
}
}
return acc;
}, []);
const overlayEffect = activeEffects.findLast(e => e.img && e.getFlag?.('core', 'overlay')); const overlayEffect = activeEffects.findLast(e => e.img && e.getFlag?.('core', 'overlay'));
// Draw effects // Draw effects
@ -56,6 +34,69 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
this.renderFlags.set({ refreshEffects: true }); 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 */ /** @inheritDoc */
_drawBar(number, bar, data) { _drawBar(number, bar, data) {
const val = Number(data.value); const val = Number(data.value);

View file

@ -8,9 +8,9 @@ export default class DhpTokenRuler extends foundry.canvas.placeables.tokens.Toke
const range = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement; const range = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement;
if (range.enabled) { if (range.enabled) {
const distance = DhMeasuredTemplate.getDistanceLabel(waypoint.measurement.distance.toNearest(0.01), range); const result = DhMeasuredTemplate.getRangeLabels(waypoint.measurement.distance.toNearest(0.01), range);
context.cost = { total: distance, units: null }; context.cost = { total: result.distance, units: result.units };
context.distance = { total: distance, units: null }; context.distance = { total: result.distance, units: result.units };
} }
return context; return context;

View file

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

View file

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

View file

@ -108,55 +108,72 @@ export const adversaryTypes = {
bruiser: { bruiser: {
id: 'bruiser', id: 'bruiser',
label: 'DAGGERHEART.CONFIG.AdversaryType.bruiser.label', label: 'DAGGERHEART.CONFIG.AdversaryType.bruiser.label',
description: 'DAGGERHEART.ACTORS.Adversary.bruiser.description' description: 'DAGGERHEART.ACTORS.Adversary.bruiser.description',
bpCost: 4
}, },
horde: { horde: {
id: 'horde', id: 'horde',
label: 'DAGGERHEART.CONFIG.AdversaryType.horde.label', label: 'DAGGERHEART.CONFIG.AdversaryType.horde.label',
description: 'DAGGERHEART.ACTORS.Adversary.horde.description' description: 'DAGGERHEART.ACTORS.Adversary.horde.description',
bpCost: 2
}, },
leader: { leader: {
id: 'leader', id: 'leader',
label: 'DAGGERHEART.CONFIG.AdversaryType.leader.label', 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: { minion: {
id: 'minion', id: 'minion',
label: 'DAGGERHEART.CONFIG.AdversaryType.minion.label', label: 'DAGGERHEART.CONFIG.AdversaryType.minion.label',
description: 'DAGGERHEART.ACTORS.Adversary.minion.description' description: 'DAGGERHEART.ACTORS.Adversary.minion.description',
bpCost: 1,
partyAmountPerBP: true
}, },
ranged: { ranged: {
id: 'ranged', id: 'ranged',
label: 'DAGGERHEART.CONFIG.AdversaryType.ranged.label', label: 'DAGGERHEART.CONFIG.AdversaryType.ranged.label',
description: 'DAGGERHEART.ACTORS.Adversary.ranged.description' description: 'DAGGERHEART.ACTORS.Adversary.ranged.description',
bpCost: 2
}, },
skulk: { skulk: {
id: 'skulk', id: 'skulk',
label: 'DAGGERHEART.CONFIG.AdversaryType.skulk.label', label: 'DAGGERHEART.CONFIG.AdversaryType.skulk.label',
description: 'DAGGERHEART.ACTORS.Adversary.skulk.description' description: 'DAGGERHEART.ACTORS.Adversary.skulk.description',
bpCost: 2
}, },
social: { social: {
id: 'social', id: 'social',
label: 'DAGGERHEART.CONFIG.AdversaryType.social.label', label: 'DAGGERHEART.CONFIG.AdversaryType.social.label',
description: 'DAGGERHEART.ACTORS.Adversary.social.description' description: 'DAGGERHEART.ACTORS.Adversary.social.description',
bpCost: 1
}, },
solo: { solo: {
id: 'solo', id: 'solo',
label: 'DAGGERHEART.CONFIG.AdversaryType.solo.label', label: 'DAGGERHEART.CONFIG.AdversaryType.solo.label',
description: 'DAGGERHEART.ACTORS.Adversary.solo.description' description: 'DAGGERHEART.ACTORS.Adversary.solo.description',
bpCost: 5
}, },
standard: { standard: {
id: 'standard', id: 'standard',
label: 'DAGGERHEART.CONFIG.AdversaryType.standard.label', label: 'DAGGERHEART.CONFIG.AdversaryType.standard.label',
description: 'DAGGERHEART.ACTORS.Adversary.standard.description' description: 'DAGGERHEART.ACTORS.Adversary.standard.description',
bpCost: 2
}, },
support: { support: {
id: 'support', id: 'support',
label: 'DAGGERHEART.CONFIG.AdversaryType.support.label', label: 'DAGGERHEART.CONFIG.AdversaryType.support.label',
description: 'DAGGERHEART.ACTORS.Adversary.support.description' description: 'DAGGERHEART.ACTORS.Adversary.support.description',
bpCost: 1
} }
}; };
export const allAdversaryTypes = () => ({
...adversaryTypes,
...game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).adversaryTypes
});
export const environmentTypes = { export const environmentTypes = {
exploration: { exploration: {
label: 'DAGGERHEART.CONFIG.EnvironmentType.exploration.label', label: 'DAGGERHEART.CONFIG.EnvironmentType.exploration.label',
@ -194,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 = { export const levelChoices = {
attributes: { attributes: {
name: '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

@ -1,4 +1,4 @@
export const displayDomainCardsAsList = 'displayDomainCardsAsList'; export const displayDomainCardsAsCard = 'displayDomainCardsAsCard';
export const narrativeCountdown = { export const narrativeCountdown = {
simple: 'countdown-narrative-simple', simple: 'countdown-narrative-simple',
position: 'countdown-narrative-position' position: 'countdown-narrative-position'
@ -8,8 +8,23 @@ export const encounterCountdown = {
position: 'countdown-encounter-position' position: 'countdown-encounter-position'
}; };
export const compendiumBrowserDefault = {
position: 'compendium-browser-default-position'
};
export const compendiumBrowserNoFolder = {
position: 'compendium-browser-no-folder-position'
};
export const compendiumBrowserLite = {
position: 'compendium-browser-lite-position'
};
export const itemAttachmentSource = 'attachmentSource'; export const itemAttachmentSource = 'attachmentSource';
export const userFlags = { export const userFlags = {
welcomeMessage: 'welcome-message' welcomeMessage: 'welcome-message',
countdownMode: 'countdown-mode'
}; };
export const combatToggle = 'combat-toggle-origin';

View file

@ -90,22 +90,22 @@ export const rangeInclusion = {
export const otherTargetTypes = { export const otherTargetTypes = {
friendly: { friendly: {
id: 'friendly', id: 'friendly',
label: 'Friendly' label: 'DAGGERHEART.CONFIG.TargetTypes.friendly'
}, },
hostile: { hostile: {
id: 'hostile', id: 'hostile',
label: 'Hostile' label: 'DAGGERHEART.CONFIG.TargetTypes.hostile'
}, },
any: { any: {
id: 'any', id: 'any',
label: 'Any' label: 'DAGGERHEART.CONFIG.TargetTypes.any'
} }
}; };
export const targetTypes = { export const targetTypes = {
self: { self: {
id: 'self', id: 'self',
label: 'Self' label: 'DAGGERHEART.CONFIG.TargetTypes.self'
}, },
...otherTargetTypes ...otherTargetTypes
}; };
@ -164,28 +164,36 @@ export const healingTypes = {
} }
}; };
export const defeatedConditions = { export const defeatedConditions = () => {
const defeated = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).defeated;
return Object.keys(defeatedConditionChoices).reduce((acc, key) => {
const choice = defeatedConditionChoices[key];
acc[key] = {
...choice,
img: defeated[`${choice.id}Icon`],
description: `DAGGERHEART.CONFIG.Condition.${choice.id}.description`
};
return acc;
}, {});
};
export const defeatedConditionChoices = {
defeated: { defeated: {
id: 'defeated', id: 'defeated',
name: 'DAGGERHEART.CONFIG.Condition.defeated.name', name: 'DAGGERHEART.CONFIG.Condition.defeated.name'
img: 'icons/magic/control/fear-fright-mask-orange.webp',
description: 'DAGGERHEART.CONFIG.Condition.defeated.description'
}, },
unconscious: { unconscious: {
id: 'unconscious', id: 'unconscious',
name: 'DAGGERHEART.CONFIG.Condition.unconscious.name', name: 'DAGGERHEART.CONFIG.Condition.unconscious.name'
img: 'icons/magic/control/sleep-bubble-purple.webp',
description: 'DAGGERHEART.CONFIG.Condition.unconscious.description'
}, },
dead: { dead: {
id: 'dead', id: 'dead',
name: 'DAGGERHEART.CONFIG.Condition.dead.name', name: 'DAGGERHEART.CONFIG.Condition.dead.name'
img: 'icons/magic/death/grave-tombstone-glow-teal.webp',
description: 'DAGGERHEART.CONFIG.Condition.dead.description'
} }
}; };
export const conditions = { export const conditions = () => ({
vulnerable: { vulnerable: {
id: 'vulnerable', id: 'vulnerable',
name: 'DAGGERHEART.CONFIG.Condition.vulnerable.name', name: 'DAGGERHEART.CONFIG.Condition.vulnerable.name',
@ -204,8 +212,8 @@ export const conditions = {
img: 'icons/magic/control/debuff-chains-shackle-movement-red.webp', img: 'icons/magic/control/debuff-chains-shackle-movement-red.webp',
description: 'DAGGERHEART.CONFIG.Condition.restrained.description' description: 'DAGGERHEART.CONFIG.Condition.restrained.description'
}, },
...defeatedConditions ...defeatedConditions()
}; });
export const defaultRestOptions = { export const defaultRestOptions = {
shortRest: () => ({ shortRest: () => ({
@ -224,7 +232,7 @@ export const defaultRestOptions = {
actionType: 'action', actionType: 'action',
chatDisplay: false, chatDisplay: false,
target: { target: {
type: 'self' type: 'friendly'
}, },
damage: { damage: {
parts: [ parts: [
@ -290,7 +298,7 @@ export const defaultRestOptions = {
actionType: 'action', actionType: 'action',
chatDisplay: false, chatDisplay: false,
target: { target: {
type: 'self' type: 'friendly'
}, },
damage: { damage: {
parts: [ parts: [
@ -333,7 +341,7 @@ export const defaultRestOptions = {
actionType: 'action', actionType: 'action',
chatDisplay: false, chatDisplay: false,
target: { target: {
type: 'self' type: 'friendly'
}, },
damage: { damage: {
parts: [ parts: [
@ -399,7 +407,7 @@ export const defaultRestOptions = {
actionType: 'action', actionType: 'action',
chatDisplay: false, chatDisplay: false,
target: { target: {
type: 'self' type: 'friendly'
}, },
damage: { damage: {
parts: [ parts: [
@ -561,6 +569,19 @@ export const refreshTypes = {
} }
}; };
export const itemAbilityCosts = {
resource: {
id: 'resource',
label: 'DAGGERHEART.GENERAL.resource',
group: 'Global'
},
quantity: {
id: 'quantity',
label: 'DAGGERHEART.GENERAL.quantity',
group: 'Global'
}
};
export const abilityCosts = { export const abilityCosts = {
hitPoints: { hitPoints: {
id: 'hitPoints', id: 'hitPoints',
@ -574,33 +595,46 @@ export const abilityCosts = {
}, },
hope: { hope: {
id: 'hope', id: 'hope',
label: 'Hope', label: 'DAGGERHEART.CONFIG.HealingType.hope.name',
group: 'TYPES.Actor.character' group: 'TYPES.Actor.character'
}, },
armor: { armor: {
id: 'armor', id: 'armor',
label: 'Armor Slot', label: 'DAGGERHEART.CONFIG.HealingType.armor.name',
group: 'TYPES.Actor.character' group: 'TYPES.Actor.character'
}, },
fear: { fear: {
id: 'fear', id: 'fear',
label: 'Fear', label: 'DAGGERHEART.CONFIG.HealingType.fear.name',
group: 'TYPES.Actor.adversary' group: 'TYPES.Actor.adversary'
} },
resource: itemAbilityCosts.resource
}; };
export const countdownTypes = { export const countdownProgressionTypes = {
spotlight: { actionRoll: {
id: 'spotlight', id: 'actionRoll',
label: 'DAGGERHEART.CONFIG.CountdownType.spotlight' label: 'DAGGERHEART.CONFIG.CountdownType.actionRoll'
}, },
characterAttack: { characterAttack: {
id: 'characterAttack', id: 'characterAttack',
label: 'DAGGERHEART.CONFIG.CountdownType.characterAttack' label: 'DAGGERHEART.CONFIG.CountdownType.characterAttack'
}, },
characterSpotlight: {
id: 'characterSpotlight',
label: 'DAGGERHEART.CONFIG.CountdownType.characterSpotlight'
},
custom: { custom: {
id: 'custom', id: 'custom',
label: 'DAGGERHEART.CONFIG.CountdownType.custom' label: 'DAGGERHEART.CONFIG.CountdownType.custom'
},
fear: {
id: 'fear',
label: 'DAGGERHEART.CONFIG.CountdownType.fear'
},
spotlight: {
id: 'spotlight',
label: 'DAGGERHEART.CONFIG.CountdownType.spotlight'
} }
}; };
export const rollTypes = { export const rollTypes = {
@ -624,8 +658,76 @@ export const rollTypes = {
} }
}; };
export const attributionSources = {
daggerheart: {
label: 'Daggerheart',
values: [{ label: 'Daggerheart SRD' }]
}
};
export const fearDisplay = { export const fearDisplay = {
token: { value: 'token', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.token' }, token: { value: 'token', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.token' },
bar: { value: 'bar', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.bar' }, bar: { value: 'bar', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.bar' },
hide: { value: 'hide', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.hide' } hide: { value: 'hide', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.hide' }
}; };
export const basicOwnershiplevels = {
0: { value: 0, label: 'OWNERSHIP.NONE' },
2: { value: 2, label: 'OWNERSHIP.OBSERVER' },
3: { value: 3, label: 'OWNERSHIP.OWNER' }
};
export const simpleOwnershiplevels = {
[-1]: { value: -1, label: 'OWNERSHIP.INHERIT' },
...basicOwnershiplevels
};
export const countdownBaseTypes = {
narrative: {
id: 'narrative',
label: 'DAGGERHEART.APPLICATIONS.Countdown.types.narrative'
},
encounter: {
id: 'encounter',
label: 'DAGGERHEART.APPLICATIONS.Countdown.types.encounter'
}
};
export const countdownLoopingTypes = {
noLooping: {
id: 'noLooping',
label: 'DAGGERHEART.APPLICATIONS.Countdown.loopingTypes.noLooping'
},
looping: {
id: 'looping',
label: 'DAGGERHEART.APPLICATIONS.Countdown.loopingTypes.looping'
},
increasing: {
id: 'increasing',
label: 'DAGGERHEART.APPLICATIONS.Countdown.loopingTypes.increasing'
},
decreasing: {
id: 'decreasing',
label: 'DAGGERHEART.APPLICATIONS.Countdown.loopingTypes.decreasing'
}
};
export const countdownAppMode = {
textIcon: 'text-icon',
iconOnly: 'icon-only'
};
export const sceneRangeMeasurementSetting = {
disable: {
id: 'disable',
label: 'Disable Daggerheart Range Measurement'
},
default: {
id: 'default',
label: 'Default'
},
custom: {
id: 'custom',
label: 'Custom'
}
};

View file

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

View file

@ -3,63 +3,63 @@ export const typeConfig = {
columns: [ columns: [
{ {
key: 'system.tier', key: 'system.tier',
label: 'Tier' label: 'DAGGERHEART.GENERAL.Tiers.singular'
}, },
{ {
key: 'system.type', key: 'system.type',
label: 'Type' label: 'DAGGERHEART.GENERAL.type'
} }
], ],
filters: [ filters: [
{ {
key: 'system.tier', key: 'system.tier',
label: 'Tier', label: 'DAGGERHEART.GENERAL.Tiers.singular',
field: 'system.api.models.actors.DhAdversary.schema.fields.tier' field: 'system.api.models.actors.DhAdversary.schema.fields.tier'
}, },
{ {
key: 'system.type', key: 'system.type',
label: 'Type', label: 'DAGGERHEART.GENERAL.type',
field: 'system.api.models.actors.DhAdversary.schema.fields.type' field: 'system.api.models.actors.DhAdversary.schema.fields.type'
}, },
{ {
key: 'system.difficulty', key: 'system.difficulty',
name: 'difficulty.min', name: 'difficulty.min',
label: 'Difficulty (Min)', label: 'DAGGERHEART.UI.ItemBrowser.difficultyMin',
field: 'system.api.models.actors.DhAdversary.schema.fields.difficulty', field: 'system.api.models.actors.DhAdversary.schema.fields.difficulty',
operator: 'gte' operator: 'gte'
}, },
{ {
key: 'system.difficulty', key: 'system.difficulty',
name: 'difficulty.max', name: 'difficulty.max',
label: 'Difficulty (Max)', label: 'DAGGERHEART.UI.ItemBrowser.difficultyMax',
field: 'system.api.models.actors.DhAdversary.schema.fields.difficulty', field: 'system.api.models.actors.DhAdversary.schema.fields.difficulty',
operator: 'lte' operator: 'lte'
}, },
{ {
key: 'system.resources.hitPoints.max', key: 'system.resources.hitPoints.max',
name: 'hp.min', name: 'hp.min',
label: 'Hit Points (Min)', label: 'DAGGERHEART.UI.ItemBrowser.hitPointsMin',
field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.hitPoints.fields.max', field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.hitPoints.fields.max',
operator: 'gte' operator: 'gte'
}, },
{ {
key: 'system.resources.hitPoints.max', key: 'system.resources.hitPoints.max',
name: 'hp.max', name: 'hp.max',
label: 'Hit Points (Max)', label: 'DAGGERHEART.UI.ItemBrowser.hitPointsMax',
field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.hitPoints.fields.max', field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.hitPoints.fields.max',
operator: 'lte' operator: 'lte'
}, },
{ {
key: 'system.resources.stress.max', key: 'system.resources.stress.max',
name: 'stress.min', name: 'stress.min',
label: 'Stress (Min)', label: 'DAGGERHEART.UI.ItemBrowser.stressMin',
field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.stress.fields.max', field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.stress.fields.max',
operator: 'gte' operator: 'gte'
}, },
{ {
key: 'system.resources.stress.max', key: 'system.resources.stress.max',
name: 'stress.max', name: 'stress.max',
label: 'Stress (Max)', label: 'DAGGERHEART.UI.ItemBrowser.stressMax',
field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.stress.fields.max', field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.stress.fields.max',
operator: 'lte' operator: 'lte'
} }
@ -69,22 +69,22 @@ export const typeConfig = {
columns: [ columns: [
{ {
key: 'type', key: 'type',
label: 'Type' label: 'DAGGERHEART.GENERAL.type'
}, },
{ {
key: 'system.secondary', key: 'system.secondary',
label: 'Subtype', label: 'DAGGERHEART.UI.ItemBrowser.subtype',
format: isSecondary => (isSecondary ? 'secondary' : isSecondary === false ? 'primary' : '-') format: isSecondary => (isSecondary ? 'secondary' : isSecondary === false ? 'primary' : '-')
}, },
{ {
key: 'system.tier', key: 'system.tier',
label: 'Tier' label: 'DAGGERHEART.GENERAL.Tiers.singular'
} }
], ],
filters: [ filters: [
{ {
key: 'type', key: 'type',
label: 'Type', label: 'DAGGERHEART.GENERAL.type',
choices: () => choices: () =>
CONFIG.Item.documentClass.TYPES.filter(t => CONFIG.Item.documentClass.TYPES.filter(t =>
['armor', 'weapon', 'consumable', 'loot'].includes(t) ['armor', 'weapon', 'consumable', 'loot'].includes(t)
@ -92,15 +92,15 @@ export const typeConfig = {
}, },
{ {
key: 'system.secondary', key: 'system.secondary',
label: 'Subtype', label: 'DAGGERHEART.UI.ItemBrowser.subtype',
choices: [ choices: [
{ value: false, label: 'Primary Weapon' }, { value: false, label: 'DAGGERHEART.ITEMS.Weapon.primaryWeapon' },
{ value: true, label: 'Secondary Weapon' } { value: true, label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon' }
] ]
}, },
{ {
key: 'system.tier', key: 'system.tier',
label: 'Tier', label: 'DAGGERHEART.GENERAL.Tiers.singular',
choices: [ choices: [
{ value: '1', label: '1' }, { value: '1', label: '1' },
{ value: '2', label: '2' }, { value: '2', label: '2' },
@ -110,36 +110,36 @@ export const typeConfig = {
}, },
{ {
key: 'system.burden', key: 'system.burden',
label: 'Burden', label: 'DAGGERHEART.GENERAL.burden',
field: 'system.api.models.items.DHWeapon.schema.fields.burden' field: 'system.api.models.items.DHWeapon.schema.fields.burden'
}, },
{ {
key: 'system.attack.roll.trait', key: 'system.attack.roll.trait',
label: 'Trait', label: 'DAGGERHEART.GENERAL.Trait.single',
field: 'system.api.models.actions.actionsTypes.attack.schema.fields.roll.fields.trait' field: 'system.api.models.actions.actionsTypes.attack.schema.fields.roll.fields.trait'
}, },
{ {
key: 'system.attack.range', key: 'system.attack.range',
label: 'Range', label: 'DAGGERHEART.GENERAL.range',
field: 'system.api.models.actions.actionsTypes.attack.schema.fields.range' field: 'system.api.models.actions.actionsTypes.attack.schema.fields.range'
}, },
{ {
key: 'system.baseScore', key: 'system.baseScore',
name: 'armor.min', name: 'armor.min',
label: 'Armor Score (Min)', label: 'DAGGERHEART.UI.ItemBrowser.armorScoreMin',
field: 'system.api.models.items.DHArmor.schema.fields.baseScore', field: 'system.api.models.items.DHArmor.schema.fields.baseScore',
operator: 'gte' operator: 'gte'
}, },
{ {
key: 'system.baseScore', key: 'system.baseScore',
name: 'armor.max', name: 'armor.max',
label: 'Armor Score (Max)', label: 'DAGGERHEART.UI.ItemBrowser.armorScoreMax',
field: 'system.api.models.items.DHArmor.schema.fields.baseScore', field: 'system.api.models.items.DHArmor.schema.fields.baseScore',
operator: 'lte' operator: 'lte'
}, },
{ {
key: 'system.itemFeatures', key: 'system.itemFeatures',
label: 'Features', label: 'DAGGERHEART.GENERAL.features',
choices: () => choices: () =>
[ [
...Object.entries(CONFIG.DH.ITEM.weaponFeatures), ...Object.entries(CONFIG.DH.ITEM.weaponFeatures),
@ -149,6 +149,102 @@ export const typeConfig = {
} }
] ]
}, },
weapons: {
columns: [
{
key: 'system.secondary',
label: 'DAGGERHEART.UI.ItemBrowser.subtype',
format: isSecondary => (isSecondary ? 'secondary' : isSecondary === false ? 'primary' : '-')
},
{
key: 'system.tier',
label: 'DAGGERHEART.GENERAL.Tiers.singular'
}
],
filters: [
{
key: 'system.secondary',
label: 'DAGGERHEART.UI.ItemBrowser.subtype',
choices: [
{ value: false, label: 'DAGGERHEART.ITEMS.Weapon.primaryWeapon' },
{ value: true, label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon' }
]
},
{
key: 'system.tier',
label: 'DAGGERHEART.GENERAL.Tiers.singular',
choices: [
{ value: '1', label: '1' },
{ value: '2', label: '2' },
{ value: '3', label: '3' },
{ value: '4', label: '4' }
]
},
{
key: 'system.burden',
label: 'DAGGERHEART.GENERAL.burden',
field: 'system.api.models.items.DHWeapon.schema.fields.burden'
},
{
key: 'system.attack.roll.trait',
label: 'DAGGERHEART.GENERAL.Trait.single',
field: 'system.api.models.actions.actionsTypes.attack.schema.fields.roll.fields.trait'
},
{
key: 'system.attack.range',
label: 'DAGGERHEART.GENERAL.range',
field: 'system.api.models.actions.actionsTypes.attack.schema.fields.range'
},
{
key: 'system.itemFeatures',
label: 'DAGGERHEART.GENERAL.features',
choices: () =>
Object.entries(CONFIG.DH.ITEM.weaponFeatures).map(([k, v]) => ({ value: k, label: v.label })),
operator: 'contains3'
}
]
},
armors: {
columns: [
{
key: 'system.tier',
label: 'DAGGERHEART.GENERAL.Tiers.singular'
}
],
filters: [
{
key: 'system.tier',
label: 'DAGGERHEART.GENERAL.Tiers.singular',
choices: [
{ value: '1', label: '1' },
{ value: '2', label: '2' },
{ value: '3', label: '3' },
{ value: '4', label: '4' }
]
},
{
key: 'system.baseScore',
name: 'armor.min',
label: 'DAGGERHEART.UI.ItemBrowser.armorScoreMin',
field: 'system.api.models.items.DHArmor.schema.fields.baseScore',
operator: 'gte'
},
{
key: 'system.baseScore',
name: 'armor.max',
label: 'DAGGERHEART.UI.ItemBrowser.armorScoreMax',
field: 'system.api.models.items.DHArmor.schema.fields.baseScore',
operator: 'lte'
},
{
key: 'system.itemFeatures',
label: 'DAGGERHEART.GENERAL.features',
choices: () =>
Object.entries(CONFIG.DH.ITEM.armorFeatures).map(([k, v]) => ({ value: k, label: v.label })),
operator: 'contains3'
}
]
},
features: { features: {
columns: [], columns: [],
filters: [] filters: []
@ -157,54 +253,54 @@ export const typeConfig = {
columns: [ columns: [
{ {
key: 'system.type', key: 'system.type',
label: 'Type' label: 'DAGGERHEART.GENERAL.type'
}, },
{ {
key: 'system.domain', key: 'system.domain',
label: 'Domain' label: 'DAGGERHEART.GENERAL.Domain.single'
}, },
{ {
key: 'system.level', key: 'system.level',
label: 'Level' label: 'DAGGERHEART.GENERAL.level'
} }
], ],
filters: [ filters: [
{ {
key: 'system.type', key: 'system.type',
label: 'Type', label: 'DAGGERHEART.GENERAL.type',
field: 'system.api.models.items.DHDomainCard.schema.fields.type' field: 'system.api.models.items.DHDomainCard.schema.fields.type'
}, },
{ {
key: 'system.domain', key: 'system.domain',
label: 'Domain', label: 'DAGGERHEART.GENERAL.Domain.single',
field: 'system.api.models.items.DHDomainCard.schema.fields.domain', field: 'system.api.models.items.DHDomainCard.schema.fields.domain',
operator: 'contains2' operator: 'contains2'
}, },
{ {
key: 'system.level', key: 'system.level',
name: 'level.min', name: 'level.min',
label: 'Level (Min)', label: 'DAGGERHEART.UI.ItemBrowser.levelMin',
field: 'system.api.models.items.DHDomainCard.schema.fields.level', field: 'system.api.models.items.DHDomainCard.schema.fields.level',
operator: 'gte' operator: 'gte'
}, },
{ {
key: 'system.level', key: 'system.level',
name: 'level.max', name: 'level.max',
label: 'Level (Max)', label: 'DAGGERHEART.UI.ItemBrowser.levelMax',
field: 'system.api.models.items.DHDomainCard.schema.fields.level', field: 'system.api.models.items.DHDomainCard.schema.fields.level',
operator: 'lte' operator: 'lte'
}, },
{ {
key: 'system.recallCost', key: 'system.recallCost',
name: 'recall.min', name: 'recall.min',
label: 'Recall Cost (Min)', label: 'DAGGERHEART.UI.ItemBrowser.recallCostMin',
field: 'system.api.models.items.DHDomainCard.schema.fields.recallCost', field: 'system.api.models.items.DHDomainCard.schema.fields.recallCost',
operator: 'gte' operator: 'gte'
}, },
{ {
key: 'system.recallCost', key: 'system.recallCost',
name: 'recall.max', name: 'recall.max',
label: 'Recall Cost (Max)', label: 'DAGGERHEART.UI.ItemBrowser.recallCostMax',
field: 'system.api.models.items.DHDomainCard.schema.fields.recallCost', field: 'system.api.models.items.DHDomainCard.schema.fields.recallCost',
operator: 'lte' operator: 'lte'
} }
@ -214,50 +310,50 @@ export const typeConfig = {
columns: [ columns: [
{ {
key: 'system.evasion', key: 'system.evasion',
label: 'Evasion' label: 'DAGGERHEART.GENERAL.evasion'
}, },
{ {
key: 'system.hitPoints', key: 'system.hitPoints',
label: 'Hit Points' label: 'DAGGERHEART.GENERAL.HitPoints.plural'
}, },
{ {
key: 'system.domains', key: 'system.domains',
label: 'Domains' label: 'DAGGERHEART.GENERAL.Domain.plural'
} }
], ],
filters: [ filters: [
{ {
key: 'system.evasion', key: 'system.evasion',
name: 'evasion.min', name: 'evasion.min',
label: 'Evasion (Min)', label: 'DAGGERHEART.UI.ItemBrowser.evasionMin',
field: 'system.api.models.items.DHClass.schema.fields.evasion', field: 'system.api.models.items.DHClass.schema.fields.evasion',
operator: 'gte' operator: 'gte'
}, },
{ {
key: 'system.evasion', key: 'system.evasion',
name: 'evasion.max', name: 'evasion.max',
label: 'Evasion (Max)', label: 'DAGGERHEART.UI.ItemBrowser.evasionMax',
field: 'system.api.models.items.DHClass.schema.fields.evasion', field: 'system.api.models.items.DHClass.schema.fields.evasion',
operator: 'lte' operator: 'lte'
}, },
{ {
key: 'system.hitPoints', key: 'system.hitPoints',
name: 'hp.min', name: 'hp.min',
label: 'Hit Points (Min)', label: 'DAGGERHEART.UI.ItemBrowser.hitPointsMin',
field: 'system.api.models.items.DHClass.schema.fields.hitPoints', field: 'system.api.models.items.DHClass.schema.fields.hitPoints',
operator: 'gte' operator: 'gte'
}, },
{ {
key: 'system.hitPoints', key: 'system.hitPoints',
name: 'hp.max', name: 'hp.max',
label: 'Hit Points (Max)', label: 'DAGGERHEART.UI.ItemBrowser.hitPointsMax',
field: 'system.api.models.items.DHClass.schema.fields.hitPoints', field: 'system.api.models.items.DHClass.schema.fields.hitPoints',
operator: 'lte' operator: 'lte'
}, },
{ {
key: 'system.domains', key: 'system.domains',
label: 'Domains', label: 'DAGGERHEART.GENERAL.Domain.plural',
choices: () => Object.values(CONFIG.DH.DOMAIN.domains).map(d => ({ value: d.id, label: d.label })), choices: () => Object.values(CONFIG.DH.DOMAIN.allDomains()).map(d => ({ value: d.id, label: d.label })),
operator: 'contains2' operator: 'contains2'
} }
] ]
@ -267,21 +363,26 @@ export const typeConfig = {
{ {
key: 'system.linkedClass', key: 'system.linkedClass',
label: 'Class', label: 'Class',
format: linkedClass => linkedClass.name format: linkedClass => linkedClass?.name ?? 'DAGGERHEART.UI.ItemBrowser.missing'
}, },
{ {
key: 'system.spellcastingTrait', key: 'system.spellcastingTrait',
label: 'Spellcasting Trait' label: 'DAGGERHEART.ITEMS.Subclass.spellcastingTrait'
} }
], ],
filters: [ filters: [
{ {
key: 'system.linkedClass.uuid', key: 'system.linkedClass.uuid',
label: 'Class', label: 'Class',
choices: (items) => { choices: items => {
const list = items.map(item => ({ value: item.system.linkedClass.uuid, label: item.system.linkedClass.name })); const list = items
return list.reduce((a,c) => { .filter(item => item.system.linkedClass)
if(!(a.find(i => i.value === c.value))) a.push(c); .map(item => ({
value: item.system.linkedClass.uuid,
label: item.system.linkedClass.name
}));
return list.reduce((a, c) => {
if (!a.find(i => i.value === c.value)) a.push(c);
return a; return a;
}, []); }, []);
} }
@ -292,22 +393,22 @@ export const typeConfig = {
columns: [ columns: [
{ {
key: 'system.tier', key: 'system.tier',
label: 'Tier' label: 'DAGGERHEART.GENERAL.Tiers.singular'
}, },
{ {
key: 'system.mainTrait', key: 'system.mainTrait',
label: 'Main Trait' label: 'DAGGERHEART.GENERAL.Trait.single'
} }
], ],
filters: [ filters: [
{ {
key: 'system.tier', key: 'system.tier',
label: 'Tier', label: 'DAGGERHEART.GENERAL.Tiers.singular',
field: 'system.api.models.items.DHBeastform.schema.fields.tier' field: 'system.api.models.items.DHBeastform.schema.fields.tier'
}, },
{ {
key: 'system.mainTrait', key: 'system.mainTrait',
label: 'Main Trait', label: 'DAGGERHEART.GENERAL.Trait.single',
field: 'system.api.models.items.DHBeastform.schema.fields.mainTrait' field: 'system.api.models.items.DHBeastform.schema.fields.mainTrait'
} }
] ]
@ -315,109 +416,144 @@ export const typeConfig = {
}; };
export const compendiumConfig = { export const compendiumConfig = {
daggerheart: { characters: {
id: 'daggerheart', id: 'characters',
label: 'DAGGERHEART', keys: ['characters'],
folders: { label: 'DAGGERHEART.UI.ItemBrowser.folders.characters',
type: ['character']
// listType: 'characters'
},
adversaries: { adversaries: {
id: 'adversaries', id: 'adversaries',
keys: ['adversaries'], keys: ['adversaries'],
label: 'Adversaries', label: 'DAGGERHEART.UI.ItemBrowser.folders.adversaries',
type: ['adversary'], type: ['adversary'],
listType: 'adversaries' listType: 'adversaries'
}, },
ancestries: { ancestries: {
id: 'ancestries', id: 'ancestries',
keys: ['ancestries'], keys: ['ancestries'],
label: 'Ancestries', label: 'DAGGERHEART.UI.ItemBrowser.folders.ancestries',
type: ['ancestry'], type: ['ancestry']
folders: { /* folders: {
features: { features: {
id: 'features', id: 'features',
keys: ['ancestries'], keys: ['ancestries'],
label: 'Features', label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
type: ['feature'] type: ['feature']
} }
} } */
}, },
equipments: { equipments: {
id: 'equipments', id: 'equipments',
keys: ['armors', 'weapons', 'consumables', 'loot'], keys: ['armors', 'weapons', 'consumables', 'loot'],
label: 'Equipment', label: 'DAGGERHEART.UI.ItemBrowser.folders.equipment',
type: ['armor', 'weapon', 'consumable', 'loot'], type: ['armor', 'weapon', 'consumable', 'loot'],
listType: 'items' listType: 'items',
folders: {
weapons: {
id: 'weapons',
keys: ['weapons'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.weapons',
type: ['weapon'],
listType: 'weapons'
},
armors: {
id: 'armors',
keys: ['armors'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.armors',
type: ['armor'],
listType: 'armors'
},
consumables: {
id: 'consumables',
keys: ['consumables'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.consumables',
type: ['consumable']
},
loots: {
id: 'loots',
keys: ['loots'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.loots',
type: ['loot']
}
}
}, },
classes: { classes: {
id: 'classes', id: 'classes',
keys: ['classes'], keys: ['classes'],
label: 'Classes', label: 'DAGGERHEART.UI.ItemBrowser.folders.classes',
type: ['class'], type: ['class'],
folders: { /* folders: {
features: { features: {
id: 'features', id: 'features',
keys: ['classes'], keys: ['classes'],
label: 'Features', label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
type: ['feature'] type: ['feature']
}, },
items: { items: {
id: 'items', id: 'items',
keys: ['classes'], keys: ['classes'],
label: 'Items', label: 'DAGGERHEART.UI.ItemBrowser.folders.items',
type: ['armor', 'weapon', 'consumable', 'loot'], type: ['armor', 'weapon', 'consumable', 'loot'],
listType: 'items' listType: 'items'
} }
}, }, */
listType: 'classes' listType: 'classes'
}, },
subclasses: { subclasses: {
id: 'subclasses', id: 'subclasses',
keys: ['subclasses'], keys: ['subclasses'],
label: 'Subclasses', label: 'DAGGERHEART.UI.ItemBrowser.folders.subclasses',
type: ['subclass'], type: ['subclass'],
listType: 'subclasses' listType: 'subclasses'
}, },
domains: { domains: {
id: 'domains', id: 'domains',
keys: ['domains'], keys: ['domains'],
label: 'Domain Cards', label: 'DAGGERHEART.UI.ItemBrowser.folders.domainCards',
type: ['domainCard'], type: ['domainCard'],
listType: 'cards' listType: 'cards'
}, },
communities: { communities: {
id: 'communities', id: 'communities',
keys: ['communities'], keys: ['communities'],
label: 'Communities', label: 'DAGGERHEART.UI.ItemBrowser.folders.communities',
type: ['community'], type: ['community']
folders: { /* folders: {
features: { features: {
id: 'features', id: 'features',
keys: ['communities'], keys: ['communities'],
label: 'Features', label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
type: ['feature'] type: ['feature']
} }
} } */
}, },
environments: { environments: {
id: 'environments', id: 'environments',
keys: ['environments'], keys: ['environments'],
label: 'Environments', label: 'DAGGERHEART.UI.ItemBrowser.folders.environments',
type: ['environment'] type: ['environment']
}, },
beastforms: { beastforms: {
id: 'beastforms', id: 'beastforms',
keys: ['beastforms'], keys: ['beastforms'],
label: 'Beastforms', label: 'DAGGERHEART.UI.ItemBrowser.folders.beastforms',
type: ['beastform'], type: ['beastform'],
listType: 'beastforms', listType: 'beastforms'
folders: { /* folders: {
features: { features: {
id: 'features', id: 'features',
keys: ['beastforms'], keys: ['beastforms'],
label: 'Features', label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
type: ['feature'] type: ['feature']
} }
} } */
} },
} features: {
id: 'features',
keys: ['features'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
type: ['feature']
} }
}; };

View file

@ -5,7 +5,6 @@ export const armorFeatures = {
actions: [ actions: [
{ {
type: 'damage', type: 'damage',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.burning.actions.burn.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.burning.actions.burn.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.burning.actions.burn.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.burning.actions.burn.description',
@ -174,7 +173,6 @@ export const armorFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.hopeful.actions.hope.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.hopeful.actions.hope.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.hopeful.actions.hope.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.hopeful.actions.hope.description',
@ -188,7 +186,6 @@ export const armorFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.impenetrable.actions.impenetrable.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.impenetrable.actions.impenetrable.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.impenetrable.actions.impenetrable.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.impenetrable.actions.impenetrable.description',
@ -231,7 +228,6 @@ export const armorFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.painful.actions.pain.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.painful.actions.pain.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.painful.actions.pain.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.painful.actions.pain.description',
@ -269,7 +265,6 @@ export const armorFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.quiet.actions.quiet.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.quiet.actions.quiet.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.quiet.actions.quiet.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.quiet.actions.quiet.description',
@ -306,7 +301,6 @@ export const armorFeatures = {
actions: [ actions: [
{ {
type: 'attack', type: 'attack',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.resilient.actions.resilient.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.resilient.actions.resilient.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.resilient.actions.resilient.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.resilient.actions.resilient.description',
@ -353,7 +347,6 @@ export const armorFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.shifting.actions.shift.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.shifting.actions.shift.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.shifting.actions.shift.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.shifting.actions.shift.description',
@ -373,7 +366,6 @@ export const armorFeatures = {
actions: [ actions: [
{ {
type: 'attack', type: 'attack',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.timeslowing.actions.slowTime.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.timeslowing.actions.slowTime.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.timeslowing.actions.slowTime.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.timeslowing.actions.slowTime.description',
@ -401,7 +393,6 @@ export const armorFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.truthseeking.actions.truthseeking.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.truthseeking.actions.truthseeking.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.truthseeking.actions.truthseeking.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.truthseeking.actions.truthseeking.description',
@ -444,7 +435,8 @@ export const armorFeatures = {
{ {
key: 'system.resistance.magical.reduction', key: 'system.resistance.magical.reduction',
mode: 2, mode: 2,
value: '@system.armorScore' value: '@system.armorScore',
priority: 21
} }
] ]
} }
@ -452,6 +444,43 @@ export const armorFeatures = {
} }
}; };
export const allArmorFeatures = () => {
const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures
.armorFeatures;
return {
...armorFeatures,
...Object.keys(homebrewFeatures).reduce((acc, key) => {
const feature = homebrewFeatures[key];
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;
}, {})
};
};
export const orderedArmorFeatures = () => {
const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures
.armorFeatures;
const allFeatures = { ...armorFeatures, ...homebrewFeatures };
const all = Object.keys(allFeatures).map(key => {
const feature = allFeatures[key];
return {
...feature,
id: key,
label: feature.label ?? feature.name
};
});
return Object.values(all).sort((a, b) => game.i18n.localize(a.label).localeCompare(game.i18n.localize(b.label)));
};
export const weaponFeatures = { export const weaponFeatures = {
barrier: { barrier: {
label: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.name', label: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.name',
@ -500,7 +529,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.bouncing.actions.bounce.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.bouncing.actions.bounce.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.bouncing.actions.bounce.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.bouncing.actions.bounce.description',
@ -545,7 +573,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.brutal.actions.addDamage.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.brutal.actions.addDamage.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.brutal.actions.addDamage.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.brutal.actions.addDamage.description',
@ -559,7 +586,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.burning.actions.burn.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.burning.actions.burn.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.burning.actions.burn.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.burning.actions.burn.description',
@ -573,7 +599,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.charged.actions.markStress.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.charged.actions.markStress.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.charged.actions.markStress.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.charged.actions.markStress.description',
@ -610,7 +635,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.concussive.actions.attack.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.concussive.actions.attack.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.concussive.actions.attack.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.concussive.actions.attack.description',
@ -651,7 +675,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.deadly.actions.extraDamage.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.deadly.actions.extraDamage.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.deadly.actions.extraDamage.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.deadly.actions.extraDamage.description',
@ -665,7 +688,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.deflecting.actions.deflect.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.deflecting.actions.deflect.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.deflecting.actions.deflect.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.deflecting.actions.deflect.description',
@ -688,7 +710,8 @@ export const weaponFeatures = {
{ {
key: 'system.evasion', key: 'system.evasion',
mode: 2, mode: 2,
value: '@system.armorScore' value: '@system.armorScore',
priority: 21
} }
] ]
} }
@ -702,7 +725,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'damage', type: 'damage',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.destructive.actions.attack.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.destructive.actions.attack.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.destructive.actions.attack.descriptive', description: 'DAGGERHEART.CONFIG.WeaponFeature.destructive.actions.attack.descriptive',
@ -747,7 +769,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.devastating.actions.devastate.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.devastating.actions.devastate.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.devastating.actions.devastate.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.devastating.actions.devastate.description',
@ -798,7 +819,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.doubledUp.actions.doubleUp.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.doubledUp.actions.doubleUp.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.doubledUp.actions.doubleUp.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.doubledUp.actions.doubleUp.description',
@ -812,7 +832,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.dueling.actions.duel.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.dueling.actions.duel.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.dueling.actions.duel.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.dueling.actions.duel.description',
@ -826,7 +845,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', // Should prompt a dc 14 reaction save on adversaries type: 'effect', // Should prompt a dc 14 reaction save on adversaries
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.eruptive.actions.erupt.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.eruptive.actions.erupt.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.eruptive.actions.erupt.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.eruptive.actions.erupt.description',
@ -840,7 +858,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.grappling.actions.grapple.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.grappling.actions.grapple.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.grappling.actions.grapple.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.grappling.actions.grapple.description',
@ -860,11 +877,13 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.greedy.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.greedy.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.greedy.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.greedy.description',
img: 'icons/commodities/currency/coins-crown-stack-gold.webp', img: 'icons/commodities/currency/coins-crown-stack-gold.webp',
target: {
type: 'self'
},
// Should cost handful of gold, // Should cost handful of gold,
effects: [ effects: [
{ {
@ -889,7 +908,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'healing', type: 'healing',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.healing.actions.heal.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.healing.actions.heal.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.healing.actions.heal.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.healing.actions.heal.description',
@ -937,7 +955,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.hooked.actions.hook.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.hooked.actions.hook.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.hooked.actions.hook.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.hooked.actions.hook.description',
@ -951,7 +968,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.hot.actions.hot.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.hot.actions.hot.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.hot.actions.hot.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.hot.actions.hot.description',
@ -965,7 +981,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.invigorating.actions.invigorate.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.invigorating.actions.invigorate.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.invigorating.actions.invigorate.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.invigorating.actions.invigorate.description',
@ -979,7 +994,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.lifestealing.actions.lifesteal.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.lifestealing.actions.lifesteal.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.lifestealing.actions.lifesteal.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.lifestealing.actions.lifesteal.description',
@ -993,7 +1007,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.lockedOn.actions.lockOn.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.lockedOn.actions.lockOn.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.lockedOn.actions.lockOn.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.lockedOn.actions.lockOn.description',
@ -1007,7 +1020,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.long.actions.long.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.long.actions.long.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.long.actions.long.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.long.actions.long.description',
@ -1021,7 +1033,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.lucky.actions.luck.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.lucky.actions.luck.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.lucky.actions.luck.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.lucky.actions.luck.description',
@ -1059,7 +1070,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.painful.actions.pain.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.painful.actions.pain.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.painful.actions.pain.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.painful.actions.pain.description',
@ -1105,7 +1115,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.parry.actions.parry.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.parry.actions.parry.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.parry.actions.parry.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.parry.actions.parry.description',
@ -1119,7 +1128,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.persuasive.actions.persuade.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.persuasive.actions.persuade.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.persuasive.actions.persuade.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.persuasive.actions.persuade.description',
@ -1156,7 +1164,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.pompous.actions.pompous.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.pompous.actions.pompous.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.pompous.actions.pompous.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.pompous.actions.pompous.description',
@ -1200,7 +1207,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.quick.actions.quick.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.quick.actions.quick.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.quick.actions.quick.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.quick.actions.quick.description',
@ -1238,7 +1244,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.reloading.actions.reload.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.reloading.actions.reload.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.reloading.actions.reload.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.reloading.actions.reload.description',
@ -1252,7 +1257,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.retractable.actions.retract.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.retractable.actions.retract.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.retractable.actions.retract.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.retractable.actions.retract.description',
@ -1266,7 +1270,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.returning.actions.return.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.returning.actions.return.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.returning.actions.return.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.returning.actions.return.description',
@ -1280,7 +1283,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.scary.actions.scare.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.scary.actions.scare.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.scary.actions.scare.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.scary.actions.scare.description',
@ -1324,7 +1326,8 @@ export const weaponFeatures = {
{ {
key: 'system.bonuses.damage.primaryWeapon.bonus', key: 'system.bonuses.damage.primaryWeapon.bonus',
mode: 2, mode: 2,
value: '@system.traits.agility.value' value: '@system.traits.agility.value',
priority: 21
} }
] ]
} }
@ -1336,7 +1339,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.sheltering.actions.shelter.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.sheltering.actions.shelter.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.sheltering.actions.shelter.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.sheltering.actions.shelter.description',
@ -1350,7 +1352,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.startling.actions.startle.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.startling.actions.startle.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.startling.actions.startle.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.startling.actions.startle.description',
@ -1370,7 +1371,6 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.timebending.actions.bendTime.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.timebending.actions.bendTime.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.timebending.actions.bendTime.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.timebending.actions.bendTime.description',
@ -1380,6 +1380,50 @@ 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];
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;
}, {})
};
};
export const orderedWeaponFeatures = () => {
const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures
.weaponFeatures;
const allFeatures = { ...weaponFeatures, ...homebrewFeatures };
const all = Object.keys(allFeatures).map(key => {
const feature = allFeatures[key];
return {
...feature,
id: key,
label: feature.label ?? feature.name
};
});
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 = { export const featureTypes = {
ancestry: { ancestry: {
id: 'ancestry', id: 'ancestry',
@ -1437,21 +1481,6 @@ export const featureSubTypes = {
mastery: 'mastery' 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 = { export const itemResourceTypes = {
simple: { simple: {
id: 'simple', id: 'simple',
@ -1460,6 +1489,10 @@ export const itemResourceTypes = {
diceValue: { diceValue: {
id: 'diceValue', id: 'diceValue',
label: 'DAGGERHEART.CONFIG.ItemResourceType.diceValue' label: 'DAGGERHEART.CONFIG.ItemResourceType.diceValue'
},
die: {
id: 'die',
label: 'DAGGERHEART.CONFIG.ItemResourceType.die'
} }
}; };
@ -1488,3 +1521,8 @@ export const beastformTypes = {
label: 'DAGGERHEART.CONFIG.BeastformType.hybrid' label: 'DAGGERHEART.CONFIG.BeastformType.hybrid'
} }
}; };
export const originItemType = {
itemCollection: 'itemCollection',
restMove: 'restMove'
};

View file

@ -27,5 +27,26 @@ export const gameSettings = {
}, },
LevelTiers: 'LevelTiers', LevelTiers: 'LevelTiers',
Countdowns: 'Countdowns', Countdowns: 'Countdowns',
LastMigrationVersion: 'LastMigrationVersion' LastMigrationVersion: 'LastMigrationVersion',
TagTeamRoll: 'TagTeamRoll',
SpotlightRequestQueue: 'SpotlightRequestQueue',
};
export const actionAutomationChoices = {
never: {
id: 'never',
label: 'Never'
},
showDialog: {
id: 'showDialog',
label: 'Show Dialog only'
},
// npcOnly: {
// id: "npcOnly",
// label: "Always for non-characters"
// },
always: {
id: 'always',
label: 'Always'
}
}; };

View file

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

View file

@ -1,9 +1,12 @@
export { default as DhCombat } from './combat.mjs'; export { default as DhCombat } from './combat.mjs';
export { default as DhCombatant } from './combatant.mjs'; export { default as DhCombatant } from './combatant.mjs';
export { default as DhTagTeamRoll } from './tagTeamRoll.mjs';
export * as countdowns from './countdowns.mjs';
export * as actions from './action/_module.mjs'; export * as actions from './action/_module.mjs';
export * as activeEffects from './activeEffect/_module.mjs'; export * as activeEffects from './activeEffect/_module.mjs';
export * as actors from './actor/_module.mjs'; export * as actors from './actor/_module.mjs';
export * as chatMessages from './chat-message/_modules.mjs'; export * as chatMessages from './chat-message/_modules.mjs';
export * as fields from './fields/_module.mjs'; export * as fields from './fields/_module.mjs';
export * as items from './item/_module.mjs'; export * as items from './item/_module.mjs';
export * as scenes from './scene/_module.mjs';

View file

@ -1,6 +1,7 @@
import AttackAction from './attackAction.mjs'; import AttackAction from './attackAction.mjs';
import BaseAction from './baseAction.mjs'; import BaseAction from './baseAction.mjs';
import BeastformAction from './beastformAction.mjs'; import BeastformAction from './beastformAction.mjs';
import CountdownAction from './countdownAction.mjs';
import DamageAction from './damageAction.mjs'; import DamageAction from './damageAction.mjs';
import EffectAction from './effectAction.mjs'; import EffectAction from './effectAction.mjs';
import HealingAction from './healingAction.mjs'; import HealingAction from './healingAction.mjs';
@ -10,6 +11,7 @@ import SummonAction from './summonAction.mjs';
export const actionsTypes = { export const actionsTypes = {
base: BaseAction, base: BaseAction,
attack: AttackAction, attack: AttackAction,
countdown: CountdownAction,
damage: DamageAction, damage: DamageAction,
healing: HealingAction, healing: HealingAction,
summon: SummonAction, summon: SummonAction,

View file

@ -37,8 +37,10 @@ export default class DHAttackAction extends DHDamageAction {
async use(event, options) { async use(event, options) {
const result = await super.use(event, options); const result = await super.use(event, options);
if (result.message.system.action.roll?.type === 'attack') {
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns; const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
await updateCountdowns(CONFIG.DH.GENERAL.countdownTypes.characterAttack.id); await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.characterAttack.id);
}
return result; return result;
} }

View file

@ -1,14 +1,10 @@
import DhpActor from '../../documents/actor.mjs'; import DhpActor from '../../documents/actor.mjs';
import D20RollDialog from '../../applications/dialogs/d20RollDialog.mjs'; import D20RollDialog from '../../applications/dialogs/d20RollDialog.mjs';
import { ActionMixin } from '../fields/actionField.mjs'; import { ActionMixin } from '../fields/actionField.mjs';
import { abilities } from '../../config/actorConfig.mjs'; import { originItemField } from '../chat-message/actorRoll.mjs';
const fields = foundry.data.fields; const fields = foundry.data.fields;
/*
!!! I'm currently refactoring the whole Action thing, it's a WIP !!!
*/
/* /*
ToDo ToDo
- Target Check / Target Picker - Target Check / Target Picker
@ -20,48 +16,108 @@ const fields = foundry.data.fields;
export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel) { export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel) {
static extraSchemas = ['cost', 'uses', 'range']; static extraSchemas = ['cost', 'uses', 'range'];
/** @inheritDoc */
static defineSchema() { static defineSchema() {
const schemaFields = { const schemaFields = {
_id: new fields.DocumentIdField({ initial: () => foundry.utils.randomID() }), _id: new fields.DocumentIdField({ initial: () => foundry.utils.randomID() }),
systemPath: new fields.StringField({ required: true, initial: 'actions' }), systemPath: new fields.StringField({ required: true, initial: 'actions' }),
type: new fields.StringField({ initial: undefined, readonly: true, required: true }), type: new fields.StringField({ initial: undefined, readonly: true, required: true }),
baseAction: new fields.BooleanField({ initial: false }),
name: new fields.StringField({ initial: undefined }), name: new fields.StringField({ initial: undefined }),
description: new fields.HTMLField(), description: new fields.HTMLField(),
img: new fields.FilePathField({ initial: undefined, categories: ['IMAGE'], base64: false }), img: new fields.FilePathField({ initial: undefined, categories: ['IMAGE'], base64: false }),
chatDisplay: new fields.BooleanField({ initial: true, label: 'DAGGERHEART.ACTIONS.Config.displayInChat' }), chatDisplay: new fields.BooleanField({ initial: true, label: 'DAGGERHEART.ACTIONS.Config.displayInChat' }),
originItem: originItemField(),
actionType: new fields.StringField({ actionType: new fields.StringField({
choices: CONFIG.DH.ITEM.actionTypes, choices: CONFIG.DH.ITEM.actionTypes,
initial: 'action', initial: 'action',
nullable: true nullable: false,
}) required: true
}),
targetUuid: new fields.StringField({ initial: undefined })
}; };
this.extraSchemas.forEach(s => { this.extraSchemas.forEach(s => {
let clsField; let clsField = this.getActionField(s);
if ((clsField = this.getActionField(s))) schemaFields[s] = new clsField(); if (clsField) schemaFields[s] = new clsField();
}); });
return schemaFields; return schemaFields;
} }
/**
* The default values to supply to schema fields when they are created in the actionConfig. Defined by implementing classes.
*/
get defaultValues() {
return {};
}
/**
* Create a Map containing each Action step based on fields define in schema. Ordered by Fields order property.
*
* Each step can be called individually as long as needed config is provided.
* Ex: <action>.workflow.get("damage").execute(config)
* @returns {Map}
*/
defineWorkflow() {
const workflow = new Map();
this.constructor.extraSchemas.forEach(s => {
let clsField = this.constructor.getActionField(s);
if (clsField?.execute) {
workflow.set(s, { order: clsField.order, execute: clsField.execute.bind(this) });
if (s === 'damage')
workflow.set('applyDamage', { order: 75, execute: clsField.applyDamage.bind(this) });
}
});
return new Map([...workflow.entries()].sort(([aKey, aValue], [bKey, bValue]) => aValue.order - bValue.order));
}
/**
* Getter returning the workflow property or creating it the first time the property is called
*/
get workflow() {
if (this.hasOwnProperty('_workflow')) return this._workflow;
const workflow = Object.freeze(this.defineWorkflow());
Object.defineProperty(this, '_workflow', { value: workflow, writable: false });
return workflow;
}
/**
* Get the Field class from ActionFields global config
* @param {string} name Field short name, equal to Action property
* @returns Action Field
*/
static getActionField(name) { static getActionField(name) {
const field = game.system.api.fields.ActionFields[`${name.capitalize()}Field`]; const field = game.system.api.fields.ActionFields[`${name.capitalize()}Field`];
return fields.DataField.isPrototypeOf(field) && field; return fields.DataField.isPrototypeOf(field) && field;
} }
/** @inheritDoc */
prepareData() { prepareData() {
this.name = this.name || game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[this.type].name); this.name = this.name || game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[this.type].name);
this.img = this.img ?? this.parent?.parent?.img; this.img = this.img ?? this.parent?.parent?.img;
/* Fallback to feature description */
this.description = this.description || this.parent?.description;
} }
/**
* Get Action ID
*/
get id() { get id() {
return this._id; return this._id;
} }
/**
* Return Item the action is attached too.
*/
get item() { get item() {
return this.parent.parent; return this.parent.parent;
} }
/**
* Return the first Actor parent found.
*/
get actor() { get actor() {
return this.item instanceof DhpActor return this.item instanceof DhpActor
? this.item ? this.item
@ -74,6 +130,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return 'trait'; return 'trait';
} }
/**
* Prepare base data based on Action Type & Parent Type
* @param {object} parent
* @returns {object}
*/
static getSourceConfig(parent) { static getSourceConfig(parent) {
const updateSource = {}; const updateSource = {};
if (parent?.parent?.type === 'weapon' && this === game.system.api.models.actions.actionsTypes.attack) { if (parent?.parent?.type === 'weapon' && this === game.system.api.models.actions.actionsTypes.attack) {
@ -96,13 +157,15 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return updateSource; return updateSource;
} }
/**
* Obtain a data object used to evaluate any dice rolls associated with this particular Action
* @param {object} [data ={}] Optional data object from previous configuration/rolls
* @returns {object}
*/
getRollData(data = {}) { getRollData(data = {}) {
if (!this.actor) return null; const actorData = this.actor ? this.actor.getRollData(false) : {};
const actorData = this.actor.getRollData(false);
// Add Roll results to RollDatas
actorData.result = data.roll?.total ?? 1; actorData.result = data.roll?.total ?? 1;
actorData.scale = data.costs?.length // Right now only return the first scalable cost. actorData.scale = data.costs?.length // Right now only return the first scalable cost.
? (data.costs.find(c => c.scalable)?.total ?? 1) ? (data.costs.find(c => c.scalable)?.total ?? 1)
: 1; : 1;
@ -111,19 +174,28 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return actorData; return actorData;
} }
async use(event, options = {}) { /**
* Execute each part of the Action workflow in order, calling a specific event before and after each part.
* @param {object} config Config object usually created from prepareConfig method
*/
async executeWorkflow(config) {
for (const [key, part] of this.workflow) {
if (Hooks.call(`${CONFIG.DH.id}.pre${key.capitalize()}Action`, this, config) === false) return;
if ((await part.execute(config)) === false) return;
if (Hooks.call(`${CONFIG.DH.id}.post${key.capitalize()}Action`, this, config) === false) return;
}
}
/**
* Main method to use the Action
* @param {Event} event Event from the button used to trigger the Action
* @returns {object}
*/
async use(event) {
if (!this.actor) throw new Error("An Action can't be used outside of an Actor context."); 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);
let { byPassRoll } = options, if (!config) return;
config = this.prepareConfig(event, byPassRoll);
for (let i = 0; i < this.constructor.extraSchemas.length; i++) {
let clsField = this.constructor.getActionField(this.constructor.extraSchemas[i]);
if (clsField?.prepareConfig) {
const keep = clsField.prepareConfig.call(this, config);
if (config.isFastForward && !keep) return;
}
}
if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return; if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return;
@ -133,283 +205,168 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
if (!config) return; if (!config) return;
} }
if (config.hasRoll) { // Execute the Action Worflow in order based of schema fields
const rollConfig = this.prepareRoll(config); await this.executeWorkflow(config);
config.roll = rollConfig; await config.resourceUpdates.updateResources();
config = await this.actor.diceRoll(config);
if (!config) return;
}
if (this.doFollowUp(config)) {
if (this.rollDamage && this.damage.parts.length) await this.rollDamage(event, config);
else if (this.trigger) await this.trigger(event, config);
else if (this.hasSave || this.hasEffect) {
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
roll._evaluated = true;
await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);
}
}
// Consume resources
await this.consume(config);
if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return; if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return;
if (this.chatDisplay) await this.toChat();
return config; return config;
} }
/* */ /**
prepareConfig(event, byPass = false) { * Create the basic config common to every action type
const hasRoll = this.getUseHasRoll(byPass); * @param {Event} event Event from the button used to trigger the Action
return { * @returns {object}
*/
prepareBaseConfig(event) {
const config = {
event, event,
title: `${this.item.name}: ${game.i18n.localize(this.name)}`, title: `${this.item instanceof CONFIG.Actor.documentClass ? '' : `${this.item.name}: `}${game.i18n.localize(this.name)}`,
source: { source: {
item: this.item._id, item: this.item._id,
originItem: this.originItem,
action: this._id, action: this._id,
actor: this.actor.uuid actor: this.actor.uuid
}, },
dialog: { dialog: {},
configure: hasRoll actionType: this.actionType,
}, hasRoll: this.hasRoll,
type: this.roll?.type ?? this.type, hasDamage: this.hasDamage,
hasRoll: hasRoll, hasHealing: this.hasHealing,
hasDamage: this.damage?.parts?.length && this.type !== 'healing', hasEffect: this.hasEffect,
hasHealing: this.damage?.parts?.length && this.type === 'healing',
hasEffect: !!this.effects?.length,
hasSave: this.hasSave, hasSave: this.hasSave,
isDirect: !!this.damage?.direct,
selectedRollMode: game.settings.get('core', 'rollMode'), selectedRollMode: game.settings.get('core', 'rollMode'),
isFastForward: event.shiftKey,
data: this.getRollData(), data: this.getRollData(),
evaluate: hasRoll evaluate: this.hasRoll,
resourceUpdates: new ResourceUpdateMap(this.actor),
targetUuid: this.targetUuid
}; };
DHBaseAction.applyKeybindings(config);
return config;
} }
/**
* Create the config for that action used for its workflow
* @param {Event} event Event from the button used to trigger the Action
* @returns {object}
*/
prepareConfig(event) {
const config = this.prepareBaseConfig(event);
for (const clsField of Object.values(this.schema.fields)) {
if (clsField?.prepareConfig) if (clsField.prepareConfig.call(this, config) === false) return false;
}
return config;
}
/**
* 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.
* @returns {boolean}
*/
requireConfigurationDialog(config) { requireConfigurationDialog(config) {
return !config.event.shiftKey && !config.hasRoll && (config.costs?.length || config.uses); return !config.event.shiftKey && !config.hasRoll && (config.costs?.length || config.uses);
} }
prepareRoll() { /**
const roll = { * Consume Action configured resources & uses.
baseModifiers: this.roll.getModifier(), * That method is only used when we want those resources to be consumed outside of the use method workflow.
label: 'Attack', * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
type: this.actionType, * @param {boolean} successCost
difficulty: this.roll?.difficulty, */
formula: this.roll.getFormula(),
advantage: CONFIG.DH.ACTIONS.advantageState[this.roll.advState].value
};
if (this.roll?.type === 'diceSet' || !this.hasRoll) roll.lite = true;
return roll;
}
doFollowUp(config) {
return !config.hasRoll;
}
async consume(config, successCost = false) { async consume(config, successCost = false) {
const actor = this.actor.system.partner ?? this.actor, await this.workflow.get('cost')?.execute(config, successCost);
usefulResources = { await this.workflow.get('uses')?.execute(config, successCost);
...foundry.utils.deepClone(actor.system.resources),
fear: {
value: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
max: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear,
reversed: false
}
};
for (var cost of config.costs) { if (config.roll && !config.roll.success && successCost) {
if (cost.keyIsID) {
usefulResources[cost.key] = {
value: cost.value,
target: this.parent.parent,
keyIsID: true
};
}
}
const resources = game.system.api.fields.ActionFields.CostField.getRealCosts(config.costs)
.filter(
c =>
(!successCost && (!c.consumeOnSuccess || config.roll?.success)) ||
(successCost && c.consumeOnSuccess)
)
.reduce((a, c) => {
const resource = usefulResources[c.key];
if (resource) {
a.push({
key: c.key,
value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1),
target: resource.target,
keyIsID: resource.keyIsID
});
return a;
}
}, []);
await actor.modifyResource(resources);
if (
config.uses?.enabled &&
((!successCost && (!config.uses?.consumeOnSuccess || config.roll?.success)) ||
(successCost && config.uses?.consumeOnSuccess))
)
this.update({ 'uses.value': this.uses.value + 1 });
if (config.roll?.success || successCost) {
setTimeout(() => { setTimeout(() => {
(config.message ?? config.parent).update({ 'system.successConsumed': true }); (config.message ?? config.parent).update({ 'system.successConsumed': true });
}, 50); }, 50);
} }
} }
/* */
/* ROLL */ /**
getUseHasRoll(byPass = false) { * Set if a configuration dialog must be shown or not if a special keyboard key is pressed.
return this.hasRoll && !byPass; * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
*/
static applyKeybindings(config) {
config.dialog.configure ??= !(config.event.shiftKey || config.event.altKey || config.event.ctrlKey);
} }
/**
* Getters to know which parts the action is composed of. A field can exist but configured to not be used.
* @returns {boolean} If that part is in the action.
*/
get hasRoll() { get hasRoll() {
return !!this.roll?.type; return !!this.roll?.type;
} }
get modifiers() { get hasDamage() {
if (!this.actor) return []; return this.damage?.parts?.length && this.type !== 'healing';
const modifiers = []; }
/** Placeholder for specific bonuses **/
return modifiers; get hasHealing() {
return this.damage?.parts?.length && this.type === 'healing';
} }
/* ROLL */
/* SAVE */
get hasSave() { get hasSave() {
return !!this.save?.trait; return !!this.save?.trait;
} }
/* SAVE */
/* EFFECTS */
get hasEffect() { get hasEffect() {
return this.effects?.length > 0; return this.effects?.length > 0;
} }
async applyEffects(event, data, targets) {
targets ??= data.system.targets;
const force = true; /* Where should this come from? */
if (!this.effects?.length || !targets.length) return;
let effects = this.effects;
targets.forEach(async token => {
if (!token.hit && !force) return;
if (this.hasSave && token.saved.success === true) {
effects = this.effects.filter(e => e.onSave === true);
}
if (!effects.length) return;
effects.forEach(async e => {
const actor = canvas.tokens.get(token.id)?.actor,
effect = this.item.effects.get(e._id);
if (!actor || !effect) return;
await this.applyEffect(effect, actor);
});
});
}
async applyEffect(effect, actor) {
const existingEffect = actor.effects.find(e => e.origin === effect.uuid);
if (existingEffect) {
return effect.update(
foundry.utils.mergeObject({
...effect.constructor.getInitialDuration(),
disabled: false
})
);
}
// Otherwise, create a new effect on the target
const effectData = foundry.utils.mergeObject({
...effect.toObject(),
disabled: false,
transfer: false,
origin: effect.uuid
});
await ActiveEffect.implementation.create(effectData, { parent: actor });
}
/* EFFECTS */
/* SAVE */
async rollSave(actor, event, message) {
if (!actor) return;
const title = actor.isNPC
? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll')
: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(abilities[this.save.trait]?.label)
});
return actor.diceRoll({
event,
title,
roll: {
trait: this.save.trait,
difficulty: this.save.difficulty ?? this.actor?.baseSaveDifficulty,
type: 'reaction'
},
type: 'trait',
hasRoll: true,
data: actor.getRollData()
});
}
updateSaveMessage(result, message, targetId) {
if (!result) return;
const updateMsg = this.updateChatMessage.bind(this, message, targetId, {
result: result.roll.total,
success: result.roll.success
});
if (game.modules.get('dice-so-nice')?.active)
game.dice3d.waitFor3DAnimationByMessageID(result.message.id ?? result.message._id).then(() => updateMsg());
else updateMsg();
}
static rollSaveQuery({ actionId, actorId, event, message }) {
return new Promise(async (resolve, reject) => {
const actor = await fromUuid(actorId),
action = await fromUuid(actionId);
if (!actor || !actor?.isOwner) reject();
action.rollSave(actor, event, message).then(result => resolve(result));
});
}
/* SAVE */
async updateChatMessage(message, targetId, changes, chain = true) {
setTimeout(async () => {
const chatMessage = ui.chat.collection.get(message._id);
await chatMessage.update({
flags: {
[game.system.id]: {
reactionRolls: {
[targetId]: changes
}
}
}
});
}, 100);
if (chain) {
if (message.system.source.message)
this.updateChatMessage(ui.chat.collection.get(message.system.source.message), targetId, changes, false);
const relatedChatMessages = ui.chat.collection.filter(c => c.system.source?.message === message._id);
relatedChatMessages.forEach(c => {
this.updateChatMessage(c, targetId, changes, false);
});
}
}
/** /**
* Generates a list of localized tags for this action. * Generates a list of localized tags for this action.
* @returns {string[]} An array of localized tag strings. * @returns {string[]} An array of localized tag strings.
*/ */
_getTags() { _getTags() {
const tags = [ const tags = [game.i18n.localize(`DAGGERHEART.ACTIONS.TYPES.${this.type}.name`)];
game.i18n.localize(`DAGGERHEART.ACTIONS.TYPES.${this.type}.name`),
game.i18n.localize(`DAGGERHEART.CONFIG.ActionType.${this.actionType}`)
];
return tags; return tags;
} }
} }
export class ResourceUpdateMap extends Map {
#actor;
constructor(actor) {
super();
this.#actor = actor;
}
addResources(resources) {
for (const resource of resources) {
if (!resource.key) continue;
const existing = this.get(resource.key);
if (existing) {
this.set(resource.key, {
...existing,
value: existing.value + (resource.value ?? 0),
total: existing.total + (resource.total ?? 0)
});
} else {
this.set(resource.key, resource);
}
}
}
#getResources() {
return Array.from(this.values());
}
async updateResources() {
if (this.#actor) {
const target = this.#actor.system.partner ?? this.#actor;
await target.modifyResource(this.#getResources());
}
}
}

View file

@ -1,10 +1,9 @@
import BeastformDialog from '../../applications/dialogs/beastformDialog.mjs';
import DHBaseAction from './baseAction.mjs'; import DHBaseAction from './baseAction.mjs';
export default class DhBeastformAction extends DHBaseAction { export default class DhBeastformAction extends DHBaseAction {
static extraSchemas = [...super.extraSchemas, 'beastform']; static extraSchemas = [...super.extraSchemas, 'beastform'];
async use(event, options) { /* async use(event, options) {
const beastformConfig = this.prepareBeastformConfig(); const beastformConfig = this.prepareBeastformConfig();
const abort = await this.handleActiveTransformations(); const abort = await this.handleActiveTransformations();
@ -82,5 +81,5 @@ export default class DhBeastformAction extends DHBaseAction {
beastformEffects.map(x => x.id) beastformEffects.map(x => x.id)
); );
return existingEffects; return existingEffects;
} } */
} }

View file

@ -0,0 +1,49 @@
import DHBaseAction from './baseAction.mjs';
export default class DhCountdownAction extends DHBaseAction {
static extraSchemas = [...super.extraSchemas, 'countdown'];
get defaultValues() {
return {
...super.defaultValues,
countdown: {
name: this.parent.parent.name,
img: this.img,
progress: {
startFormula: '1'
}
}
};
}
/** @inheritdoc */
static getSourceConfig(parent) {
const updateSource = game.system.api.data.actions.actionsTypes.base.getSourceConfig(parent);
updateSource.name = game.i18n.localize('DAGGERHEART.ACTIONS.Config.countdown.startCountdown');
updateSource['countdown'] = [
{
...game.system.api.data.countdowns.DhCountdown.defaultCountdown(),
name: parent.parent.name,
img: parent.parent.img,
progress: {
startFormula: '1'
}
}
];
return updateSource;
}
/** @inheritDoc */
static migrateData(source) {
for (const countdown of source.countdown) {
if (countdown.progress.max) {
countdown.progress.startFormula = countdown.progress.max;
countdown.progress.start = 1;
countdown.progress.max = null;
}
}
return super.migrateData(source);
}
}

View file

@ -1,65 +1,18 @@
import { setsEqual } from '../../helpers/utils.mjs';
import DHBaseAction from './baseAction.mjs'; import DHBaseAction from './baseAction.mjs';
export default class DHDamageAction extends DHBaseAction { export default class DHDamageAction extends DHBaseAction {
static extraSchemas = [...super.extraSchemas, 'damage', 'target', 'effects']; static extraSchemas = [...super.extraSchemas, 'damage', 'target', 'effects'];
getFormulaValue(part, data) { /**
let formulaValue = part.value; * Return a display ready damage formula string
* @returns Formula string
if (data.hasRoll && part.resultBased && data.roll.result.duality === -1) return part.valueAlt; */
getDamageFormula() {
const isAdversary = this.actor.type === 'adversary'; const strings = [];
if (isAdversary && this.actor.system.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id) { for (const { value } of this.damage.parts) {
const hasHordeDamage = this.actor.effects.find(x => x.type === 'horde'); strings.push(Roll.replaceFormulaData(value.getFormula(), this.actor?.getRollData() ?? {}));
if (hasHordeDamage && !hasHordeDamage.disabled) return part.valueAlt;
} }
return formulaValue; return strings.join(' + ');
}
formatFormulas(formulas, systemData) {
const formattedFormulas = [];
formulas.forEach(formula => {
if (isNaN(formula.formula))
formula.formula = Roll.replaceFormulaData(formula.formula, this.getRollData(systemData));
const same = formattedFormulas.find(
f => setsEqual(f.damageTypes, formula.damageTypes) && f.applyTo === formula.applyTo
);
if (same) same.formula += ` + ${formula.formula}`;
else formattedFormulas.push(formula);
});
return formattedFormulas;
}
async rollDamage(event, data) {
const systemData = data.system ?? data;
let formulas = this.damage.parts.map(p => ({
formula: this.getFormulaValue(p, systemData).getFormula(this.actor),
damageTypes: p.applyTo === 'hitPoints' && !p.type.size ? new Set(['physical']) : p.type,
applyTo: p.applyTo
}));
if (!formulas.length) return;
formulas = this.formatFormulas(formulas, systemData);
delete systemData.evaluate;
const config = {
...systemData,
roll: formulas,
dialog: {},
data: this.getRollData()
};
if (this.hasSave) config.onSave = this.save.damageMod;
if (data.system) {
config.source.message = data._id;
config.directDamage = false;
} else {
config.directDamage = true;
}
return CONFIG.Dice.daggerheart.DamageRoll.build(config);
} }
} }

View file

@ -2,15 +2,4 @@ import DHBaseAction from './baseAction.mjs';
export default class DHMacroAction extends DHBaseAction { export default class DHMacroAction extends DHBaseAction {
static extraSchemas = [...super.extraSchemas, 'macro']; static extraSchemas = [...super.extraSchemas, 'macro'];
async trigger(event, ...args) {
const fixUUID = !this.macro.includes('Macro.') ? `Macro.${this.macro}` : this.macro,
macro = await fromUuid(fixUUID);
try {
if (!macro) throw new Error(`No macro found for the UUID: ${this.macro}.`);
macro.execute();
} catch (error) {
ui.notifications.error(error);
}
}
} }

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 { export default class BaseEffect extends foundry.abstract.TypeDataModel {
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
@ -30,4 +44,24 @@ export default class BaseEffect extends foundry.abstract.TypeDataModel {
}) })
}; };
} }
static getDefaultObject() {
return {
name: 'New Effect',
id: foundry.utils.randomID(),
disabled: false,
img: 'icons/magic/life/heart-cross-blue.webp',
description: '',
statuses: [],
changes: [],
system: {
rangeDependence: {
enabled: false,
type: CONFIG.DH.GENERAL.rangeInclusion.withinRange.id,
target: CONFIG.DH.GENERAL.otherTargetTypes.hostile.id,
range: CONFIG.DH.GENERAL.range.melee.id
}
}
};
}
} }

View file

@ -6,10 +6,12 @@ export default class BeastformEffect extends BaseEffect {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
characterTokenData: new fields.SchemaField({ characterTokenData: new fields.SchemaField({
usesDynamicToken: new fields.BooleanField({ initial: false }),
tokenImg: new fields.FilePathField({ tokenImg: new fields.FilePathField({
categories: ['IMAGE'], categories: ['IMAGE'],
base64: false, base64: false,
nullable: true nullable: true,
wildcard: true
}), }),
tokenRingImg: new fields.FilePathField({ tokenRingImg: new fields.FilePathField({
initial: 'icons/svg/mystery-man.svg', initial: 'icons/svg/mystery-man.svg',
@ -17,8 +19,8 @@ export default class BeastformEffect extends BaseEffect {
base64: false base64: false
}), }),
tokenSize: new fields.SchemaField({ tokenSize: new fields.SchemaField({
height: new fields.NumberField({ integer: true, nullable: true }), height: new fields.NumberField({ integer: false, nullable: true }),
width: new fields.NumberField({ integer: true, nullable: true }) width: new fields.NumberField({ integer: false, nullable: true })
}) })
}), }),
advantageOn: new fields.ArrayField(new fields.StringField()), advantageOn: new fields.ArrayField(new fields.StringField()),
@ -27,6 +29,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) { async _onCreate(_data, _options, userId) {
if (userId !== game.user.id) return; if (userId !== game.user.id) return;
@ -38,20 +48,57 @@ export default class BeastformEffect extends BaseEffect {
async _preDelete() { async _preDelete() {
if (this.parent.parent.type === 'character') { if (this.parent.parent.type === 'character') {
const update = { const baseUpdate = {
height: this.characterTokenData.tokenSize.height, height: this.characterTokenData.tokenSize.height,
width: this.characterTokenData.tokenSize.width, width: this.characterTokenData.tokenSize.width
};
const update = {
...baseUpdate,
texture: { texture: {
src: this.characterTokenData.tokenImg src: this.characterTokenData.tokenImg
}, },
ring: { ring: {
enabled: this.characterTokenData.usesDynamicToken,
subject: { subject: {
texture: this.characterTokenData.tokenRingImg texture: this.characterTokenData.tokenRingImg
} }
} }
}; };
await updateActorTokens(this.parent.parent, update); const updateToken = token => {
let x = null,
y = null;
if (token.object?.scene?.grid) {
const positionData = game.system.api.documents.DhToken.getSnappedPositionInSquareGrid(
token.object.scene.grid,
{ x: token.x, y: token.y, elevation: token.elevation },
baseUpdate.width,
baseUpdate.height
);
x = positionData.x;
y = positionData.y;
}
return {
...baseUpdate,
x,
y,
'texture': {
enabled: this.characterTokenData.usesDynamicToken,
src: token.flags.daggerheart?.beastformTokenImg ?? this.characterTokenData.tokenImg
},
'ring': {
subject: {
texture:
token.flags.daggerheart?.beastformSubjectTexture ?? this.characterTokenData.tokenRingImg
}
},
'flags.daggerheart': { '-=beastformTokenImg': null, '-=beastformSubjectTexture': null }
};
};
await updateActorTokens(this.parent.parent, update, updateToken);
await this.parent.parent.deleteEmbeddedDocuments('Item', this.featureIds); await this.parent.parent.deleteEmbeddedDocuments('Item', this.featureIds);
await this.parent.parent.deleteEmbeddedDocuments('ActiveEffect', this.effectIds); await this.parent.parent.deleteEmbeddedDocuments('ActiveEffect', this.effectIds);

View file

@ -2,12 +2,14 @@ import DhCharacter from './character.mjs';
import DhCompanion from './companion.mjs'; import DhCompanion from './companion.mjs';
import DhAdversary from './adversary.mjs'; import DhAdversary from './adversary.mjs';
import DhEnvironment from './environment.mjs'; import DhEnvironment from './environment.mjs';
import DhParty from './party.mjs';
export { DhCharacter, DhCompanion, DhAdversary, DhEnvironment }; export { DhCharacter, DhCompanion, DhAdversary, DhEnvironment, DhParty };
export const config = { export const config = {
character: DhCharacter, character: DhCharacter,
companion: DhCompanion, companion: DhCompanion,
adversary: DhAdversary, adversary: DhAdversary,
environment: DhEnvironment environment: DhEnvironment,
party: DhParty
}; };

View file

@ -1,6 +1,6 @@
import DHAdversarySettings from '../../applications/sheets-configs/adversary-settings.mjs'; import DHAdversarySettings from '../../applications/sheets-configs/adversary-settings.mjs';
import { ActionField } from '../fields/actionField.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'; import { resourceField, bonusField } from '../fields/actorField.mjs';
export default class DhpAdversary extends BaseDataActor { export default class DhpAdversary extends BaseDataActor {
@ -10,7 +10,9 @@ export default class DhpAdversary extends BaseDataActor {
return foundry.utils.mergeObject(super.metadata, { return foundry.utils.mergeObject(super.metadata, {
label: 'TYPES.Actor.adversary', label: 'TYPES.Actor.adversary',
type: 'adversary', type: 'adversary',
settingSheet: DHAdversarySettings settingSheet: DHAdversarySettings,
hasAttribution: true,
usesSize: true
}); });
} }
@ -26,7 +28,7 @@ export default class DhpAdversary extends BaseDataActor {
}), }),
type: new fields.StringField({ type: new fields.StringField({
required: true, required: true,
choices: CONFIG.DH.ACTOR.adversaryTypes, choices: CONFIG.DH.ACTOR.allAdversaryTypes,
initial: CONFIG.DH.ACTOR.adversaryTypes.standard.id initial: CONFIG.DH.ACTOR.adversaryTypes.standard.id
}), }),
motivesAndTactics: new fields.StringField(), motivesAndTactics: new fields.StringField(),
@ -38,6 +40,7 @@ export default class DhpAdversary extends BaseDataActor {
integer: true, integer: true,
label: 'DAGGERHEART.GENERAL.hordeHp' label: 'DAGGERHEART.GENERAL.hordeHp'
}), }),
criticalThreshold: new fields.NumberField({ required: true, integer: true, min: 1, max: 20, initial: 20 }),
damageThresholds: new fields.SchemaField({ damageThresholds: new fields.SchemaField({
major: new fields.NumberField({ major: new fields.NumberField({
required: true, required: true,
@ -56,6 +59,9 @@ export default class DhpAdversary extends BaseDataActor {
hitPoints: resourceField(0, 0, 'DAGGERHEART.GENERAL.HitPoints.plural', true), hitPoints: resourceField(0, 0, 'DAGGERHEART.GENERAL.HitPoints.plural', true),
stress: resourceField(0, 0, 'DAGGERHEART.GENERAL.stress', true) stress: resourceField(0, 0, 'DAGGERHEART.GENERAL.stress', true)
}), }),
rules: new fields.SchemaField({
...commonActorRules()
}),
attack: new ActionField({ attack: new ActionField({
initial: { initial: {
name: 'Attack', name: 'Attack',
@ -120,6 +126,10 @@ export default class DhpAdversary extends BaseDataActor {
return this.parent.items.filter(x => x.type === 'feature'); return this.parent.items.filter(x => x.type === 'feature');
} }
isItemValid(source) {
return source.type === 'feature';
}
async _preUpdate(changes, options, user) { async _preUpdate(changes, options, user) {
const allowed = await super._preUpdate(changes, options, user); const allowed = await super._preUpdate(changes, options, user);
if (allowed === false) return false; if (allowed === false) return false;
@ -169,4 +179,13 @@ export default class DhpAdversary extends BaseDataActor {
} }
} }
} }
_getTags() {
const tags = [
game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${this.tier}`),
`${game.i18n.localize(`DAGGERHEART.CONFIG.AdversaryType.${this.type}.label`)}`,
`${game.i18n.localize('DAGGERHEART.GENERAL.difficulty')}: ${this.difficulty}`
];
return tags;
}
} }

View file

@ -1,21 +1,24 @@
import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs'; import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs';
import { createScrollText, getScrollTextData } from '../../helpers/utils.mjs'; import DHItem from '../../documents/item.mjs';
import { getScrollTextData } from '../../helpers/utils.mjs';
const fields = foundry.data.fields;
const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) => const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) =>
new foundry.data.fields.SchemaField({ new fields.SchemaField({
resistance: new foundry.data.fields.BooleanField({ resistance: new fields.BooleanField({
initial: false, initial: false,
label: `${resistanceLabel}.label`, label: `${resistanceLabel}.label`,
hint: `${resistanceLabel}.hint`, hint: `${resistanceLabel}.hint`,
isAttributeChoice: true isAttributeChoice: true
}), }),
immunity: new foundry.data.fields.BooleanField({ immunity: new fields.BooleanField({
initial: false, initial: false,
label: `${immunityLabel}.label`, label: `${immunityLabel}.label`,
hint: `${immunityLabel}.hint`, hint: `${immunityLabel}.hint`,
isAttributeChoice: true isAttributeChoice: true
}), }),
reduction: new foundry.data.fields.NumberField({ reduction: new fields.NumberField({
integer: true, integer: true,
initial: 0, initial: 0,
label: `${reductionLabel}.label`, 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 * Describes metadata about the actor data model type
* @typedef {Object} ActorDataModelMetadata * @typedef {Object} ActorDataModelMetadata
@ -39,7 +61,10 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
type: 'base', type: 'base',
isNPC: true, isNPC: true,
settingSheet: null, settingSheet: null,
hasResistances: true hasResistances: true,
hasAttribution: false,
hasLimitedView: true,
usesSize: false
}; };
} }
@ -50,9 +75,15 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
/** @inheritDoc */ /** @inheritDoc */
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields;
const schema = {}; const schema = {};
if (this.metadata.hasAttribution) {
schema.attribution = new fields.SchemaField({
source: new fields.StringField(),
page: new fields.NumberField(),
artist: new fields.StringField()
});
}
if (this.metadata.isNPC) schema.description = new fields.HTMLField({ required: true, nullable: true }); if (this.metadata.isNPC) schema.description = new fields.HTMLField({ required: true, nullable: true });
if (this.metadata.hasResistances) if (this.metadata.hasResistances)
schema.resistance = new fields.SchemaField({ schema.resistance = new fields.SchemaField({
@ -67,6 +98,13 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
'DAGGERHEART.GENERAL.DamageResistance.magicalReduction' '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; return schema;
} }
@ -78,6 +116,13 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
*/ */
static DEFAULT_ICON = null; static DEFAULT_ICON = null;
get attributionLabel() {
if (!this.attribution) return;
const { source, page } = this.attribution;
return [source, page ? `pg ${page}.` : null].filter(x => x).join('. ');
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
@ -90,6 +135,32 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
return data; 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 */
if (this.parent.parties.size > 0) {
const tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
await tagTeam.updateSource({
initiator: this.parent.id === tagTeam.initiator ? null : tagTeam.initiator,
members: Object.keys(tagTeam.members).find(x => x === this.parent.id)
? { [`-=${this.parent.id}`]: null }
: {}
});
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, tagTeam);
}
}
async _preUpdate(changes, options, userId) { async _preUpdate(changes, options, userId) {
const allowed = await super._preUpdate(changes, options, userId); const allowed = await super._preUpdate(changes, options, userId);
if (allowed === false) return; if (allowed === false) return;
@ -138,6 +209,6 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
_onUpdate(changes, options, userId) { _onUpdate(changes, options, userId) {
super._onUpdate(changes, options, userId); super._onUpdate(changes, options, userId);
createScrollText(this.parent, options.scrollingTextData); if (options.scrollingTextData) this.parent.queueScrollText(options.scrollingTextData);
} }
} }

View file

@ -1,7 +1,7 @@
import { burden } from '../../config/generalConfig.mjs'; import { burden } from '../../config/generalConfig.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import DhLevelData from '../levelData.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 { attributeField, resourceField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs';
import { ActionField } from '../fields/actionField.mjs'; import { ActionField } from '../fields/actionField.mjs';
import DHCharacterSettings from '../../applications/sheets-configs/character-settings.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 }), companion: new ForeignDocumentUUIDField({ type: 'Actor', nullable: true, initial: null }),
rules: new fields.SchemaField({ 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({ maxArmorMarked: new fields.SchemaField({
value: new fields.NumberField({ value: new fields.NumberField({
required: true, required: true,
@ -234,7 +237,9 @@ export default class DhCharacter extends BaseDataActor {
}) })
}), }),
stressDamageReduction: new fields.SchemaField({ 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'), major: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.major'),
minor: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.minor'), minor: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.minor'),
any: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.any') any: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.any')
@ -245,12 +250,8 @@ export default class DhCharacter extends BaseDataActor {
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.label', label: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.label',
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint' 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 })
}),
disabledArmor: new fields.BooleanField({ intial: false }) disabledArmor: new fields.BooleanField({ intial: false })
}
}), }),
attack: new fields.SchemaField({ attack: new fields.SchemaField({
damage: new fields.SchemaField({ damage: new fields.SchemaField({
@ -426,6 +427,33 @@ export default class DhCharacter extends BaseDataActor {
return attack; 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() { get sheetLists() {
const ancestryFeatures = [], const ancestryFeatures = [],
communityFeatures = [], communityFeatures = [],
@ -434,7 +462,7 @@ export default class DhCharacter extends BaseDataActor {
companionFeatures = [], companionFeatures = [],
features = []; 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) { if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.ancestry.id) {
ancestryFeatures.push(item); ancestryFeatures.push(item);
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.community.id) { } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.community.id) {
@ -442,20 +470,7 @@ export default class DhCharacter extends BaseDataActor {
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.class.id) { } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.class.id) {
classFeatures.push(item); classFeatures.push(item);
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.subclass.id) { } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.subclass.id) {
if (this.class.subclass) {
const prop = item.system.multiclassOrigin ? 'multiclass' : 'class';
const subclassState = this[prop].subclass?.system?.featureState;
if (!subclassState) continue;
if (
item.system.identifier === CONFIG.DH.ITEM.featureSubTypes.foundation ||
(item.system.identifier === CONFIG.DH.ITEM.featureSubTypes.specialization &&
subclassState >= 2) ||
(item.system.identifier === CONFIG.DH.ITEM.featureSubTypes.mastery && subclassState >= 3)
) {
subclassFeatures.push(item); subclassFeatures.push(item);
}
}
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.companion.id) { } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.companion.id) {
companionFeatures.push(item); companionFeatures.push(item);
} else if (item.type === 'feature' && !item.system.type) { } else if (item.type === 'feature' && !item.system.type) {
@ -669,8 +684,16 @@ export default class DhCharacter extends BaseDataActor {
} }
async _preDelete() { async _preDelete() {
super._preDelete();
if (this.companion) { if (this.companion) {
this.companion.updateLevel(1); this.companion.updateLevel(1);
} }
} }
_getTags() {
return [this.class.value?.name, this.class.subclass?.name, this.community?.name, this.ancestry?.name].filter(
t => !!t
);
}
} }

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