Compare commits

...

324 commits
1.0.3 ... 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
WBHarry
dc54fdc096
[Fix] 1091 - Improve Homebrew Domain Container (#1095)
* Simple fix for now

* Corrected to 2 rows of height

* Corrected scrollbar
2025-08-26 20:30:27 +02:00
ff396cd2f0
added note to translations (#1093)
* added note to translations

* clarified note
2025-08-26 22:42:07 +10: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
76d753cd88
Fixed so subclasses can be dragged to the sheet when they're stored in a folder (#1081) 2025-08-26 10:14:09 +10:00
WBHarry
ff65a85458
[Fix] 1086 - Enrichment Buttons (#1087)
* Fixed so enriched buttons are button type

* Fixed DamageEnrihed button
2025-08-26 10:04:22 +10:00
WBHarry
0a5828c8fa
[Fix] Character Sheet Subclass Compendium Preset (#1083)
* Added the preset for the subclass Compendium Browser from character sheet

* Raised version
2025-08-26 09:54:07 +10:00
WBHarry
2aeb255033
Corrected so secondary weapons will show that they are secondary in the tooltip (#1082) 2025-08-26 09:27:38 +10:00
WBHarry
9aba4dc66d
Corrected pendant name (#1085) 2025-08-26 09:17:58 +10:00
WBHarry
990c73987e Merged with main 2025-08-24 21:31:57 +02:00
WBHarry
f480027b95
Improved text color on chat-message flavor texts (#1080) 2025-08-24 16:09:21 -03:00
Dapoulp
7a1d259e82
Fix/1022 (#1079)
* Temp ActionField attack type missing

* Move missing attack type to getModel

* Show hit/miss to GM and hide to players if no OBSERVER permission
2025-08-24 20:55:35 +02:00
WBHarry
d24fef7494
[Fix] Hints For Drag/Drop (#1076)
* Added hints for drag-drop areas

* Changed to drag-area style. Updated localization
2025-08-24 14:15:42 -03:00
WBHarry
d686ac1e0c
. (#1056) 2025-08-24 13:40:06 -03:00
WBHarry
5a38e28a84
Fixed the migration to respect null for multiclass (#1075) 2025-08-24 21:36:46 +10: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
WBHarry
fbeff1b908
The basic attack damage shown in the sidebar is modified if horde damage is active (#1071) 2025-08-24 10:21:00 +10:00
WBHarry
46ea4addd0
[Fix] Subclass Links (#1069)
* Attempt at making subclass aware of its class

* Update compendium classes/subclasses

* Fixed multiclass

* Update compenidum browser subclass class filter

* Added migration for subclass.linkedClass

* Using foundry's isNewer function rather than custom one

* Added migration for existing actor features

---------

Co-authored-by: Dapoolp <elcatnet@gmail.com>
2025-08-24 10:13:15 +10:00
WBHarry
0b2694b007
Improved feature request label (#1072) 2025-08-23 17:50:10 -04: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
WBHarry
0c3ebd6e11
Fixed description on Dracon and Orderborne (#1067) 2025-08-23 21:55:31 +10:00
WBHarry
936c96a1be
Fixed so enriched buttons are inline by default. Can be set to 'inline:true' to make them fit with the text better (#1068) 2025-08-23 21:49:10 +10: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
f76515eac1 Revert "Fixed multiclass"
This reverts commit 2d20fb0df4.
2025-08-23 13:20:02 +02:00
WBHarry
2d20fb0df4 Fixed multiclass 2025-08-23 13:18:14 +02:00
Chris Ryan
b83adbf09b
Fixed the Cult Initiate Group Attack to use Fear instead of marking Stress (#1062)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-08-23 10:32:17 +02:00
WBHarry
d5f7e17339
Added Custom Adversary Types (#1048) 2025-08-23 10:18:25 +10:00
Dapoulp
7016f71da7
Fix/spellcast modifier (#1061)
* Temp ActionField attack type missing

* Move missing attack type to getModel

* Fix spellcast modifier bonus
2025-08-23 10:05:16 +10:00
WBHarry
86d451f0d7
. (#1060) 2025-08-23 09:55:18 +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
WBHarry
523ecb506b
[Fix] ItemLink Fix (#1032)
* .

* .

* Removed outcommented code

* Raised to minor version

* Added confirmation on import of old character data
2025-08-22 01:38:07 +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
WBHarry
218f180fa0
[Fix] DiceSoNice DamageRolls (#1026)
* Fixed so the 3d damage dice can be seen by everyone when not whispered

* Fixed typo
2025-08-20 18:13:51 +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
a415ab9955
[Fix] Fixed 1.0.6 Errors (#1020)
* Raised version

* Removed references to the unreleased expandChatMessage

* Raised version

* Restored 1.0.6 as version
2025-08-19 20:43:34 +02:00
WBHarry
b3062bf5b9
RollTable chat message cleanup (#1012) 2025-08-19 21:16:08 +10:00
WBHarry
540ee49f50
Fixed so that active effects can be dragged between all sheets (#1011) 2025-08-19 21:12:09 +10: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
bd8cfe0297
Clicking on the resource input of a feature no longer toggles the description (#993) 2025-08-18 12:06:10 +10: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
Nikhil Nagarajan
649b4d64bc
Initial Commit (#992) 2025-08-17 21:12:59 -04:00
WBHarry
577ed5f491
Added tooltips for experience description (#975) 2025-08-18 11:00:30 +10:00
Nikhil Nagarajan
233e9eddbe
[PR] testing something (#995)
* testing something

* added template for PR test
2025-08-17 21:59:13 +02: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
Luiz HD Costa
e692f3814d
Add setup script for development (#981) 2025-08-17 18:51:29 +02:00
Chris Ryan
505ee6d3cf
Fix/add translation key for Unarmed Attack (#973)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-08-17 18:50:53 +02:00
Dapoulp
6100f3cf9e
[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
2025-08-17 18:40:47 +02:00
Dapoulp
667edda50a
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
2025-08-17 16:04:16 +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
Psitacus
264ba44a14
add typo report template (#971)
Co-authored-by: Psitacus <walther.johnson@ucalgary.ca>
2025-08-16 12:05:02 +10:00
WBHarry
2820c96259 Merge branch 'main' into development 2025-08-16 02:22:27 +02:00
WBHarry
b241b36653
[Fix] Adversary Compendium Pass (#960)
* Went over all adversaries to fix missing ranges

* Fixed translation for itemFeatures Protective

* Updated all instances of the 'Paired' WeaponFeature in the compendium

* Changed all adversary attacks to be chatDisplay=false
2025-08-16 01:56:51 +02:00
WBHarry
ab76d9e91d
[Fix] 940 Character Import (#962)
* Changed Class/Multiclass to simple getters to avoid having to keep them up to date

* Corrected variable
2025-08-16 01:56:17 +02:00
WBHarry
0e43bf197d
[Fix] 948 - Damage Reroll Parts (#965)
* Fixed so rerolling damage doesn't make damageParts an object

* Improved to properly handle theoretical additional damage parts
2025-08-16 01:35:04 +02:00
WBHarry
88be4c953d
[Fix] Creation/Levelup disable finish button (#969)
* Set the finish buttons on levelup and creation to disable after first click

* Raised system version
2025-08-16 01:34:22 +02:00
Dapoulp
a07819611e
Fix armor slot reduction (#968)
* Temp ActionField attack type missing

* Move missing attack type to getModel

* Fix armor slot reduction if armor as part of damage
2025-08-16 01:05:05 +02:00
Dapoulp
16e931179f
Fix/attack type migrate data (#949)
* Temp ActionField attack type missing

* Move missing attack type to getModel

* Move attack type fix to actor migratedata
2025-08-15 21:43:33 +02:00
Dapoulp
ba42892eed
Fix sheet img (#966)
* Temp ActionField attack type missing

* Move missing attack type to getModel

* Fix sheet portrait image css
2025-08-15 21:29:24 +02:00
Dapoulp
841819ee89
Fix/941 companion hope ranger (#950)
* Temp ActionField attack type missing

* Move missing attack type to getModel

* Fix Companion not updating Partner resources
2025-08-15 21:27:55 +02:00
WBHarry
b3c0344b91 Merged in main 2025-08-14 20:39:53 +02:00
WBHarry
006f9dde1f
[Fix] Levelup Blackbox Issue (#934)
* Made the original inputs related to tagify be display: none to avoid css issues

* Raised system.json version
2025-08-14 19:49:45 +02:00
WBHarry
3d05c7fe0a Merged in release to fix history 2025-08-14 19:41:38 +02:00
WBHarry
633600f271 Merge branch 'release' 2025-08-14 19:40:02 +02:00
WBHarry
16299a9865
Fixed damage reduction any (#937) 2025-08-14 13:21:12 -04:00
Dapoulp
47754e78cd
Fix roll dialog default rollmode value (#938) 2025-08-14 13:08:59 -04:00
Dapoulp
207d0d6335
Fix/929 flat modifier disappears (#936)
* Temp ActionField attack type missing

* Move missing attack type to getModel
2025-08-14 15:51:56 +02:00
Chris Ryan
89e4e43026
Set the Variant Rules settings to GM Only (#935)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-08-14 14:41:36 +02:00
Chris Ryan
6942ec7d14
Stress is a isReversed resource, so need to supply different values to clear/add (#932)
Co-authored-by: Chris Ryan <chrisr@blackhole>
2025-08-14 10:26:07 +02:00
WBHarry
ecd33caa7b Merged with main 2025-08-14 00:51:43 +02:00
WBHarry
94484bd024 Merged release into main 2025-08-14 00:31:39 +02:00
Dapoulp
94c235bc08
Temp ActionField attack type missing (#923) 2025-08-14 00:25:03 +02:00
WBHarry
24114fbdf6
Removed duplicated methods so logic gets correct (#920) 2025-08-13 23:36:12 +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
WBHarry
5e56c7ec2e
Hotfix 1.0.2 (#916)
* 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)

* Impproved Adversary Sheet Data Display (#751)

* Impproved Adversary Sheet Data Display
Fixes #604

* FIX: formula lables for attacks and weapons

---------

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

* 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

---------

Co-authored-by: Chris Ryan <chrisr@blackhole>
Co-authored-by: WBHarry <williambjrklund@gmail.com>

* Fix movement (#885)

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

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

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

* 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

---------

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

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

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

Fixes #910

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

* 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 21:53:20 +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
1634 changed files with 37836 additions and 30650 deletions

2
.env.example Normal file
View file

@ -0,0 +1,2 @@
FOUNDRY_MAIN_PATH=/path/to/foundry/resources/app/main.js
FOUNDRY_DATA_PATH=/path/to/foundry/data

View file

@ -1,10 +1,10 @@
--- ---
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a bug report to help us identify issues and resolve them
title: "[BUG] - " title: "[Bug] <Insert Title here> "
labels: bug labels: bug
type: bug
assignees: '' assignees: ''
--- ---
**Describe the bug** **Describe the bug**
@ -24,10 +24,10 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Setup Information:** **Setup Information:**
- OS: [e.g. iOS] - OS: [e.g. iOS, Windows]
- Browser [e.g. chrome, safari] - Browser [e.g. chrome, safari]
- Foundry Version [e.g. v13 b342] - Foundry Version [e.g. v13 b342]
- System Version [e.g. main-3593f44] - System Version [e.g. v.1.0, v.1.0.1]
**Additional context** **Additional context**

View file

@ -0,0 +1,14 @@
---
name: Feature request
about: Create a feature request for suggestions on improving the system
title: "[Feature] <Insert Title here> "
labels: enhancement, discussion, maybe
type: feature
assignees: ''
---
**Description**
A clear and concise description of what feature needs to be implemented.
**Screenshots**
If applicable, add screenshots to help explain the feature that needs to be implemented.

9
.github/ISSUE_TEMPLATE/typo_report.md vendored Normal file
View file

@ -0,0 +1,9 @@
---
name: Typo report
about: Create a new issue to report a compendium typo
title: "[TYPO] - "
labels: compendium, typo
type: bug
assignees: ''
---

View file

@ -0,0 +1,55 @@
---
name: Pull Request
about: Create a new pull request
title: "[Community PR] <Insert Title here>"
labels: community pr
assignees: ''
---
## Description
Please include a summary of the change and which issue is fixed (if applicable). Also include relevant context or motivation for the change.
- Fixes #(issue)
- Closes #(issue)
## Type of Change
Please check the relevant options:
- [ ] Bug fix
- [ ] New feature
- [ ] Code cleanup/refactor
- [ ] Documentation update
- [ ] Test coverage
- [ ] Dependency update
- [ ] Configuration change
- [ ] Other (please describe):
## How Has This Been Tested?
Please describe the tests you ran to verify your changes:
- [ ] Manual testing
- [ ] Other:
## Screenshots (if applicable)
Include screenshots or GIFs to help explain your changes visually.
## Checklist
- [ ] My code follows the project style guidelines
- [ ] I have performed a self-review of my code
- [ ] I have commented my code where necessary
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings or errors
- [ ] I have added tests that prove my fix or feature works
- [ ] New and existing tests pass locally with my changes
## Additional Comments
Add any other context or questions here.
---
> Thank you for your contribution! 🎉

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
.vscode .vscode
.env
node_modules node_modules
/packs /packs
Build Build

View file

@ -17,6 +17,10 @@ We welcome contributions of all kinds:
Please be respectful and collaborative — were all here to build something great together. Please be respectful and collaborative — were all here to build something great together.
### Community Translations
Please note that we are not accepting community translations in the main project. Instead, community translations should be published as a module.
--- ---
## 🧭 General Guidelines ## 🧭 General Guidelines
@ -40,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
@ -67,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**!
🐸🛠️ 🐸🛠️

View file

@ -24,24 +24,41 @@ You can find the documentation here: https://github.com/Foundryborne/daggerheart
## Development Setup ## Development Setup
- Open a terminal in the directory with the repo `cd <path>/<to>/<repo>` 1. **Navigate to the repo directory:**
- NOTE: The repo should be placed in the system files are or somewhere else and a link (if on linux) is placed in the system directory
- NOTE: Linux link can be made using `ln -snf <path to development folder> daggerheart` inside the system folder
- Install npm `npm install`
- Update package.json to match your profile
``` ```bash
"start": "concurrently \"rollup -c --watch\" \"node C:/FoundryDev/resources/app/main.js --dataPath=C:/FoundryDevFiles --noupnp\" \"gulp\"", cd <path>/<to>/<repo>
"start-test": "node C:/FoundryDev/resources/app/main.js --dataPath=C:/FoundryDevFiles && rollup -c --watch && gulp", ```
``` 2. **Install dependencies:**
- Replace `C:/FoundryDev/resources/app/main.js` with `<your>/<path>/<to>/<foundry>/<main.js>` ```bash
- The main is likely in `<Foundry Install Location>/resouces/app/main.js` npm install
- Replace `--dataPath=C:/FoundryDevFiles` with `<your>/<path>/<to>/<foundry>/<data>` ```
Now you should be able to build the app using `npm start` 3. **Configure your Foundry paths:**
[Foundry VTT Website][1]
```bash
npm run setup:dev -- --foundry-path="/path/to/foundry/main.js" --data-path="/path/to/data"
```
4. **Start developing:**
```bash
npm start
```
### Available Scripts
- `npm start` - Start development with file watching and Foundry launching
- `npm run build` - One-time build
- `npm run setup:dev -- --foundry-path="<path>" --data-path="<path>"` - Configure development environment
### Notes
- The repo should be placed in your Foundry `Data/systems/` directory or symlinked there
- Linux symlink can be made using `ln -snf <path to development folder> daggerheart` inside the systems folder
- Your `.env` file is ignored by git, so each developer can have their own configuration
[Foundry VTT Website][1]
[1]: https://foundryvtt.com/ [1]: https://foundryvtt.com/

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,97 +8,177 @@ 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 { 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,
settingsRegistration, settingsRegistration,
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 = [...CONFIG.Dice.rolls, DHRoll, DualityRoll, D20Roll, DamageRoll];
Roll.CHAT_TEMPLATE = 'systems/daggerheart/templates/ui/chat/foundryRoll.hbs';
Roll.TOOLTIP_TEMPLATE = 'systems/daggerheart/templates/ui/chat/foundryRollTooltip.hbs';
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,
@ -109,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)) {
@ -170,13 +249,15 @@ Hooks.on('ready', async () => {
game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.welcomeMessage, true); game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.welcomeMessage, true);
} }
} }
runMigrations();
}); });
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));
}); });
@ -229,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));

File diff suppressed because it is too large Load diff

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,26 +422,30 @@ 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
} }
}; };
if (type == 'domains') if (type === 'domains')
presets.filter = { presets.filter = {
'level.max': { key: 'level.max', value: 1 }, 'level.max': { key: 'level.max', value: 1 },
'system.domain': { key: 'system.domain', value: this.setup.class?.system.domains ?? null } 'system.domain': { key: 'system.domain', value: this.setup.class?.system.domains ?? null }
}; };
if (type === 'subclasses')
presets.filter = {
'system.linkedClass.uuid': { key: 'system.linkedClass.uuid', value: this.setup.class?.uuid }
};
if (equipment.includes(type)) if (equipment.includes(type))
presets.filter = { presets.filter = {
'system.tier': { key: 'system.tier', value: 1 }, 'system.tier': { key: 'system.tier', value: 1 },
'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) {
@ -472,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';
@ -494,7 +495,9 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
this.render(); this.render();
} }
static async finish() { static async finish(_, button) {
button.disabled = true;
const primaryAncestryFeature = this.setup.primaryAncestry.system.primaryFeature; const primaryAncestryFeature = this.setup.primaryAncestry.system.primaryFeature;
const secondaryAncestryFeature = this.setup.secondaryAncestry?.uuid const secondaryAncestryFeature = this.setup.secondaryAncestry?.uuid
? this.setup.secondaryAncestry.system.secondaryFeature ? this.setup.secondaryAncestry.system.secondaryFeature
@ -560,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
@ -39,7 +40,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.availableStressReductions = Object.keys(actor.system.rules.damageReduction.stressDamageReduction).reduce( this.availableStressReductions = Object.keys(actor.system.rules.damageReduction.stressDamageReduction).reduce(
(acc, key) => { (acc, key) => {
const dr = actor.system.rules.damageReduction.stressDamageReduction[key]; const dr = actor.system.rules.damageReduction.stressDamageReduction[key];
if (dr.enabled) { if (dr.cost) {
if (acc === null) acc = {}; if (acc === null) acc = {};
const damage = damageKeyToNumber(key); const damage = damageKeyToNumber(key);
@ -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;
@ -260,7 +271,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
const reducedDamage = currentDamage !== this.damage ? getDamageLabel(currentDamage) : null; const reducedDamage = currentDamage !== this.damage ? getDamageLabel(currentDamage) : null;
const currentDamageLabel = reducedDamage ?? getDamageLabel(this.damage); const currentDamageLabel = reducedDamage ?? getDamageLabel(this.damage);
if (stressReduction.from !== currentDamageLabel) return; if (!stressReduction.any && stressReduction.from !== currentDamageLabel) return;
stressReduction.selected = true; stressReduction.selected = true;
this.render(); this.render();

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) {
@ -650,7 +658,9 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
this.render(); this.render();
} }
static async save() { static async save(_, button) {
button.disabled = true;
const levelupData = Object.keys(this.levelup.levels).reduce((acc, level) => { const levelupData = Object.keys(this.levelup.levels).reduce((acc, level) => {
if (level >= this.levelup.startLevel) { if (level >= this.levelup.startLevel) {
acc[level] = this.levelup.levels[level].toObject(); acc[level] = this.levelup.levels[level].toObject();
@ -660,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

@ -1,26 +1,60 @@
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 */
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 */
@ -32,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;
} }
@ -44,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);
@ -53,6 +121,16 @@ export default class AdversarySheet extends DHBaseActorSheet {
return context; return context;
} }
/**@inheritdoc */
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement.querySelectorAll('.inventory-item-resource').forEach(element => {
element.addEventListener('change', this.updateItemResource.bind(this));
element.addEventListener('click', e => e.stopPropagation());
});
}
/** /**
* Prepare render context for the Biography part. * Prepare render context for the Biography part.
* @param {ApplicationRenderContext} context * @param {ApplicationRenderContext} context
@ -98,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}
@ -112,13 +221,61 @@ 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()
}; };
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 */
/* -------------------------------------------- */
async updateItemResource(event) {
const item = await getDocFromElement(event.currentTarget);
if (!item) return;
const max = event.currentTarget.max ? Number(event.currentTarget.max) : null;
const value = max ? Math.min(Number(event.currentTarget.value), max) : event.currentTarget.value;
await item.update({ 'system.resource.value': value });
this.render();
}
} }

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'
} }
}; };
@ -114,28 +138,51 @@ export default class CharacterSheet extends DHBaseActorSheet {
htmlElement.querySelectorAll('.inventory-item-resource').forEach(element => { htmlElement.querySelectorAll('.inventory-item-resource').forEach(element => {
element.addEventListener('change', this.updateItemResource.bind(this)); element.addEventListener('change', this.updateItemResource.bind(this));
}); element.addEventListener('click', e => e.stopPropagation());
htmlElement.querySelectorAll('.inventory-item-quantity').forEach(element => {
element.addEventListener('change', this.updateItemQuantity.bind(this));
}); });
// 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 */
@ -154,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;
} }
@ -209,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);
} }
/** /**
@ -279,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',
@ -550,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;
@ -585,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 });
} }
/** /**
@ -604,14 +677,22 @@ 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:
key === 'subclasses'
? {
'system.linkedClass.uuid': {
key: 'system.linkedClass.uuid',
value: this.document.system.class.value._stats.compendiumSource
}
}
: undefined,
render: { render: {
noFolder: true noFolder: true
} }
}; };
return new ItemBrowser({ presets }).render({ force: true }); ui.compendiumBrowser.open(presets);
} }
/** /**
@ -629,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
@ -708,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}
@ -752,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}
@ -778,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}
@ -788,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: [
{ {
@ -121,10 +124,47 @@ export default function DHApplicationMixin(Base) {
} }
} }
], ],
dragDrop: [], dragDrop: [{ dragSelector: '.inventory-item[data-type="effect"]', dropSelector: null }],
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`;
@ -249,14 +370,38 @@ export default function DHApplicationMixin(Base) {
* @param {DragEvent} event * @param {DragEvent} event
* @protected * @protected
*/ */
_onDragStart(event) {} async _onDragStart(event) {
const inventoryItem = event.currentTarget.closest('.inventory-item');
if (inventoryItem) {
const { type, itemUuid } = inventoryItem.dataset;
if (type === 'effect') {
const effect = await foundry.utils.fromUuid(itemUuid);
const effectData = {
type: 'ActiveEffect',
data: { ...effect.toObject(), _id: null },
fromInternal: this.document.uuid
};
event.dataTransfer.setData('text/plain', JSON.stringify(effectData));
event.dataTransfer.setDragImage(inventoryItem.querySelector('img'), 60, 0);
}
}
}
/** /**
* Handle drop event. * Handle drop event.
* @param {DragEvent} event * @param {DragEvent} event
* @protected * @protected
*/ */
_onDrop(event) {} _onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (data.type === 'ActiveEffect' && data.fromInternal !== this.document.uuid) {
this.document.createEmbeddedDocuments('ActiveEffect', [data.data]);
} else {
// Fallback to super, but note that item sheets do not have this function
return super._onDrop?.(event);
}
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Context Menu */ /* Context Menu */
@ -359,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);
} }
}); });
@ -378,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)
@ -419,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 || {};
@ -514,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 }
@ -545,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 });
} }
/** /**
@ -568,7 +723,6 @@ export default function DHApplicationMixin(Base) {
if (featureOnCharacter) { if (featureOnCharacter) {
systemData = { systemData = {
originItemType: this.document.type, originItemType: this.document.type,
originId: this.document.id,
identifier: this.document.system.isMulticlass ? 'multiclass' : null identifier: this.document.system.isMulticlass ? 'multiclass' : null
}; };
} }
@ -582,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') {
@ -618,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);
} }
@ -627,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,6 +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);
return;
} }
const item = await getDocFromElement(event.target);
if (item) {
const dragData = {
originActor: this.document.uuid,
originId: item.id,
type: item.documentName,
uuid: item.uuid
};
event.dataTransfer.setData('text/plain', JSON.stringify(dragData));
}
super._onDragStart(event);
} }
} }

View file

@ -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);
@ -149,12 +167,12 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
const { type } = target.dataset; const { type } = target.dataset;
const cls = foundry.documents.Item.implementation; const cls = foundry.documents.Item.implementation;
const multiclass = this.document.system.isMulticlass ? 'multiclass' : null;
let systemData = {}; let systemData = {};
if (this.document.parent?.type === 'character') { if (this.document.parent?.type === 'character') {
systemData = { systemData = {
originItemType: this.document.type, originItemType: this.document.type,
originId: this.document.id, identifier: multiclass ?? type
identifier: this.document.system.isMulticlass ? 'multiclass' : null
}; };
} }
@ -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,35 +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);
} event.dataTransfer.setData('text/plain', JSON.stringify(dragData));
event.dataTransfer.setDragImage(dragItemData.querySelector('img'), 60, 0);
} }
} }
@ -261,8 +288,10 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
* @param {DragEvent} event - The drag event * @param {DragEvent} event - The drag event
*/ */
async _onDrop(event) { async _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);
@ -271,14 +300,15 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
if (this.document.parent?.type === 'character') { if (this.document.parent?.type === 'character') {
const itemData = item.toObject(); const itemData = item.toObject();
const multiclass = this.document.system.isMulticlass ? 'multiclass' : null;
item = await cls.create( item = await cls.create(
{ {
...itemData, ...itemData,
_stats: { compendiumSource: this.document.uuid },
system: { system: {
...itemData.system, ...itemData.system,
originItemType: this.document.type, originItemType: this.document.type,
originId: this.document.id, identifier: multiclass ?? target.dataset.type
identifier: this.document.system.isMulticlass ? 'multiclass' : null
} }
}, },
{ parent: this.document.parent } { parent: this.document.parent }

View file

@ -41,7 +41,7 @@ export default function ItemAttachmentSheet(Base) {
} }
async _onDrop(event) { async _onDrop(event) {
const data = TextEditor.getDragEventData(event); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const attachmentsSection = event.target.closest('.attachments-section'); const attachmentsSection = event.target.closest('.attachments-section');
if (!attachmentsSection) return super._onDrop(event); if (!attachmentsSection) return super._onDrop(event);

View file

@ -27,6 +27,9 @@ export default class AncestrySheet extends DHHeritageSheet {
* @param {DragEvent} event - The drag event * @param {DragEvent} event - The drag event
*/ */
async _onDrop(event) { async _onDrop(event) {
const data = TextEditor.getDragEventData(event);
if (data.type === 'ActiveEffect') return super._onDrop(event);
const target = event.target.closest('fieldset.drop-section'); const target = event.target.closest('fieldset.drop-section');
const typeField = const typeField =
this.document.system[target.dataset.type === 'primary' ? 'primaryFeature' : 'secondaryFeature']; this.document.system[target.dataset.type === 'primary' ? 'primaryFeature' : 'secondaryFeature'];

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'
} }
@ -116,15 +126,25 @@ export default class ClassSheet extends DHBaseItemSheet {
event.stopPropagation(); event.stopPropagation();
const data = TextEditor.getDragEventData(event); const data = TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid); const item = await fromUuid(data.uuid);
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 (item.type === 'subclass') { if (itemType === 'subclass') {
if (item.system.linkedClass) {
return ui.notifications.warn(
game.i18n.format('DAGGERHEART.UI.Notifications.subclassAlreadyLinked', {
name: item.name,
class: this.document.name
})
);
}
await item.update({ 'system.linkedClass': this.document.uuid });
await this.document.update({ await this.document.update({
'system.subclasses': [...this.document.system.subclasses.map(x => x.uuid), item.uuid] 'system.subclasses': [...this.document.system.subclasses.map(x => x.uuid), item.uuid]
}); });
} else if (item.type === 'feature') { } else if (['feature', 'ActiveEffect'].includes(itemType)) {
super._onDrop(event); super._onDrop(event);
} else if (this.document.parent?.type !== 'character') { } else if (this.document.parent?.type !== 'character') {
if (item.type === 'weapon') { if (itemType === 'weapon') {
if (target.classList.contains('primary-weapon-section')) { if (target.classList.contains('primary-weapon-section')) {
if (!item.system.secondary) if (!item.system.secondary)
await this.document.update({ await this.document.update({
@ -136,21 +156,21 @@ export default class ClassSheet extends DHBaseItemSheet {
'system.characterGuide.suggestedSecondaryWeapon': item.uuid 'system.characterGuide.suggestedSecondaryWeapon': item.uuid
}); });
} }
} else if (item.type === 'armor') { } else if (itemType === 'armor') {
if (target.classList.contains('armor-section')) { if (target.classList.contains('armor-section')) {
await this.document.update({ await this.document.update({
'system.characterGuide.suggestedArmor': item.uuid 'system.characterGuide.suggestedArmor': item.uuid
}); });
} }
} else if (target.classList.contains('choice-a-section')) { } else if (target.classList.contains('choice-a-section')) {
if (item.type === 'loot' || item.type === 'consumable') { if (itemType === 'loot' || itemType === 'consumable') {
const filteredChoiceA = this.document.system.inventory.choiceA; const filteredChoiceA = this.document.system.inventory.choiceA;
if (filteredChoiceA.length < 2) if (filteredChoiceA.length < 2)
await this.document.update({ await this.document.update({
'system.inventory.choiceA': [...filteredChoiceA.map(x => x.uuid), item.uuid] 'system.inventory.choiceA': [...filteredChoiceA.map(x => x.uuid), item.uuid]
}); });
} }
} else if (item.type === 'loot') { } else if (itemType === 'loot') {
if (target.classList.contains('take-section')) { if (target.classList.contains('take-section')) {
const filteredTake = this.document.system.inventory.take.filter(x => x); const filteredTake = this.document.system.inventory.take.filter(x => x);
if (filteredTake.length < 3) if (filteredTake.length < 3)
@ -180,7 +200,13 @@ export default class ClassSheet extends DHBaseItemSheet {
static async #removeItemFromCollection(_event, element) { static async #removeItemFromCollection(_event, element) {
const { uuid, target } = element.dataset; const { uuid, target } = element.dataset;
const prop = foundry.utils.getProperty(this.document.system, target); const prop = foundry.utils.getProperty(this.document.system, target);
await this.document.update({ [`system.${target}`]: prop.filter(i => i.uuid !== uuid).map(x => x.uuid) });
if (target === 'subclasses') {
const subclass = await foundry.utils.fromUuid(uuid);
await subclass?.update({ 'system.linkedClass': null });
}
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 (game.user.character?.id !== actor.id && !game.user.isGM) 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) {
@ -235,8 +313,14 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
filters.forEach(f => { filters.forEach(f => {
if (typeof f.field === 'string') f.field = foundry.utils.getProperty(game, f.field); if (typeof f.field === 'string') f.field = foundry.utils.getProperty(game, f.field);
else if (typeof f.choices === 'function') { else if (typeof f.choices === 'function') {
f.choices = f.choices(); 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;
}); });
@ -248,11 +332,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Create and initialize search filter instances for the inventory and loadout sections. * Create and initialize search filter instance.
* *
* 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 * @private
*/ */
_createSearchFilter() { _createSearchFilter() {
@ -317,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;
@ -348,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);
} }
} }
@ -387,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') {
@ -408,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;
} }
}); });
@ -441,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

@ -2,404 +2,558 @@ export const typeConfig = {
adversaries: { adversaries: {
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'
}, }
] ]
}, },
items: { items: {
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: () => CONFIG.Item.documentClass.TYPES.filter(t => ["armor", "weapon", "consumable", "loot"].includes(t)).map(t => ({ value: t, label: t })) choices: () =>
CONFIG.Item.documentClass.TYPES.filter(t =>
['armor', 'weapon', 'consumable', 'loot'].includes(t)
).map(t => ({ value: t, label: t }))
}, },
{ {
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: [{ value: "1", label: "1"}, { value: "2", label: "2"}, { value: "3", label: "3"}, { value: "4", label: "4"}] choices: [
{ value: '1', label: '1' },
{ value: '2', label: '2' },
{ value: '3', label: '3' },
{ value: '4', label: '4' }
]
}, },
{ {
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: () => [...Object.entries(CONFIG.DH.ITEM.weaponFeatures), ...Object.entries(CONFIG.DH.ITEM.armorFeatures)].map(([k,v]) => ({ value: k, label: v.label})), choices: () =>
operator: "contains3" [
...Object.entries(CONFIG.DH.ITEM.weaponFeatures),
...Object.entries(CONFIG.DH.ITEM.armorFeatures)
].map(([k, v]) => ({ value: k, label: v.label })),
operator: 'contains3'
}
]
},
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: [
]
}, },
cards: { cards: {
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'
} }
] ]
}, },
classes: { classes: {
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'
} }
] ]
}, },
subclasses: { subclasses: {
columns: [ columns: [
{ {
key: "id", key: 'system.linkedClass',
label: "Class", label: 'Class',
format: (id) => { format: linkedClass => linkedClass?.name ?? 'DAGGERHEART.UI.ItemBrowser.missing'
return "";
}
}, },
{ {
key: "system.spellcastingTrait", key: 'system.spellcastingTrait',
label: "Spellcasting Trait" label: 'DAGGERHEART.ITEMS.Subclass.spellcastingTrait'
}
],
filters: []
},
beastforms: {
columns: [
{
key: "system.tier",
label: "Tier"
},
{
key: "system.mainTrait",
label: "Main Trait"
} }
], ],
filters: [ filters: [
{ {
key: "system.tier", key: 'system.linkedClass.uuid',
label: "Tier", label: 'Class',
choices: items => {
const list = items
.filter(item => item.system.linkedClass)
.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;
}, []);
}
}
]
},
beastforms: {
columns: [
{
key: 'system.tier',
label: 'DAGGERHEART.GENERAL.Tiers.singular'
},
{
key: 'system.mainTrait',
label: 'DAGGERHEART.GENERAL.Trait.single'
}
],
filters: [
{
key: 'system.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'
} }
] ]
} }
} };
export const compendiumConfig = { export const compendiumConfig = {
"daggerheart": { characters: {
id: "daggerheart", id: 'characters',
label: "DAGGERHEART", keys: ['characters'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.characters',
type: ['character']
// listType: 'characters'
},
adversaries: {
id: 'adversaries',
keys: ['adversaries'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.adversaries',
type: ['adversary'],
listType: 'adversaries'
},
ancestries: {
id: 'ancestries',
keys: ['ancestries'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.ancestries',
type: ['ancestry']
/* folders: {
features: {
id: 'features',
keys: ['ancestries'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
type: ['feature']
}
} */
},
equipments: {
id: 'equipments',
keys: ['armors', 'weapons', 'consumables', 'loot'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.equipment',
type: ['armor', 'weapon', 'consumable', 'loot'],
listType: 'items',
folders: { folders: {
"adversaries": { weapons: {
id: "adversaries", id: 'weapons',
keys: ["adversaries"], keys: ['weapons'],
label: "Adversaries", label: 'DAGGERHEART.UI.ItemBrowser.folders.weapons',
type: ["adversary"], type: ['weapon'],
listType: "adversaries" listType: 'weapons'
}, },
"ancestries": { armors: {
id: "ancestries", id: 'armors',
keys: ["ancestries"], keys: ['armors'],
label: "Ancestries", label: 'DAGGERHEART.UI.ItemBrowser.folders.armors',
type: ["ancestry"], type: ['armor'],
folders: { listType: 'armors'
"features": { },
id: "features", consumables: {
keys: ["ancestries"], id: 'consumables',
label: "Features", keys: ['consumables'],
type: ["feature"] label: 'DAGGERHEART.UI.ItemBrowser.folders.consumables',
type: ['consumable']
},
loots: {
id: 'loots',
keys: ['loots'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.loots',
type: ['loot']
} }
} }
}, },
"equipments": { classes: {
id: "equipments", id: 'classes',
keys: ["armors", "weapons", "consumables", "loot"], keys: ['classes'],
label: "Equipment", label: 'DAGGERHEART.UI.ItemBrowser.folders.classes',
type: ["armor", "weapon", "consumable", "loot"], type: ['class'],
listType: "items" /* folders: {
features: {
id: 'features',
keys: ['classes'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
type: ['feature']
}, },
"classes": { items: {
id: "classes", id: 'items',
keys: ["classes"], keys: ['classes'],
label: "Classes", label: 'DAGGERHEART.UI.ItemBrowser.folders.items',
type: ["class"], type: ['armor', 'weapon', 'consumable', 'loot'],
folders: { listType: 'items'
"features": { }
id: "features", }, */
keys: ["classes"], listType: 'classes'
label: "Features",
type: ["feature"]
}, },
"items": { subclasses: {
id: "items", id: 'subclasses',
keys: ["classes"], keys: ['subclasses'],
label: "Items", label: 'DAGGERHEART.UI.ItemBrowser.folders.subclasses',
type: ["armor", "weapon", "consumable", "loot"], type: ['subclass'],
listType: "items" listType: 'subclasses'
}
}, },
listType: "classes" domains: {
id: 'domains',
keys: ['domains'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.domainCards',
type: ['domainCard'],
listType: 'cards'
}, },
"subclasses": { communities: {
id: "subclasses", id: 'communities',
keys: ["subclasses"], keys: ['communities'],
label: "Subclasses", label: 'DAGGERHEART.UI.ItemBrowser.folders.communities',
type: ["subclass"], type: ['community']
listType: "subclasses" /* folders: {
features: {
id: 'features',
keys: ['communities'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
type: ['feature']
}
} */
}, },
"domains": { environments: {
id: "domains", id: 'environments',
keys: ["domains"], keys: ['environments'],
label: "Domain Cards", label: 'DAGGERHEART.UI.ItemBrowser.folders.environments',
type: ["domainCard"], type: ['environment']
listType: "cards"
}, },
"communities": { beastforms: {
id: "communities", id: 'beastforms',
keys: ["communities"], keys: ['beastforms'],
label: "Communities", label: 'DAGGERHEART.UI.ItemBrowser.folders.beastforms',
type: ["community"], type: ['beastform'],
folders: { listType: 'beastforms'
"features": { /* folders: {
id: "features", features: {
keys: ["communities"], id: 'features',
label: "Features", keys: ['beastforms'],
type: ["feature"] label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
} type: ['feature']
} }
} */
}, },
"environments": { features: {
id: "environments", id: 'features',
keys: ["environments"], keys: ['features'],
label: "Environments", label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
type: ["environment"] type: ['feature']
},
"beastforms": {
id: "beastforms",
keys: ["beastforms"],
label: "Beastforms",
type: ["beastform"],
listType: "beastforms",
folders: {
"features": {
id: "features",
keys: ["beastforms"],
label: "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

@ -26,5 +26,27 @@ export const gameSettings = {
Fear: 'ResourcesFear' Fear: 'ResourcesFear'
}, },
LevelTiers: 'LevelTiers', LevelTiers: 'LevelTiers',
Countdowns: 'Countdowns' Countdowns: 'Countdowns',
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;
} }
@ -51,11 +53,13 @@ export default class DHAttackAction extends DHDamageAction {
const labels = []; const labels = [];
const { roll, range, damage } = this; const { roll, range, damage } = this;
if (roll.trait) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${roll.trait}.short`)) if (roll.trait) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${roll.trait}.short`));
if (range) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Range.${range}.short`)); if (range) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Range.${range}.short`));
for (const { value, type } of damage.parts) { const useAltDamage = this.actor?.effects?.find(x => x.type === 'horde')?.active;
const str = Roll.replaceFormulaData(value.getFormula(), this.actor?.getRollData() ?? {}); for (const { value, valueAlt, type } of damage.parts) {
const usedValue = useAltDamage ? valueAlt : value;
const str = Roll.replaceFormulaData(usedValue.getFormula(), this.actor?.getRollData() ?? {});
const icons = Array.from(type) const icons = Array.from(type)
.map(t => CONFIG.DH.GENERAL.damageTypes[t]?.icon) .map(t => CONFIG.DH.GENERAL.damageTypes[t]?.icon)

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,282 +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}: ${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.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 usefulResources = { await this.workflow.get('cost')?.execute(config, successCost);
...foundry.utils.deepClone(this.actor.system.resources), await this.workflow.get('uses')?.execute(config, successCost);
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 (this.actor.system.partner ?? this.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;
} } */
} }

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