Compare commits

...

296 commits
1.9.14 ... main

Author SHA1 Message Date
Carlos Fernandez
a4428fd5be
Replace prettier with stylistic, improve types, and add no-undef rule (#1975)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-06-05 15:53:15 -04:00
WBHarry
6312a171e2
[Housekeeping] Styles Index:ification (#1977) 2026-06-05 15:36:07 -04:00
WBHarry
3527fd7959 Raised version
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-06-05 12:31:30 +02:00
Carlos Fernandez
f0a7539018
Update README.md (#1976) 2026-06-05 12:25:44 +02:00
Carlos Fernandez
5be79f4ab8
Fix several issues with inline damage (#1973)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-06-05 11:33:20 +02:00
Carlos Fernandez
2fc5b01f09
Fix rerolling when hope/fear automation is enabled (#1972) 2026-06-05 11:31:01 +02:00
WBHarry
52b81de11f
Fixed so that the saved data for an experience that is in the character data is used over that in the levelup data if available (#1971)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-06-04 18:30:41 -04:00
Carlos Fernandez
c0c9095847
[Fix] Preload ancestry and community features in description (#1967)
* Preload ancestry and community features in description

* Corrected comments

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>
2026-06-04 20:08:40 +02:00
Carlos Fernandez
5ac4fc3b9c
[Fix] visual quirk with blur in unfocused countdown (#1970)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
* Fix visual quirk with blur in unfocused countdown

* Snuck in fixes and refactors
2026-06-04 11:42:17 +02:00
Carlos Fernandez
6747be49b2
Allow removing empty string domains (#1968) 2026-06-04 11:15:41 +02:00
WBHarry
77c5cfcbb7 Fixed so that actions on homebrew downtime/items don't crash due to note having metadata
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-06-03 21:46:23 +02:00
WBHarry
5dbcd94480 Raised version
Some checks failed
Project CI / build (24.x) (push) Has been cancelled
2026-06-01 22:22:50 +02:00
WBHarry
d98a7c951e
[Fix] Tooltip Color Scope (#1964)
* Added DH style to tooltips

* Setting dh-style for ResourceManagementTooltip and ArmorManagementTooltip
2026-06-01 22:20:06 +02:00
WBHarry
3c36c5747d
[Fix] Base Attack Context Menu (#1961)
* Fixed Adversary standard attack context menu

* Fixed Character base attack context menu

* Fixed Companion base attack context menu
2026-06-01 22:02:42 +02:00
WBHarry
bcf274f1d0
[Fix] ChatMessage Saves Pending Confirmation (#1963) 2026-06-01 15:59:44 -04:00
WBHarry
df4a2c5d57
Fixed Countdown.migrationData assuming an array when it's actually an object (#1962) 2026-06-01 15:53:28 -04:00
WBHarry
646ebc8bdf
Added a temp fix for status effect rows (#1965) 2026-06-01 15:52:27 -04:00
WBHarry
6448666579 Updated combat contextmenu 2026-06-01 19:47:06 +02:00
WBHarry
d0c29ede56
Fixed so the combat tracker doesn't error out if a combatant has no attached token (#1960)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-06-01 06:24:00 -04:00
Carlos Fernandez
98ce49b928
Avoid default type on name and item create dialogs (#1958) 2026-06-01 11:06:24 +02:00
WBHarry
318d00b47d
Add NPC Improvements 2026-06-01 04:24:44 -04:00
WBHarry
c8d0df87c8 Raised version
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-06-01 00:04:29 +02:00
WBHarry
983f48b415
Fixed ActiveEffectConfig not opening because NPC was not excluded. Fixed active effects not being appliable to NPCs because they lack system.rules (#1956) 2026-06-01 00:03:37 +02:00
WBHarry
bfd483698b
Changed so that EffectField applyEffect doesn't run EmitAsGM (#1953) 2026-05-31 22:58:08 +02:00
WBHarry
3eb33a71af
Fixed the centering of the Select-Roll circles in TagTeamDialog. Fixed so that damage formulas that are just a modifier don't get a prefix '+' rendered (#1955) 2026-05-31 19:54:42 +02:00
Carlos Fernandez
3fbc1e97c6
Replace scroll shadows with scroll animation timeline (#1951)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-05-31 12:29:54 +02:00
Carlos Fernandez
729e8bca42
Fix companion effects not being scrollable when overflowing (#1952) 2026-05-31 11:00:28 +02:00
WBHarry
53f15a7fde
[Feature] NPC Actors (#1949)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-05-30 21:11:43 -04:00
WBHarry
c23ac61ee5
Corrected the data path for showing the difficulty marker in roll chat messages (#1950) 2026-05-30 21:05:13 -04:00
Carlos Fernandez
d3141059ac
Create index files for actor sheet styles (#1945)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-05-31 01:02:51 +02:00
Carlos Fernandez
61db7ca371
Fix tag team roll results where one of them has stress (#1948) 2026-05-31 01:00:12 +02:00
WBHarry
2bc1c04c93 Fixed an issue where hope/fear dice size could no longer be changed in the roll dialog
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-05-30 12:56:42 +02:00
Carlos Fernandez
493998cc95
Preload class and subclass features for description (#1940)
Co-authored-by: WBHarry <williambjrklund@gmail.com>
2026-05-30 12:51:39 +02:00
Carlos Fernandez
251d7e4e13
Swap order of thresholds and resources in actor editor (#1943) 2026-05-30 12:49:06 +02:00
Carlos Fernandez
a209b035c8
Make prosemirror button nicer (#1946) 2026-05-30 12:48:20 +02:00
Carlos Fernandez
9487b07e43
Fix tier adjustment on actions that use standard attack damage (#1942) 2026-05-30 12:47:06 +02:00
WBHarry
f1a530f57f
[Feature] Full Rerolls (#1928)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
* Initial

* Removed damage dialogs

* Fixed DamageReroll

* Fixed d20 modifiers

* Fixed

* Fixed DiceSoNice multiple damageType reroll

* Added triggerChatRollFx

* Fixed dice.denomination being lost on damage reroll
2026-05-29 12:19:08 +02:00
WBHarry
ddf4747310 Raised version
Some checks failed
Project CI / build (24.x) (push) Has been cancelled
2026-05-27 22:25:00 +02:00
Carlos Fernandez
ac72012387
Fix setting dialogs created from overriden light sheet actors (#1939) 2026-05-27 22:20:53 +02:00
Carlos Fernandez
1ab8170d2f
[Refactor] Define more border and input color variables (#1937)
* Define more border and input color variables

* Rename custom color variables

* Fix assignment of variables

* Apply border color variable to matching borders

* Add trait header colors and shadow contrast
2026-05-27 22:20:16 +02:00
Carlos Fernandez
48f9ffc318
Fix recent regression for scope rules (#1938)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-05-26 21:58:05 -04:00
Carlos Fernandez
fa6f9d56b8
Add emphatic color variable and set up scoped based overrides for core variables (#1932)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-05-26 22:18:52 +02:00
Carlos Fernandez
c2f8b34ef2
Show actor sheet when clicking actors in item browser (#1930) 2026-05-26 15:46:08 +02:00
Carlos Fernandez
de0ab9d047
Include more item types when viewing compendium browser from item tab (#1931) 2026-05-26 15:43:36 +02:00
Carlos Fernandez
b9416ead5a
Fix usable checks on adversary features and locked compendium actors (#1934) 2026-05-26 15:41:26 +02:00
Carlos Fernandez
ccc4186e42
Disable contenteditable inputs when read only (#1935)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-05-26 12:27:17 +02:00
WBHarry
e529dd0f88
Fixed so that advantage dice do not get duplicated (#1929)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-05-26 00:49:46 +02:00
Carlos Fernandez
0e8c3dc74a
[UI] Adjust actor sheet headers (#1923) 2026-05-25 17:55:57 -04:00
WBHarry
58824d5bbf
Fixed so that ActiveEffects on homebrew features can set 'Transfer To Actor' (#1926)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-05-25 00:55:43 +02:00
WBHarry
e095587305
[Fix] ContextMenu v14 Deprecation (#1927)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
* Updated from the deprecated 'callback' to 'onClick' for ContextMenu entries

* Missed parameter reversal in api/base-item.mjs
2026-05-24 19:05:55 +02:00
WBHarry
f7f1bdce2b
Fixed so that we are not looking at the now non-existing metadata.isQuantifiable anymore (#1922) 2026-05-24 17:15:17 +02:00
WBHarry
e4a3f105dc
[Feature] V14 Cleanup (#1918)
Some checks failed
Project CI / build (24.x) (push) Has been cancelled
* Fixedin PrototypeToken preview

* Fixed translations

* Fixed tokenSize linking to token.depth

* Fixed beastform depth

* Raised foundry version
2026-05-23 12:16:25 +02:00
Carlos Fernandez
2931377d53
Remove edit and remove icons from adversary and party features (#1919) 2026-05-23 11:53:44 +02:00
Carlos Fernandez
53e8da77c6
Use the main deleteDoc handler instead of the party sheet specific one (#1920) 2026-05-23 11:52:23 +02:00
WBHarry
bae9006f64 Raised foundry verified version
Some checks failed
Project CI / build (24.x) (push) Has been cancelled
2026-05-21 19:15:30 +02:00
WBHarry
273f666784 Raised version
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-05-21 01:44:13 +02:00
WBHarry
d782b25254
[Feature] Class/Multiclass Feature Split (#1911)
* Changed so that multiclass features and multiclassSubclass features are displayed in separate fieldsets from the base class features in the character sheet

* Changed to tertiaries for class/multiclass feature divide
2026-05-21 01:38:31 +02:00
WBHarry
f4c21a6a1b
[Fix] CompendiumBrowser Pack Toggling (#1909)
* Fixed so that CompendiumBrowserSettings saves source/pack names as slugified version to avoid foundrdy not saving names with dots in the middle

* Updated excludedPacks with another layer of TypedObjectField

* Renmamed variable

* Update module/applications/dialogs/CompendiumBrowserSettings.mjs

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

---------

Co-authored-by: Carlos Fernandez <CarlosFdez@users.noreply.github.com>
2026-05-21 01:33:07 +02:00
WBHarry
da06381748
[Fix] Remove System Slugify (#1913) 2026-05-20 17:08:18 -04:00
Carlos Fernandez
ed53d9ed4c
[Feature] Add way to delete unbound character creation items (#1907)
* Add way to delete unbound character creation items

* Temporarily reduce functionality

* Fixed missing fetchSubclass call

* Revert "Fixed missing fetchSubclass call"

This reverts commit 4fc9ee39b6.

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>
2026-05-20 22:18:40 +02:00
WBHarry
b631525b6e
Fixed so that Reaction roll chat messages do not mention 'withHope'/'withFear' (#1912) 2026-05-20 15:44:50 -04:00
WBHarry
b145f515d0
Fixed so that a non-existing class link uuid on a subclass doesn't make the subclass filter error (#1910) 2026-05-20 15:43:11 -04:00
Carlos Fernandez
b23095cb2f
[Fix] finishing levelup with a multiclass (#1906)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
* Fix finishing levelup with a multiclass

* Fix removal when de-leveling

* Also delete multiclass related stuff if reducing below the minimum multiclass level
2026-05-20 18:45:42 +02:00
Carlos Fernandez
2f589c1b8e
Minor visual tweaks to daggerheart setting dialogs (#1905) 2026-05-20 16:54:22 +02:00
Carlos Fernandez
10a608a1a5
Refocus armor slots when using number keys (#1908) 2026-05-20 16:49:45 +02:00
Carlos Fernandez
6a2d09caac
Create sourcemaps for css files (#1904)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-05-19 23:18:10 +02:00
Carlos Fernandez
4504379fcf
Fix journey end calculation and hope reduction when gaining scars (#1900)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-05-19 11:13:59 +02:00
Carlos Fernandez
d152bfc906
Fix party rerenders from shields and weapons (#1899) 2026-05-19 11:11:38 +02:00
WBHarry
b91d943dd1 Raised foundry minimum version 2026-05-19 10:45:31 +02:00
WBHarry
ac5f84fff7
Fixed so that the tokenHUD matches the current V14 foundry layout. Fixed so that the tooltips have the correct translations (#1894) 2026-05-19 10:16:50 +02:00
WBHarry
d78c6b1183
Fixed so that already destroyed companion tokens that foundry still lists are not considered (#1901) 2026-05-19 10:14:03 +02:00
Carlos Fernandez
98049bd76b
Remove unused resources tab styling (#1902) 2026-05-19 10:13:49 +02:00
Carlos Fernandez
ab412367f9
Add tier/type headers and filters to environments in browser (#1895)
Some checks failed
Project CI / build (24.x) (push) Has been cancelled
2026-05-17 20:07:54 +02:00
Carlos Fernandez
0492507bd1
Further decrease sheet opacity (#1897)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-05-17 13:43:55 +02:00
Carlos Fernandez
47960fdd61
Fix uses of font awesome and adjust browser filter spacing (#1896) 2026-05-17 13:27:10 +02:00
Carlos Fernandez
d372f3df9b
Fix creating domain cards on character sheet (#1890)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-05-16 18:38:07 +02:00
WBHarry
e6c27926d0 Raised version
Some checks failed
Project CI / build (24.x) (push) Has been cancelled
2026-05-15 09:48:41 +02:00
Carlos Fernandez
88e64531b4
[UI] Minor updates to tag team initialization visuals (#1882)
* Visual updates to tag team initialization

* Center players and handlet he case where is 5 or 6 players
2026-05-15 09:32:00 +02:00
Carlos Fernandez
dd2aa10871
[Fix] selecting multiclass and multiple sheet issues (#1884)
* Fix error with adding multiclass

* Make it more card like

* Fix issues with responsiveness when resized

* Fix cards spilling out of container when multiple lines

* Remove mask and fix regression in scrollbar
2026-05-15 09:23:55 +02:00
Carlos Fernandez
855f4549ec
[Fix] Errors when updating max hp and implement bar update animations (#1883)
* Fix an error that can break the canvas when importing or resetting a character

* Animate bar updates

* Use non-static mix function instead
2026-05-15 09:21:05 +02:00
Carlos Fernandez
6b4de71a0a
Fix compendium browser sometimes spawning behind other windows (#1885) 2026-05-15 09:04:22 +02:00
Carlos Fernandez
46e552eb3d
Fix scroll restoration of party members (#1886) 2026-05-15 09:01:28 +02:00
Carlos Fernandez
bc3c09fa2e
Move scrollbar definition to global styling (#1887) 2026-05-15 09:01:03 +02:00
Carlos Fernandez
829a6161ff
Add hot reload configuration (#1881)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-05-15 00:46:18 +02:00
Carlos Fernandez
24993970da
Slightly improve visibility of countdowns and fix mixing light and dark modes (#1878)
Some checks failed
Project CI / build (24.x) (push) Has been cancelled
2026-05-14 01:21:06 +02:00
WBHarry
d86ab2053c
[Feature] 1876 - Adversary Disposition Split (#1877)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-05-13 06:34:38 -04:00
Carlos Fernandez
24813e7e4f
Make background tab glassy in character sheet (#1868)
Some checks failed
Project CI / build (24.x) (push) Has been cancelled
* Make fieldsets glassy in character sheet

* Remove glassy from character features tab
2026-05-11 22:30:39 +02:00
WBHarry
abd7824c96 Raised version
Some checks failed
Project CI / build (24.x) (push) Has been cancelled
2026-05-09 16:33:45 +02:00
WBHarry
80e314ca84 Minor style changes for character/header.hbs 2026-05-09 16:28:43 +02:00
WBHarry
4064701c16
Fixed so that the overridden ?? label is actually applied for private messages (#1872) 2026-05-09 16:25:43 +02:00
Carlos Fernandez
e8828b70db
Remove white border around level input in character sheet (#1870)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-05-09 11:15:35 +02:00
Carlos Fernandez
9ef4929693
Implement another traits redesign (#1871)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
* Implement another traits redesign

* Adjust traits shape
2026-05-08 15:08:25 +02:00
Carlos Fernandez
40804f3339
Adjust spacing of character hope (#1864)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-05-08 14:58:14 +02:00
WBHarry
b7bc452bf5
[Fix] Improve Class-Subclass Linkage (#1846)
Some checks failed
Project CI / build (24.x) (push) Has been cancelled
* Initial thoughts

* .

* Fixed linting

* Continue work on updating identifier

* Change to uuid approach

* Localization and minor fix

* Fixed CompendiumBrowser Class filter for Subclass view

* Fixed the class name display in the subclass view

* Improved missing class visual for subclass

* Fixed character creation

* Rerender class sheets when subclass link is changed

* Use compendium source over actual uuid in search

---------

Co-authored-by: Carlos Fernandez <cfern1990@gmail.com>
2026-05-05 22:15:21 +02:00
Carlos Fernandez
fb5e3672dc
Disable spellcheck and autocorrect on all adversary sheets (#1869) 2026-05-05 22:14:45 +02:00
WBHarry
cca468e8af Raised foundry version
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-05-05 21:21:27 +02:00
WBHarry
94852cec21 Fixed character sheet error:ing out if opened when the character doesn't have a spellcastModifierTrait
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-05-04 16:45:17 +02:00
Carlos Fernandez
0128106de6
Don't show unusable non-weapons in character sidebar (#1862)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-05-04 12:40:29 +02:00
Carlos Fernandez
2ffe678503
Add setting to hide party stats (#1861) 2026-05-04 12:39:20 +02:00
Carlos Fernandez
e95ea3c281
[UI] Rework styling of traits section of the character sheet (#1865)
* Rework styling of traits section of the character sheet

* Adjust spacing in header a smidge
2026-05-04 12:34:52 +02:00
Carlos Fernandez
c91d53b4d4
Avoid shifting elements when mousing over a tags (#1866) 2026-05-04 12:32:02 +02:00
WBHarry
85ca7efc6d Raised version
Some checks failed
Project CI / build (24.x) (push) Has been cancelled
2026-05-02 23:02:24 +02:00
WBHarry
edbf5aa55f
[Fix] Player Created Regions (#1855)
* Fixed so that creating regions without behaviors work for players. Fixed so that creating regions with behaviors works via GmEmit for players

* Updated previous uses of emitAsGM to emitGMUpdate

* Fixed linting

* Update module/documents/chatMessage.mjs

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

---------

Co-authored-by: Carlos Fernandez <CarlosFdez@users.noreply.github.com>
2026-05-02 22:34:53 +02:00
WBHarry
54d1b2bdc0
[Fix] 1854 - Delete Scene Environments (#1858) 2026-05-02 16:04:52 -04:00
WBHarry
516928f92b
Fixed a data path error in character._preUpdate when scars are changed by an update (#1856) 2026-05-02 16:02:57 -04:00
WBHarry
94e93222a4
[Fix] Downtime Use Reset RollData (#1857) 2026-05-02 16:01:16 -04:00
WBHarry
4685ec3c77
Added event.stopPropagation to avoid double matches and therefore double drops on drag/drop (#1860) 2026-05-02 15:58:10 -04:00
WBHarry
4558fbdcf6 Raised version
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-05-01 23:09:45 +02:00
Carlos Fernandez
c7159eff11
Fix retrieving parent documents when the model is null (#1853) 2026-05-01 23:00:03 +02:00
WBHarry
d0c2c783f1
Improved armor source names (#1851) 2026-05-01 16:53:20 -04:00
WBHarry
905d1f7e88 Corrected a typo in Greater Earth Elemental and Huge Green Ooze 2026-05-01 20:58:21 +02:00
Carlos Fernandez
b22ce9697d
Fix detection of negative modifiers (#1847) 2026-05-01 20:54:18 +02:00
WBHarry
404640a0a3 Fixed SRD DireBat experience value
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-05-01 17:45:50 +02:00
WBHarry
20056cd950 Corrected contributing link in readme
Some checks failed
Project CI / build (24.x) (push) Has been cancelled
2026-04-29 22:04:08 +02:00
Carlos Fernandez
118c52a996
[Fix] console noise when starting a tag team dialog or group roll (#1842)
Some checks failed
Project CI / build (24.x) (push) Has been cancelled
2026-04-28 01:47:11 -04:00
WBHarry
ca32aa5d35
Fixed so that the delete option is available in the compendium (#1843) 2026-04-28 01:46:46 -04:00
WBHarry
1cece731ee Corrected Glowing Rings damage
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-04-27 16:06:33 +02:00
WBHarry
b186f22cc7 Raised version 2026-04-26 11:29:55 +02:00
Carlos Fernandez
047e77154a
[Fix] companion hope levelup feature and default stress (#1839)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
* Fix issues with companions

* Remove unused companion hope resource
2026-04-26 11:20:06 +02:00
Carlos Fernandez
53f77972e3
Order character sidebar equip items (#1837) 2026-04-26 10:43:42 +02:00
Carlos Fernandez
cc822856e5
[Feature] Redesign group roll dialog (#1824)
* Redesign group roll dialog

* Style and restore aid modifiers

* Move successful buttons to inner result

* Localize and finish dialog

* Remove empty line

* Fix error and remove old styles

* Make icon pop a tiny bit better

* Fix advantage/disadvantage
2026-04-26 10:43:10 +02:00
Carlos Fernandez
6d09c5504d
[Fix] origin change values and prioritize item options (#1835)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
* Fix origin key replacement and prioritize item options

* Rename key to value

* Some fixes

* Attempt to always retrieve the source item for same actor origin

* Fix getters in item roll data

* Performance improvement

* Allow apply to item system data

* Replace implementation with shallow proxy

* Add delete to the shallow proxy

* Add proxy trap for the in operator

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>
2026-04-26 00:06:41 +02:00
WBHarry
c82bcbeb01 Linted 2026-04-25 22:50:59 +02:00
WBHarry
d0afee59d8 Corrected secondaryWeapon translation 2026-04-25 18:51:49 +02:00
WBHarry
4d17a7d9bf Fixed that new weapons couldn't be created on the Character Sheet 2026-04-25 17:45:50 +02:00
WBHarry
b8e00b2807 Translation fixes 2026-04-25 17:39:20 +02:00
WBHarry
e5ae56f45c
Fixed so that replaceFormulaValue doesn't loose getter values (#1836)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-04-25 13:01:13 +02:00
Carlos Fernandez
ccb0073cef
Prevent class sheet and character errors when features/domains no longer exist (#1833)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-04-24 20:49:42 +02:00
Carlos Fernandez
4a60c56462
Fix husky/lint-staged integration (#1831) 2026-04-24 20:48:42 +02:00
Carlos Fernandez
84afec31a7
[Fix] class domains when setting max domains to null or 0 (#1832)
* Fix class domains when setting max domains to null or 0

* Convert max loadout to use data prep
2026-04-24 20:47:33 +02:00
WBHarry
da11510e02 Reverted party auto permission
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-04-24 00:28:08 +02:00
WBHarry
f45b1210c7 Adding so that newly creatd parties default to having the ownership default be owner
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-04-23 23:01:12 +02:00
WBHarry
41829bc9d5 Fixed so that Homebrew Item Features without effects don't error out because effects are expected 2026-04-23 22:51:54 +02:00
WBHarry
d73760fc39 Raised version 2026-04-23 17:14:46 +02:00
WBHarry
276aee4747 Corrected fireball to be two different actions, one for the casting and one for the explosion
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-04-23 00:38:55 +02:00
WBHarry
fae05c24a8
1794 - Include Item Damage (#1827)
* Went ahead and made IncludeItemDamage work again, smashing the formulas together

* .

* Lint fix
2026-04-23 00:22:08 +02:00
WBHarry
e6d5a2f7d3
[Feature] Action Area SRD Updates (#1822)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
* Updated Environments

* Added remaining
2026-04-22 00:27:11 +02:00
Carlos Fernandez
545934aa60
Add background color to light theme header button (#1823) 2026-04-22 00:13:11 +02:00
Carlos Fernandez
7a4f9d7bc8
Tighten appearance of ability use chat messages (#1821) 2026-04-21 23:44:03 +02:00
WBHarry
3eda3c4c05
[Feature] Action Areas (#1815)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
* Functioning setup

* .

* Fixes

* Completed

* Apply suggestions from code review

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

* using function.call instead of function.bind

* Run lint fix on action areas PR (#1820)

* .

* .

* Restructured getTemplateShape to be a lot more readable

* .

* .

* Changed from 'area' to 'areas'

* .

* Moved the areas button to the left

* Fix regression with actions list

* Updated all SRD adversaries

---------

Co-authored-by: Carlos Fernandez <CarlosFdez@users.noreply.github.com>
Co-authored-by: Carlos Fernandez <cfern1990@gmail.com>
2026-04-21 22:27:52 +02:00
WBHarry
646e0debbd Fixed a situation where TagTeamDialog's rollSelection part could get rendered in a bad situation 2026-04-21 18:25:23 +02:00
Carlos Fernandez
3cbc18f42b
Add eslint and run linter in workflow (#1819)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run
2026-04-21 10:15:39 +10:00
Carlos Fernandez
f850cbda76
[Feature] Updates to inventory icons, context menus, action use, and observer visibility (#1814)
* Update inventory controls and permissions filtering

* Also disable prosemirror editors

* Address context menu deprecation warnings

* Fix context menu detection for actions

* Refine logic for use action when hovering over item icon
2026-04-20 15:30:43 +02:00
WBHarry
f2ec5ef458 Merge branch 'main' of https://github.com/Foundryborne/daggerheart 2026-04-20 15:20:42 +02:00
WBHarry
c683bc4352 Fixed IncludeBaseDamage to be an override 2026-04-20 15:20:35 +02:00
Carlos Fernandez
fa04c9920f
Fix translation string (#1817) 2026-04-20 08:11:17 +02:00
WBHarry
03110377e1 Fixed so that resource reset on downtime can handle math expressions 2026-04-20 00:04:03 +02:00
WBHarry
1fea8438ba Fixed DowntimeMove actions not opening 2026-04-19 11:29:47 +02:00
Carlos Fernandez
4944722139
Avoid error when backing out of action (#1813) 2026-04-18 23:31:56 +02:00
Carlos Fernandez
4b92001f97
Fix issues with party sheet resources in light mode (#1810) 2026-04-17 19:29:37 +02:00
Carlos Fernandez
2fde61a1d5
[Feature] Make all item types quantifiable in the party (#1808)
* Show notification when invalid item types are added to actors

* Make all item types quantifiable in the party actor

* Remove from comment

* Use isInventoryItem to set quantity

* Fix formatting
2026-04-16 11:12:36 +02:00
Carlos Fernandez
d9b322406d
Fix party sheet rerenders from member updates interfering with currency and note input (#1809) 2026-04-16 10:23:55 +02:00
WBHarry
16c07d23bb Changed diceFaces->dieFaces for consistency 2026-04-16 09:57:16 +02:00
WBHarry
91aff8b10d
[Feature] Advantage Default Dice (#1802) 2026-04-16 03:55:12 -04:00
Carlos Fernandez
7e9385bc39
Add toggle for party sheet (#1806) 2026-04-16 09:22:26 +02:00
Carlos Fernandez
aa8771bf0d
Show notification when invalid item types are added to actors (#1807) 2026-04-16 08:23:25 +02:00
WBHarry
7d5cdeb09d
[Feature] Active Party (#1803) 2026-04-15 20:26:39 -04:00
WBHarry
8808e4646d Corrected use of Foundry's Reset translation 2026-04-15 18:47:20 +02:00
Carlos Fernandez
a77d2088a0
Increase reuse of gold and inventory styling (#1804) 2026-04-15 18:42:30 +02:00
WBHarry
c6335980ba Merge branch 'v14-dev' 2026-04-14 20:55:14 +02:00
WBHarry
1176328f62 Updated deploy.yml 2026-04-14 20:55:10 +02:00
a62d28cd96
updated contributing guidelines (#1800) 2026-04-14 18:51:28 +02:00
WBHarry
8d8dea81fe
[Fix] Compendium Advantage Sources (#1796)
* Duration Description field didn't show initially for type temporary

* Corrected SRD
2026-04-13 20:41:28 +02:00
Nikhil Nagarajan
fb07938e54
container fix (#1795) 2026-04-12 19:10:51 +02:00
WBHarry
c337338c8b Merge branch 'v14-dev' of https://github.com/Foundryborne/daggerheart into v14-dev 2026-04-12 13:58:51 +02:00
WBHarry
f900011510 Fixed trait counting in CharacterCreation. Fixed description layout and labels in CharacterCreation. Fixed drag/drop images from Compendium Browser 2026-04-12 13:58:43 +02:00
WBHarry
56a6613a73
Fixed more missing translations (#1792) 2026-04-12 11:38:15 +02:00
WBHarry
e003db3ec1 Corrected updateActorsRangeDependentEffects when token is null 2026-04-12 11:22:00 +02:00
WBHarry
66c90d69e3 Added saefety to updateActorsRangeDepenedentEffects 2026-04-12 11:10:02 +02:00
WBHarry
a839ca0066 Corrected deploy.yml for new branch 2026-04-12 00:36:24 +02:00
WBHarry
6ed975f5b7 Merge branch 'v14-dev' of https://github.com/Foundryborne/daggerheart into v14-dev 2026-04-12 00:33:04 +02:00
WBHarry
e2c97a7b61 Raised version 2026-04-12 00:32:59 +02:00
WBHarry
3ec013ff50
Reworked summon action and clowncar functionality to work with levels (#1791) 2026-04-12 00:25:43 +02:00
WBHarry
94f1fbdd9b Updated system.json 2026-04-12 00:21:16 +02:00
WBHarry
f22b67367b Updated system.json to point to V14 2026-04-11 23:57:33 +02:00
WBHarry
8a0b1b8e22
[Fix] Translation Fixes (#1789)
* Fixed translations

* .
2026-04-11 22:55:26 +02:00
WBHarry
1a57b55723 Fixed H4 elements in editors being hard to see in light mode 2026-04-11 22:51:23 +02:00
WBHarry
f910cf9795 Fixed Battlepoints menu being hard to see in light-mode 2026-04-11 22:46:30 +02:00
WBHarry
6804bfe047 Fixed so that bonus rest moves can be negative 2026-04-11 19:23:34 +02:00
WBHarry
28d9254883 Changed TagTeam initiasation cost label to be 'Hope Cost' 2026-04-11 16:23:41 +02:00
WBHarry
b076c2481b Fixed the extra unarmored severe threshold bonus being applied to Bare Bones 2026-04-11 14:59:44 +02:00
WBHarry
bdfc97bb3b Style improvements to groupRoll when a character name is long 2026-04-11 14:24:00 +02:00
WBHarry
e111f7c2ae Corrected BareBones damageThreshold application 2026-04-11 13:58:57 +02:00
WBHarry
bb179db758 Corrected minimum foundy compatability 2026-04-11 11:23:20 +02:00
WBHarry
bbc1781d01 Raised version 2026-04-11 11:20:07 +02:00
WBHarry
a897037dc4
[Feature] Group Roll Rework (#1785)
* Initial

* .

* Improvements

* .

* Renamed 'Main Charater' to 'Leader'

* Localization fixes

* .

* Fixed roll sound coming when canceling a roll. Fixed the leader PART not being disabled when the player isn't the leader
2026-04-11 11:14:36 +02:00
WBHarry
97636fa134 Fixed Countdown Actions not actually setting their DefaultOwnership. Fixed GMs not always getting Ownership of a countdown 2026-04-11 02:53:51 +02:00
WBHarry
e7be2a7d2b Raised Foundry version 2026-04-11 02:34:18 +02:00
WBHarry
9bea8d6a97 Merged with main 2026-04-11 00:05:18 +02:00
Carlos Fernandez
7ca420ae0e
[Feature] Redesign and merge party members and resources tabs (#1784) 2026-04-10 15:33:44 -04:00
WBHarry
ae480157d1
[Feature] 1766 - Group Attack (#1770)
* Implemented group attack logic

* Updated all minions in the SRD to use the group attack functionality

* .

* Renamed groupAttack.nr to groupAttack.numAttackers

* Moved the flag vs global setting logic to documents/scene

* .
2026-04-09 22:07:51 +02:00
Carlos Fernandez
b505e15eb2
Fix editing of bar attributes in v14 (#1786) 2026-04-09 21:36:48 +02:00
WBHarry
087e69694c
Changed the character setup button to be more obvious (#1782) 2026-04-06 11:40:57 +02:00
WBHarry
fad830580c
Added checkboxes for muting the sounds of dice animations (#1781) 2026-04-06 00:18:43 +02:00
WBHarry
4c2d31b2f4
Fixed so that expanded damage info without any dice will show the correct value (#1780) 2026-04-05 19:28:27 +02:00
WBHarry
67d142df3d Fixed migration 2026-04-05 17:27:02 +02:00
WBHarry
fdfd8c5a8d Fixed selecting which roll to use in TagTeamRolls becoming impossible when using an Ability option 2026-04-05 11:28:41 +02:00
WBHarry
dbcef140a2 Fixed armorEffects erroring on isSuppressed when not on an actor 2026-04-05 11:09:00 +02:00
WBHarry
90f4339898 Restoring current version number 2026-04-05 10:23:02 +02:00
WBHarry
0d7469801e
Updated the longrest repair armor to the new armor max path along with a migration (#1777) 2026-04-04 23:22:25 +02:00
WBHarry
70e21f34db Corrected system.json 2026-04-04 13:21:17 +02:00
WBHarry
7057504a9e
Fixes (#1774) 2026-04-04 13:01:24 +02:00
WBHarry
331f1ebf75 Fixed prose-mirror width 2026-04-04 12:42:50 +02:00
WBHarry
f91c140d34
Fixed so that multi term expressions get evaluated into a single number (#1772) 2026-04-04 11:48:41 +02:00
WBHarry
3a117ef117
Added evasion to party resources (#1771) 2026-04-03 23:32:30 +02:00
WBHarry
01619ef067 Corrected github deploy manifest path 2026-04-03 19:15:20 +02:00
WBHarry
622b38ac08 Fixed certain fields in actionConfig not getting translated 2026-04-03 19:05:49 +02:00
WBHarry
02cca277da Updated github deploy manifest to be the latest on the main branch 2026-04-03 00:13:07 +02:00
WBHarry
3265613767
[Fix] Template Scene Awareness (#1769)
* .

* Using foundry values from schema for fallback gridSize and gridDistance

* .
2026-04-02 23:22:09 +02:00
WBHarry
36ffe8a23a Fixed labrys axe and unneccessary hr separations in enrichedItemDescriptions when prefix is empty 2026-04-02 22:18:18 +02:00
WBHarry
96eba49dc1 Fixed RainofBlades 2026-04-02 22:04:07 +02:00
WBHarry
f92f9f7132 Fixed so that tokens with vision range set to Infinity doesn't make summon actions error out 2026-04-02 17:10:13 +02:00
WBHarry
0747994686 Improved TagTeamRoll initialization when done by a player 2026-04-02 16:00:04 +02:00
WBHarry
56dc9afe8f Corrected some action labels and missing translations 2026-04-02 13:37:38 +02:00
WBHarry
0f1ac406df Fixed adding a new damage instance to an action default to prof, which doesn't work for adversaries 2026-04-02 12:22:31 +02:00
WBHarry
e8ac3012ad Raised version 2026-04-02 11:24:17 +02:00
WBHarry
582a15be77 Fixed scene-navigation styling for levels 2026-04-02 11:23:57 +02:00
WBHarry
7fa03c58e0 Fixed armor-slots styling in CharacterSheet 2026-04-01 23:53:13 +02:00
WBHarry
1df248925e Raised foundry version 2026-04-01 23:10:16 +02:00
Carlos Fernandez
2a55e317fc Fix adversary damage rolls 2026-04-01 16:56:44 -04:00
Carlos Fernandez
2b8e4cb2fa
[v14] Add toggle for critical damage (#1762)
* Fix rolling critical damage after rerolling into a crit

* Add toggle for critical damage

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>
2026-04-01 19:39:26 +02:00
Nikhil Nagarajan
e3b433cce9
Text edit for Rally implementation in Bard Class (#1760) 2026-04-01 09:53:30 +02:00
Carlos Fernandez
29734c5fb5
Fix rolling critical damage after rerolling into a crit (#1761) 2026-04-01 09:47:20 +02:00
WBHarry
25264c26e9 Fixed Adversary roll failing 2026-03-31 18:01:12 +02:00
Nikhil Nagarajan
d284bd7398
[Fix] Fix CSS trait consistency in Character Creator and Sheet (#1737)
* Fixed on CC and sheet

* SVG fixes

* Revert "SVG fixes"

This reverts commit 72c5075f3f.

* SVG repaired and CSS padding revert

* Remove comments

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>
2026-03-31 17:31:21 +02:00
Carlos Fernandez
259b66236c
[V14] Update duality and fate chat commands (#1759)
* Update duality and fate chat commands for v14

* FateRoll withfear/withHope wasn't working after merging with v14-Dev. Fixed

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>
2026-03-31 17:26:45 +02:00
WBHarry
f156b12d79
[V14] Message Rolls Rework (#1757)
* Basic rework to the roll data in messages

* .

* Fixed advantage/disadvantage

* .

* .

* Fixed TagTeamDialog

* Reuse getter in faces setter

* Simplify fate roll type css class

* Add more caution to the dualityRoll fromData function

* Apply suggestion from @CarlosFdez

* Compute modifiers using deterministic terms (#1758)

---------

Co-authored-by: Carlos Fernandez <cfern1990@gmail.com>
Co-authored-by: Carlos Fernandez <CarlosFdez@users.noreply.github.com>
2026-03-31 17:20:22 +02:00
WBHarry
dbd5ef8bb0
Added fall and collision damage buttons in the GM Menu (#1756) 2026-03-29 23:54:45 +02:00
WBHarry
e2b13d6717 Removed a temporary override function in document/token. Doesn't seem needed anymore, and it was outdated, making bar2 on tokens never visualy update 2026-03-29 12:50:34 +02:00
WBHarry
e8f052faf3 Fixed drag/drop on application-sheet 2026-03-29 11:07:16 +02:00
WBHarry
740216ada2 Fixed spotlight case outside of combat 2026-03-28 03:09:41 +01:00
WBHarry
24d22dde59
[V14] [Feature] Spotlight Without Combat (#1755) 2026-03-27 22:01:50 -04:00
WBHarry
2a294684d4
Remade branch (#1754) 2026-03-28 00:38:50 +01:00
WBHarry
c730cc3d4d
[Feature] 1740 - Beastform Info (#1750)
* Improved the EffectDisplay tooltip of the beastform effect to show the info about the active beastform

* .

* Move template to more sorted location

---------

Co-authored-by: Carlos Fernandez <cfern1990@gmail.com>
2026-03-28 00:18:35 +01:00
WBHarry
e3c4f1ce9f Raised version to Testing 3 2026-03-28 00:17:53 +01:00
WBHarry
7f8e3fee6e Fixed ActiveEffect preCreate blocking multiple effects with origin=null 2026-03-27 22:24:13 +01:00
WBHarry
d79c236cfe Fixed effects not being creatable when not on an actor 2026-03-27 22:11:01 +01:00
WBHarry
d7ce388cad Fixed resource error on TagTeamDialog reroll 2026-03-27 10:29:01 +01:00
WBHarry
8d8fa983ef Merge branch 'main' into v14-Dev 2026-03-27 08:27:37 +01:00
WBHarry
394d1d338d Corrected BaseEffect scrollText armorUpdate logic 2026-03-26 16:27:09 +01:00
WBHarry
4319fbabb9 Merged with main 2026-03-26 16:17:23 +01:00
WBHarry
e3e1395de6 Minor ActionConfig improvements 2026-03-25 16:36:36 +01:00
WBHarry
931217577a Fixed BaseEffect emiting a scroll text of 0 armor change on every update 2026-03-25 14:30:42 +01:00
WBHarry
aa1d117c43
[V14] Effect Stacking (#1667)
* Added the ability for effects to have stacks

* Fixed effect stacking

* Improved token overlay spacing

* Compendium updaetes

* Simplify effect click event (#1748)

* Fixed a bunch of deprecations

* Corrected AgileScout Beastform json data

* Updated TokenHUD to the new v14

* Removed DestroyOnEmpty from consumables

* Fixed so that tooltips don't get stuck (#1745)

* [Feature] TagTeam Partial Rendering (#1735)

* I done did it, I think

* Think I fixed the partial rendering bug for gm->player

* [V14] 1743 - Damage Update Error (#1746)

* Fixed DamageParts causing errors on update

* Fixed ActionBaseConfig error when no damage present on the action

* Fix removal of damage field

* Removed unneccessary default value function for parts

---------

Co-authored-by: Carlos Fernandez <cfern1990@gmail.com>

* Simplify effect click event

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>
Co-authored-by: WBHarry <89362246+WBHarry@users.noreply.github.com>

* Fixed stacking-value pointer event

* Set the stacking value in EffectsDisplay to be tabular-nums for monospacing

* Made baseEffect.stacking nullable instead of having an enabled property

* .

* Fixed so that actor._onUpdateDescantDocuments re-renders the EffectDisplay if effects were updated

---------

Co-authored-by: Carlos Fernandez <CarlosFdez@users.noreply.github.com>
2026-03-25 13:54:14 +01:00
WBHarry
64ce615116
[V14] 1743 - Damage Update Error (#1746)
* Fixed DamageParts causing errors on update

* Fixed ActionBaseConfig error when no damage present on the action

* Fix removal of damage field

* Removed unneccessary default value function for parts

---------

Co-authored-by: Carlos Fernandez <cfern1990@gmail.com>
2026-03-24 17:33:17 +01:00
WBHarry
f119daff07
[Feature] TagTeam Partial Rendering (#1735)
* I done did it, I think

* Think I fixed the partial rendering bug for gm->player
2026-03-23 17:07:22 +01:00
WBHarry
7a7940aa04 Merged with main 2026-03-23 01:14:20 +01:00
WBHarry
3d25ceeb4a Updated TokenHUD to the new v14 2026-03-23 00:19:22 +01:00
WBHarry
2de0b490e9 Corrected AgileScout Beastform json data 2026-03-22 17:13:21 +01:00
WBHarry
de801924e6 Fixed a bunch of deprecations 2026-03-22 16:59:23 +01:00
WBHarry
ef53a7c561
[V14] 1354 - Armor Effect (#1652)
* Initial

* progress

* Working armor application

* .

* Added a updateArmorValue function that updates armoreffects according to an auto order

* .

* Added createDialog

* .

* Updated Armor SRD

* .

* Fixed character sheet armor update

* Updated itemconfig

* Actions now use createDialog for effects

* .

* .

* Fixed ArmorEffect max being a string

* Fixed SRD armor effects

* Finally finished the migration ._.

* SRD finalization

* Added ArmoreEffect.armorInteraction option

* Added ArmorManagement menu

* Fixed DamageReductionDialog

* Fixed ArmorManagement pip syle

* feat: add style to armors tooltip, add a style to make armor slot label more clear that was a button and add a tooltip location

* .

* Removed tooltip on manageArmor

* Fixes

* Fixed Downtime armor repair

* Removed ArmorScore from character data model and instead adding it in basePrep

* [Feature] ArmorEffect reworked into ChangeType on BaseEffect (#1739)

* Initial

* .

* Single armor rework start

* More fixes

* Fixed DamageReductionDialog

* Removed last traces of ArmorEffect

* .

* Corrected the SRD to use base effects again

* Removed bare bones armor item

* [V14] Refactor ArmorChange schema and fix some bugs (#1742)

* Refactor ArmorChange schema and fix some bugs

* Add current back to schema

* Fixed so changing armor values and taking damage works again

* Fixed so that scrolltexts for armor changes work again

* Removed old marks on armor.system

* Restored damageReductionDialog armorScore.value

* Use toggle for css class addition/removal

* Fix armor change type choices

* Added ArmorChange DamageThresholds

---------

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

* [V14] Armor System ArmorScore (#1744)

* Readded so that armor items have their system defined armor instead of using an ActiveEffect

* Consolidate armor source retrieval

* Fix regression with updating armor when sources are disabled

* Simplify armor pip update

* Use helper in damage reduction dialog

* .

* Corrected SRD Armor Items

---------

Co-authored-by: Carlos Fernandez <cfern1990@gmail.com>

* Updated migrations

* Migrations are now not horrible =D

---------

Co-authored-by: Murilo Brito <dev.murilobrito@gmail.com>
Co-authored-by: Carlos Fernandez <CarlosFdez@users.noreply.github.com>
Co-authored-by: Carlos Fernandez <cfern1990@gmail.com>
2026-03-22 01:57:46 +01:00
WBHarry
a3f515cf6d Fixed TokenConfig error:ing on save 2026-03-21 11:39:18 +01:00
WBHarry
461247b285 Raised version 2026-03-21 00:52:37 +01:00
Carlos Fernandez
b3e9c3fd9f
Use ActiveEffect Config for settings as well (#1741) 2026-03-21 00:46:30 +01:00
Carlos Fernandez
15fc879f9b
Fix damage icon when retrieving tooltip for attack (#1736) 2026-03-18 18:34:11 -04:00
WBHarry
d5244eedbf Merged with main 2026-03-17 22:46:08 +01:00
WBHarry
ad8caabf71 TagTeam Fixes 2026-03-16 20:29:12 +01:00
WBHarry
3031531b14
[V14] TagTeamRoll Rework (#1732)
* Initial rolls working

* Fixed reroll

* more

* More work

* Added results section

* .

* Visual improvements

* .

* Removed traces of old TagTeamRoll

* Added initiator handling

* Added updating for other players

* Fixed sync start

* Completed finish method

* Damage reroll

* Fixed localization

* Fixed crit damage

* Fixes

* Added visual of advantage and disadvantage dice
2026-03-16 09:31:15 +01:00
WBHarry
a7eda31aec Merged with main 2026-03-16 01:33:50 +01:00
WBHarry
e77b927a75 Merged with main 2026-03-15 11:45:21 +01:00
Carlos Fernandez
37b088fe7d Apply low performance styling to all daggerheart sheets 2026-03-13 20:00:05 -04:00
WBHarry
7c0ab25e10 Fixed contextmenu 2026-03-14 00:10:48 +01:00
Carlos Fernandez
1160f51347 Fix hasDamage checks on non-attack actions 2026-03-13 18:47:55 -04:00
Carlos Fernandez
92bcaf4962 Fix clean type in v14 2026-03-13 18:09:19 -04:00
WBHarry
fb4ddf227c Merged with main 2026-03-13 01:29:29 +01:00
WBHarry
b87e630a0a Merged main 2026-03-13 00:25:50 +01:00
WBHarry
ee3bbaec53 Fixed drag/drop 2026-03-12 22:12:45 +01:00
WBHarry
5c8d16e100 Raised version 2026-03-12 21:35:26 +01:00
WBHarry
fcadf119b7
[V14] Emanation AutoSizing (#1686)
* Added so that emanation templates auto size their base to any token at the spot its placed

* Correct option useage

* Also snap emanations when done from scene controls

---------

Co-authored-by: Carlos Fernandez <cfern1990@gmail.com>
2026-03-09 11:30:03 +01:00
WBHarry
5a4bbc91f5
[Feature] Damage Iterrable Rework (#1685)
* Initial

* More

* Fixed current actionConfig damage

* Reworked ActionConfig damage ui

* .

* Updated all Adversary compendium damage entries

* more

* The rest

* Fixed misses

* .

* .

* Also migrate sub fields of MappingField

* Removed MappingField

* Fix regression with re-tiering adversaries when dealing non-hp damage

* Allow iterable object to be detected as an object by foundry

---------

Co-authored-by: Carlos Fernandez <cfern1990@gmail.com>
2026-03-08 00:58:24 +01:00
WBHarry
d3ebd30e59 . 2026-03-07 22:26:54 +01:00
WBHarry
c2807e0676 Added in the new v14 subtract changeMode type for ActiveEffects 2026-03-07 18:02:01 +01:00
WBHarry
876e496d24 Merged with main 2026-03-07 01:39:52 +01:00
WBHarry
1ca866f87b Merged with main 2026-02-26 16:12:06 +01:00
WBHarry
37c53ad74e Fixed Template range texts 2026-02-25 22:45:55 +01:00
WBHarry
bcb30a6ff7 Fixed environment potentialAdversaries 2026-02-25 21:57:28 +01:00
WBHarry
4aab5d315a
[V14] 1604 - ActiveEffect Durations (#1634)
* Added daggerheart durations and auto expiration of them

* Added duration to all tier1 adversaries

* Finished all adversaries and environments

* Remaining compendiums updated

* Improved styling of duration in tooltips

* .
2026-02-17 18:57:03 +01:00
WBHarry
e2eb31c12e Merged with main 2026-02-16 20:40:28 +01:00
WBHarry
9b63371f4a Merged with main 2026-02-13 13:17:00 +01:00
WBHarry
063ff3d999 Merged with main 2026-02-09 15:44:54 +01:00
WBHarry
593105b163 Fixed ActiveEffect create dialog options 2026-02-04 20:08:52 +01:00
WBHarry
115a31423e Corrected ActiveEffect template styling and added useage of the new 'showIcon' property 2026-02-04 10:15:42 +01:00
WBHarry
ac998adaa6 Merged with development 2026-02-04 00:25:58 +01:00
WBHarry
6a0a8d8d6e Updated the sidebar so the new Placeables tab coems in 2026-02-02 23:56:57 +01:00
WBHarry
c17020c2c8 Fixed ActiveEffect.getChangeValue not expecting a number as change.value 2026-02-02 20:00:15 +01:00
WBHarry
578b090f08
[V14] 1605 - Template Migration (#1621)
* Fixed so that our templates make use of SceneRegions instead

* Fixed visibility
2026-02-01 17:21:56 +01:00
WBHarry
57e51ee841 Added in comments about Beastform failing 2026-02-01 00:25:40 +01:00
WBHarry
da368f3df5 Merge branch 'development' into v14-Dev 2026-01-31 19:46:47 +01:00
WBHarry
307af5b990 Merged with development 2026-01-31 19:35:46 +01:00
WBHarry
ae91d6786f Fixed actions not being delete:able 2026-01-31 19:32:51 +01:00
WBHarry
cd52aa8f9c Updated from special database update syntax to DataFieldOperators 2026-01-31 18:31:10 +01:00
WBHarry
9553f3387f
Fixed sceneConfig, sceneNavigation and SceneEnvironments (#1616) 2026-01-31 15:09:07 +01:00
WBHarry
4c51bb5899 1613-Rolltable-delete-formula-buttons 2026-01-31 15:08:07 +01:00
WBHarry
2c36da8433 Raised v14 version and fixed startup warnings 2026-01-31 01:46:24 +01:00
WBHarry
6e2d700945 . 2026-01-29 18:51:13 +01:00
WBHarry
1a928e950c Initial v14 fixes 2026-01-29 18:46:39 +01:00
1081 changed files with 22797 additions and 13327 deletions

View file

@ -1,3 +1,6 @@
[*]
indent_size = 4
indent_style = spaces
end_of_line = lf
[*.yml]
indent_size = 2

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
* text=auto eol=lf
*.json text eol=lf

42
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: Project CI
on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [24.x]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- uses: pnpm/action-setup@v4
with:
version: 10
- name: Cache NPM Deps
id: cache-npm
uses: actions/cache@v3
with:
path: node_modules/
key: npm-${{ hashFiles('package-lock.json') }}
- name: Install NPM Deps
if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }}
run: npm ci
- name: Lint
run: npm run lint

View file

@ -35,8 +35,9 @@ jobs:
env:
version: ${{steps.get_version.outputs.version-without-v}}
url: https://github.com/${{github.repository}}
manifest: https://github.com/${{github.repository}}/releases/latest/download/system.json
manifest: https://raw.githubusercontent.com/${{github.repository}}/v14/system.json
download: https://github.com/${{github.repository}}/releases/download/${{github.event.release.tag_name}}/system.zip
flags.hotReload: false
# Create a zip file with all files required by the module to add to the release
- run: zip -r ./system.zip system.json README.md LICENSE build/daggerheart.js build/tagify.css styles/daggerheart.css assets/ templates/ packs/ lang/

1
.gitignore vendored
View file

@ -6,3 +6,4 @@ Build
build
foundry
styles/daggerheart.css
styles/daggerheart.css.map

View file

@ -1,13 +0,0 @@
{
"trailingComma": "none",
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": true,
"quoteProps": "consistent",
"bracketSpacing": true,
"arrowParens": "avoid",
"printWidth": 120,
"endOfLine": "lf",
"bracketSameLine": true
}

View file

@ -1,78 +1,9 @@
# Contributing to Foundryborne
# Contributing to Daggerheart
Welcome! This is a community-driven project to bring [Daggerheart](https://www.daggerheart.com/) to [FoundryVTT](https://foundryvtt.com/) as a full system. We're excited to have you here and appreciate your interest in contributing.
Thank you for your interest in contributing to the Foundryborne project!
---
To ensure that all contributions align with our project goals and architectural standards, we ask that you **do not submit outside contributions without first receiving feedback from the development team.**
## 🤝 How to Contribute
If you have an idea or a fix you'd like to contribute, please start a discussion or open an issue first. We'd love to hear from you and collaborate on the best way to move forward!
We welcome contributions of all kinds:
- Bug reports
- Feature suggestions
- Code contributions
- UI/UX mockups
- Documentation improvements
- Questions and discussions
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
- **Use GitHub Issues** to report bugs or propose features
- **Start a Discussion** for larger ideas or questions
- **Open a Pull Request** once you've confirmed your work aligns with project direction
- **Keep things modular and maintainable** — if you're not sure how to structure something, ask!
- **Orient your code on existing examples**, and feel free to suggest a standard if it makes things clearer
---
## 🗂️ Project Structure
Please try to follow the general logic of the existing code when submitting PRs.
We encourage contributors to leave comments or open Discussions when proposing structural or organizational changes.
---
## 🧾 Issue & PR Best Practices
**For Issues:**
- Use clear, descriptive titles
- Provide a concise explanation of the problem or idea
- Include reproduction steps or example scenarios if it's a bug
- Add screenshots or logs if helpful
**For Pull Requests:**
- Use a clear title summarizing the change
- Provide a brief description of what your code does and why
- Link to any related Issues
- Keep PRs focused — smaller is better
---
## 🔖 Labels and Boards
We use GitHub labels to help organize contributions. If your issue or PR relates to a specific category, feel free to tag it. There is also a GitHub Project Board to help track active work and priorities.
---
## 📣 Communication
Discussions are currently happening on GitHub — in Issues, PRs, and [GitHub Discussions](https://github.com/Foundryborne/daggerheart/discussions). You're welcome to use any of these, though we may consolidate to one in the future.
---
## 🤗 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**!
🐸🛠️
Thank you for your understanding and support.

View file

@ -64,7 +64,11 @@ You can find the documentation here: https://github.com/Foundryborne/daggerheart
## Contributing
Looking to contribute to the project? Look no further, check out our [contributing guide](contributing.md), and keep the [Code of Conduct](coc.md) in mind when working on things.
Looking to contribute to the project? Look no further, check out our [contributing guide](CONTRIBUTING.md), and keep the [Code of Conduct](coc.md) in mind when working on things.
## AI Policy
The Foundryborne Daggerheart system does not make use of AI (generative or otherwise) for any area of its implementation. We expect all contributors to follow this same policy when contributing with a pull request; contributions made using AI will be rejected outright.
## Disclaimer:

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="M418.813 30.625c-21.178 26.27-49.712 50.982-84.125 70.844-36.778 21.225-75.064 33.62-110.313 38.06a310.317 310.317 0 0 0 6.813 18.25c16.01.277 29.366-.434 36.406-1.5l9.47-1.53 8.436-1.28.22 10.186a307.48 307.48 0 0 1-1.095 18.72l56.625 8.843c.86-.095 1.713-.15 2.563-.157 11.188-.114 21.44 7.29 24.468 18.593.657 2.448.922 4.903.845 7.313 5.972-2.075 11.753-4.305 17.28-6.72l9.595-4.188 2.313 10.22a340.211 340.211 0 0 1 7.375 48.062C438.29 247.836 468.438 225.71 493 197.5c-3.22-36.73-16.154-78.04-39.125-117.813a290.509 290.509 0 0 0-2.22-3.78l-27.56 71.374c5.154.762 10.123 3.158 14.092 7.126 9.81 9.807 9.813 25.69 0 35.5-9.812 9.81-25.722 9.807-35.53 0-8.86-8.858-9.69-22.68-2.532-32.5l38.938-100.844a322.02 322.02 0 0 0-20.25-25.937zM51.842 118.72c-8.46 17.373-15.76 36.198-21.187 56.436-14.108 52.617-13.96 103.682-2.812 143.438 13.3-2.605 26.442-3.96 39.312-4.03 1.855-.012 3.688.02 5.53.06 20.857.48 40.98 4.332 59.97 11.5a355.064 355.064 0 0 1-1.656-34.218c0-27.8 3.135-54.377 9-78.937l2.47-10.407 9.655 4.562c29.467 13.98 66.194 23.424 106.28 25.22 5.136-20.05 8.19-39.78 9.408-58.75-35.198 4.83-75.387 2.766-116.407-8.22-38.363-10.272-72.314-26.78-99.562-46.656zm230.594 82.218c-1.535 10.452-3.615 21.03-6.218 31.687a312.754 312.754 0 0 0 46-3.97 24.98 24.98 0 0 1-1.532-21.748l-38.25-5.97zM105 201.375l4.156 18.22-21.594 4.905c8.75 5.174 13.353 15.703 10.594 26-3.32 12.394-16.045 19.758-28.437 16.438-12.394-3.32-19.76-16.075-16.44-28.47a23.235 23.235 0 0 1 3.126-6.874l-21.062 4.78-4.125-18.218 73.78-16.78zm388.594 22.813c-25.53 25.46-55.306 45.445-86.906 60.5.05 2.397.093 4.8.093 7.218 0 9.188-.354 18.232-1.03 27.125 16.635 1.33 32.045-1.7 45.344-9.374 25.925-14.962 40.608-45.694 42.5-85.47zm-338.844 3c-4.03 19.993-6.33 41.31-6.406 63.593l.125-.342c30.568 10.174 62.622 17.572 95.25 21.375l7.5.875.718 7.5 5.687 60.125-18.625 1.75-2.53-26.75a23.117 23.117 0 0 1-14.845.968c-12.393-3.32-19.76-16.042-16.438-28.436.285-1.06.647-2.08 1.063-3.063a496.627 496.627 0 0 1-57.406-14.53c2.69 49.62 16.154 94.04 36.094 126.656 22.366 36.588 52.13 57.78 83.968 57.78 31.838.003 61.602-21.19 83.97-57.78 19.536-31.96 32.846-75.244 35.905-123.656a499.132 499.132 0 0 1-48.25 11.656c1.914 4.57 2.415 9.78 1.033 14.938-3.322 12.394-16.045 19.758-28.438 16.437a23.01 23.01 0 0 1-2.125-.686l-2.5 26.47-18.594-1.752 5.688-60.125.72-7.5 7.498-.875c29.245-3.407 57.995-9.717 85.657-18.312v-1.594c0-21.573-2.27-42.23-6.064-61.75C351.132 242.653 313.092 250 272.312 250c-43.59 0-83.986-8.658-117.562-22.813zm-87.5 105.968c-10.87.102-21.995 1.22-33.375 3.313 12.695 31.62 33.117 53.07 59 60 16.9 4.523 34.896 2.536 52.813-5.25-4.382-13.89-7.874-28.606-10.344-43.97-21.115-9.623-43.934-14.32-68.094-14.094zm137.5 80.22h130.813c-40.082 44.594-92.623 42.844-130.813 0z" fill="#fff" fill-opacity="1"></path></g></svg>

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -1,3 +1,3 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.12012 0.5H51.8799C55.2901 0.500041 57.8779 3.57175 57.2998 6.93262L50.4639 46.6777C50.1604 48.4411 49.0179 49.9467 47.4014 50.7139L31.3584 58.3271C29.8661 59.0354 28.1339 59.0354 26.6416 58.3271L10.5986 50.7139C8.98214 49.9467 7.83959 48.4411 7.53613 46.6777L0.700195 6.93262C0.122088 3.57175 2.7099 0.500042 6.12012 0.5Z" fill="transparent" stroke="#18162e"/>
<path d="M 7.12 0.5 H 52.88 C 56.29 0.5 58.88 3.57 58.3 6.93 L 51.46 46.68 C 51.16 48.44 50.02 49.95 48.4 50.71 L 32.36 58.33 C 30.87 59.04 29.13 59.04 27.64 58.33 L 11.6 50.71 C 9.98 49.95 8.84 48.44 8.54 46.68 L 1.7 6.93 C 1.12 3.57 3.71 0.5 7.12 0.5 Z" fill="transparent" stroke="#18162e"/>
</svg>

Before

Width:  |  Height:  |  Size: 476 B

After

Width:  |  Height:  |  Size: 397 B

Before After
Before After

View file

@ -1,3 +1,3 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.12012 0.5H51.8799C55.2901 0.500041 57.8779 3.57175 57.2998 6.93262L50.4639 46.6777C50.1604 48.4411 49.0179 49.9467 47.4014 50.7139L31.3584 58.3271C29.8661 59.0354 28.1339 59.0354 26.6416 58.3271L10.5986 50.7139C8.98214 49.9467 7.83959 48.4411 7.53613 46.6777L0.700195 6.93262C0.122088 3.57175 2.7099 0.500042 6.12012 0.5Z" fill="#18152E" stroke="#F3C267"/>
<path d="M 7.12 0.5 H 52.88 C 56.29 0.5 58.88 3.57 58.3 6.93 L 51.46 46.68 C 51.16 48.44 50.02 49.95 48.4 50.71 L 32.36 58.33 C 30.87 59.04 29.13 59.04 27.64 58.33 L 11.6 50.71 C 9.98 49.95 8.84 48.44 8.54 46.68 L 1.7 6.93 C 1.12 3.57 3.71 0.5 7.12 0.5 Z" fill="#18152E" stroke="#F3C267"/>
</svg>

Before

Width:  |  Height:  |  Size: 472 B

After

Width:  |  Height:  |  Size: 393 B

Before After
Before After

24
daggerheart.d.ts vendored
View file

@ -1,8 +1,11 @@
import '@client/global.mjs';
import '@common/global.mjs';
import '@common/primitives/global.mjs';
import Canvas from '@client/canvas/board.mjs';
// Foundry's use of `Object.assign(globalThis) means many globally available objects are not read as such
// This declare global hopefully fixes that
// Note: eslint is not aware of these, whatever is added here should go in the eslint's globals list
declare global {
/**
* A simple event framework used throughout Foundry Virtual Tabletop.
@ -12,9 +15,28 @@ declare global {
class Hooks extends foundry.helpers.Hooks {}
const fromUuid = foundry.utils.fromUuid;
const fromUuidSync = foundry.utils.fromUuidSync;
/**
* A representation of a color in hexadecimal format.
* This class provides methods for transformations and manipulations of colors.
*/
class Color extends foundry.utils.Color {}
/**
* The singleton game canvas
*/
const canvas: Canvas;
const ActiveEffect: foundry.documents.ActiveEffect;
const Actor: foundry.documents.Actor;
const BaseScene: foundry.documents.BaseScene;
const ChatMessage: foundry.documents.ChatMessage;
const Combat: foundry.documents.Combat;
const Combatant: foundry.documents.Combatant;
const Item: foundry.documents.Item;
const Macro: foundry.documents.Macro;
const Scene: foundry.documents.Scene;
const TokenDocument: foundry.documents.TokenDocument;
const Collection: foundry.utils.Collection;
const FormDataExtended: foundry.applications.ux.FormDataExtended;
const TextEditor: foundry.applications.ux.TextEditor;
}

View file

@ -9,10 +9,7 @@ import * as dice from './module/dice/_module.mjs';
import * as fields from './module/data/fields/_module.mjs';
import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs';
import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs';
import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs';
import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll, FateRoll } from './module/dice/_module.mjs';
import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs';
import { enrichedFateRoll, getFateTypeData } from './module/enrichers/FateRollEnricher.mjs';
import {
handlebarsRegistration,
runMigrations,
@ -21,7 +18,6 @@ import {
} from './module/systemRegistration/_module.mjs';
import { placeables, DhTokenLayer } from './module/canvas/_module.mjs';
import './node_modules/@yaireo/tagify/dist/tagify.css';
import TemplateManager from './module/documents/templateManager.mjs';
import TokenManager from './module/documents/tokenManager.mjs';
CONFIG.DH = SYSTEM;
@ -36,6 +32,13 @@ CONFIG.Dice.daggerheart = {
FateRoll: FateRoll
};
CONFIG.RegionBehavior.dataModels = {
...CONFIG.RegionBehavior.dataModels,
...data.regionBehaviors
};
Object.assign(CONFIG.Dice.termTypes, dice.diceTypes);
CONFIG.Actor.documentClass = documents.DhpActor;
CONFIG.Actor.dataModels = models.actors.config;
CONFIG.Actor.collection = collections.DhActorCollection;
@ -45,6 +48,7 @@ CONFIG.Item.dataModels = models.items.config;
CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect;
CONFIG.ActiveEffect.dataModels = models.activeEffects.config;
CONFIG.ActiveEffect.changeTypes = { ...CONFIG.ActiveEffect.changeTypes, ...models.activeEffects.changeEffects };
CONFIG.Combat.documentClass = documents.DhpCombat;
CONFIG.Combat.dataModels = { base: models.DhCombat };
@ -56,11 +60,13 @@ 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.Canvas.layers.regions.layerClass = placeables.DhRegionLayer;
CONFIG.Canvas.layers.tokens.layerClass = DhTokenLayer;
CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate;
CONFIG.Region.objectClass = placeables.DhRegion;
CONFIG.RollTable.documentClass = documents.DhRollTable;
CONFIG.RollTable.resultTemplate = 'systems/daggerheart/templates/ui/chat/table-result.hbs';
@ -84,7 +90,6 @@ CONFIG.ui.resources = applications.ui.DhFearTracker;
CONFIG.ui.countdowns = applications.ui.DhCountdowns;
CONFIG.ux.ContextMenu = applications.ux.DHContextMenu;
CONFIG.ux.TooltipManager = documents.DhTooltipManager;
CONFIG.ux.TemplateManager = new TemplateManager();
CONFIG.ux.TokenManager = new TokenManager();
CONFIG.debug.triggers = false;
@ -191,6 +196,11 @@ Hooks.once('init', () => {
makeDefault: true,
label: sheetLabel('TYPES.Actor.environment')
});
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.NPC, {
types: ['npc'],
makeDefault: true,
label: sheetLabel('TYPES.Actor.npc')
});
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Party, {
types: ['party'],
makeDefault: true,
@ -213,6 +223,7 @@ Hooks.once('init', () => {
SYSTEM.id,
applications.sheetConfigs.ActiveEffectConfig,
{
types: ['base', 'beastform', 'horde'],
makeDefault: true,
label: sheetLabel('DOCUMENT.ActiveEffect')
}
@ -270,7 +281,6 @@ Hooks.on('setup', () => {
...damageThresholds,
'proficiency',
'evasion',
'armorScore',
'scars',
'levelData.level.current'
]
@ -332,79 +342,33 @@ Hooks.on('renderHandlebarsApplication', (_, element) => {
enricherRenderSetup(element);
});
Hooks.on('chatMessage', (_, message) => {
if (message.startsWith('/dr')) {
const result =
message.trim().toLowerCase() === '/dr' ? { result: {} } : rollCommandToJSON(message.replace(/\/dr\s?/, ''));
if (!result) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.dualityParsing'));
return false;
}
Hooks.on(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, async data => {
if (data.openForAllPlayers && data.partyId) {
const party = game.actors.get(data.partyId);
if (!party) return;
const { result: rollCommand, flavor } = result;
const reaction = rollCommand.reaction;
const traitValue = rollCommand.trait?.toLowerCase();
const advantage = rollCommand.advantage
? CONFIG.DH.ACTIONS.advantageState.advantage.value
: rollCommand.disadvantage
? CONFIG.DH.ACTIONS.advantageState.disadvantage.value
: undefined;
const difficulty = rollCommand.difficulty;
const grantResources = rollCommand.grantResources;
const target = getCommandTarget({ allowNull: true });
const title =
(flavor ?? traitValue)
? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(SYSTEM.ACTOR.abilities[traitValue].label)
})
: game.i18n.localize('DAGGERHEART.GENERAL.duality');
enrichedDualityRoll({
reaction,
traitValue,
target,
difficulty,
title,
label: game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll'),
actionType: null,
advantage,
grantResources
});
return false;
const TagTeamDialog = game.system.api.applications.dialogs.TagTeamDialog;
const dialog = foundry.applications.instances.get(`TagTeamDialog-${party.id}`) ?? new TagTeamDialog(party);
dialog.tabGroups.application = 'tagTeamRoll';
await dialog.render({ force: true });
}
});
if (message.startsWith('/fr')) {
const result =
message.trim().toLowerCase() === '/fr' ? { result: {} } : rollCommandToJSON(message.replace(/\/fr\s?/, ''));
Hooks.on(CONFIG.DH.HOOKS.hooksConfig.groupRollStart, async data => {
if (data.openForAllPlayers && data.partyId) {
const party = game.actors.get(data.partyId);
if (!party) return;
if (!result) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateParsing'));
return false;
}
const { result: rollCommand, flavor } = result;
const fateTypeData = getFateTypeData(rollCommand?.type);
if (!fateTypeData)
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing'));
const { value: fateType, label: fateTypeLabel } = fateTypeData;
const target = getCommandTarget({ allowNull: true });
const title = flavor ?? game.i18n.localize('DAGGERHEART.GENERAL.fateRoll');
enrichedFateRoll({
target,
title,
label: fateTypeLabel,
fateType
});
return false;
const GroupRollDialog = game.system.api.applications.dialogs.GroupRollDialog;
const dialog = foundry.applications.instances.get(`GroupRollDialog-${party.id}`) ?? new GroupRollDialog(party);
dialog.tabGroups.application = 'groupRoll';
await dialog.render({ force: true });
}
});
const updateActorsRangeDependentEffects = async token => {
if (!token) return;
const rangeMeasurement = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.variantRules
@ -482,3 +446,33 @@ Hooks.on('canvasTearDown', canvas => {
Hooks.on('canvasReady', canas => {
game.system.registeredTriggers.registerSceneTriggers(canvas.scene);
});
/** Make the user to select a document type, instead of having a default doc type for them to accidentally keep */
Hooks.on('renderDialogV2', (_dialog, html) => {
if (!html.classList.contains('dialog')) return;
const cls = html.classList.contains('item-create')
? documents.DHItem.implementation
: html.classList.contains('actor-create')
? documents.DhpActor.implementation
: null;
if (!cls) return;
const form = html.querySelector('form');
const submit = html.querySelector('button[type=submit]');
const select = html.querySelector('select[name=type]');
const nameInput = html.querySelector('input[name=name]');
if (!form || !select || !submit || !nameInput) return;
nameInput.placeholder = cls.defaultName({});
const emptyOption = document.createElement('option');
emptyOption.value = '';
emptyOption.selected = true;
select.required = true;
select.prepend(emptyOption);
submit.addEventListener('click', event => {
if (!form.reportValidity()) {
event.preventDefault();
event.stopPropagation();
}
});
});

101
eslint.config.mjs Normal file
View file

@ -0,0 +1,101 @@
import globals from 'globals';
import { defineConfig, globalIgnores } from 'eslint/config';
import tseslint from 'typescript-eslint';
import js from '@eslint/js';
import stylistic from '@stylistic/eslint-plugin';
/** @type {Partial<RulesConfig>} */
export const stylisticRules = {
'@stylistic/indent': [
'error',
4,
{
SwitchCase: 1
}
],
'@stylistic/max-len': ['error', {
code: 120,
ignoreComments: true,
ignoreStrings: true,
ignoreTemplateLiterals: true,
ignoreRegExpLiterals: true
}],
'@stylistic/quotes': ['error', 'single', { allowTemplateLiterals: 'always' }],
'@stylistic/arrow-parens': ['error', 'as-needed'],
'@stylistic/quote-props': ['error', 'as-needed'],
'@stylistic/array-bracket-newline': ['error', 'consistent'],
'@stylistic/key-spacing': 'error',
'@stylistic/comma-dangle': ['error', 'never'],
'@stylistic/space-in-parens': ['error', 'never'],
'@stylistic/space-infix-ops': 2,
'@stylistic/keyword-spacing': 2,
'@stylistic/semi-spacing': 2,
'@stylistic/no-multi-spaces': 2,
'@stylistic/no-extra-semi': 2,
'@stylistic/no-whitespace-before-property': 2,
'@stylistic/space-unary-ops': 2
};
export default defineConfig([
globalIgnores(['foundry/**/*', 'build/**/*']),
{
files: ['gulpfile.js', 'postcss.config.js'],
languageOptions: { globals: globals.node }
},
{
files: ['**/*.{js,mjs,cjs}'],
plugins: {
'@stylistic': stylistic
},
languageOptions: {
globals: {
...globals.browser,
CONFIG: 'readonly',
CONST: 'readonly',
// Global classes
Color: 'readonly',
Handlebars: 'readonly',
Hooks: 'readonly',
PIXI: 'readonly',
ProseMirror: 'readonly',
Roll: 'readonly',
// global namespaces
canvas: 'readonly',
foundry: 'readonly',
game: 'readonly',
ui: 'readonly',
// global functions
fromUuid: 'readonly',
fromUuidSync: 'readonly',
getDocumentClass: 'readonly',
_del: 'readonly',
_replace: 'readonly',
_loc: 'readonly',
// Documents
ActiveEffect: 'readonly',
Actor: 'readonly',
BaseScene: 'readonly',
ChatMessage: 'readonly',
Combat: 'readonly',
Combatant: 'readonly',
Item: 'readonly',
Macro: 'readonly',
Scene: 'readonly',
TokenDocument: 'readonly',
// Other
Collection: 'readonly',
FormDataExtended: 'readonly',
TextEditor: 'readonly'
}
},
rules: {
'no-undef': 'error',
// 'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
...stylisticRules
}
},
{
files: ['**/*.ts'],
extends: [js.configs.recommended, tseslint.configs.recommended]
}
]);

View file

@ -1,9 +1,15 @@
// Less configuration
var gulp = require('gulp');
var less = require('gulp-less');
var sourcemaps = require('gulp-sourcemaps');
gulp.task('less', function (cb) {
gulp.src('styles/daggerheart.less').pipe(less()).pipe(gulp.dest('styles'));
gulp.src('styles/daggerheart.less')
.pipe(sourcemaps.init())
.pipe(less())
.on('error', console.error.bind(console))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest('styles'));
cb();
});

View file

@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "ES6",
"target": "ES6",
"module": "es2022",
"target": "es2022",
"paths": {
"@client/*": ["./foundry/client/*"],
"@common/*": ["./foundry/common/*"]

View file

@ -14,13 +14,16 @@
"beastform": "Beastform"
},
"ActiveEffect": {
"beastform": "Beastform"
"base": "Standard",
"beastform": "Beastform",
"horde": "Horde"
},
"Actor": {
"character": "Character",
"companion": "Companion",
"adversary": "Adversary",
"environment": "Environment",
"npc": "NPC",
"party": "Party"
}
},
@ -53,6 +56,7 @@
},
"damage": {
"name": "Damage",
"critical": "Damage (Critical)",
"tooltip": "Direct damage without a roll."
},
"effect": {
@ -71,9 +75,7 @@
"name": "Summon",
"tooltip": "Create tokens in the scene.",
"error": "You do not have permission to summon tokens or there is no active scene.",
"invalidDrop": "You can only drop Actor entities to summon.",
"chatMessageTitle": "Test2",
"chatMessageHeaderTitle": "Summoning"
"invalidDrop": "You can only drop Actor entities to summon."
},
"transform": {
"name": "Transform",
@ -87,9 +89,14 @@
},
"Config": {
"beastform": {
"exact": "Beastform Max Tier",
"exactHint": "The Character's Tier is used if empty",
"label": "Beastform"
"exact": { "label": "Beastform Max Tier", "hint": "The Character's Tier is used if empty" },
"modifications": {
"traitBonuses": {
"label": { "single": "Trait Bonus", "plural": "Trait Bonuses" },
"hint": "Pick bonuses you apply to freely chosen traits at the time of transforming",
"bonus": "Bonus Amount"
}
}
},
"countdown": {
"defaultOwnership": "Default Ownership",
@ -103,9 +110,17 @@
"customFormula": "Custom Formula",
"formula": "Formula"
},
"area": {
"sectionTitle": "Areas",
"shape": "Shape",
"size": "Size"
},
"displayInChat": "Display in chat",
"deleteTriggerTitle": "Delete Trigger",
"deleteTriggerContent": "Are you sure you want to delete the {trigger} trigger?"
"deleteTriggerContent": "Are you sure you want to delete the {trigger} trigger?",
"advantageState": "Advantage State",
"damageOnSave": "Damage on Save",
"useDefaultItemValues": "Use default Item values"
},
"RollField": {
"diceRolling": {
@ -117,10 +132,11 @@
}
},
"Settings": {
"attackBonus": "Attack Bonus",
"attackModifier": "Attack Modifier",
"attackName": "Attack Name",
"criticalThreshold": "Critical Threshold",
"includeBase": { "label": "Include Item Damage" },
"groupAttack": { "label": "Group Attack" },
"multiplier": "Multiplier",
"saveHint": "Set a default Trait to enable Reaction Roll. It can be changed later in Reaction Roll Dialog.",
"resultBased": {
@ -151,7 +167,9 @@
"Config": {
"rangeDependence": {
"title": "Range Dependence"
}
},
"stacking": { "title": "Stacking" },
"targetDispositions": "Affected Dispositions"
},
"RangeDependance": {
"hint": "Settings for an optional distance at which this effect should activate",
@ -198,7 +216,13 @@
"type": { "label": "Type" }
},
"hordeDamage": "Horde Damage",
"horderHp": "Horde/HP"
"horderHp": "Horde/HP",
"adversaryReactionRoll": {
"headerTitle": "Adversary Reaction Roll"
}
},
"Base": {
"CannotAddType": "Cannot add {itemType} items to {actorType} actors."
},
"Character": {
"advantageSources": {
@ -223,6 +247,8 @@
},
"defaultHopeDice": "Default Hope Dice",
"defaultFearDice": "Default Fear Dice",
"defaultAdvantageDice": "Default Advantage Dice",
"defaultDisadvantageDice": "Default Disadvantage Dice",
"disadvantageSources": {
"label": "Disadvantage Sources",
"hint": "Add single words or short text as reminders and hints of what a character has disadvantage on."
@ -307,6 +333,27 @@
}
},
"newAdversary": "New Adversary"
},
"NPC": {
"FIELDS": {
"motives": { "label": "Motives" }
}
},
"Party": {
"Subtitle": {
"character": "{community} {ancestry} | {subclass} {class}",
"companion": "Companion of {partner}"
},
"RemoveConfirmation": {
"title": "Remove member {name}",
"text": "Are you sure you want to remove {name} from the party?"
},
"Thresholds": {
"minor": "MIN",
"major": "MAJ",
"severe": "SEV"
},
"triggerRestContent": "This will trigger a dialog to players make their downtime moves. Are you sure?"
}
},
"APPLICATIONS": {
@ -342,7 +389,7 @@
"selectSecondaryWeapon": "Select Secondary Weapon",
"selectSubclass": "Select Subclass",
"setupSkipTitle": "Skipping Character Setup",
"setupSkipContent": "You are skipping the Character Setup by adding this manually. The character setup is the blinking arrows in the top-right. Are you sure you want to continue?",
"setupSkipContent": "You are skipping the Character Setup by adding this manually. The character setup is the blinking button in the top-right. Are you sure you want to continue?",
"startingItems": "Starting Items",
"story": "Story",
"storyExplanation": "Select which background and connection prompts you want to copy into your character's background.",
@ -365,7 +412,11 @@
"giveSpotlight": "Give The Spotlight",
"requestingSpotlight": "Requesting The Spotlight",
"requestSpotlight": "Request The Spotlight",
"openCountdowns": "Countdowns"
"openCountdowns": "Countdowns",
"adversaryCategories": {
"friendly": "Friendly",
"adversaries": "Adversaries"
}
},
"CompendiumBrowserSettings": {
"title": "Enable Compendiums",
@ -440,16 +491,21 @@
"defaultOwnershipTooltip": "The default player ownership of countdowns",
"hideNewCountdowns": "Hide New Countdowns"
},
"CreateItemDialog": {
"createItem": "Create Item",
"browseCompendium": "Browse Compendium"
},
"DaggerheartMenu": {
"title": "GM Tools",
"refreshFeatures": "Refresh Features"
"refreshFeatures": "Refresh Features",
"fallingAndCollision": "Falling And Collision Damage"
},
"DeleteConfirmation": {
"title": "Delete {type} - {name}",
"text": "Are you sure you want to delete {name}?"
},
"DamageReduction": {
"armorMarks": "Armor Marks",
"maxUseableArmor": "Useable Armor Slots",
"armorWithStress": "Spend 1 stress to use an extra mark",
"thresholdImmunities": "Threshold Immunities",
"stress": "Stress",
@ -653,15 +709,15 @@
"noPlayers": "No players to assign ownership to",
"default": "Default Ownership"
},
"PendingReactionsDialog": {
"title": "Pending Reaction Rolls Found",
"unfinishedRolls": "Some Tokens have not finished their Reaction Rolls.",
"warning": "Unfinished reaction rolls will be considered as failed.",
"confirmation": "Are you sure you want to continue?"
},
"ReactionRoll": {
"title": "Reaction Roll: {trait}"
},
"RerollDialog": {
"title": "Reroll",
"damageTitle": "Reroll Damage",
"deselectDiceNotification": "Deselect one of the selected dice first",
"acceptCurrentRolls": "Accept Current Rolls"
},
"ResourceDice": {
"title": "{name} Resource",
"rerollDice": "Reroll Dice"
@ -675,19 +731,56 @@
},
"TagTeamSelect": {
"title": "Tag Team Roll",
"FIELDS": {
"initiator": {
"memberId": { "label": "Initiating Character" },
"cost": { "label": "Hope Cost" }
}
},
"leaderTitle": "Initiating Character",
"membersTitle": "Participants",
"partyTeam": "Party Team",
"hopeCost": "Hope Cost",
"initiatingCharacter": "Initiating Character",
"selectParticipants": "Select the two participants",
"startTagTeamRoll": "Start Tag Team Roll",
"openDialogForAll": "Open Dialog For All",
"rollType": "Roll Type",
"makeYourRoll": "Make your roll",
"cancelTagTeamRoll": "Cancel Tag Team Roll",
"finishTagTeamRoll": "Finish Tag Team Roll",
"linkMessageHint": "Make a roll from your character sheet to link it to the Tag Team Roll",
"damageNotRolled": "Damage not rolled in chat message yet",
"insufficientHope": "The initiating character doesn't have enough hope",
"createTagTeam": "Create TagTeam Roll",
"chatMessageRollTitle": "Roll"
"createTagTeam": "Create Tag Team Roll",
"chatMessageRollTitle": "Roll",
"cancelConfirmTitle": "Cancel Tag Team Roll",
"cancelConfirmText": "Are you sure you want to cancel the Tag Team Roll? This will close it for all other players too.",
"hints": {
"completeRolls": "Set up and complete the rolls for the characters",
"selectRoll": "Select which roll value to be used for the Tag Team"
}
},
"GroupRollSelect": {
"cancelConfirmText": "Are you sure you want to cancel the Group Roll? This will close it for all other players too.",
"cancelConfirmTitle": "Cancel Group Roll",
"initializationTitle": "Character Selection",
"finishGroupRoll": "Finish Group Roll",
"leader": "Leader",
"leaderRoll": "Leader Roll",
"members": "Members",
"openDialogForAll": "Open Dialog For All",
"removeRoll": "Remove Roll",
"resultsHint": "Results will appear when characters roll",
"selectLeaderHint": "Select one Character to be the leader",
"selectParticipantsHint": "Select one Character to be the leader",
"startGroupRoll": "Start Group Roll",
"title": "Group Roll"
},
"TokenConfig": {
"actorSizeUsed": "Actor size is set, determining the dimensions"
"actorSizeUsed": "Actor size is set, determining the dimensions",
"tokenSize": "Token Size",
"sizeCategory": "Size Category"
}
},
"CLASS": {
@ -697,6 +790,20 @@
}
},
"CONFIG": {
"ActiveEffectDuration": {
"temporary": "Temporary",
"act": "Next Spotlight",
"scene": "Next Scene",
"shortRest": "Next Rest",
"longRest": "Next Long Rest",
"session": "Next Session",
"custom": "Custom"
},
"ActionAutomationChoices": {
"never": "Never",
"showDialog": "Show Dialog Only",
"always": "Always"
},
"AdversaryTrait": {
"relentless": {
"name": "Relentless",
@ -764,6 +871,11 @@
"bruiser": "for each Bruiser adversary.",
"solo": "for each Solo adversary."
},
"ArmorInteraction": {
"none": { "label": "Ignores Armor" },
"active": { "label": "Active w/ Armor" },
"inactive": { "label": "Inactive w/ Armor" }
},
"ArmorFeature": {
"burning": {
"name": "Burning",
@ -1114,6 +1226,12 @@
"description": ""
}
},
"fallAndCollision": {
"veryClose": { "label": "Very Close", "chatTitle": "Fall Damage: Very Close" },
"close": { "label": "Close", "chatTitle": "Fall Damage: Close" },
"far": { "label": "Far", "chatTitle": "Fall Damage: Far" },
"collision": { "label": "Collision", "chatTitle": "Dangerous Collision" }
},
"FeatureForm": {
"label": "Feature Form",
"passive": "Passive",
@ -1215,10 +1333,20 @@
"on": "On",
"onWithToggle": "On With Toggle"
},
"SceneRangeMeasurementTypes": {
"disable": "Disable Daggerheart Range Measurement",
"default": "Default",
"custom": "Custom"
},
"SelectAction": {
"selectType": "Select Action Type",
"selectAction": "Action Selection"
},
"TagTeamRollTypes": {
"trait": "Trait",
"ability": "Ability",
"damageAbility": "Damage Ability"
},
"TargetTypes": {
"any": "Any",
"friendly": "Friendly",
@ -1231,8 +1359,8 @@
"cone": "Cone",
"emanation": "Emanation",
"inFront": "In Front",
"rect": "Rectangle",
"ray": "Ray"
"rectangle": "Rectangle",
"line": "Line"
},
"TokenSize": {
"tiny": "Tiny",
@ -1847,6 +1975,17 @@
"name": "Healing Roll"
}
},
"ChangeTypes": {
"armor": {
"newArmorEffect": "Armor Effect",
"FIELDS": {
"interaction": {
"label": "Armor Interaction",
"hint": "Does the character wearing armor suppress this effect?"
}
}
}
},
"Duration": {
"passive": "Passive",
"temporary": "Temporary"
@ -1871,6 +2010,10 @@
}
},
"GENERAL": {
"Ability": {
"single": "Ability",
"plural": "Abilities"
},
"Action": {
"single": "Action",
"plural": "Actions"
@ -1894,6 +2037,10 @@
"hint": "Multiply any damage dealt to you by this number"
}
},
"Battlepoints": {
"full": "Battlepoints",
"short": "BP"
},
"Bonuses": {
"rest": {
"downtimeAction": "Downtime Action",
@ -2252,6 +2399,7 @@
"duality": "Duality",
"dualityDice": "Duality Dice",
"dualityRoll": "Duality Roll",
"effect": "Effect",
"enabled": "Enabled",
"evasion": "Evasion",
"equipment": "Equipment",
@ -2300,13 +2448,17 @@
"single": "Miss",
"plural": "Miss"
},
"missingX": "Missing {x}",
"maxWithThing": "Max {thing}",
"missingDragDropThing": "Drop {thing} here",
"multiclass": "Multiclass",
"name": "Name",
"newCategory": "New Category",
"newThing": "New {thing}",
"next": "Next",
"none": "None",
"noTarget": "No current target",
"optionalThing": "Optional {thing}",
"partner": "Partner",
"player": {
"single": "Player",
@ -2324,14 +2476,20 @@
"rerolled": "Rerolled",
"rerollThing": "Reroll {thing}",
"resource": "Resource",
"result": {
"single": "Result",
"plural": "Results"
},
"roll": "Roll",
"rollAll": "Roll All",
"rollDamage": "Roll Damage",
"rollWith": "{roll} Roll",
"save": "Save",
"saveSettings": "Save Settings",
"scalable": "Scalable",
"scars": "Scars",
"situationalBonus": "Situational Bonus",
"searchPlaceholder": "Search...",
"spent": "Spent",
"step": "Step",
"stress": "Stress",
@ -2381,6 +2539,9 @@
"recovery": { "label": "Recovery" },
"type": { "label": "Type" },
"value": { "label": "Value" }
},
"identifier": {
"label": "Identifier"
}
},
"Ancestry": {
@ -2406,10 +2567,11 @@
"tokenImg": { "label": "Token Image" },
"tokenRingImg": { "label": "Subject Texture" },
"tokenSize": {
"placeholder": "Using character dimensions",
"disabledPlaceholder": "Set by character size",
"placeholder": "Token Size",
"disabledPlaceholder": "Token Size",
"height": { "label": "Height" },
"width": { "label": "Width" },
"depth": { "label": "Depth" },
"scale": { "label": "Token Scale" }
},
"evolved": {
@ -2482,15 +2644,20 @@
},
"Weapon": {
"weaponType": "Weapon Type",
"primaryWeapon": "Primary Weapon",
"secondaryWeapon": "Secondary Weapon"
"primaryWeapon": {
"full": "Primary Weapon",
"short": "Primary"
},
"secondaryWeapon": {
"full": "Secondary Weapon",
"short": "Secondary"
}
}
},
"MACROS": {
"Spotlight": {
"errors": {
"noActiveCombat": "There is no active encounter",
"noCombatantSelected": "A combatant token must be either selected or hovered to spotlight it"
"noTokenSelected": "A token on the canvas must either be selected or hovered to spotlight it"
}
}
},
@ -2565,6 +2732,10 @@
"hint": "Automatically increase the GM's fear pool on a fear duality roll result."
},
"FIELDS": {
"autoExpireActiveEffects": {
"label": "Auto Expire Active Effects",
"hint": "Active Effects with set durations will automatically be removed when their durations are up"
},
"damageReductionRulesDefault": {
"label": "Damage Reduction Rules Default",
"hint": "Wether using armor and reductions has rules on by default"
@ -2662,6 +2833,15 @@
"hideObserverPermissionInChat": {
"label": "Hide Chat Info From Players",
"hint": "Information such as hit/miss on attack rolls against adversaries will be hidden"
},
"hidePartyStats": {
"label": "Hide Party Stats",
"hint": "Resources and stats in the party sheet's member list will be hidden to the following users, even if the user is part of the same party",
"choices": {
"never": "Never, always show",
"players": "Hide From Players",
"always": "Hide from Everyone"
}
}
}
},
@ -2734,6 +2914,10 @@
}
},
"Keybindings": {
"partySheet": {
"name": "Toggle Party Sheet",
"hint": "Open or close the active party's sheet"
},
"spotlight": {
"name": "Spotlight Combatant",
"hint": "Move the spotlight to a hovered or selected token that's present in an active encounter"
@ -2778,6 +2962,7 @@
"system": "Dice Preset",
"font": "Font",
"critical": "Duality Critical Animation",
"muted": "Muted",
"diceAppearance": "Dice Appearance",
"animations": "Animations",
"defaultAnimations": "Set Animations As Player Defaults",
@ -2886,18 +3071,6 @@
"immunityTo": "Immunity: {immunities}"
},
"featureTitle": "Class Feature",
"groupRoll": {
"title": "Group Roll",
"leader": "Leader",
"partyTeam": "Party Team",
"team": "Team",
"selectLeader": "Select a Leader",
"selectMember": "Select a Member",
"rerollTitle": "Reroll Group Roll",
"rerollContent": "Are you sure you want to reroll your {trait} roll?",
"rerollTooltip": "Reroll",
"wholePartySelected": "The whole party is selected"
},
"healingRoll": {
"title": "Heal - {damage}",
"heal": "Heal",
@ -2915,12 +3088,16 @@
"resourceRoll": {
"playerMessage": "{user} rerolled their {name}"
},
"saveRoll": {
"reactionRollAllTargets": "Reaction Roll All Targets"
},
"tagTeam": {
"title": "Tag Team",
"membersTitle": "Members"
}
},
"ChatLog": {
"rerollActionRoll": "Reroll Action",
"rerollDamage": "Reroll Damage",
"assignTagRoll": "Assign as Tag Roll"
},
@ -2937,13 +3114,15 @@
},
"EffectsDisplay": {
"removeThing": "[Right Click] Remove {thing}",
"increaseStacks": "[Left Click] Increment Stacks",
"decreaseStacks": "[Right Click] Decrement Stacks",
"appliedBy": "Applied By: {by}"
},
"ItemBrowser": {
"title": "Daggerheart Compendium Browser",
"windowTitle": "Compendium Browser",
"hint": "Select a Folder in sidebar to start browsing through the compendium",
"browserSettings": "Browser Settings",
"searchPlaceholder": "Search...",
"columnName": "Name",
"tooltipFilters": "Filters",
"tooltipErase": "Erase",
@ -2979,7 +3158,7 @@
"weapons": "Weapons",
"armors": "Armors",
"consumables": "Consumables",
"loots": "Loots"
"loots": "Loot"
}
},
"Notifications": {
@ -3052,7 +3231,6 @@
"subclassesAlreadyPresent": "You already have a class and multiclass subclass",
"noDiceSystem": "Your selected dice {system} does not have a {faces} dice",
"gmMenuRefresh": "You refreshed all actions and resources {types}",
"subclassAlreadyLinked": "{name} is already a subclass in the class {class}. Remove it from there if you want it to be a subclass to this class.",
"gmRequired": "This action requires an online GM",
"gmOnly": "This can only be accessed by the GM",
"noActorOwnership": "You do not have permissions for this character",
@ -3061,7 +3239,12 @@
"tokenActorsMissing": "[{names}] missing Actors",
"domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used",
"knowTheTide": "Know The Tide gained a token",
"lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}"
"lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}",
"noTokenTargeted": "No token is targeted",
"behaviorRegionRequiresGM": "Creating a Region with an attached Behavior requires an online GM"
},
"Progress": {
"migrationLabel": "Performing system migration. Please wait and do not close Foundry."
},
"Sidebar": {
"actorDirectory": {
@ -3070,6 +3253,9 @@
"companion": "Level {level} - {partner}",
"companionNoPartner": "No Partner",
"duplicateToNewTier": "Duplicate to New Tier",
"activateParty": "Make Active Party",
"partyIsActive": "Active",
"createAdversary": "Create Adversary",
"pickTierTitle": "Pick a new tier for this adversary"
},
"daggerheartMenu": {
@ -3081,6 +3267,7 @@
"Tooltip": {
"disableEffect": "Disable Effect",
"enableEffect": "Enable Effect",
"edit": "Edit",
"openItemWorld": "Open Item World",
"openActorWorld": "Open Actor World",
"sendToChat": "Send to Chat",

View file

@ -11,7 +11,10 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
this.character = character;
this.setup = {
traits: this.character.system.traits,
traits: Object.keys(this.character.system.traits).reduce((acc, key) => {
acc[key] = { value: null };
return acc;
}, {}),
ancestryName: {
primary: '',
secondary: ''
@ -151,8 +154,8 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
v.active = this.tabGroups[v.group]
? this.tabGroups[v.group] === v.id
: this.tabGroups.primary !== 'equipment'
? v.active
: false;
? v.active
: false;
v.cssClass = v.active ? 'active' : '';
switch (v.id) {
@ -208,9 +211,9 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
context.suggestedTraits = this.setup.class.system
? Object.keys(this.setup.class.system.characterGuide.suggestedTraits).map(traitKey => {
const trait = this.setup.class.system.characterGuide.suggestedTraits[traitKey];
return `${game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${traitKey}.short`)} ${trait > 0 ? `+${trait}` : trait}`;
})
const trait = this.setup.class.system.characterGuide.suggestedTraits[traitKey];
return `${game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${traitKey}.short`)} ${trait > 0 ? `+${trait}` : trait}`;
})
: [];
context.traits = {
values: Object.keys(this.setup.traits).map(traitKey => {
@ -377,8 +380,10 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
];
return Object.values(this.setup.traits).reduce((acc, x) => {
const index = traitCompareArray.indexOf(x.value);
if (index === -1) return acc;
traitCompareArray.splice(index, 1);
acc += index !== -1;
acc += 1;
return acc;
}, 0);
}
@ -434,15 +439,18 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
'system.domain': { key: 'system.domain', value: this.setup.class?.system.domains ?? null }
};
if (type === 'subclasses')
if (type === 'subclasses') {
const classItem = this.setup.class;
const uuid = classItem?._stats.compendiumSource ?? classItem?.uuid;
presets.filter = {
'system.linkedClass.uuid': { key: 'system.linkedClass.uuid', value: this.setup.class?.uuid }
'system.linkedClass': { key: 'system.linkedClass', value: uuid }
};
}
if (equipment.includes(type))
presets.filter = {
'system.tier': { key: 'system.tier', value: 1 },
'type': { key: 'type', value: type }
type: { key: 'type', value: type }
};
ui.compendiumBrowser.open(presets);
@ -554,7 +562,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
experiences: {
...this.setup.experiences,
...Object.keys(this.character.system.experiences).reduce((acc, key) => {
acc[`-=${key}`] = null;
acc[`${key}`] = _del;
return acc;
}, {})
}
@ -605,7 +613,8 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
[foundry.utils.randomID()]: {}
};
} else if (item.type === 'subclass' && event.target.closest('.subclass-card')) {
if (this.setup.class.system.subclasses.every(subclass => subclass.uuid !== item.uuid)) {
const classSubclasses = await this.setup.class.system.fetchSubclasses();
if (classSubclasses.every(subclass => subclass.uuid !== item.uuid)) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInClass'));
return;
}

View file

@ -50,7 +50,7 @@ export default class CompendiumBrowserSettings extends HandlebarsApplicationMixi
const excludedSourceData = this.browserSettings.excludedSources;
const excludedPackData = this.browserSettings.excludedPacks;
context.typePackCollections = game.packs.reduce((acc, pack) => {
const { type, label, packageType, packageName: basePackageName, id } = pack.metadata;
const { type, label, packageType, packageName: basePackageName, name, id } = pack.metadata;
if (!CompendiumBrowserSettings.#browserPackTypes.includes(type)) return acc;
const isWorldPack = packageType === 'world';
@ -68,13 +68,15 @@ export default class CompendiumBrowserSettings extends HandlebarsApplicationMixi
if (!acc[type].sources[packageName])
acc[type].sources[packageName] = { label: sourceLabel, checked: sourceChecked, packs: [] };
const checked = !excludedPackData[id] || !excludedPackData[id].excludedDocumentTypes.includes(type);
const included =
!excludedPackData[packageName] ||
!excludedPackData[packageName][name]?.excludedDocumentTypes.includes(type);
acc[type].sources[packageName].packs.push({
pack: id,
name,
type,
label: id === game.system.id ? game.system.title : game.i18n.localize(label),
checked: checked
checked: included
});
return acc;
@ -106,16 +108,16 @@ export default class CompendiumBrowserSettings extends HandlebarsApplicationMixi
toggleTypedPack(event) {
event.stopPropagation();
const { type, pack } = event.target.dataset;
const currentlyExcluded = this.browserSettings.excludedPacks[pack]
? this.browserSettings.excludedPacks[pack].excludedDocumentTypes.includes(type)
const { type, source, packName } = event.target.dataset;
const currentlyExcluded = this.browserSettings.excludedPacks[source]?.[packName]
? this.browserSettings.excludedPacks[source][packName].excludedDocumentTypes.includes(type)
: false;
if (!this.browserSettings.excludedPacks[pack])
this.browserSettings.excludedPacks[pack] = { excludedDocumentTypes: [] };
this.browserSettings.excludedPacks[pack].excludedDocumentTypes = currentlyExcluded
? this.browserSettings.excludedPacks[pack].excludedDocumentTypes.filter(x => x !== type)
: [...(this.browserSettings.excludedPacks[pack]?.excludedDocumentTypes ?? []), type];
this.browserSettings.excludedPacks[source] ??= {};
this.browserSettings.excludedPacks[source][packName] ??= { excludedDocumentTypes: [] };
this.browserSettings.excludedPacks[source][packName].excludedDocumentTypes = currentlyExcluded
? this.browserSettings.excludedPacks[source][packName].excludedDocumentTypes.filter(x => x !== type)
: [...(this.browserSettings.excludedPacks[source][packName]?.excludedDocumentTypes ?? []), type];
this.render();
}

View file

@ -10,10 +10,9 @@ export { default as ImageSelectDialog } from './imageSelectDialog.mjs';
export { default as ItemTransferDialog } from './itemTransfer.mjs';
export { default as MulticlassChoiceDialog } from './multiclassChoiceDialog.mjs';
export { default as OwnershipSelection } from './ownershipSelection.mjs';
export { default as RerollDamageDialog } from './rerollDamageDialog.mjs';
export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs';
export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs';
export { default as GroupRollDialog } from './group-roll-dialog.mjs';
export { default as TagTeamDialog } from './tagTeamDialog.mjs';
export { default as GroupRollDialog } from './groupRollDialog.mjs';
export { default as RiskItAllDialog } from './riskItAllDialog.mjs';
export { default as CompendiumBrowserSettingsDialog } from './CompendiumBrowserSettings.mjs';

View file

@ -72,7 +72,7 @@ export default class ActionSelectionDialog extends HandlebarsApplicationMixin(Ap
static async #onChooseAction(event, button) {
const { actionId } = button.dataset;
this.#action = this.#item.system.actionsList.find(a => a._id === actionId);
this.#action = this.item.system.actionsList.find(a => a._id === actionId);
Object.defineProperty(this.#event, 'shiftKey', {
get() {
return event.shiftKey;

View file

@ -10,6 +10,12 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
this.selected = null;
this.evolved = { form: null };
this.hybrid = { forms: {}, advantages: {}, features: {} };
this.modifications = {
traitBonuses: configData.modifications.traitBonuses.map(x => ({
trait: null,
bonus: x.bonus
}))
};
this._dragDrop = this._createDragDropHandlers();
}
@ -28,6 +34,7 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
selectBeastform: this.selectBeastform,
toggleHybridFeature: this.toggleHybridFeature,
toggleHybridAdvantage: this.toggleHybridAdvantage,
toggleTraitBonus: this.toggleTraitBonus,
submitBeastform: this.submitBeastform
},
form: {
@ -48,6 +55,7 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
tabs: { template: 'systems/daggerheart/templates/dialogs/beastform/tabs.hbs' },
beastformTier: { template: 'systems/daggerheart/templates/dialogs/beastform/beastformTier.hbs' },
advanced: { template: 'systems/daggerheart/templates/dialogs/beastform/advanced.hbs' },
modifications: { template: 'systems/daggerheart/templates/dialogs/beastform/modifications.hbs' },
footer: { template: 'systems/daggerheart/templates/dialogs/beastform/footer.hbs' }
};
@ -146,6 +154,9 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
{}
);
context.modifications = this.modifications;
context.traits = CONFIG.DH.ACTOR.abilities;
context.tier = beastformTiers[this.tabGroups.primary];
context.tierKey = this.tabGroups.primary;
@ -155,6 +166,9 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
}
canSubmit() {
const modificationsFinished = this.modifications.traitBonuses.every(x => x.trait);
if (!modificationsFinished) return false;
if (this.selected) {
switch (this.selected.system.beastformType) {
case 'normal':
@ -261,6 +275,13 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
this.render();
}
static toggleTraitBonus(_, button) {
const { index, trait } = button.dataset;
this.modifications.traitBonuses[index].trait =
this.modifications.traitBonuses[index].trait === trait ? null : trait;
this.render();
}
static async submitBeastform() {
await this.close({ submitted: true });
}
@ -292,6 +313,23 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
}
}
const beastformEffect = selected.effects.find(x => x.type === 'beastform');
for (const traitBonus of app.modifications.traitBonuses) {
const existingChange = beastformEffect.changes.find(
x => x.key === `system.traits.${traitBonus.trait}.value`
);
if (existingChange) {
existingChange.value = Number.parseInt(existingChange.value) + traitBonus.bonus;
} else {
beastformEffect.changes.push({
key: `system.traits.${traitBonus.trait}.value`,
mode: 2,
priority: null,
value: traitBonus.bonus
});
}
}
resolve({
selected: selected,
evolved: { ...app.evolved, form: evolved },

View file

@ -77,8 +77,8 @@ export default class CharacterResetDialog extends HandlebarsApplicationMixin(App
if (!this.data.optional.portrait.keep) {
foundry.utils.setProperty(update, 'img', this.actor.schema.fields.img.initial(this.actor));
foundry.utils.setProperty(update, 'prototypeToken.==texture', {});
foundry.utils.setProperty(update, 'prototypeToken.==ring', {});
foundry.utils.setProperty(update, 'prototypeToken.texture', _replace({}));
foundry.utils.setProperty(update, 'prototypeToken.ring', _replace({}));
}
if (this.data.optional.biography.keep)
@ -89,7 +89,7 @@ export default class CharacterResetDialog extends HandlebarsApplicationMixin(App
const { system, ...rest } = update;
await this.actor.update({
...rest,
'==system': system ?? {}
system: _replace(system ?? {})
});
const inventoryItemTypes = ['weapon', 'armor', 'consumable', 'loot'];

View file

@ -35,7 +35,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
updateIsAdvantage: this.updateIsAdvantage,
selectExperience: this.selectExperience,
toggleReaction: this.toggleReaction,
toggleTagTeamRoll: this.toggleTagTeamRoll,
toggleSelectedEffect: this.toggleSelectedEffect,
submitRoll: this.submitRoll
},
@ -71,8 +70,8 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.rollConfig = this.config;
context.hasRoll = !!this.config.roll;
context.canRoll = true;
context.selectedRollMode = this.config.selectedRollMode ?? game.settings.get('core', 'rollMode');
context.rollModes = Object.entries(CONFIG.Dice.rollModes).map(([action, { label, icon }]) => ({
context.selectedMessageMode = this.config.selectedMessageMode ?? game.settings.get('core', 'messageMode');
context.rollModes = Object.entries(CONFIG.ChatMessage.modes).map(([action, { label, icon }]) => ({
action,
label,
icon
@ -124,6 +123,10 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.advantage = this.config.roll?.advantage;
context.disadvantage = this.config.roll?.disadvantage;
context.diceOptions = CONFIG.DH.GENERAL.diceTypes;
context.dieFaces = CONFIG.DH.GENERAL.dieFaces.reduce((acc, face) => {
acc[face] = `d${face}`;
return acc;
}, {});
context.isLite = this.config.roll?.lite;
context.extraFormula = this.config.extraFormula;
context.formula = this.roll.constructFormula(this.config);
@ -133,12 +136,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
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;
}
@ -149,19 +146,17 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
}));
}
static updateRollConfiguration(event, _, formData) {
static updateRollConfiguration(_event, _, formData) {
const { ...rest } = foundry.utils.expandObject(formData.object);
this.config.selectedRollMode = rest.selectedRollMode;
this.config.selectedMessageMode = rest.selectedMessageMode;
if (this.config.costs) {
this.config.costs = foundry.utils.mergeObject(this.config.costs, rest.costs);
}
if (this.config.uses) this.config.uses = foundry.utils.mergeObject(this.config.uses, rest.uses);
if (rest.roll?.dice) {
Object.entries(rest.roll.dice).forEach(([key, value]) => {
this.roll[key] = value;
});
this.roll = foundry.utils.mergeObject(this.roll, rest.roll.dice);
}
if (rest.hasOwnProperty('trait')) {
this.config.roll.trait = rest.trait;
@ -180,6 +175,15 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.disadvantage = advantage === -1;
this.config.roll.advantage = this.config.roll.advantage === advantage ? 0 : advantage;
if (this.config.roll.advantage === 0) return this.render();
const defaultFaces =
this.config.roll.advantage === 1
? this.config.data.rules.roll.defaultAdvantageDice
: this.config.data.rules.roll.defaultDisadvantageDice;
const faces = Number.parseInt(defaultFaces);
this.roll.advantageFaces = Number.isNaN(faces) ? this.roll.advantageFaces : faces;
this.render();
}
@ -192,14 +196,14 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
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,
{
extKey: button.dataset.key,
key: this.config?.data?.parent?.isNPC ? 'fear' : 'hope',
value: 1,
name: this.config.data?.system.experiences?.[button.dataset.key]?.name
}
];
...this.config.costs,
{
extKey: button.dataset.key,
key: this.config?.data?.parent?.isNPC ? 'fear' : 'hope',
value: 1,
name: this.config.data?.system.experiences?.[button.dataset.key]?.name
}
];
this.render();
}
@ -209,17 +213,12 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.config.actionType = this.reactionOverride
? 'reaction'
: this.config.actionType === 'reaction'
? 'action'
: this.config.actionType;
? 'action'
: this.config.actionType;
this.render();
}
}
static toggleTagTeamRoll() {
this.config.tagTeamSelected = !this.config.tagTeamSelected;
this.render();
}
static toggleSelectedEffect(_event, button) {
this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected;
this.render();

View file

@ -22,6 +22,8 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
},
actions: {
toggleSelectedEffect: this.toggleSelectedEffect,
updateGroupAttack: this.updateGroupAttack,
toggleCritical: this.toggleCritical,
submitRoll: this.submitRoll
},
form: {
@ -52,8 +54,9 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
context.formula = this.roll.constructFormula(this.config);
context.hasHealing = this.config.hasHealing;
context.directDamage = this.config.directDamage;
context.selectedRollMode = this.config.selectedRollMode;
context.rollModes = Object.entries(CONFIG.Dice.rollModes).map(([action, { label, icon }]) => ({
context.selectedMessageMode = this.config.selectedMessageMode;
context.isCritical = this.config.isCritical;
context.rollModes = Object.entries(CONFIG.ChatMessage.modes).map(([action, { label, icon }]) => ({
action,
label,
icon
@ -62,15 +65,45 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
context.hasSelectedEffects = Boolean(Object.keys(this.selectedEffects).length);
context.selectedEffects = this.selectedEffects;
context.damageOptions = this.config.damageOptions;
context.rangeOptions = CONFIG.DH.GENERAL.groupAttackRange;
return context;
}
static updateRollConfiguration(_event, _, formData) {
const { ...rest } = foundry.utils.expandObject(formData.object);
foundry.utils.mergeObject(this.config.roll, rest.roll);
foundry.utils.mergeObject(this.config.modifiers, rest.modifiers);
this.config.selectedRollMode = rest.selectedRollMode;
const data = foundry.utils.expandObject(formData.object);
foundry.utils.mergeObject(this.config.roll, data.roll);
foundry.utils.mergeObject(this.config.modifiers, data.modifiers);
this.config.selectedMessageMode = data.selectedMessageMode;
if (data.damageOptions) {
const numAttackers = data.damageOptions.groupAttack?.numAttackers;
if (typeof numAttackers !== 'number' || numAttackers % 1 !== 0) {
data.damageOptions.groupAttack.numAttackers = null;
}
foundry.utils.mergeObject(this.config.damageOptions, data.damageOptions);
}
this.render();
}
static updateGroupAttack() {
const targets = Array.from(game.user.targets);
if (targets.length === 0)
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.noTokenTargeted'));
const actorId = this.roll.data.parent.id;
const range = this.config.damageOptions.groupAttack.range;
const groupAttackTokens = game.system.api.fields.ActionFields.DamageField.getGroupAttackTokens(actorId, range);
this.config.damageOptions.groupAttack.numAttackers = groupAttackTokens.length;
this.render();
}
static toggleCritical() {
this.config.isCritical = !this.config.isCritical;
this.render();
}

View file

@ -1,4 +1,4 @@
import { damageKeyToNumber, getDamageLabel } from '../../helpers/utils.mjs';
import { damageKeyToNumber, getArmorSources, getDamageLabel } from '../../helpers/utils.mjs';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
@ -10,6 +10,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.reject = reject;
this.actor = actor;
this.damage = damage;
this.damageType = damageType;
this.rulesDefault = game.settings.get(
CONFIG.DH.id,
@ -20,14 +21,21 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.rulesDefault
);
const canApplyArmor = damageType.every(t => actor.system.armorApplicableDamageTypes[t] === true);
const availableArmor = actor.system.armorScore - actor.system.armor.system.marks.value;
const maxArmorMarks = canApplyArmor ? availableArmor : 0;
const orderedArmorSources = getArmorSources(actor).filter(s => !s.disabled);
const armor = orderedArmorSources.reduce((acc, { name, document }) => {
const { current, max } = document.type === 'armor' ? document.system.armor : document.system.armorData;
acc.push({
name,
effect: document,
marks: [...Array(max).keys()].reduce((acc, _, index) => {
const spent = index < current;
acc[foundry.utils.randomID()] = { selected: false, disabled: spent, spent };
return acc;
}, {})
});
const armor = [...Array(maxArmorMarks).keys()].reduce((acc, _) => {
acc[foundry.utils.randomID()] = { selected: false };
return acc;
}, {});
}, []);
const stress = [...Array(actor.system.rules.damageReduction.maxArmorMarked.stressExtra ?? 0).keys()].reduce(
(acc, _) => {
acc[foundry.utils.randomID()] = { selected: false };
@ -121,36 +129,42 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
context.thresholdImmunities =
Object.keys(this.thresholdImmunities).length > 0 ? this.thresholdImmunities : null;
const { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage } =
const { selectedStressMarks, stressReductions, currentMarks, currentDamage, maxArmorUsed, availableArmor } =
this.getDamageInfo();
context.armorScore = this.actor.system.armorScore;
context.armorScore = this.actor.system.armorScore.max;
context.armorMarks = currentMarks;
context.basicMarksUsed =
selectedArmorMarks.length === this.actor.system.rules.damageReduction.maxArmorMarked.value;
const stressReductionStress = this.availableStressReductions
? stressReductions.reduce((acc, red) => acc + red.cost, 0)
: 0;
const stress = this.actor.system.resources.stress;
context.stress =
selectedStressMarks.length > 0 || this.availableStressReductions
? {
value:
this.actor.system.resources.stress.value + selectedStressMarks.length + stressReductionStress,
max: this.actor.system.resources.stress.max
}
value: stress.value + selectedStressMarks.length + stressReductionStress,
max: stress.max
}
: null;
const maxArmor = this.actor.system.rules.damageReduction.maxArmorMarked.value;
context.marks = {
armor: Object.keys(this.marks.armor).reduce((acc, key, index) => {
const mark = this.marks.armor[key];
if (!this.rulesOn || index + 1 <= maxArmor) acc[key] = mark;
context.maxArmorUsed = maxArmorUsed;
context.availableArmor = availableArmor;
context.basicMarksUsed = availableArmor === 0 || selectedStressMarks.length;
return acc;
}, {}),
const armorSources = [];
for (const source of this.marks.armor) {
armorSources.push({
label: source.name,
uuid: source.effect.uuid,
marks: source.marks
});
}
context.marks = {
armor: armorSources,
stress: this.marks.stress
};
context.usesStressArmor = Object.keys(context.marks.stress).length;
context.availableStressReductions = this.availableStressReductions;
context.damage = getDamageLabel(this.damage);
@ -167,27 +181,31 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
}
getDamageInfo = () => {
const selectedArmorMarks = Object.values(this.marks.armor).filter(x => x.selected);
const selectedArmorMarks = this.marks.armor.flatMap(x => Object.values(x.marks).filter(x => x.selected));
const selectedStressMarks = Object.values(this.marks.stress).filter(x => x.selected);
const stressReductions = this.availableStressReductions
? Object.values(this.availableStressReductions).filter(red => red.selected)
: [];
const currentMarks =
this.actor.system.armor.system.marks.value + selectedArmorMarks.length + selectedStressMarks.length;
const currentMarks = this.actor.system.armorScore.value + selectedArmorMarks.length;
const maxArmorUsed = this.actor.system.rules.damageReduction.maxArmorMarked.value + selectedStressMarks.length;
const availableArmor =
maxArmorUsed -
this.marks.armor.reduce((acc, source) => {
acc += Object.values(source.marks).filter(x => x.selected).length;
return acc;
}, 0);
const armorMarkReduction =
selectedArmorMarks.length * this.actor.system.rules.damageReduction.increasePerArmorMark;
let currentDamage = Math.max(
this.damage - armorMarkReduction - selectedStressMarks.length - stressReductions.length,
0
);
let currentDamage = Math.max(this.damage - armorMarkReduction - stressReductions.length, 0);
if (this.reduceSeverity) {
currentDamage = Math.max(currentDamage - this.reduceSeverity, 0);
}
if (this.thresholdImmunities[currentDamage]) currentDamage = 0;
return { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage };
return { selectedStressMarks, stressReductions, currentMarks, currentDamage, maxArmorUsed, availableArmor };
};
static toggleRules() {
@ -195,13 +213,10 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
const maxArmor = this.actor.system.rules.damageReduction.maxArmorMarked.value;
this.marks = {
armor: Object.keys(this.marks.armor).reduce((acc, key, index) => {
const mark = this.marks.armor[key];
armor: this.marks.armor.map((mark, index) => {
const keepSelectValue = !this.rulesOn || index + 1 <= maxArmor;
acc[key] = { ...mark, selected: keepSelectValue ? mark.selected : false };
return acc;
}, {}),
return { ...mark, selected: keepSelectValue ? mark.selected : false };
}),
stress: this.marks.stress
};
@ -209,8 +224,8 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
}
static setMarks(_, target) {
const currentMark = this.marks[target.dataset.type][target.dataset.key];
const { selectedStressMarks, stressReductions, currentMarks, currentDamage } = this.getDamageInfo();
const currentMark = foundry.utils.getProperty(this.marks, target.dataset.path);
const { selectedStressMarks, stressReductions, currentDamage, availableArmor } = this.getDamageInfo();
if (!currentMark.selected && currentDamage === 0) {
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.damageAlreadyNone'));
@ -218,12 +233,18 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
}
if (this.rulesOn) {
if (!currentMark.selected && currentMarks === this.actor.system.armorScore) {
if (target.dataset.type === 'armor' && !currentMark.selected && !availableArmor) {
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noAvailableArmorMarks'));
return;
}
}
const stressUsed = selectedStressMarks.length;
if (target.dataset.type === 'armor' && stressUsed) {
const updateResult = this.updateStressArmor(target.dataset.id, !currentMark.selected);
if (updateResult === false) return;
}
if (currentMark.selected) {
const currentDamageLabel = getDamageLabel(currentDamage);
for (let reduction of stressReductions) {
@ -232,8 +253,16 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
}
}
if (target.dataset.type === 'armor' && selectedStressMarks.length > 0) {
selectedStressMarks.forEach(mark => (mark.selected = false));
if (target.dataset.type === 'stress' && currentMark.armorMarkId) {
for (const source of this.marks.armor) {
const match = Object.keys(source.marks).find(key => key === currentMark.armorMarkId);
if (match) {
source.marks[match].selected = false;
break;
}
}
currentMark.armorMarkId = null;
}
}
@ -241,6 +270,25 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.render();
}
updateStressArmor(armorMarkId, select) {
let stressMarkKey = null;
if (select) {
stressMarkKey = Object.keys(this.marks.stress).find(
key => this.marks.stress[key].selected && !this.marks.stress[key].armorMarkId
);
} else {
stressMarkKey = Object.keys(this.marks.stress).find(
key => this.marks.stress[key].armorMarkId === armorMarkId
);
if (!stressMarkKey)
stressMarkKey = Object.keys(this.marks.stress).find(key => this.marks.stress[key].selected);
}
if (!stressMarkKey) return false;
this.marks.stress[stressMarkKey].armorMarkId = select ? armorMarkId : null;
}
static useStressReduction(_, target) {
const damageValue = Number(target.dataset.reduction);
const stressReduction = this.availableStressReductions[damageValue];
@ -279,11 +327,18 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
}
static async takeDamage() {
const { selectedArmorMarks, selectedStressMarks, stressReductions, currentDamage } = this.getDamageInfo();
const armorSpent = selectedArmorMarks.length + selectedStressMarks.length;
const stressSpent = selectedStressMarks.length + stressReductions.reduce((acc, red) => acc + red.cost, 0);
const { selectedStressMarks, stressReductions, currentDamage } = this.getDamageInfo();
const armorChanges = this.marks.armor.reduce((acc, source) => {
const amount = Object.values(source.marks).filter(x => x.selected).length;
if (amount) acc.push({ uuid: source.effect.uuid, amount });
this.resolve({ modifiedDamage: currentDamage, armorSpent, stressSpent });
return acc;
}, []);
const stressSpent =
selectedStressMarks.filter(x => x.armorMarkId).length +
stressReductions.reduce((acc, red) => acc + red.cost, 0);
this.resolve({ modifiedDamage: currentDamage, armorChanges, stressSpent });
await this.close(true);
}

View file

@ -57,6 +57,7 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
let returnMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.avoidScar');
if (config.roll.fate.value <= this.actor.system.levelData.level.current) {
const maxHope = this.actor.system.resources.hope.max + this.actor.system.scars;
const newScarAmount = this.actor.system.scars + 1;
await this.actor.update({
system: {
@ -64,7 +65,7 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
}
});
if (newScarAmount >= this.actor.system.resources.hope.max) {
if (newScarAmount >= maxHope) {
await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id);
return game.i18n.format('DAGGERHEART.UI.Chat.deathMove.journeysEnd', { scars: newScarAmount });
}

View file

@ -1,4 +1,4 @@
import { refreshIsAllowed } from '../../helpers/utils.mjs';
import { expireActiveEffects, refreshIsAllowed } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -203,7 +203,7 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
const msg = {
user: game.user.id,
system: {
moves: moves,
moves: moves.map(move => ({ ...move, actions: Array.from(move.actions) })),
actor: this.actor.uuid
},
speaker: cls.getSpeaker(),
@ -259,11 +259,16 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
const resetValue = increasing
? 0
: feature.system.resource.max
? Roll.replaceFormulaData(feature.system.resource.max, this.actor)
: 0;
? new Roll(
Roll.replaceFormulaData(feature.system.resource.max, this.actor.getRollData())
).evaluateSync().total
: 0;
await feature.update({ 'system.resource.value': resetValue });
}
expireActiveEffects(this.actor, [this.shortRest ? 'shortRest' : 'longRest']);
this.close();
} else {
this.render();

View file

@ -1,204 +0,0 @@
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}`.replaceAll(
' ',
'&nbsp;'
);
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}`.replaceAll(
' ',
'&nbsp;'
);
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,500 @@
import { ResourceUpdateMap } from '../../data/action/baseAction.mjs';
import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import Party from '../sheets/actors/party.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class GroupRollDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(party) {
super({ id: `GroupRollDialog-${party.id}` });
this.party = party;
this.partyMembers = party.system.partyMembers
.filter(x => Party.DICE_ROLL_ACTOR_TYPES.includes(x.type))
.map(member => ({
...member.toObject(),
uuid: member.uuid,
id: member.id,
selected: true,
owned: member.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)
}));
this.leader = null;
this.openForAllPlayers = true;
this.tabGroups.application = Object.keys(party.system.groupRoll.participants).length
? 'groupRoll'
: 'initialization';
Hooks.on(socketEvent.Refresh, this.groupRollRefresh.bind());
}
get title() {
return game.i18n.localize('DAGGERHEART.APPLICATIONS.GroupRollSelect.title');
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'group-roll-dialog'],
position: { width: 390, height: 'auto' },
window: {
icon: 'fa-solid fa-users'
},
actions: {
toggleSelectMember: this.#toggleSelectMember,
startGroupRoll: this.#startGroupRoll,
makeRoll: this.#makeRoll,
removeRoll: this.#removeRoll,
rerollDice: this.#rerollDice,
markSuccessful: this.#markSuccessful,
cancelRoll: this.#onCancelRoll,
finishRoll: this.#finishRoll
},
form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false }
};
static PARTS = {
initialization: {
id: 'initialization',
template: 'systems/daggerheart/templates/dialogs/groupRollDialog/initialization.hbs'
},
main: {
id: 'main',
template: 'systems/daggerheart/templates/dialogs/groupRollDialog/main.hbs'
},
leader: {
id: 'leader',
template: 'systems/daggerheart/templates/dialogs/groupRollDialog/parts/member.hbs'
},
result: {
id: 'result',
template: 'systems/daggerheart/templates/dialogs/groupRollDialog/parts/result.hbs'
},
footer: {
id: 'footer',
template: 'systems/daggerheart/templates/dialogs/groupRollDialog/parts/footer.hbs'
}
};
/** @inheritdoc */
static TABS = {
application: {
tabs: [{ id: 'initialization' }, { id: 'groupRoll' }]
}
};
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement
.querySelector('.main-character-field')
?.addEventListener('input', this.updateLeaderField.bind(this));
}
_configureRenderParts(options) {
const parts = super._configureRenderParts(options);
for (const memberKey of Object.keys(this.party.system.groupRoll.aidingCharacters)) {
parts[memberKey] = {
id: memberKey,
template: 'systems/daggerheart/templates/dialogs/groupRollDialog/parts/member.hbs'
};
}
return parts;
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.isGM = game.user.isGM;
context.isEditable =
game.user.isGM ||
this.party.system.partyMembers.some(actor => {
const selected = Boolean(this.party.system.groupRoll.participants[actor.id]);
return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER);
});
context.fields = this.party.system.schema.fields.groupRoll.fields;
context.data = this.party.system.groupRoll;
context.traitOptions = CONFIG.DH.ACTOR.abilities;
context.members = {};
context.aidKeys = Object.keys(this.party.system.groupRoll.aidingCharacters);
context.allHaveRolled = Object.keys(context.data.participants).every(key => {
const data = context.data.participants[key];
return Boolean(data.rollData);
});
return context;
}
async _preparePartContext(partId, context, options) {
const partContext = await super._preparePartContext(partId, context, options);
partContext.partId = partId;
partContext.leader = this.getRollCharacterData(this.party.system.groupRoll.leader);
switch (partId) {
case 'initialization':
partContext.groupRollFields = this.party.system.schema.fields.groupRoll.fields;
partContext.memberSelection = this.partyMembers;
const selectedMembers = partContext.memberSelection.filter(x => x.selected);
partContext.selectedLeader = this.leader;
partContext.selectedLeaderOptions = selectedMembers
.filter(actor => actor.owned)
.map(x => ({ value: x.id, label: x.name }));
partContext.selectedLeaderDisabled = !selectedMembers.length;
partContext.canStartGroupRoll = selectedMembers.length > 1 && this.leader?.memberId;
partContext.openForAllPlayers = this.openForAllPlayers;
break;
case 'result':
const leader = this.party.system.groupRoll.leader;
partContext.hasRolled =
leader?.rollData ||
Object.values(this.party.system.groupRoll?.aidingCharacters ?? {}).some(x => x.successful !== null);
const { modifierTotal, modifiers } = Object.values(this.party.system.groupRoll.aidingCharacters).reduce(
(acc, curr) => {
const modifier = curr.successful === true ? 1 : curr.successful === false ? -1 : null;
if (modifier) {
acc.modifierTotal += modifier;
acc.modifiers.push(modifier);
}
return acc;
},
{ modifierTotal: 0, modifiers: [] }
);
const leaderTotal = leader?.rollData ? leader.roll.total : null;
partContext.groupRoll = {
totalLabel: leader?.rollData
? game.i18n.format('DAGGERHEART.GENERAL.withThing', {
thing: leader.roll.totalLabel
})
: null,
totalDualityClass: leader?.roll?.isCritical ? 'critical' : leader?.roll?.withHope ? 'hope' : 'fear',
total: leaderTotal + modifierTotal,
leaderTotal: leaderTotal,
modifiers
};
break;
case 'footer':
partContext.canFinishRoll =
Boolean(this.party.system.groupRoll.leader?.rollData) &&
Object.values(this.party.system.groupRoll.aidingCharacters).every(x => x.successful !== null);
break;
}
if (Object.keys(this.party.system.groupRoll.aidingCharacters).includes(partId)) {
const characterData = this.party.system.groupRoll.aidingCharacters[partId];
partContext.members[partId] = this.getRollCharacterData(characterData, partId);
}
return partContext;
}
getRollCharacterData(data, partId) {
if (!data) return {};
const actor = game.actors.get(data.id);
const isLeader = data === this.party.system.groupRoll.leader;
const roll = data.roll;
const withTypeSuffix = !roll ? null : roll.isCritical ? 'criticalShort' : roll.withHope ? 'hope' : 'fear';
const thing = withTypeSuffix ? _loc(`DAGGERHEART.GENERAL.${withTypeSuffix}`) : null;
return {
...data,
type: isLeader ? 'leader' : 'aid',
basePath: isLeader ? 'system.groupRoll.leader' : `system.groupRoll.aidingCharacters.${data.id}`,
rollChoiceLabel: _loc(CONFIG.DH.ACTOR.abilities[data.rollChoice]?.label),
roll: data.roll,
isEditable: actor?.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER),
key: partId,
readyToRoll: Boolean(data.rollChoice),
hasRolled: Boolean(data.rollData),
modifier: data.successful ? 1 : data.successful === false ? -1 : 0,
withLabelShort: thing ? _loc('DAGGERHEART.GENERAL.withThing', { thing }) : null
};
}
#getCharacterDataById(id) {
if (!id) return null;
const groupRoll = this.party.system.groupRoll;
if (id === 'leader' || id === groupRoll.leader?.id) {
return { data: groupRoll.leader, basePath: 'system.groupRoll.leader' };
} else if (id in groupRoll.aidingCharacters) {
return { data: groupRoll.aidingCharacters[id], basePath: `system.groupRoll.aidingCharacters.${id}` };
}
return null;
}
static async updateData(event, _, formData) {
const partyData = foundry.utils.expandObject(formData.object);
this.updatePartyData(partyData, this.getUpdatingParts(event.target));
}
async updatePartyData(update, updatingParts, options = { render: true }) {
if (!game.users.activeGM)
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.gmRequired'));
const gmUpdate = async update => {
await this.party.update(update);
this.render({ parts: updatingParts });
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: { refreshType: RefreshType.GroupRoll, action: 'refresh', parts: updatingParts }
});
};
await emitGMUpdate(
GMUpdateEvent.UpdateDocument,
gmUpdate,
update,
this.party.uuid,
options.render ? { refreshType: RefreshType.GroupRoll, action: 'refresh', parts: updatingParts } : undefined
);
}
getUpdatingParts(target) {
const { initialization, leader, result, footer } = this.constructor.PARTS;
const isInitialization = this.tabGroups.application === initialization.id;
const updatingMember = target.closest('.member-roll-container.aid')?.dataset?.memberKey;
const updatingLeader = target.closest('.member-roll-container.leader');
return [
...(isInitialization ? [initialization.id] : []),
...(updatingMember ? [updatingMember] : []),
...(updatingLeader ? [leader.id] : []),
...(!isInitialization ? [result.id, footer.id] : [])
];
}
groupRollRefresh = ({ refreshType, action, parts }) => {
if (refreshType !== RefreshType.GroupRoll) return;
switch (action) {
case 'startGroupRoll':
this.tabGroups.application = 'groupRoll';
break;
case 'refresh':
this.render({ parts });
break;
case 'close':
this.close();
break;
}
};
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(socketEvent.Refresh, this.groupRollRefresh);
return super.close(options);
}
//#region Initialization
static #toggleSelectMember(_, button) {
const member = this.partyMembers.find(x => x.id === button.dataset.id);
member.selected = !member.selected;
if (this.leader?.memberId === member.id) {
this.leader = null;
}
this.render();
}
updateLeaderField(event) {
if (!this.leader) this.leader = {};
this.leader.memberId = event.target.value;
this.render();
}
static async #startGroupRoll() {
const leader = this.partyMembers.find(x => x.id === this.leader.memberId);
const aidingCharacters = this.partyMembers.reduce((acc, curr) => {
if (curr.selected && curr.id !== this.leader.memberId)
acc[curr.id] = { id: curr.id, name: curr.name, img: curr.img };
return acc;
}, {});
await this.party.update({
'system.groupRoll': _replace(
new game.system.api.data.GroupRollData({
...this.party.system.groupRoll.toObject(),
leader: { id: leader.id, name: leader.name, img: leader.img },
aidingCharacters
})
)
});
const hookData = { openForAllPlayers: this.openForAllPlayers, partyId: this.party.id };
Hooks.callAll(CONFIG.DH.HOOKS.hooksConfig.groupRollStart, hookData);
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GroupRollStart,
data: hookData
});
this.render();
}
//#endregion
/** @this GroupRollDialog */
static async #makeRoll(_event, button) {
const member = button.closest('[data-member-key]').dataset.memberKey;
const { data, basePath } = this.#getCharacterDataById(member);
const actor = game.actors.find(x => x.id === data.id);
if (!actor) return;
const result = await actor.rollTrait(data.rollChoice, {
skips: {
createMessage: true,
resources: true,
triggers: true
}
});
if (!result) return;
const rollData = result.messageRoll.toJSON();
delete rollData.options.messageRoll;
this.updatePartyData(
{
[basePath]: { rollData, successful: null }
},
this.getUpdatingParts(button)
);
}
/** @this GroupRollDialog */
static async #removeRoll(_event, button) {
const member = button.closest('[data-member-key]').dataset.memberKey;
const { basePath } = this.#getCharacterDataById(member);
this.updatePartyData(
{
[basePath]: {
rollData: null,
rollChoice: null,
selected: false,
successful: null
}
},
this.getUpdatingParts(button)
);
}
/** @this GroupRollDialog */
static async #rerollDice(_, button) {
const { diceType } = button.dataset;
const { data, basePath } = this.#getCharacterDataById(button.dataset.member);
const dieIndex = diceType === 'hope' ? 0 : diceType === 'fear' ? 1 : 2;
const newRoll = game.system.api.dice.DualityRoll.fromData(data.rollData);
const dice = newRoll.dice[dieIndex];
await dice.reroll(`/r1=${dice.total}`, {
liveRoll: {
roll: newRoll,
isReaction: true
}
});
const rollData = newRoll.toJSON();
this.updatePartyData(
{
[`${basePath}.rollData`]: rollData
},
this.getUpdatingParts(button)
);
}
static #markSuccessful(_event, button) {
const memberKey = button.closest('[data-member-key]').dataset.memberKey;
const previousValue = this.party.system.groupRoll.aidingCharacters[memberKey].successful;
const newValue = Boolean(button.dataset.success === 'true');
this.updatePartyData(
{
[`system.groupRoll.aidingCharacters.${memberKey}.successful`]:
previousValue === newValue ? null : newValue
},
this.getUpdatingParts(button)
);
}
static async #onCancelRoll(_event, _button, options = { confirm: true }) {
this.cancelRoll(options);
}
async cancelRoll(options = { confirm: true }) {
if (options.confirm) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.GroupRollSelect.cancelConfirmTitle')
},
content: game.i18n.localize('DAGGERHEART.APPLICATIONS.GroupRollSelect.cancelConfirmText')
});
if (!confirmed) return;
}
await this.updatePartyData(
{
'system.groupRoll': {
leader: null,
aidingCharacters: _replace({})
}
},
[],
{ render: false }
);
this.close();
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: { refreshType: RefreshType.GroupRoll, action: 'close' }
});
}
static async #finishRoll() {
const totalRoll = this.party.system.groupRoll.leader.roll;
for (const character of Object.values(this.party.system.groupRoll.aidingCharacters)) {
totalRoll.terms.push(new foundry.dice.terms.OperatorTerm({ operator: character.successful ? '+' : '-' }));
totalRoll.terms.push(new foundry.dice.terms.NumericTerm({ number: 1 }));
}
await totalRoll._evaluate();
const systemData = totalRoll.options;
const actor = game.actors.get(this.party.system.groupRoll.leader.id);
const cls = getDocumentClass('ChatMessage'),
msgData = {
type: 'dualityRoll',
user: game.user.id,
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.GroupRollSelect.title'),
speaker: cls.getSpeaker({ actor }),
system: systemData,
rolls: [JSON.stringify(totalRoll)],
sound: null,
flags: { core: { RollTable: true } }
};
await cls.create(msgData);
const resourceMap = new ResourceUpdateMap(actor);
if (totalRoll.isCritical) {
resourceMap.addResources([
{ key: 'stress', value: -1, total: 1 },
{ key: 'hope', value: 1, total: 1 }
]);
} else if (totalRoll.withHope) {
resourceMap.addResources([{ key: 'hope', value: 1, total: 1 }]);
} else {
resourceMap.addResources([{ key: 'fear', value: 1, total: 1 }]);
}
resourceMap.updateResources();
/* Fin */
this.cancelRoll({ confirm: false });
}
}

View file

@ -38,13 +38,15 @@ export default class ItemTransferDialog extends HandlebarsApplicationMixin(Appli
originActor ??= item?.actor;
const homebrewKey = CONFIG.DH.SETTINGS.gameSettings.Homebrew;
const currencySetting = game.settings.get(CONFIG.DH.id, homebrewKey).currency?.[currency] ?? null;
const max = item?.system.quantity ?? originActor.system.gold[currency] ?? 0;
return {
originActor,
targetActor,
itemImage: item?.img,
currencyIcon: currencySetting?.icon,
max: item?.system.quantity ?? originActor.system.gold[currency] ?? 0,
max,
initial: targetActor.system.metadata.quantifiable?.includes(item.type) ? max : 1,
title: item?.name ?? currencySetting?.label
};
}

View file

@ -1,290 +0,0 @@
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(message, options = {}) {
super(options);
this.message = message;
this.damage = Object.keys(message.system.damage).reduce((acc, typeKey) => {
const type = message.system.damage[typeKey];
acc[typeKey] = Object.keys(type.parts).reduce((acc, partKey) => {
const part = type.parts[partKey];
acc[partKey] = Object.keys(part.dice).reduce((acc, diceKey) => {
const dice = part.dice[diceKey];
const activeResults = dice.results.filter(x => x.active);
acc[diceKey] = {
dice: dice.dice,
selectedResults: activeResults.length,
maxSelected: activeResults.length,
results: activeResults.map(x => ({ ...x, selected: true }))
};
return acc;
}, {});
return acc;
}, {});
return acc;
}, {});
}
static DEFAULT_OPTIONS = {
id: 'reroll-dialog',
classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'reroll-dialog'],
window: {
icon: 'fa-solid fa-dice'
},
actions: {
toggleResult: RerollDamageDialog.#toggleResult,
selectRoll: RerollDamageDialog.#selectRoll,
doReroll: RerollDamageDialog.#doReroll,
save: RerollDamageDialog.#save
}
};
/** @override */
static PARTS = {
main: {
id: 'main',
template: 'systems/daggerheart/templates/dialogs/rerollDialog/damage/main.hbs'
},
footer: {
id: 'footer',
template: 'systems/daggerheart/templates/dialogs/rerollDialog/footer.hbs'
}
};
get title() {
return game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.damageTitle');
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement.querySelectorAll('.to-reroll-input').forEach(element => {
element.addEventListener('change', this.toggleDice.bind(this));
});
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.damage = this.damage;
context.disabledReroll = !this.getRerollDice().length;
context.saveDisabled = !this.isSelectionDone();
return context;
}
static async #save() {
const update = {
'system.damage': Object.keys(this.damage).reduce((acc, typeKey) => {
const type = this.damage[typeKey];
let typeTotal = 0;
const messageType = this.message.system.damage[typeKey];
const parts = Object.keys(type).map(partKey => {
const part = type[partKey];
const messagePart = messageType.parts[partKey];
let partTotal = messagePart.modifierTotal;
const dice = Object.keys(part).map(diceKey => {
const dice = part[diceKey];
const total = dice.results.reduce((acc, result) => {
if (result.active) acc += result.result;
return acc;
}, 0);
partTotal += total;
const messageDice = messagePart.dice[diceKey];
return {
...messageDice,
total: total,
results: dice.results.map(x => ({
...x,
hasRerolls: dice.results.length > 1
}))
};
});
typeTotal += partTotal;
return {
...messagePart,
total: partTotal,
dice: dice
};
});
acc[typeKey] = {
...messageType,
total: typeTotal,
parts: parts
};
return acc;
}, {})
};
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();
}
getRerollDice() {
const rerollDice = [];
Object.keys(this.damage).forEach(typeKey => {
const type = this.damage[typeKey];
Object.keys(type).forEach(partKey => {
const part = type[partKey];
Object.keys(part).forEach(diceKey => {
const dice = part[diceKey];
Object.keys(dice.results).forEach(resultKey => {
const result = dice.results[resultKey];
if (result.toReroll) {
rerollDice.push({
...result,
dice: dice.dice,
type: typeKey,
part: partKey,
dice: diceKey,
result: resultKey
});
}
});
});
});
});
return rerollDice;
}
isSelectionDone() {
const diceFinishedData = [];
Object.keys(this.damage).forEach(typeKey => {
const type = this.damage[typeKey];
Object.keys(type).forEach(partKey => {
const part = type[partKey];
Object.keys(part).forEach(diceKey => {
const dice = part[diceKey];
const selected = dice.results.reduce((acc, result) => acc + (result.active ? 1 : 0), 0);
diceFinishedData.push(selected === dice.maxSelected);
});
});
});
return diceFinishedData.every(x => x);
}
toggleDice(event) {
const target = event.target;
const { type, part, dice } = target.dataset;
const toggleDice = this.damage[type][part][dice];
const existingDiceRerolls = this.getRerollDice().filter(
x => x.type === type && x.part === part && x.dice === dice
);
const allRerolled = existingDiceRerolls.length === toggleDice.results.filter(x => x.active).length;
toggleDice.toReroll = !allRerolled;
toggleDice.results.forEach(result => {
if (result.active) {
result.toReroll = !allRerolled;
}
});
this.render();
}
static #toggleResult(event) {
event.stopPropagation();
const target = event.target.closest('.to-reroll-result');
const { type, part, dice, result } = target.dataset;
const toggleDice = this.damage[type][part][dice];
const toggleResult = toggleDice.results[result];
toggleResult.toReroll = !toggleResult.toReroll;
const existingDiceRerolls = this.getRerollDice().filter(
x => x.type === type && x.part === part && x.dice === dice
);
const allToReroll = existingDiceRerolls.length === toggleDice.results.filter(x => x.active).length;
toggleDice.toReroll = allToReroll;
this.render();
}
static async #selectRoll(_, button) {
const { type, part, dice, result } = button.dataset;
const diceVal = this.damage[type][part][dice];
const diceResult = diceVal.results[result];
if (!diceResult.active && diceVal.results.filter(x => x.active).length === diceVal.maxSelected) {
return ui.notifications.warn(
game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.deselectDiceNotification')
);
}
if (diceResult.active) {
diceVal.toReroll = false;
diceResult.toReroll = false;
}
diceVal.selectedResults += diceResult.active ? -1 : 1;
diceResult.active = !diceResult.active;
this.render();
}
static async #doReroll() {
const toReroll = this.getRerollDice().map(x => {
const { type, part, dice, result } = x;
const diceData = this.damage[type][part][dice].results[result];
return {
...diceData,
dice: this.damage[type][part][dice].dice,
typeKey: type,
partKey: part,
diceKey: dice,
resultsIndex: result
};
});
const roll = await new Roll(toReroll.map(x => `1${x.dice}`).join(' + ')).evaluate();
if (game.modules.get('dice-so-nice')?.active) {
const diceSoNiceRoll = {
_evaluated: true,
dice: roll.dice,
options: { appearance: {} }
};
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
}
toReroll.forEach((data, index) => {
const { typeKey, partKey, diceKey, resultsIndex } = data;
const rerolledDice = roll.dice[index];
const dice = this.damage[typeKey][partKey][diceKey];
dice.toReroll = false;
dice.results[resultsIndex].active = false;
dice.results[resultsIndex].discarded = true;
dice.results[resultsIndex].toReroll = false;
dice.results.splice(dice.results.length, 0, {
...rerolledDice.results[0],
toReroll: false,
selected: true
});
});
this.render();
}
}

View file

@ -1,279 +0,0 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class RerollDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(message, options = {}) {
super(options);
this.message = message;
this.damage = Object.keys(message.system.damage).reduce((acc, typeKey) => {
const type = message.system.damage[typeKey];
acc[typeKey] = Object.keys(type.parts).reduce((acc, partKey) => {
const part = type.parts[partKey];
acc[partKey] = Object.keys(part.dice).reduce((acc, diceKey) => {
const dice = part.dice[diceKey];
const activeResults = dice.results.filter(x => x.active);
acc[diceKey] = {
dice: dice.dice,
selectedResults: activeResults.length,
maxSelected: activeResults.length,
results: activeResults.map(x => ({ ...x, selected: true }))
};
return acc;
}, {});
return acc;
}, {});
return acc;
}, {});
}
static DEFAULT_OPTIONS = {
id: 'reroll-dialog',
classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'reroll-dialog'],
window: {
icon: 'fa-solid fa-dice'
},
actions: {
toggleResult: RerollDialog.#toggleResult,
selectRoll: RerollDialog.#selectRoll,
doReroll: RerollDialog.#doReroll,
save: RerollDialog.#save
}
};
/** @override */
static PARTS = {
main: {
id: 'main',
template: 'systems/daggerheart/templates/dialogs/rerollDialog/main.hbs'
},
footer: {
id: 'footer',
template: 'systems/daggerheart/templates/dialogs/rerollDialog/footer.hbs'
}
};
get title() {
return game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.title');
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement.querySelectorAll('.to-reroll-input').forEach(element => {
element.addEventListener('change', this.toggleDice.bind(this));
});
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.damage = this.damage;
context.disabledReroll = !this.getRerollDice().length;
context.saveDisabled = !this.isSelectionDone();
return context;
}
static async #save() {
const update = {
'system.damage': Object.keys(this.damage).reduce((acc, typeKey) => {
const type = this.damage[typeKey];
let typeTotal = 0;
const messageType = this.message.system.damage[typeKey];
const parts = Object.keys(type).map(partKey => {
const part = type[partKey];
const messagePart = messageType.parts[partKey];
let partTotal = messagePart.modifierTotal;
const dice = Object.keys(part).map(diceKey => {
const dice = part[diceKey];
const total = dice.results.reduce((acc, result) => {
if (result.active) acc += result.result;
return acc;
}, 0);
partTotal += total;
const messageDice = messagePart.dice[diceKey];
return {
...messageDice,
total: total,
results: dice.results.map(x => ({
...x,
hasRerolls: dice.results.length > 1
}))
};
});
typeTotal += partTotal;
return {
...messagePart,
total: partTotal,
dice: dice
};
});
acc[typeKey] = {
...messageType,
total: typeTotal,
parts: parts
};
return acc;
}, {})
};
await this.message.update(update);
await this.close();
}
getRerollDice() {
const rerollDice = [];
Object.keys(this.damage).forEach(typeKey => {
const type = this.damage[typeKey];
Object.keys(type).forEach(partKey => {
const part = type[partKey];
Object.keys(part).forEach(diceKey => {
const dice = part[diceKey];
Object.keys(dice.results).forEach(resultKey => {
const result = dice.results[resultKey];
if (result.toReroll) {
rerollDice.push({
...result,
dice: dice.dice,
type: typeKey,
part: partKey,
dice: diceKey,
result: resultKey
});
}
});
});
});
});
return rerollDice;
}
isSelectionDone() {
const diceFinishedData = [];
Object.keys(this.damage).forEach(typeKey => {
const type = this.damage[typeKey];
Object.keys(type).forEach(partKey => {
const part = type[partKey];
Object.keys(part).forEach(diceKey => {
const dice = part[diceKey];
const selected = dice.results.reduce((acc, result) => acc + (result.active ? 1 : 0), 0);
diceFinishedData.push(selected === dice.maxSelected);
});
});
});
return diceFinishedData.every(x => x);
}
toggleDice(event) {
const target = event.target;
const { type, part, dice } = target.dataset;
const toggleDice = this.damage[type][part][dice];
const existingDiceRerolls = this.getRerollDice().filter(
x => x.type === type && x.part === part && x.dice === dice
);
const allRerolled = existingDiceRerolls.length === toggleDice.results.filter(x => x.active).length;
toggleDice.toReroll = !allRerolled;
toggleDice.results.forEach(result => {
if (result.active) {
result.toReroll = !allRerolled;
}
});
this.render();
}
static #toggleResult(event) {
event.stopPropagation();
const target = event.target.closest('.to-reroll-result');
const { type, part, dice, result } = target.dataset;
const toggleDice = this.damage[type][part][dice];
const toggleResult = toggleDice.results[result];
toggleResult.toReroll = !toggleResult.toReroll;
const existingDiceRerolls = this.getRerollDice().filter(
x => x.type === type && x.part === part && x.dice === dice
);
const allToReroll = existingDiceRerolls.length === toggleDice.results.length;
toggleDice.toReroll = allToReroll;
this.render();
}
static async #selectRoll(_, button) {
const { type, part, dice, result } = button.dataset;
const diceVal = this.damage[type][part][dice];
const diceResult = diceVal.results[result];
if (!diceResult.active && diceVal.results.filter(x => x.active).length === diceVal.maxSelected) {
return ui.notifications.warn(
game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.deselectDiceNotification')
);
}
if (diceResult.active) {
diceVal.toReroll = false;
diceResult.toReroll = false;
}
diceVal.selectedResults += diceResult.active ? -1 : 1;
diceResult.active = !diceResult.active;
this.render();
}
static async #doReroll() {
const toReroll = this.getRerollDice().map(x => {
const { type, part, dice, result } = x;
const diceData = this.damage[type][part][dice].results[result];
return {
...diceData,
dice: this.damage[type][part][dice].dice,
typeKey: type,
partKey: part,
diceKey: dice,
resultsIndex: result
};
});
const roll = await new Roll(toReroll.map(x => `1${x.dice}`).join(' + ')).evaluate();
if (game.modules.get('dice-so-nice')?.active) {
const diceSoNiceRoll = {
_evaluated: true,
dice: roll.dice,
options: { appearance: {} }
};
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
}
toReroll.forEach((data, index) => {
const { typeKey, partKey, diceKey, resultsIndex } = data;
const rerolledDice = roll.dice[index];
const dice = this.damage[typeKey][partKey][diceKey];
dice.toReroll = false;
dice.results[resultsIndex].active = false;
dice.results[resultsIndex].discarded = true;
dice.results[resultsIndex].toReroll = false;
dice.results.splice(dice.results.length, 0, {
...rerolledDice.results[0],
toReroll: false,
selected: true
});
});
this.render();
}
}

View file

@ -1,4 +1,4 @@
import { itemAbleRollParse } from '../../helpers/utils.mjs';
import { itemAbleRollParse, triggerChatRollFx } from '../../helpers/utils.mjs';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
@ -69,7 +69,7 @@ export default class ResourceDiceDialog extends HandlebarsApplicationMixin(Appli
const max = itemAbleRollParse(this.item.system.resource.max, this.actor, this.item);
const diceFormula = `${max}${this.item.system.resource.dieFaces}`;
const roll = await new Roll(diceFormula).evaluate();
if (game.modules.get('dice-so-nice')?.active) await game.dice3d.showForRoll(roll, game.user, true);
await triggerChatRollFx([roll]);
this.rollValues = roll.terms[0].results.map(x => ({ value: x.result, used: false }));
this.resetUsed = true;

File diff suppressed because it is too large Load diff

View file

@ -50,11 +50,11 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
).showGenericStatusEffects;
context.genericStatusEffects = useGeneric
? Object.keys(context.statusEffects).reduce((acc, key) => {
const effect = context.statusEffects[key];
if (!effect.systemEffect) acc[key] = effect;
const effect = context.statusEffects[key];
if (!effect.systemEffect) acc[key] = effect;
return acc;
}, {})
return acc;
}, {})
: null;
context.hasCompanion = this.actor.system.companion;
@ -68,11 +68,11 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
const warning =
tokensWithoutActors.length === 1
? game.i18n.format('DAGGERHEART.UI.Notifications.tokenActorMissing', {
name: tokensWithoutActors[0].name
})
name: tokensWithoutActors[0].name
})
: game.i18n.format('DAGGERHEART.UI.Notifications.tokenActorsMissing', {
names: tokensWithoutActors.map(x => x.name).join(', ')
});
names: tokensWithoutActors.map(x => x.name).join(', ')
});
const tokens = canvas.tokens.controlled
.filter(t => t.actor && !DHTokenHUD.#nonCombatTypes.includes(t.actor.type))
@ -122,15 +122,16 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
async toggleClowncar(actors) {
const animationDuration = 500;
const activeTokens = actors.flatMap(member => member.getActiveTokens());
const scene = game.scenes.get(game.user.viewedScene);
/* getDependentTokens returns already removed tokens with id = null. Need to filter that until it's potentially fixed from Foundry */
const activeTokens = actors.flatMap(member =>
member.getDependentTokens({ scenes: scene }).filter(x => x._id && !x._destroyed)
);
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);
await token.update({ x: actorX, y: actorY, alpha: 0 }, { animation: { duration: animationDuration } });
setTimeout(() => token.delete(), animationDuration);
}
} else {
const activeScene = game.scenes.find(x => x.id === game.user.viewedScene);
@ -140,11 +141,16 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
tokenData.push(data.toObject());
}
const viewedLevel = game.scenes.get(game.user.viewedScene).levels.get(game.user.viewedLevel);
const elevation = this.actor.token?.elevation ?? viewedLevel.elevation.bottom;
const newTokens = await activeScene.createEmbeddedDocuments(
'Token',
tokenData.map(tokenData => ({
...tokenData,
alpha: 0,
level: viewedLevel,
elevation: elevation,
x: actorX,
y: actorY
}))
@ -168,8 +174,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
nonZeroIndex === sideMiddle
? 0
: nonZeroIndex < sideMiddle
? -nonZeroIndex
: nonZeroIndex - sideMiddle;
? -nonZeroIndex
: nonZeroIndex - sideMiddle;
return { x: actorX - sizeX * distance, y: actorY - sizeY * distanceCoefficient };
} else if (index < side + inbetween) {
const inbetweenIndex = nonZeroIndex - side;
@ -177,8 +183,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
inbetweenIndex === inbetweenMiddle
? 0
: inbetweenIndex < inbetweenMiddle
? -inbetweenIndex
: 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;
@ -186,8 +192,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
sideIndex === sideMiddle
? 0
: sideIndex < sideMiddle
? sideIndex
: -(sideIndex - sideMiddle);
? sideIndex
: -(sideIndex - sideMiddle);
return { x: actorX + sizeX * distance, y: actorY + sizeY * distanceCoefficient };
} else {
const inbetweenIndex = nonZeroIndex - 2 * side - inbetween;
@ -195,8 +201,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
inbetweenIndex === inbetweenMiddle
? 0
: inbetweenIndex < inbetweenMiddle
? inbetweenIndex
: -(inbetweenIndex - inbetweenMiddle);
? inbetweenIndex
: -(inbetweenIndex - inbetweenMiddle);
return { x: actorX - sizeX * distanceCoefficient, y: actorY + sizeY * distance };
}
})

View file

@ -156,6 +156,7 @@ export default class DhCharacterLevelUp extends LevelUpBase {
if (multiclasses?.[0]) {
const data = multiclasses[0];
const multiclass = data.data.length > 0 ? await foundry.utils.fromUuid(data.data[0]) : {};
const subclasses = (await multiclass?.system?.fetchSubclasses()) ?? [];
context.multiclass = {
...data,
@ -175,13 +176,12 @@ export default class DhCharacterLevelUp extends LevelUpBase {
alreadySelected
};
}) ?? [],
subclasses:
multiclass?.system?.subclasses.map(subclass => ({
...subclass,
uuid: subclass.uuid,
selected: data.secondaryData.subclass === subclass.uuid,
disabled: data.secondaryData.subclass && data.secondaryData.subclass !== subclass.uuid
})) ?? [],
subclasses: subclasses.map(subclass => ({
...subclass,
uuid: subclass.uuid,
selected: data.secondaryData.subclass === subclass.uuid,
disabled: data.secondaryData.subclass && data.secondaryData.subclass !== subclass.uuid
})),
compendium: 'classes',
limit: 1
};
@ -210,9 +210,9 @@ export default class DhCharacterLevelUp extends LevelUpBase {
achievementExperiences = level.achievements.experiences
? Object.values(level.achievements.experiences).reduce((acc, experience) => {
if (experience.name) acc.push(experience);
return acc;
}, [])
if (experience.name) acc.push(experience);
return acc;
}, [])
: [];
}
@ -315,15 +315,15 @@ export default class DhCharacterLevelUp extends LevelUpBase {
: null;
advancement[choiceKey] = multiclassItem
? {
...multiclassItem.toObject(),
domain: checkbox.secondaryData.domain
? game.i18n.localize(
CONFIG.DH.DOMAIN.allDomains()[checkbox.secondaryData.domain]
.label
)
: null,
subclass: subclass ? subclass.name : null
}
...multiclassItem.toObject(),
domain: checkbox.secondaryData.domain
? game.i18n.localize(
CONFIG.DH.DOMAIN.allDomains()[checkbox.secondaryData.domain]
.label
)
: null,
subclass: subclass ? subclass.name : null
}
: {};
break;
}

View file

@ -67,7 +67,7 @@ export default class DhCompanionLevelUp extends BaseLevelUp {
break;
case 'summary':
const levelKeys = Object.keys(this.levelup.levels);
const actorDamageDice = this.actor.system.attack.damage.parts[0].value.dice;
const actorDamageDice = this.actor.system.attack.damage.parts.hitPoints.value.dice;
const actorRange = this.actor.system.attack.range;
let achievementExperiences = [];
@ -77,9 +77,9 @@ export default class DhCompanionLevelUp extends BaseLevelUp {
achievementExperiences = level.achievements.experiences
? Object.values(level.achievements.experiences).reduce((acc, experience) => {
if (experience.name) acc.push(experience);
return acc;
}, [])
if (experience.name) acc.push(experience);
return acc;
}, [])
: [];
}
context.achievements = {
@ -155,15 +155,15 @@ export default class DhCompanionLevelUp extends BaseLevelUp {
vicious: {
damage: advancement.vicious?.damage
? {
old: actorDamageDice,
new: advancement.vicious.damage
}
old: actorDamageDice,
new: advancement.vicious.damage
}
: null,
range: advancement.vicious?.range
? {
old: game.i18n.localize(`DAGGERHEART.CONFIG.Range.${actorRange}.name`),
new: game.i18n.localize(advancement.vicious.range.label)
}
old: game.i18n.localize(`DAGGERHEART.CONFIG.Range.${actorRange}.name`),
new: game.i18n.localize(advancement.vicious.range.label)
}
: null
},
simple: advancement.simple ?? {}

View file

@ -135,192 +135,6 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
context.tabs.advancements.progress = { selected: selections, max: currentLevel.maxSelections };
context.showTabs = this.tabGroups.primary !== 'summary';
break;
const actorArmor = this.actor.system.armor;
const levelKeys = Object.keys(this.levelup.levels);
let achivementProficiency = 0;
const achievementCards = [];
let achievementExperiences = [];
for (var levelKey of levelKeys) {
const level = this.levelup.levels[levelKey];
if (Number(levelKey) < this.levelup.startLevel) continue;
achivementProficiency += level.achievements.proficiency ?? 0;
const cards = level.achievements.domainCards ? Object.values(level.achievements.domainCards) : null;
if (cards) {
for (var card of cards) {
const itemCard = await foundry.utils.fromUuid(card.uuid);
achievementCards.push(itemCard);
}
}
achievementExperiences = level.achievements.experiences
? Object.values(level.achievements.experiences).reduce((acc, experience) => {
if (experience.name) acc.push(experience);
return acc;
}, [])
: [];
}
context.achievements = {
proficiency: {
old: this.actor.system.proficiency,
new: this.actor.system.proficiency + achivementProficiency,
shown: achivementProficiency > 0
},
damageThresholds: {
major: {
old: this.actor.system.damageThresholds.major,
new: this.actor.system.damageThresholds.major + changedActorLevel - currentActorLevel
},
severe: {
old: this.actor.system.damageThresholds.severe,
new:
this.actor.system.damageThresholds.severe +
(actorArmor
? changedActorLevel - currentActorLevel
: (changedActorLevel - currentActorLevel) * 2)
},
unarmored: !actorArmor
},
domainCards: {
values: achievementCards,
shown: achievementCards.length > 0
},
experiences: {
values: achievementExperiences
}
};
const advancement = {};
for (var levelKey of levelKeys) {
const level = this.levelup.levels[levelKey];
if (Number(levelKey) < this.levelup.startLevel) continue;
for (var choiceKey of Object.keys(level.choices)) {
const choice = level.choices[choiceKey];
for (var checkbox of Object.values(choice)) {
switch (choiceKey) {
case 'proficiency':
case 'hitPoint':
case 'stress':
case 'evasion':
advancement[choiceKey] = advancement[choiceKey]
? advancement[choiceKey] + Number(checkbox.value)
: Number(checkbox.value);
break;
case 'trait':
if (!advancement[choiceKey]) advancement[choiceKey] = {};
for (var traitKey of checkbox.data) {
if (!advancement[choiceKey][traitKey]) advancement[choiceKey][traitKey] = 0;
advancement[choiceKey][traitKey] += 1;
}
break;
case 'domainCard':
if (!advancement[choiceKey]) advancement[choiceKey] = [];
if (checkbox.data.length === 1) {
const choiceItem = await foundry.utils.fromUuid(checkbox.data[0]);
advancement[choiceKey].push(choiceItem.toObject());
}
break;
case 'experience':
if (!advancement[choiceKey]) advancement[choiceKey] = [];
const data = checkbox.data.map(data => {
const experience = Object.keys(this.actor.system.experiences).find(
x => x === data
);
return this.actor.system.experiences[experience]?.description ?? '';
});
advancement[choiceKey].push({ data: data, value: checkbox.value });
break;
case 'subclass':
if (checkbox.data[0]) {
const subclassItem = await foundry.utils.fromUuid(checkbox.data[0]);
if (!advancement[choiceKey]) advancement[choiceKey] = [];
advancement[choiceKey].push({
...subclassItem.toObject(),
featureLabel: game.i18n.localize(
subclassFeatureLabels[Number(checkbox.secondaryData.featureState)]
)
});
}
break;
case 'multiclass':
const multiclassItem = await foundry.utils.fromUuid(checkbox.data[0]);
const subclass = multiclassItem
? await foundry.utils.fromUuid(checkbox.secondaryData.subclass)
: null;
advancement[choiceKey] = multiclassItem
? {
...multiclassItem.toObject(),
domain: checkbox.secondaryData.domain
? game.i18n.localize(
CONFIG.DH.DOMAIN.allDomains()[checkbox.secondaryData.domain]
.label
)
: null,
subclass: subclass ? subclass.name : null
}
: {};
break;
}
}
}
}
context.advancements = {
statistics: {
proficiency: {
old: context.achievements.proficiency.new,
new: context.achievements.proficiency.new + (advancement.proficiency ?? 0)
},
hitPoints: {
old: this.actor.system.resources.hitPoints.max,
new: this.actor.system.resources.hitPoints.max + (advancement.hitPoint ?? 0)
},
stress: {
old: this.actor.system.resources.stress.max,
new: this.actor.system.resources.stress.max + (advancement.stress ?? 0)
},
evasion: {
old: this.actor.system.evasion,
new: this.actor.system.evasion + (advancement.evasion ?? 0)
}
},
traits: Object.keys(this.actor.system.traits).reduce((acc, traitKey) => {
if (advancement.trait?.[traitKey]) {
if (!acc) acc = {};
acc[traitKey] = {
label: game.i18n.localize(abilities[traitKey].label),
old: this.actor.system.traits[traitKey].value,
new: this.actor.system.traits[traitKey].value + advancement.trait[traitKey]
};
}
return acc;
}, null),
domainCards: advancement.domainCard ?? [],
experiences:
advancement.experience?.flatMap(x => x.data.map(data => ({ name: data, modifier: x.value }))) ??
[],
multiclass: advancement.multiclass,
subclass: advancement.subclass
};
context.advancements.statistics.proficiency.shown =
context.advancements.statistics.proficiency.new > context.advancements.statistics.proficiency.old;
context.advancements.statistics.hitPoints.shown =
context.advancements.statistics.hitPoints.new > context.advancements.statistics.hitPoints.old;
context.advancements.statistics.stress.shown =
context.advancements.statistics.stress.new > context.advancements.statistics.stress.old;
context.advancements.statistics.evasion.shown =
context.advancements.statistics.evasion.new > context.advancements.statistics.evasion.old;
context.advancements.statistics.shown =
context.advancements.statistics.proficiency.shown ||
context.advancements.statistics.hitPoints.shown ||
context.advancements.statistics.stress.shown ||
context.advancements.statistics.evasion.shown;
break;
}
return context;
@ -358,14 +172,14 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const experienceIncreaseTagify = htmlElement.querySelector('.levelup-experience-increases');
if (experienceIncreaseTagify) {
const allExperiences = {
...this.actor.system.experiences,
...Object.values(this.levelup.levels).reduce((acc, level) => {
for (const key of Object.keys(level.achievements.experiences)) {
acc[key] = level.achievements.experiences[key];
}
return acc;
}, {})
}, {}),
...this.actor.system.experiences
};
tagifyElement(
experienceIncreaseTagify,
@ -384,37 +198,35 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
this._dragDrop.forEach(d => d.bind(htmlElement));
}
tagifyUpdate =
type =>
async (_, { option, removed }) => {
const updatePath = Object.keys(this.levelup.levels[this.levelup.currentLevel].choices).reduce(
(acc, choiceKey) => {
const choice = this.levelup.levels[this.levelup.currentLevel].choices[choiceKey];
Object.keys(choice).forEach(checkboxNr => {
const checkbox = choice[checkboxNr];
if (
choiceKey === type &&
(removed ? checkbox.data.includes(option) : checkbox.data.length < checkbox.amount)
) {
acc = `levels.${this.levelup.currentLevel}.choices.${choiceKey}.${checkboxNr}.data`;
}
});
tagifyUpdate = type => async (_, { option, removed }) => {
const updatePath = Object.keys(this.levelup.levels[this.levelup.currentLevel].choices).reduce(
(acc, choiceKey) => {
const choice = this.levelup.levels[this.levelup.currentLevel].choices[choiceKey];
Object.keys(choice).forEach(checkboxNr => {
const checkbox = choice[checkboxNr];
if (
choiceKey === type &&
(removed ? checkbox.data.includes(option) : checkbox.data.length < checkbox.amount)
) {
acc = `levels.${this.levelup.currentLevel}.choices.${choiceKey}.${checkboxNr}.data`;
}
});
return acc;
},
null
);
return acc;
},
null
);
if (!updatePath) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.noSelectionsLeft'));
return;
}
if (!updatePath) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.noSelectionsLeft'));
return;
}
const currentData = foundry.utils.getProperty(this.levelup, updatePath);
const updatedData = removed ? currentData.filter(x => x !== option) : [...currentData, option];
await this.levelup.updateSource({ [updatePath]: updatedData });
this.render();
};
const currentData = foundry.utils.getProperty(this.levelup, updatePath);
const updatedData = removed ? currentData.filter(x => x !== option) : [...currentData, option];
await this.levelup.updateSource({ [updatePath]: updatedData });
this.render();
};
static async updateForm(event, _, formData) {
const { levelup } = foundry.utils.expandObject(formData.object);
@ -477,7 +289,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const secondaryData = Object.keys(
foundry.utils.getProperty(this.levelup, `${target.dataset.path}.secondaryData`)
).reduce((acc, key) => {
acc[`-=${key}`] = null;
acc[key] = _del;
return acc;
}, {});
await this.levelup.updateSource({
@ -511,9 +323,9 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const current = foundry.utils.getProperty(this.levelup, `${basePath}.${button.dataset.option}`);
if (Number(button.dataset.cost) > 1 || Object.keys(current).length === 1) {
// Simple handling that doesn't cover potential Custom LevelTiers.
update[`${basePath}.-=${button.dataset.option}`] = null;
update[`${basePath}.${button.dataset.option}`] = _del;
} else {
update[`${basePath}.${button.dataset.option}.-=${button.dataset.checkboxNr}`] = null;
update[`${basePath}.${button.dataset.option}.${button.dataset.checkboxNr}`] = _del;
}
} else {
if (this.levelup.levels[this.levelup.currentLevel].nrSelections.available < Number(button.dataset.cost)) {
@ -593,10 +405,10 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const domainCards = this.levelup.levels[this.levelup.currentLevel].achievements.domainCards;
const illegalDomainCards = option.secondaryData.domain
? Object.keys(domainCards)
.map(key => ({ ...domainCards[key], key }))
.filter(
x => x.uuid && foundry.utils.fromUuidSync(x.uuid).system.domain === option.secondaryData.domain
)
.map(key => ({ ...domainCards[key], key }))
.filter(
x => x.uuid && foundry.utils.fromUuidSync(x.uuid).system.domain === option.secondaryData.domain
)
: [];
illegalDomainCards.forEach(card => {
update[`levels.${this.levelup.currentLevel}.achievements.domainCards.${card.key}.uuid`] = null;

View file

@ -62,7 +62,15 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
}
async _onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (data.type === 'Level') {
const level = await foundry.documents.Level.fromDropData(data);
if (level?.parent === this.document) return this._onSortLevel(event, level);
return;
}
const item = await foundry.utils.fromUuid(data.uuid);
if (item instanceof game.system.api.documents.DhpActor && item.type === 'environment') {
let sceneUuid = data.uuid;
@ -112,12 +120,6 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
foundry.utils.fromUuidSync(x)
);
for (const key of Object.keys(this.document._source.flags.daggerheart?.sceneEnvironments ?? {})) {
if (!submitData.flags.daggerheart.sceneEnvironments[key]) {
submitData.flags.daggerheart.sceneEnvironments[`-=${key}`] = null;
}
}
super._processSubmitData(event, form, submitData, options);
}
}

View file

@ -118,8 +118,13 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
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' }
{
type: 'button',
action: 'reset',
icon: 'fa-solid fa-arrow-rotate-left',
label: game.i18n.localize('SETTINGS.UI.ACTIONS.Reset')
},
{ type: 'submit', icon: 'fa-solid fa-floppy-disk', label: game.i18n.localize('EDITOR.Save') }
];
break;
}

View file

@ -1,6 +1,5 @@
import { DhHomebrew } from '../../data/settings/_module.mjs';
import { Resource } from '../../data/settings/Homebrew.mjs';
import { slugify } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -112,7 +111,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
switch (partId) {
case 'domains':
const selectedDomain = this.selected.domain ? this.settings.domains[this.selected.domain] : null;
const selectedDomain = this.settings.domains[this.selected.domain] ?? null;
const enrichedDescription = selectedDomain
? await foundry.applications.ux.TextEditor.implementation.enrichHTML(selectedDomain.description)
: null;
@ -252,8 +251,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
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');
? 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,
@ -298,7 +297,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
const isDowntime = ['shortRest', 'longRest'].includes(type);
const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`;
await this.settings.updateSource({
[`${path}.-=${id}`]: null
[`${path}.${id}`]: _del
});
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
@ -322,7 +321,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
const fields = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).schema.fields;
const removeUpdate = Object.keys(this.settings.restMoves[target.dataset.type].moves).reduce((acc, key) => {
acc[`-=${key}`] = null;
acc[key] = _del;
return acc;
}, {});
@ -382,7 +381,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
[`itemFeatures.${target.dataset.type}`]: Object.keys(
this.settings.itemFeatures[target.dataset.type]
).reduce((acc, key) => {
acc[`-=${key}`] = null;
acc[key] = _del;
return acc;
}, {})
@ -403,12 +402,12 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
const domainName = button.form.elements.domainName.value;
if (!domainName) return;
const newSlug = slugify(domainName);
const newSlug = domainName.slugify();
const existingDomains = [
...Object.values(this.settings.domains),
...Object.values(CONFIG.DH.DOMAIN.domains)
];
if (existingDomains.find(x => slugify(game.i18n.localize(x.label)) === newSlug)) {
if (existingDomains.find(x => x.id === newSlug)) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.domains.duplicateDomain'));
return;
}
@ -455,12 +454,12 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
if (!confirmed) return;
await this.settings.updateSource({
[`domains.-=${this.selected.domain}`]: null
[`domains.${this.selected.domain}`]: _del
});
const currentSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew);
if (currentSettings.domains[this.selected.domain]) {
await currentSettings.updateSource({ [`domains.-=${this.selected.domain}`]: null });
await currentSettings.updateSource({ [`domains.${this.selected.domain}`]: _del });
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, currentSettings);
}
@ -507,7 +506,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
static async deleteAdversaryType(_, target) {
const { key } = target.dataset;
await this.settings.updateSource({ [`adversaryTypes.-=${key}`]: null });
await this.settings.updateSource({ [`adversaryTypes.${key}`]: _del });
this.selected.adversaryType = this.selected.adversaryType === key ? null : this.selected.adversaryType;
this.render();
@ -529,7 +528,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
const identifier = button.form.elements.identifier.value;
if (!identifier) return;
const sluggedIdentifier = slugify(identifier);
const sluggedIdentifier = identifier.slugify();
await this.settings.updateSource({
[`resources.${actorType}.resources.${sluggedIdentifier}`]: Resource.getDefaultResourceData(identifier)
@ -563,7 +562,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
const { actorType, resourceKey } = target.dataset;
await this.settings.updateSource({
[`resources.${actorType}.resources.-=${resourceKey}`]: null
[`resources.${actorType}.resources.${resourceKey}`]: _del
});
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());

View file

@ -2,8 +2,8 @@ export { default as ActionConfig } from './action-config.mjs';
export { default as ActionSettingsConfig } from './action-settings-config.mjs';
export { default as CharacterSettings } from './character-settings.mjs';
export { default as AdversarySettings } from './adversary-settings.mjs';
export { default as NPCSettings } from './npc-settings.mjs';
export { default as CompanionSettings } from './companion-settings.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 ActiveEffectConfig } from './activeEffectConfig.mjs';

View file

@ -1,3 +1,4 @@
import { getUnusedDamageTypes } from '../../helpers/utils.mjs';
import DaggerheartSheet from '../sheets/daggerheart-sheet.mjs';
const { ApplicationV2 } = foundry.applications.api;
@ -35,7 +36,9 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
editDoc: this.editDoc,
addTrigger: this.addTrigger,
removeTrigger: this.removeTrigger,
expandTrigger: this.expandTrigger
expandTrigger: this.expandTrigger,
addBeastformTraitBonus: this.addBeastformTraitBonus,
removeBeastformTraitBonus: this.removeBeastformTraitBonus
},
form: {
handler: this.updateForm,
@ -104,7 +107,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
}
};
static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects', 'summon'];
static CLEAN_ARRAYS = ['cost', 'effects', 'summon'];
_getTabs(tabs) {
for (const v of Object.values(tabs)) {
@ -153,8 +156,13 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
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;
if (this.action.damage) {
context.allDamageTypesUsed = !getUnusedDamageTypes(this.action.damage.parts).length;
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);
@ -196,7 +204,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
};
}
if (this.action.parent.metadata?.isQuantifiable) {
if (this.action.parent.metadata?.isInventoryItem) {
options.quantity = {
label: 'DAGGERHEART.GENERAL.itemQuantity',
group: 'Global'
@ -256,7 +264,9 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
key = event.target.closest('[data-key]').dataset.key;
if (!this.action[key]) return;
data[key].push(this.action.defaultValues[key] ?? {});
const value = key === 'areas' ? { name: this.action.item.name } : {};
data[key].push(this.action.defaultValues[key] ?? value);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
@ -291,18 +301,64 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
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) });
const choices = getUnusedDamageTypes(this.action._source.damage.parts);
const content = new foundry.data.fields.StringField({
label: game.i18n.localize('Damage Type'),
choices,
required: true
}).toFormGroup(
{},
{
name: 'type',
localize: true,
nameAttr: 'value',
labelAttr: 'label'
}
).outerHTML;
const callback = (_, button) => {
const data = this.action.toObject();
const type = choices[button.form.elements.type.value].value;
const part = this.action.schema.fields.damage.fields.parts.element.getInitialValue();
part.applyTo = type;
if (type === CONFIG.DH.GENERAL.healingTypes.hitPoints.id)
part.type = this.action.schema.fields.damage.fields.parts.element.fields.type.element.initial;
data.damage.parts[type] = part;
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
};
const typeDialog = new foundry.applications.api.DialogV2({
buttons: [
foundry.utils.mergeObject(
{
action: 'ok',
label: 'Confirm',
icon: 'fas fa-check',
default: true
},
{ callback: callback }
)
],
content: content,
rejectClose: false,
modal: false,
window: {
title: game.i18n.localize('Add Damage')
},
position: { width: 300 }
});
typeDialog.render(true);
}
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);
const data = this.action.toObject();
const key = button.dataset.key;
delete data.damage.parts[key];
data.damage.parts[`${key}`] = _del;
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
@ -360,6 +416,21 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
}
}
static async addBeastformTraitBonus() {
const data = this.action.toObject();
data.beastform.modifications.traitBonuses = [
...data.beastform.modifications.traitBonuses,
this.action.schema.fields.beastform.fields.modifications.fields.traitBonuses.element.getInitialValue()
];
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static async removeBeastformTraitBonus(_event, button) {
const data = this.action.toObject();
data.beastform.modifications.traitBonuses.splice(button.dataset.index, 1);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
updateSummonCount(event) {
event.stopPropagation();
const wrapper = event.target.closest('.summon-count-wrapper');

View file

@ -19,17 +19,19 @@ export default class DHActionConfig extends DHActionBaseConfig {
return context;
}
static async addEffect(_event) {
static async addEffect(event) {
const { areaIndex } = event.target.dataset;
if (!this.action.effects) return;
const effectData = this._addEffectData.bind(this)();
const data = this.action.toObject();
const [created] = await this.action.item.createEmbeddedDocuments('ActiveEffect', [effectData], {
render: false
});
data.effects.push({ _id: created._id });
const created = await this.action.item.createEmbeddedDocuments('ActiveEffect', [
game.system.api.data.activeEffects.BaseEffect.getDefaultObject({ transfer: false })
]);
if (areaIndex !== undefined) data.areas[areaIndex].effects.push(created[0]._id);
else data.effects.push({ _id: created[0]._id });
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[0]._id).sheet.render(true);
}
/**
@ -52,9 +54,19 @@ export default class DHActionConfig extends DHActionBaseConfig {
static removeEffect(event, button) {
if (!this.action.effects) return;
const index = button.dataset.index,
const { areaIndex, index } = button.dataset;
let effectId = null;
if (areaIndex !== undefined) {
effectId = this.action.areas[areaIndex].effects[index];
const data = this.action.toObject();
data.areas[areaIndex].effects.splice(index, 1);
this.constructor.updateForm.call(this, null, null, { object: foundry.utils.flattenObject(data) });
} else {
effectId = this.action.effects[index]._id;
this.constructor.removeElement.bind(this)(event, button);
this.constructor.removeElement.call(this, event, button);
}
this.action.item.deleteEmbeddedDocuments('ActiveEffect', [effectId]);
}

View file

@ -31,21 +31,35 @@ export default class DHActionSettingsConfig extends DHActionBaseConfig {
}
static async addEffect(_event) {
const { areaIndex } = event.target.dataset;
if (!this.action.effects) return;
const effectData = game.system.api.data.activeEffects.BaseEffect.getDefaultObject();
const effectData = game.system.api.data.activeEffects.BaseEffect.getDefaultObject({ transfer: false });
const data = this.action.toObject();
this.sheetUpdate(data, effectData);
this.effects = [...this.effects, effectData];
data.effects.push({ _id: effectData.id });
if (areaIndex !== undefined) data.areas[areaIndex].effects.push(effectData.id);
else 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,
const { areaIndex, index } = button.dataset;
let effectId = null;
if (areaIndex !== undefined) {
effectId = this.action.areas[areaIndex].effects[index];
const data = this.action.toObject();
data.areas[areaIndex].effects.splice(index, 1);
this.constructor.updateForm.call(this, null, null, { object: foundry.utils.flattenObject(data) });
} else {
effectId = this.action.effects[index]._id;
this.constructor.removeElement.bind(this)(event, button);
this.constructor.removeElement.call(this, event, button);
}
this.sheetUpdate(
this.action.toObject(),
this.effects.find(x => x.id === effectId),
@ -55,7 +69,7 @@ export default class DHActionSettingsConfig extends DHActionBaseConfig {
static async editEffect(event) {
const id = event.target.closest('[data-effect-id]')?.dataset?.effectId;
const updatedEffect = await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(
const updatedEffect = await game.system.api.applications.sheetConfigs.ActiveEffectConfig.configureSetting(
this.getEffectDetails(id)
);
if (!updatedEffect) return;

View file

@ -18,6 +18,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' },
changes: {
template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs',
templates: ['systems/daggerheart/templates/sheets/activeEffect/change.hbs'],
scrollable: ['ol[data-changes]']
},
footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' }
@ -40,7 +41,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
* @returns {ChangeChoice { value: string, label: string, hint: string, group: string }[]}
*/
static getChangeChoices() {
const ignoredActorKeys = ['config', 'DhEnvironment', 'DhParty'];
const ignoredActorKeys = ['config', 'DhEnvironment', 'DhParty', 'DhNPC'];
const getAllLeaves = (root, group, parentPath = '') => {
const leaves = [];
@ -149,6 +150,18 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
minLength: 0
});
});
htmlElement
.querySelector('.stacking-change-checkbox')
?.addEventListener('change', this.stackingChangeToggle.bind(this));
htmlElement
.querySelector('.armor-change-checkbox')
?.addEventListener('change', this.armorChangeToggle.bind(this));
htmlElement
.querySelector('.armor-damage-thresholds-checkbox')
?.addEventListener('change', this.armorDamageThresholdToggle.bind(this));
}
async _prepareContext(options) {
@ -162,6 +175,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
const partContext = await super._preparePartContext(partId, context);
switch (partId) {
case 'details':
partContext.isItemEffect = partContext.isItemEffect || this.options.isSetting;
const useGeneric = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.appearance
@ -173,8 +187,166 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
}));
}
break;
case 'settings':
const groups = {
time: _loc('EFFECT.DURATION.UNITS.GROUPS.time'),
combat: _loc('EFFECT.DURATION.UNITS.GROUPS.combat')
};
partContext.durationUnits = CONST.ACTIVE_EFFECT_DURATION_UNITS.map(value => ({
value,
label: _loc(`EFFECT.DURATION.UNITS.${value}`),
group: CONST.ACTIVE_EFFECT_TIME_DURATION_UNITS.includes(value) ? groups.time : groups.combat
}));
break;
case 'changes':
const singleTypes = ['armor'];
const typedChanges = context.source.changes.reduce((acc, change, index) => {
if (singleTypes.includes(change.type)) {
acc[change.type] = { ...change, index };
}
return acc;
}, {});
partContext.changes = partContext.changes.filter(c => !!c);
partContext.typedChanges = typedChanges;
break;
}
return partContext;
}
stackingChangeToggle(event) {
const stackingFields = this.document.system.schema.fields.stacking.fields;
const systemData = {
stacking: event.target.checked
? { value: stackingFields.value.initial, max: stackingFields.max.initial }
: null
};
return this.submit({ updateData: { system: systemData } });
}
armorChangeToggle(event) {
if (event.target.checked) {
this.addArmorChange();
} else {
this.removeTypedChange(event.target.dataset.index);
}
}
/* Could be generalised if needed later */
addArmorChange() {
const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form));
const changes = Object.values(submitData.system?.changes ?? {});
changes.push(game.system.api.data.activeEffects.changeTypes.armor.getInitialValue());
return this.submit({ updateData: { system: { changes } } });
}
removeTypedChange(indexString) {
const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form));
const changes = Object.values(submitData.system.changes);
const index = Number(indexString);
changes.splice(index, 1);
return this.submit({ updateData: { system: { changes } } });
}
armorDamageThresholdToggle(event) {
const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form));
const changes = Object.values(submitData.system?.changes ?? {});
const index = Number(event.target.dataset.index);
if (event.target.checked) {
changes[index].value.damageThresholds = { major: 0, severe: 0 };
} else {
changes[index].value.damageThresholds = null;
}
return this.submit({ updateData: { system: { changes } } });
}
/** @inheritdoc */
_renderChange(context) {
const { change, index, defaultPriority } = context;
if (!(change.type in CONFIG.DH.GENERAL.baseActiveEffectModes)) return null;
const changeTypesSchema = this.document.system.schema.fields.changes.element.types;
const fields = context.fields ?? (changeTypesSchema[change.type] ?? changeTypesSchema.add).fields;
if (typeof change.value !== 'string') change.value = JSON.stringify(change.value);
Object.assign(
change,
['key', 'type', 'value', 'priority'].reduce((paths, fieldName) => {
paths[`${fieldName}Path`] = `system.changes.${index}.${fieldName}`;
return paths;
}, {})
);
return (
game.system.api.documents.DhActiveEffect.CHANGE_TYPES[change.type].render?.(
change,
index,
defaultPriority
) ??
foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/activeEffect/change.hbs',
{
change,
index,
defaultPriority,
fields,
types: Object.keys(CONFIG.DH.GENERAL.baseActiveEffectModes).reduce((r, key) => {
r[key] = CONFIG.DH.GENERAL.baseActiveEffectModes[key].label;
return r;
}, {})
}
)
);
}
/** @inheritDoc */
_onChangeForm(_formConfig, event) {
if (foundry.utils.isElementInstanceOf(event.target, 'select') && event.target.name === 'system.duration.type') {
const durationSection = this.element.querySelector('.custom-duration-section');
if (event.target.value === 'custom') durationSection.classList.add('visible');
else durationSection.classList.remove('visible');
const durationDescription = this.element.querySelector('.duration-description');
if (event.target.value === 'temporary') durationDescription.classList.add('visible');
else durationDescription.classList.remove('visible');
}
}
/** @inheritDoc */
_processFormData(event, form, formData) {
const submitData = super._processFormData(event, form, formData);
if (submitData.start && !submitData.start.time) submitData.start.time = '0';
else if (!submitData) submitData.start = null;
return submitData;
}
/** @inheritDoc */
_processSubmitData(event, form, submitData, options) {
if (this.options.isSetting) {
// Settings should update source instead
this.document.updateSource(submitData);
this.render();
} else {
return super._processSubmitData(event, form, submitData, options);
}
}
/** Creates an active effect config for a setting */
static async configureSetting(effect, options = {}) {
const document = new CONFIG.ActiveEffect.documentClass({ ...foundry.utils.duplicate(effect), _id: effect.id });
return new Promise(resolve => {
const app = new this({ document, ...options, isSetting: true });
app.addEventListener(
'close',
() => {
const newEffect = app.document.toObject(true);
newEffect.id = newEffect._id;
delete newEffect._id;
resolve(newEffect);
},
{ once: true }
);
app.render({ force: true });
});
}
}

View file

@ -95,7 +95,7 @@ export default class DHAdversarySettings extends DHBaseActorSettings {
});
if (!confirmed) return;
await this.actor.update({ [`system.experiences.-=${target.dataset.experience}`]: null });
await this.actor.update({ [`system.experiences.${target.dataset.experience}`]: _del });
}
async _onDragStart(event) {
@ -110,6 +110,7 @@ export default class DHAdversarySettings extends DHBaseActorSettings {
}
async _onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const item = await fromUuid(data.uuid);

View file

@ -101,8 +101,8 @@ export default class DHCharacterSettings extends DHBaseActorSettings {
if (relinkAchievementData.length > 0) {
relinkAchievementData.forEach(data => {
updates[`system.levelData.levelups.${data.levelKey}.achievements.experiences.-=${data.experience}`] =
null;
updates[`system.levelData.levelups.${data.levelKey}.achievements.experiences.${data.experience}`] =
_del;
});
} else if (relinkSelectionData.length > 0) {
relinkSelectionData.forEach(data => {
@ -137,7 +137,7 @@ export default class DHCharacterSettings extends DHBaseActorSettings {
await this.actor.update({
...updates,
[`system.experiences.-=${target.dataset.experience}`]: null
[`system.experiences.${target.dataset.experience}`]: _del
});
}
}

View file

@ -117,6 +117,6 @@ export default class DHCompanionSettings extends DHBaseActorSettings {
});
if (!confirmed) return;
await this.actor.update({ [`system.experiences.-=${target.dataset.experience}`]: null });
await this.actor.update({ [`system.experiences.${target.dataset.experience}`]: _del });
}
}

View file

@ -68,9 +68,9 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
*/
static async #addCategory() {
await this.actor.update({
[`system.potentialAdversaries.${foundry.utils.randomID()}.label`]: game.i18n.localize(
'DAGGERHEART.ACTORS.Environment.newAdversary'
)
[`system.potentialAdversaries.${foundry.utils.randomID()}`]: {
label: game.i18n.localize('DAGGERHEART.ACTORS.Environment.newAdversary')
}
});
}
@ -79,7 +79,7 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
* @type {ApplicationClickAction}
*/
static async #removeCategory(_, target) {
await this.actor.update({ [`system.potentialAdversaries.-=${target.dataset.categoryId}`]: null });
await this.actor.update({ [`system.potentialAdversaries.${target.dataset.categoryId}`]: _del });
}
/**
@ -121,6 +121,7 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
}
async _onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const item = await fromUuid(data.uuid);
if (data.fromInternal && item?.parent?.uuid === this.actor.uuid) return;

View file

@ -0,0 +1,85 @@
import DHBaseActorSettings from '../sheets/api/actor-setting.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
export default class DHNPCSettings extends DHBaseActorSettings {
/**@inheritdoc */
static DEFAULT_OPTIONS = {
classes: ['npc-settings'],
position: { width: 455, height: 'auto' },
actions: {},
dragDrop: [
{ dragSelector: null, dropSelector: '.tab.features' },
{ dragSelector: '.feature-item', dropSelector: null }
]
};
/**@override */
static PARTS = {
header: {
id: 'header',
template: 'systems/daggerheart/templates/sheets-settings/npc-settings/header.hbs'
},
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
details: {
id: 'details',
template: 'systems/daggerheart/templates/sheets-settings/npc-settings/details.hbs'
},
features: {
id: 'features',
template: 'systems/daggerheart/templates/sheets-settings/npc-settings/features.hbs'
}
};
/** @override */
static TABS = {
primary: {
tabs: [{ id: 'details' }, { id: 'features' }],
initial: 'details',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}
};
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;
}
/* -------------------------------------------- */
async _onDragStart(event) {
const featureItem = event.currentTarget.closest('.feature-item');
if (featureItem) {
const feature = this.actor.items.get(featureItem.id);
const featureData = { type: 'Item', uuid: feature.uuid, fromInternal: true };
event.dataTransfer.setData('text/plain', JSON.stringify(featureData));
event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0);
}
}
async _onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const item = await fromUuid(data.uuid);
if (item?.type === 'feature') {
if (data.fromInternal && item.parent?.uuid === this.actor.uuid) {
return;
}
const itemData = item.toObject();
delete itemData._id;
await this.actor.createEmbeddedDocuments('Item', [itemData]);
}
}
}

View file

@ -1,223 +0,0 @@
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);
this.changeChoices = game.system.api.applications.sheetConfigs.ActiveEffectConfig.getChangeChoices();
}
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}`.replaceAll(
' ',
'&nbsp;'
);
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

@ -147,7 +147,7 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
const effectIndex = this.move.effects.findIndex(x => x.id === id);
const effect = this.move.effects[effectIndex];
const updatedEffect =
await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect);
await game.system.api.applications.sheetConfigs.ActiveEffectConfig.configureSetting(effect);
if (!updatedEffect) return;
await this.updateMove({
@ -168,8 +168,8 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
updatedEffects = deleteEffect
? currentEffects.filter(x => x.id !== effectData.id)
: existingEffectIndex === -1
? [...currentEffects, effectData]
: currentEffects.with(existingEffectIndex, effectData);
? [...currentEffects, effectData]
: currentEffects.with(existingEffectIndex, effectData);
await this.updateMove({
[`${this.movePath}.effects`]: updatedEffects
});
@ -188,8 +188,9 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
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) {
const actionEffects = action.effects ?? [];
const remainingEffects = actionEffects.filter(x => x._id !== id);
if (actionEffects.length !== remainingEffects.length) {
await action.update({
effects: remainingEffects.map(x => {
const { _id, ...rest } = x;
@ -205,7 +206,7 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
}
});
} else {
await this.updateMove({ [`${this.actionsPath}.-=${target.dataset.id}`]: null });
await this.updateMove({ [`${this.actionsPath}.${target.dataset.id}`]: _del });
}
this.render();
@ -234,9 +235,9 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
return this.hasEffects
? tabs
: Object.keys(tabs).reduce((acc, key) => {
if (key !== 'effects') acc[key] = tabs[key];
return acc;
}, {});
if (key !== 'effects') acc[key] = tabs[key];
return acc;
}, {});
}
/** @override */

View file

@ -67,9 +67,9 @@ export default function DHTokenConfigMixin(Base) {
changes.height = tokenSize;
}
const deletions = { '-=actorId': null, '-=actorLink': null };
const mergeOptions = { inplace: false, performDeletions: true };
this._preview.updateSource(mergeObject(changes, deletions, mergeOptions));
// const deletions = { actorId: _del };
// const mergeOptions = { inplace: false, performDeletions: true, actorLink: false };
this._preview.updateSource(changes);
if (this._preview?.object?.destroyed === false) {
this._preview.object.initializeSources();

View file

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

View file

@ -31,6 +31,16 @@ export default class AdversarySheet extends DHBaseActorSheet {
dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
dropSelector: null
}
],
contextMenus: [
{
handler: DHBaseActorSheet.getBaseAttackContextOptions,
selector: '[data-item-uuid][data-type="attack"]',
options: {
parentClassHooks: false,
fixed: true
}
}
]
};
@ -217,8 +227,8 @@ export default class AdversarySheet extends DHBaseActorSheet {
static #reactionRoll(event) {
const config = {
event,
title: `Reaction Roll: ${this.actor.name}`,
headerTitle: 'Adversary Reaction Roll',
title: game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll'),
headerTitle: game.i18n.localize('DAGGERHEART.ACTORS.Adversary.adversaryReactionRoll.headerTitle'),
roll: {
type: 'trait'
},

View file

@ -1,10 +1,9 @@
import DHBaseActorSheet from '../api/base-actor.mjs';
import DhDeathMove from '../../dialogs/deathMove.mjs';
import { abilities } from '../../../config/actorConfig.mjs';
import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
import FilterMenu from '../../ux/filter-menu.mjs';
import { getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs';
import { getArmorSources, getDocFromElement, getDocFromElementSync, sortBy } from '../../../helpers/utils.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
@ -13,8 +12,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
static DEFAULT_OPTIONS = {
classes: ['character'],
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: {
toggleVault: CharacterSheet.#toggleVault,
rollAttribute: CharacterSheet.#rollAttribute,
@ -35,7 +32,8 @@ export default class CharacterSheet extends DHBaseActorSheet {
cancelBeastform: CharacterSheet.#cancelBeastform,
toggleResourceManagement: CharacterSheet.#toggleResourceManagement,
useDowntime: this.useDowntime,
viewParty: CharacterSheet.#viewParty
viewParty: CharacterSheet.#viewParty,
toggleArmorMangement: CharacterSheet.#toggleArmorManagement
},
window: {
resizable: true,
@ -59,6 +57,22 @@ export default class CharacterSheet extends DHBaseActorSheet {
}
],
contextMenus: [
{
handler: CharacterSheet.#getCreationMainContextOptions,
selector: '.character-details [data-action="editDoc"]',
options: {
parentClassHooks: false,
fixed: true
}
},
{
handler: DHBaseActorSheet.getBaseAttackContextOptions,
selector: '[data-item-uuid][data-type="attack"]',
options: {
parentClassHooks: false,
fixed: true
}
},
{
handler: CharacterSheet.#getDomainCardContextOptions,
selector: '[data-item-uuid][data-type="domainCard"]',
@ -68,7 +82,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
}
},
{
handler: CharacterSheet.#getEquipamentContextOptions,
handler: CharacterSheet.#getEquipmentContextOptions,
selector: '[data-item-uuid][data-type="armor"], [data-item-uuid][data-type="weapon"]',
options: {
parentClassHooks: false,
@ -170,6 +184,19 @@ export default class CharacterSheet extends DHBaseActorSheet {
return applicationOptions;
}
/** @inheritdoc */
_toggleDisabled(disabled) {
// Overriden to only disable text inputs by default.
// Everything else is done by checking @root.editable in the sheet
const form = this.form;
for (const input of form.querySelectorAll('input:not([type=search]), .editor.prosemirror')) {
input.disabled = disabled;
}
for (const element of form.querySelectorAll('.input[contenteditable]')) {
element.classList.toggle('disabled', disabled);
}
}
/** @inheritDoc */
async _onRender(context, options) {
await super._onRender(context, options);
@ -201,8 +228,9 @@ export default class CharacterSheet extends DHBaseActorSheet {
context.attributes = Object.keys(this.document.system.traits).reduce((acc, key) => {
acc[key] = {
...this.document.system.traits[key],
name: game.i18n.localize(CONFIG.DH.ACTOR.abilities[key].name),
verbs: CONFIG.DH.ACTOR.abilities[key].verbs.map(x => game.i18n.localize(x))
label: _loc(CONFIG.DH.ACTOR.abilities[key].label),
verbs: CONFIG.DH.ACTOR.abilities[key].verbs.map(x => game.i18n.localize(x)),
isSpellcasting: this.document.system.spellcastModifierTrait?.key === key
};
return acc;
@ -218,6 +246,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
context.resources.stress.emptyPips =
context.resources.stress.max < maxResource ? maxResource - context.resources.stress.max : 0;
context.equippedItems = sortBy(
this.document.items.filter(i => i.system.equipped && (i.type === 'weapon' || i.usable)),
i => (i.type === 'weapon' ? (i.system.secondary ? 1 : 0) : 2)
);
context.beastformActive = this.document.effects.find(x => x.type === 'beastform');
return context;
@ -305,6 +338,56 @@ export default class CharacterSheet extends DHBaseActorSheet {
/* Context Menu */
/* -------------------------------------------- */
static #getCreationMainContextOptions() {
/** Returns true if the item is managed by the level up wizard. Such items shouldn't allow things like manual removal */
function isItemWizardManaged(item) {
const actor = item?.actor;
if (!actor) return false;
// If levelup automation is off in general or for this character, all items are unmanaged
// This is disabled until we have proper granted feature removal, for now this feature is to correct errors
// const levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto;
// if (!levelupAuto) return false;
// Core items aren't part of levelup data. TODO: add some way to flag a specific character as no auto leveling
const classPair = actor.system.class;
const coreItems = [actor.system.ancestry, actor.system.community, classPair?.value, classPair?.subclass];
if (coreItems.includes(item)) return true;
const levelups = Object.values(actor.system.levelData?.levelups) ?? [];
const uuid = item.uuid;
const sourceUuid = item._stats.compendiumSource; // on older characters this may be missing
return levelups.some(data => {
if (item.type === 'subclass') {
const selectedSubclasses = data.selections.map(s => s.secondaryData?.subclass).filter(s => !!s);
return sourceUuid
? selectedSubclasses.includes(sourceUuid)
: selectedSubclasses.length && item.system.isMulticlass;
}
const matchesCard = data.achievements.domainCards.some(i => i.itemUuid === uuid);
const matchesSelection = data.selections.some(s => s.itemUuid === uuid);
return matchesCard || matchesSelection;
});
}
return [
{
label: 'CONTROLS.CommonDelete',
icon: 'fa-solid fa-trash',
visible: target => {
const doc = getDocFromElementSync(target);
return doc?.isOwner && !isItemWizardManaged(doc);
},
onClick: async (event, target) => {
const doc = await getDocFromElement(target);
if (event.shiftKey) return doc.delete();
else return doc.deleteDialog();
}
}
];
}
/**
* Get the set of ContextMenu options for DomainCards.
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
@ -315,13 +398,13 @@ export default class CharacterSheet extends DHBaseActorSheet {
/**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */
const options = [
{
name: 'toLoadout',
label: 'toLoadout',
icon: 'fa-solid fa-arrow-up',
condition: target => {
visible: target => {
const doc = getDocFromElementSync(target);
return doc && doc.system.inVault;
return doc?.isOwner && doc.system.inVault;
},
callback: async target => {
onClick: async (_, target) => {
const doc = await getDocFromElement(target);
const actorLoadout = doc.actor.system.loadoutSlot;
if (actorLoadout.available) return doc.update({ 'system.inVault': false });
@ -329,13 +412,13 @@ export default class CharacterSheet extends DHBaseActorSheet {
}
},
{
name: 'recall',
label: 'recall',
icon: 'fa-solid fa-bolt-lightning',
condition: target => {
visible: target => {
const doc = getDocFromElementSync(target);
return doc && doc.system.inVault;
return doc?.isOwner && doc.system.inVault;
},
callback: async (target, event) => {
onClick: async (event, target) => {
const doc = await getDocFromElement(target);
const actorLoadout = doc.actor.system.loadoutSlot;
if (!actorLoadout.available) {
@ -368,17 +451,17 @@ export default class CharacterSheet extends DHBaseActorSheet {
}
},
{
name: 'toVault',
label: 'toVault',
icon: 'fa-solid fa-arrow-down',
condition: target => {
visible: target => {
const doc = getDocFromElementSync(target);
return doc && !doc.system.inVault;
return doc?.isOwner && !doc.system.inVault;
},
callback: async target => (await getDocFromElement(target)).update({ 'system.inVault': true })
onClick: async (_, target) => (await getDocFromElement(target)).update({ 'system.inVault': true })
}
].map(option => ({
...option,
name: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.name}`,
label: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.label}`,
icon: `<i class="${option.icon}"></i>`
}));
@ -391,29 +474,29 @@ export default class CharacterSheet extends DHBaseActorSheet {
* @this {CharacterSheet}
* @protected
*/
static #getEquipamentContextOptions() {
static #getEquipmentContextOptions() {
const options = [
{
name: 'equip',
label: 'equip',
icon: 'fa-solid fa-hands',
condition: target => {
visible: target => {
const doc = getDocFromElementSync(target);
return doc && !doc.system.equipped;
return doc.isOwner && doc && !doc.system.equipped;
},
callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target)
onClick: (event, target) => CharacterSheet.#toggleEquipItem.call(this, event, target)
},
{
name: 'unequip',
label: 'unequip',
icon: 'fa-solid fa-hands',
condition: target => {
visible: target => {
const doc = getDocFromElementSync(target);
return doc && doc.system.equipped;
return doc.isOwner && doc && doc.system.equipped;
},
callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target)
onClick: (event, target) => CharacterSheet.#toggleEquipItem.call(this, event, target)
}
].map(option => ({
...option,
name: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.name}`,
label: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.label}`,
icon: `<i class="${option.icon}"></i>`
}));
@ -639,12 +722,12 @@ export default class CharacterSheet extends DHBaseActorSheet {
}
async updateArmorMarks(event) {
const armor = this.document.system.armor;
if (!armor) return;
const inputValue = Number(event.currentTarget.value);
const { value, max } = this.document.system.armorScore;
const changeValue = Math.min(inputValue - value, max - value);
const maxMarks = this.document.system.armorScore;
const value = Math.min(Math.max(Number(event.currentTarget.value), 0), maxMarks);
await armor.update({ 'system.marks.value': value });
event.currentTarget.value = inputValue < 0 ? 0 : value + changeValue;
this.document.system.updateArmorValue({ value: changeValue });
}
/* -------------------------------------------- */
@ -702,11 +785,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
filter:
key === 'subclasses'
? {
'system.linkedClass.uuid': {
key: 'system.linkedClass.uuid',
value: this.document.system.class.value._stats.compendiumSource
}
}
'system.linkedClass.uuid': {
key: 'system.linkedClass.uuid',
value: this.document.system.class.value?._stats.compendiumSource
}
}
: undefined,
render: {
noFolder: true
@ -720,35 +803,16 @@ export default class CharacterSheet extends DHBaseActorSheet {
* Rolls an attribute check based on the clicked button's dataset attribute.
* @type {ApplicationClickAction}
*/
static async #rollAttribute(event, button) {
const abilityLabel = game.i18n.localize(abilities[button.dataset.attribute].label);
const config = {
event: event,
title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`,
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: abilityLabel
}),
effects: await game.system.api.data.actions.actionsTypes.base.getEffects(this.document),
roll: {
trait: button.dataset.attribute,
type: 'trait'
},
hasRoll: true,
actionType: 'action',
headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`,
title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: abilityLabel
})
};
const result = await this.document.diceRoll(config);
static async #rollAttribute(_event, button) {
const result = await this.document.rollTrait(button.dataset.attribute);
if (!result) return;
/* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */
const costResources =
result.costs?.filter(x => x.enabled).map(cost => ({ ...cost, value: -cost.value, total: -cost.total })) ||
{};
config.resourceUpdates.addResources(costResources);
await config.resourceUpdates.updateResources();
result.resourceUpdates.addResources(costResources);
await result.resourceUpdates.updateResources();
}
//TODO: redo toggleEquipItem method
@ -823,10 +887,13 @@ export default class CharacterSheet extends DHBaseActorSheet {
* 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 });
static async #toggleArmor(_, button, _element) {
const { value, max } = this.document.system.armorScore;
const inputValue = Number.parseInt(button.dataset.value);
const newValue = value >= inputValue ? inputValue - 1 : inputValue;
const changeValue = Math.min(newValue - value, max - value);
this.document.system.updateArmorValue({ value: changeValue });
}
/**
@ -952,6 +1019,99 @@ export default class CharacterSheet extends DHBaseActorSheet {
});
}
static async #toggleArmorManagement(_event, target) {
const existingTooltip = document.body.querySelector('.locked-tooltip .armor-management-container');
if (existingTooltip) {
game.tooltip.dismissLockedTooltips();
return;
}
const armorSources = getArmorSources(this.document)
.filter(s => !s.disabled)
.toReversed()
.map(({ name, document, data }) => ({
...data,
uuid: document.uuid,
name
}));
if (!armorSources.length) return;
const useResourcePips = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.appearance
).useResourcePips;
const html = document.createElement('div');
html.innerHTML = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/armorManagement.hbs`,
{
sources: armorSources,
useResourcePips
}
);
game.tooltip.dismissLockedTooltips();
game.tooltip.activate(target, {
html,
locked: true,
cssClass: 'bordered-tooltip dh-style',
direction: 'DOWN'
});
html.querySelectorAll('.armor-slot').forEach(element => {
element.addEventListener('click', CharacterSheet.armorSourcePipUpdate);
});
}
static async armorSourceInput(event) {
const effect = await foundry.utils.fromUuid(event.target.dataset.uuid);
const value = Math.max(Math.min(Number.parseInt(event.target.value), effect.system.armorData.max), 0);
event.target.value = value;
const progressBar = event.target.closest('.status-bar.armor-slots').querySelector('progress');
progressBar.value = value;
}
/** Update specific armor source */
static async armorSourcePipUpdate(event) {
const target = event.target.closest('.armor-slot');
const { uuid, value } = target.dataset;
const document = await foundry.utils.fromUuid(uuid);
let inputValue = Number.parseInt(value);
let decreasing = false;
let newCurrent = 0;
if (document.type === 'armor') {
decreasing = document.system.armor.current >= inputValue;
newCurrent = decreasing ? inputValue - 1 : inputValue;
await document.update({ 'system.armor.current': newCurrent });
} else if (document.system.armorData) {
const { current } = document.system.armorData;
decreasing = current >= inputValue;
newCurrent = decreasing ? inputValue - 1 : inputValue;
const newChanges = document.system.changes.map(change => ({
...change,
value: change.type === 'armor' ? { ...change.value, current: newCurrent } : change.value
}));
await document.update({ 'system.changes': newChanges });
} else {
return;
}
const container = target.closest('.slot-bar');
for (const armorSlot of container.querySelectorAll('.armor-slot i')) {
const index = Number.parseInt(armorSlot.dataset.index);
if (decreasing && index >= newCurrent) {
armorSlot.classList.remove('fa-shield');
armorSlot.classList.add('fa-shield-halved');
} else if (!decreasing && index < newCurrent) {
armorSlot.classList.add('fa-shield');
armorSlot.classList.remove('fa-shield-halved');
}
}
}
static async #toggleResourceManagement(event, button) {
event.stopPropagation();
const existingTooltip = document.body.querySelector('.locked-tooltip .resource-management-container');
@ -985,12 +1145,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
);
const target = button.closest('.resource-section');
game.tooltip.dismissLockedTooltips();
game.tooltip.activate(target, {
html,
locked: true,
cssClass: 'bordered-tooltip',
cssClass: 'bordered-tooltip dh-style',
direction: 'DOWN',
noOffset: true
});

View file

@ -11,7 +11,17 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
toggleStress: DhCompanionSheet.#toggleStress,
actionRoll: DhCompanionSheet.#actionRoll,
levelManagement: DhCompanionSheet.#levelManagement
}
},
contextMenus: [
{
handler: DHBaseActorSheet.getBaseAttackContextOptions,
selector: '[data-item-uuid][data-type="attack"]',
options: {
parentClassHooks: false,
fixed: true
}
}
]
};
static PARTS = {

View file

@ -0,0 +1,136 @@
import DHBaseActorSheet from '../api/base-actor.mjs';
export default class NPCSheet extends DHBaseActorSheet {
/** @inheritDoc */
static DEFAULT_OPTIONS = {
classes: ['npc'],
position: { width: 660, height: 600 },
window: { resizable: true },
actions: {},
window: {
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 = {
header: { template: 'systems/daggerheart/templates/sheets/actors/npc/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/actors/npc/navigation.hbs' },
features: {
template: 'systems/daggerheart/templates/sheets/actors/npc/features.hbs',
scrollable: ['.feature-section']
},
notes: {
template: 'systems/daggerheart/templates/sheets/actors/npc/notes.hbs'
}
};
/** @inheritdoc */
static TABS = {
primary: {
tabs: [{ id: 'notes' }, { id: 'features' }],
initial: 'notes',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}
};
/** @inheritdoc */
_prepareTabs(group) {
const result = super._prepareTabs(group);
if (group === 'primary') {
result.features.empty = this.document.system.features.length === 0;
}
return result;
}
/** @inheritdoc */
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
switch (partId) {
case 'header':
await this._prepareHeaderContext(context, options);
break;
case 'features':
await this._prepareFeaturesContext(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 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 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
})
};
}
}
}

View file

@ -1,10 +1,9 @@
import DHBaseActorSheet from '../api/base-actor.mjs';
import { getDocFromElement } from '../../../helpers/utils.mjs';
import { getDocFromElement, sortBy } 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';
export default class Party extends DHBaseActorSheet {
@ -18,15 +17,15 @@ export default class Party extends DHBaseActorSheet {
static DEFAULT_OPTIONS = {
classes: ['party'],
position: {
width: 550,
width: 600,
height: 900
},
window: {
resizable: true
},
actions: {
openDocument: Party.#openDocument,
deletePartyMember: Party.#deletePartyMember,
deleteItem: Party.#deleteItem,
toggleHope: Party.#toggleHope,
toggleHitPoints: Party.#toggleHitPoints,
toggleStress: Party.#toggleStress,
@ -35,9 +34,7 @@ export default class Party extends DHBaseActorSheet {
refeshActions: Party.#refeshActions,
triggerRest: Party.#triggerRest,
tagTeamRoll: Party.#tagTeamRoll,
groupRoll: Party.#groupRoll,
selectRefreshable: DaggerheartMenu.selectRefreshable,
refreshActors: DaggerheartMenu.refreshActors
groupRoll: Party.#groupRoll
},
dragDrop: [{ dragSelector: '[data-item-id]', dropSelector: null }]
};
@ -46,16 +43,10 @@ export default class Party extends DHBaseActorSheet {
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',
partyMembers: {
template: 'systems/daggerheart/templates/sheets/actors/party/party-members.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']
@ -66,20 +57,13 @@ export default class Party extends DHBaseActorSheet {
/** @inheritdoc */
static TABS = {
primary: {
tabs: [
{ id: 'partyMembers' },
{ id: 'resources' },
/* NOT YET IMPLEMENTED */
// { id: 'projects' },
{ id: 'inventory' },
{ id: 'notes' }
],
tabs: [{ id: 'partyMembers' }, { id: 'inventory' }, { id: 'notes' }],
initial: 'partyMembers',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}
};
static ALLOWED_ACTOR_TYPES = ['character', 'companion', 'adversary'];
static ALLOWED_ACTOR_TYPES = ['character', 'companion', 'adversary', 'npc'];
static DICE_ROLL_ACTOR_TYPES = ['character'];
async _onRender(context, options) {
@ -92,12 +76,22 @@ export default class Party extends DHBaseActorSheet {
/* Prepare Context */
/* -------------------------------------------- */
async _prepareContext(options) {
const context = await super._prepareContext(options);
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming);
context.showStats =
settings.hidePartyStats === 'never' || (settings.hidePartyStats === 'players' && game.user.isGM);
return context;
}
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
switch (partId) {
case 'header':
await this._prepareHeaderContext(context, options);
break;
case 'partyMembers':
await this._prepareMembersContext(context, options);
case 'notes':
await this._prepareNotesContext(context, options);
break;
@ -120,6 +114,61 @@ export default class Party extends DHBaseActorSheet {
secrets: this.document.isOwner,
relativeTo: this.document
});
context.tagTeamActive = Boolean(this.document.system.tagTeam.initiator);
context.groupRollActive = Boolean(this.document.system.groupRoll.leader);
}
async _prepareMembersContext(context, _options) {
context.partyMembers = [];
const traits = ['agility', 'strength', 'finesse', 'instinct', 'presence', 'knowledge'];
for (const actor of this.document.system.partyMembers) {
const weapons = [];
if (actor.type === 'character') {
if (actor.system.usedUnarmed) {
weapons.push(actor.system.usedUnarmed);
}
const equipped = actor.items.filter(i => i.system.equipped && i.type === 'weapon');
weapons.push(...sortBy(equipped, i => (i.system.secondary ? 1 : 0)));
}
context.partyMembers.push({
uuid: actor.uuid,
img: actor.img,
name: actor.name,
subtitle: (() => {
if (!['character', 'companion'].includes(actor.type)) {
return game.i18n.format(`TYPES.Actor.${actor.type}`);
}
const { value: classItem, subclass } = actor.system.class ?? {};
const partner = actor.system.partner;
const ancestry = actor.system.ancestry;
const community = actor.system.community;
if (partner || (classItem && subclass && ancestry && community)) {
return game.i18n.format(`DAGGERHEART.ACTORS.Party.Subtitle.${actor.type}`, {
class: classItem?.name,
subclass: subclass?.name,
partner: partner?.name,
ancestry: ancestry?.name,
community: community?.name
});
}
})(),
type: actor.type,
resources: actor.system.resources,
armorScore: actor.system.armorScore,
damageThresholds: actor.system.damageThresholds,
evasion: actor.system.evasion,
difficulty: actor.system.difficulty,
traits: actor.system.traits
? traits.map(t => ({
label: game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${t}.short`),
value: actor.system.traits[t].value
}))
: null,
weapons
});
}
}
/**
@ -150,6 +199,12 @@ export default class Party extends DHBaseActorSheet {
}
}
static async #openDocument(_, target) {
const uuid = target.dataset.uuid;
const document = await foundry.utils.fromUuid(uuid);
document?.sheet?.render(true);
}
/**
* Toggles a hope resource value.
* @type {ApplicationClickAction}
@ -190,11 +245,14 @@ export default class Party extends DHBaseActorSheet {
* 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 });
static async #toggleArmorSlot(_, target) {
const actor = await foundry.utils.fromUuid(target.dataset.actorId);
const { value, max } = actor.system.armorScore;
const inputValue = Number.parseInt(target.dataset.value);
const newValue = value >= inputValue ? inputValue - 1 : inputValue;
const changeValue = Math.min(newValue - value, max - value);
await actor.system.updateArmorValue({ value: changeValue });
this.render();
}
@ -229,7 +287,7 @@ export default class Party extends DHBaseActorSheet {
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?',
content: game.i18n.localize('DAGGERHEART.ACTORS.Party.triggerRestContent'),
classes: ['daggerheart', 'dialog', 'dh-style']
});
@ -248,24 +306,18 @@ export default class Party extends DHBaseActorSheet {
static async downtimeMoveQuery({ actorId, downtimeType }) {
const actor = await foundry.utils.fromUuid(actorId);
if (!actor || !actor?.isOwner) reject();
if (!actor || !actor?.isOwner) return;
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
});
new game.system.api.applications.dialogs.TagTeamDialog(this.document).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 });
new game.system.api.applications.dialogs.GroupRollDialog(this.document).render({ force: true });
}
/* -------------------------------------------- */
@ -427,43 +479,22 @@ export default class Party extends DHBaseActorSheet {
}
static async #deletePartyMember(event, target) {
const doc = await getDocFromElement(target.closest('.inventory-item'));
const doc = await foundry.utils.fromUuid(target.closest('[data-uuid]')?.dataset.uuid);
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'),
title: game.i18n.format('DAGGERHEART.ACTORS.Party.RemoveConfirmation.title', {
name: doc.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: doc.name })
content: game.i18n.format('DAGGERHEART.ACTORS.Party.RemoveConfirmation.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]);
const newMembersList = currentMembers.filter(uuid => uuid !== doc.uuid);
await this.document.update({ 'system.partyMembers': newMembersList });
}
}

View file

@ -72,20 +72,15 @@ const typeSettingsMap = {
*/
export default function DHApplicationMixin(Base) {
class DHSheetV2 extends HandlebarsApplicationMixin(Base) {
#nonHeaderAttribution = ['environment', 'ancestry', 'community', 'domainCard'];
/**
* @param {DHSheetV2Configuration} [options={}]
*/
constructor(options = {}) {
super(options);
/**
* @type {foundry.applications.ux.DragDrop[]}
* @private
*/
this._dragDrop = this._createDragDropHandlers();
}
#nonHeaderAttribution = ['environment', 'ancestry', 'community', 'domainCard'];
/**
* The default options for the sheet.
* @type {DHSheetV2Configuration}
@ -94,7 +89,7 @@ export default function DHApplicationMixin(Base) {
classes: ['daggerheart', 'sheet', 'dh-style'],
actions: {
triggerContextMenu: DHSheetV2.#triggerContextMenu,
createDoc: DHSheetV2.#createDoc,
createDoc: DHSheetV2.#onCreateDoc,
editDoc: DHSheetV2.#editDoc,
deleteDoc: DHSheetV2.#deleteDoc,
toChat: DHSheetV2.#toChat,
@ -102,8 +97,8 @@ export default function DHApplicationMixin(Base) {
viewItem: DHSheetV2.#viewItem,
toggleEffect: DHSheetV2.#toggleEffect,
toggleExtended: DHSheetV2.#toggleExtended,
addNewItem: DHSheetV2.#addNewItem,
browseItem: DHSheetV2.#browseItem,
addNewItem: DHSheetV2.#onAddNewItem,
browseItem: DHSheetV2.#onBrowseItem,
editAttribution: DHSheetV2.#editAttribution
},
contextMenus: [
@ -177,7 +172,6 @@ export default function DHApplicationMixin(Base) {
/**@inheritdoc */
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
this._dragDrop.forEach(d => d.bind(htmlElement));
// Handle delta inputs
for (const deltaInput of htmlElement.querySelectorAll('input[data-allow-delta]')) {
@ -290,6 +284,16 @@ export default function DHApplicationMixin(Base) {
async _onRender(context, options) {
await super._onRender(context, options);
this._createTagifyElements(this.options.tagifyConfigs);
for (const d of this.options.dragDrop) {
new foundry.applications.ux.DragDrop.implementation({
...d,
callbacks: {
dragstart: this._onDragStart.bind(this),
drop: this._onDrop.bind(this)
}
}).bind(this.element);
}
}
/* -------------------------------------------- */
@ -350,21 +354,6 @@ export default function DHApplicationMixin(Base) {
/* Drag and Drop */
/* -------------------------------------------- */
/**
* Creates drag-drop handlers from the configured options.
* @returns {foundry.applications.ux.DragDrop[]}
* @private
*/
_createDragDropHandlers() {
return this.options.dragDrop.map(d => {
d.callbacks = {
dragstart: this._onDragStart.bind(this),
drop: this._onDrop.bind(this)
};
return new foundry.applications.ux.DragDrop.implementation(d);
});
}
/**
* Handle dragStart event.
* @param {DragEvent} event
@ -429,26 +418,26 @@ export default function DHApplicationMixin(Base) {
/**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */
const options = [
{
name: 'disableEffect',
label: 'disableEffect',
icon: 'fa-solid fa-lightbulb',
condition: element => {
visible: element => {
const target = element.closest('[data-item-uuid]');
return !target.dataset.disabled && target.dataset.itemType !== 'beastform';
},
callback: async target => (await getDocFromElement(target)).update({ disabled: true })
onClick: async (_, target) => (await getDocFromElement(target)).update({ disabled: true })
},
{
name: 'enableEffect',
label: 'enableEffect',
icon: 'fa-regular fa-lightbulb',
condition: element => {
visible: element => {
const target = element.closest('[data-item-uuid]');
return target.dataset.disabled && target.dataset.itemType !== 'beastform';
},
callback: async target => (await getDocFromElement(target)).update({ disabled: false })
onClick: async (_, target) => (await getDocFromElement(target)).update({ disabled: false })
}
].map(option => ({
...option,
name: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.name}`,
label: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.label}`,
icon: `<i class="${option.icon}"></i>`
}));
@ -479,29 +468,34 @@ export default function DHApplicationMixin(Base) {
_getContextMenuCommonOptions({ usable = false, toChat = false, deletable = true }) {
const options = [
{
name: 'CONTROLS.CommonEdit',
label: 'CONTROLS.CommonEdit',
icon: 'fa-solid fa-pen-to-square',
condition: target => {
visible: target => {
const { dataset } = target.closest('[data-item-uuid]');
const doc = getDocFromElementSync(target);
return (
(!dataset.noCompendiumEdit && !doc) ||
(doc && (!doc?.hasOwnProperty('systemPath') || doc?.inCollection))
(doc?.isOwner && (!doc?.hasOwnProperty('systemPath') || doc?.inCollection))
);
},
callback: async target => (await getDocFromElement(target)).sheet.render({ force: true })
onClick: async (_, target) => {
return (await getDocFromElement(target)).sheet.render({ force: true });
}
}
];
if (usable) {
options.unshift({
name: 'DAGGERHEART.GENERAL.damage',
label: 'DAGGERHEART.GENERAL.damage',
icon: 'fa-solid fa-explosion',
condition: target => {
visible: target => {
const doc = getDocFromElementSync(target);
return doc?.system?.attack?.damage.parts.length || doc?.damage?.parts.length;
const hasDamage =
!foundry.utils.isEmpty(doc?.system?.attack?.damage.parts) ||
!foundry.utils.isEmpty(doc?.damage?.parts);
return doc?.isOwner && hasDamage;
},
callback: async (target, event) => {
onClick: async (event, target) => {
const doc = await getDocFromElement(target),
action = doc?.system?.attack ?? doc;
const config = action.prepareConfig(event);
@ -515,32 +509,33 @@ export default function DHApplicationMixin(Base) {
});
options.unshift({
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem',
label: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem',
icon: 'fa-solid fa-burst',
condition: target => {
visible: target => {
const doc = getDocFromElementSync(target);
return doc && !(doc.type === 'domainCard' && doc.system.inVault);
return doc?.isOwner && !(doc.type === 'domainCard' && doc.system.inVault);
},
callback: async (target, event) => (await getDocFromElement(target)).use(event)
onClick: async (event, target) => (await getDocFromElement(target)).use(event)
});
}
if (toChat)
options.push({
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
label: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
icon: 'fa-solid fa-message',
callback: async target => (await getDocFromElement(target)).toChat(this.document.uuid)
onClick: async (_, target) => (await getDocFromElement(target)).toChat(this.document.uuid)
});
if (deletable)
options.push({
name: 'CONTROLS.CommonDelete',
label: 'CONTROLS.CommonDelete',
icon: 'fa-solid fa-trash',
condition: element => {
visible: element => {
const target = element.closest('[data-item-uuid]');
return target.dataset.itemType !== 'beastform';
const doc = getDocFromElementSync(target);
return doc?.isOwner !== false && target.dataset.itemType !== 'beastform';
},
callback: async (target, event) => {
onClick: async (event, target) => {
const doc = await getDocFromElement(target);
if (event.shiftKey) return doc.delete();
else return doc.deleteDialog();
@ -646,18 +641,18 @@ export default function DHApplicationMixin(Base) {
/* Application Clicks Actions */
/* -------------------------------------------- */
static async #addNewItem(event, target) {
static async #onAddNewItem(event, target) {
const createChoice = await foundry.applications.api.DialogV2.wait({
classes: ['dh-style', 'two-big-buttons'],
buttons: [
{
action: 'create',
label: 'Create Item',
label: game.i18n.localize('DAGGERHEART.APPLICATIONS.CreateItemDialog.createItem'),
icon: 'fa-solid fa-plus'
},
{
action: 'browse',
label: 'Browse Compendium',
label: game.i18n.localize('DAGGERHEART.APPLICATIONS.CreateItemDialog.browseCompendium'),
icon: 'fa-solid fa-book'
}
]
@ -665,11 +660,11 @@ export default function DHApplicationMixin(Base) {
if (!createChoice) return;
if (createChoice === 'browse') return DHSheetV2.#browseItem.call(this, event, target);
else return DHSheetV2.#createDoc.call(this, event, target);
if (createChoice === 'browse') return DHSheetV2.#onBrowseItem.call(this, event, target);
else return DHSheetV2.#onCreateDoc.call(this, event, target);
}
static async #browseItem(event, target) {
static async #onBrowseItem(_event, target) {
const type = target.dataset.compendium ?? target.dataset.type;
const presets = {
@ -720,17 +715,17 @@ export default function DHApplicationMixin(Base) {
* Create an embedded document.
* @type {ApplicationClickAction}
*/
static async #createDoc(event, target) {
static async #onCreateDoc(event, target) {
const { documentClass, type, inVault, disabled } = target.dataset;
const parentIsItem = this.document.documentName === 'Item';
const featureOnCharacter = this.document.parent?.type === 'character' && type === 'feature';
const parent = featureOnCharacter
? this.document.parent
: parentIsItem && documentClass === 'Item'
? type === 'action'
? this.document.system
: null
: this.document;
? type === 'action'
? this.document.system
: null
: this.document;
let systemData = {};
if (featureOnCharacter) {
@ -742,15 +737,21 @@ export default function DHApplicationMixin(Base) {
const cls =
type === 'action' ? game.system.api.models.actions.actionsTypes.base : getDocumentClass(documentClass);
const data = {
name: cls.defaultName({ type, parent }),
type,
system: systemData
};
if (inVault) data['system.inVault'] = true;
if (disabled) data.disabled = true;
if (type === 'domainCard' && parent?.system.domains?.length) {
data.system.domain = parent.system.domains[0];
if (type === 'domainCard') {
if (parent?.system.domains?.length) data.system.domain = parent.system.domains[0];
if (inVault) data.system.inVault = true;
} else if (type === 'weapon') {
// Passing an empty system object to weapon causes validation failure due to attack action initialization
// todo: determine why, fix it at its source, then remove this fallback
delete data.system;
}
const doc = await cls.create(data, { parent, renderSheet: !event.shiftKey });

View file

@ -73,7 +73,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
.hideAttribution;
// Prepare inventory data
if (['party', 'character'].includes(this.document.type)) {
if (this.document.system.metadata.hasInventory) {
context.inventory = {
currencies: {},
weapons: this.document.itemTypes.weapon.sort((a, b) => a.sort - b.sort),
@ -160,12 +160,21 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
inactives: []
};
for (const effect of this.actor.allApplicableEffects()) {
for (const effect of this.actor.allApplicableEffects({ noTransferArmor: true })) {
const list = effect.active ? context.effects.actives : context.effects.inactives;
list.push(effect);
}
}
/** Add support for input content editables */
_toggleDisabled(disabled) {
super._toggleDisabled(disabled);
const form = this.form;
for (const element of form.querySelectorAll('.input[contenteditable]')) {
element.classList.toggle('disabled', disabled);
}
}
/* -------------------------------------------- */
/* Context Menu */
/* -------------------------------------------- */
@ -180,6 +189,43 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true });
}
/**
* Get the set of ContextMenu options for the base attack.
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
* @this {CharacterSheet}
* @protected
*/
static getBaseAttackContextOptions() {
/**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */
return [
{
label: 'DAGGERHEART.CONFIG.RollTypes.attack.name',
icon: 'fa-solid fa-burst',
onClick: async (event, target) => (await getDocFromElement(target)).use(event)
},
{
label: 'DAGGERHEART.GENERAL.damage',
icon: 'fa-solid fa-explosion',
onClick: async (event, target) => {
const doc = await getDocFromElement(target),
action = doc?.system?.attack ?? doc;
const config = action.prepareConfig(event);
config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(
this.document,
doc
);
config.hasRoll = false;
return action && action.workflow.get('damage').execute(config, null, true);
}
},
{
label: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
icon: 'fa-solid fa-message',
onClick: async (_, target) => (await getDocFromElement(target)).toChat(this.document.uuid)
}
];
}
/* -------------------------------------------- */
/* Application Listener Actions */
/* -------------------------------------------- */
@ -228,7 +274,6 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
'systems/daggerheart/templates/ui/chat/action.hbs',
systemData
),
title: game.i18n.localize('DAGGERHEART.ACTIONS.Config.displayInChat'),
speaker: cls.getSpeaker(),
flags: {
daggerheart: {
@ -284,11 +329,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
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)
) {
if (!originActor || originActor.uuid === this.document.uuid || !this.document.system.metadata.hasInventory) {
return super._onDropItem(event, item);
}
@ -303,47 +344,79 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
);
}
if (item.system.metadata.isQuantifiable) {
const actorItem = originActor.items.get(data.originId);
const quantityTransfered = await game.system.api.applications.dialogs.ItemTransferDialog.configure({
// Perform the actual transfer, showing a dialog when doing it
const availableQuantity = Math.max(1, item.system.quantity);
const actorItem = originActor.items.get(data.originId) ?? item;
if (availableQuantity > 1) {
const quantityTransferred = await game.system.api.applications.dialogs.ItemTransferDialog.configure({
item,
targetActor: this.document
});
if (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
}
}
]);
}
if (quantityTransfered === actorItem.system.quantity) {
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
} else {
await actorItem.update({
'system.quantity': actorItem.system.quantity - quantityTransfered
});
}
}
return this.#transferItem(actorItem, quantityTransferred);
} else {
await this.document.createEmbeddedDocuments('Item', [item.toObject()]);
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
return this.#transferItem(actorItem, availableQuantity);
}
}
}
/**
* Helper to perform the actual transfer of an item to this actor, including stack/unstack logic based on target quantifiability.
* Make sure item is the actor item before calling this method or there will be issues
*/
async #transferItem(item, quantity) {
const originActor = item.actor;
const targetActor = this.document;
const allowStacking = targetActor.system.metadata.quantifiable?.includes(item.type);
const batch = [];
// First add/update the item to the target actor
const existing = allowStacking ? targetActor.items.find(x => itemIsIdentical(x, item)) : null;
if (existing) {
batch.push({
action: 'update',
documentName: 'Item',
parent: targetActor,
updates: [{ _id: existing.id, 'system.quantity': existing.system.quantity + quantity }]
});
} else {
const itemsToCreate = [];
if (allowStacking) {
itemsToCreate.push(foundry.utils.mergeObject(item.toObject(true), { system: { quantity } }));
} else {
const createData = new Array(Math.max(1, quantity))
.fill(0)
.map(() => foundry.utils.mergeObject(item.toObject(), { system: { quantity: 1 } }));
itemsToCreate.push(...createData);
}
batch.push({
action: 'create',
documentName: 'Item',
parent: targetActor,
data: itemsToCreate
});
}
// Remove the item from the original actor (by either deleting it, or updating its quantity)
if (quantity >= item.system.quantity) {
batch.push({
action: 'delete',
documentName: 'Item',
parent: originActor,
ids: [item.id]
});
} else {
batch.push({
action: 'update',
documentName: 'Item',
parent: originActor,
updates: [{ _id: item.id, 'system.quantity': item.system.quantity - quantity }]
});
}
return foundry.documents.modifyBatch(batch);
}
/**
* On dragStart on the item.
* @param {DragEvent} event - The drag event

View file

@ -126,7 +126,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
options.push({
name: 'CONTROLS.CommonDelete',
icon: '<i class="fa-solid fa-trash"></i>',
callback: async target => {
onClick: async (_, target) => {
const feature = await getDocFromElement(target);
if (!feature) return;
const confirmed = await foundry.applications.api.DialogV2.confirm({

View file

@ -29,16 +29,6 @@ export default function ItemAttachmentSheet(Base) {
}
};
async _preparePartContext(partId, context) {
await super._preparePartContext(partId, context);
if (partId === 'attachments') {
context.attachedItems = await prepareAttachmentContext(this.document);
}
return context;
}
async _onDrop(event) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);

View file

@ -31,11 +31,12 @@ export default class AncestrySheet extends DHHeritageSheet {
if (data.type === 'ActiveEffect') return super._onDrop(event);
const target = event.target.closest('fieldset.drop-section');
const typeField =
this.document.system[target.dataset.type === 'primary' ? 'primaryFeature' : 'secondaryFeature'];
if (!typeField) {
super._onDrop(event);
if (target) {
const typeField =
this.document.system[target.dataset.type === 'primary' ? 'primaryFeature' : 'secondaryFeature'];
if (!typeField) {
super._onDrop(event);
}
}
}
}

View file

@ -47,6 +47,15 @@ export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
return context;
}
async updateArmorEffect(event) {
const value = Number.parseInt(event.target.value);
const armorEffect = this.document.system.armorEffect;
if (Number.isNaN(value) || !armorEffect) return;
await armorEffect.system.armorChange.updateArmorMax(value);
this.render();
}
/**
* Callback function used by `tagifyElement`.
* @param {Array<Object>} selectedOptions - The currently selected tag objects.

View file

@ -102,7 +102,7 @@ export default class BeastformSheet extends DHBaseItemSheet {
async advantageOnRemove(event) {
await this.document.update({
[`system.advantageOn.-=${event.detail.data.value}`]: null
[`system.advantageOn.${event.detail.data.value}`]: _del
});
}
}

View file

@ -104,9 +104,10 @@ export default class ClassSheet extends DHBaseItemSheet {
}
/**@inheritdoc */
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.domains = this.document.system.domains;
context.subclasses = await this.document.system.fetchSubclasses();
return context;
}
@ -128,20 +129,8 @@ export default class ClassSheet extends DHBaseItemSheet {
const item = await fromUuid(data.uuid);
const itemType = data.type === 'ActiveEffect' ? data.type : item.type;
const target = event.target.closest('fieldset.drop-section');
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({
'system.subclasses': [...this.document.system.subclasses.map(x => x.uuid), item.uuid]
});
} else if (['feature', 'ActiveEffect'].includes(itemType)) {
if (['feature', 'ActiveEffect'].includes(itemType)) {
super._onDrop(event);
} else if (this.document.parent?.type !== 'character') {
if (itemType === 'weapon') {
@ -200,12 +189,6 @@ export default class ClassSheet extends DHBaseItemSheet {
static async #removeItemFromCollection(_event, element) {
const { uuid, target } = element.dataset;
const prop = foundry.utils.getProperty(this.document.system, target);
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,7 +31,7 @@ export default class FeatureSheet extends DHBaseItemSheet {
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}
};
//Might be wrong location but testing out if here is okay.
//Might be wrong location but testing out if here is okay.
/**@override */
async _prepareContext(options) {
const context = await super._prepareContext(options);

View file

@ -40,4 +40,36 @@ export default class SubclassSheet extends DHBaseItemSheet {
get relatedDocs() {
return this.document.system.features.map(x => x.item);
}
async _prepareContext(options) {
const context = await super._prepareContext(options);
if (this.document.system.linkedClass) {
const classData = await fromUuid(this.document.system.linkedClass);
context.class = classData ?? {
name: _loc('DAGGERHEART.GENERAL.missingX', { x: _loc('TYPES.Item.class') }),
missing: true
};
}
return context;
}
async _onDrop(event) {
event.stopPropagation();
const data = TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid);
const itemType = data.type === 'ActiveEffect' ? data.type : item.type;
if (itemType === 'class') {
const uuid = item._stats.compendiumSource ?? item.uuid;
if (this.document.system.linkedClass !== uuid) {
await this.document.update({ 'system.linkedClass': uuid });
// Re-render all class sheets for instant feedback
for (const app of foundry.applications.instances.values()) {
if (app.document?.type === 'class') app.render();
}
}
return;
}
return super._onDrop(event);
}
}

View file

@ -108,14 +108,15 @@ export default class DhRollTableSheet extends foundry.applications.sheets.RollTa
getSystemFlagUpdate() {
const deleteUpdate = Object.keys(this.document._source.flags.daggerheart?.altFormula ?? {}).reduce(
(acc, formulaKey) => {
if (!this.daggerheartFlag.altFormula[formulaKey]) acc.altFormula[`-=${formulaKey}`] = null;
if (!this.daggerheartFlag.altFormula[formulaKey]) acc.altFormula[formulaKey] = _del;
return acc;
},
{ altFormula: {} }
);
return { ['flags.daggerheart']: foundry.utils.mergeObject(this.daggerheartFlag.toObject(), deleteUpdate) };
const flagData = this.daggerheartFlag.toObject();
return { ...flagData, altFormula: { ...flagData.altFormula, ...deleteUpdate.altFormula } };
}
static async #addFormula() {
@ -127,7 +128,7 @@ export default class DhRollTableSheet extends foundry.applications.sheets.RollTa
static async #removeFormula(_event, target) {
await this.daggerheartFlag.updateSource({
[`altFormula.-=${target.dataset.key}`]: null
[`altFormula.${target.dataset.key}`]: _del
});
this.render({ internalRefresh: true });
}

View file

@ -1,52 +1,19 @@
export default class DhSidebar extends foundry.applications.sidebar.Sidebar {
static buildTabs() {
const { settings, ...tabs } = super.TABS;
return {
...tabs,
daggerheartMenu: {
tooltip: 'DAGGERHEART.UI.Sidebar.daggerheartMenu.title',
img: 'systems/daggerheart/assets/logos/FoundryBorneLogoWhite.svg',
gmOnly: true
},
settings
};
}
/** @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'
}
};
static TABS = DhSidebar.buildTabs();
/** @override */
static PARTS = {

View file

@ -13,8 +13,8 @@ export default class DhActorDirectory extends foundry.applications.sidebar.tabs.
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;
? game.i18n.localize(environmentTypes[document.system.type]?.label ?? 'TYPES.Actor.environment')
: null;
};
}
@ -46,50 +46,67 @@ export default class DhActorDirectory extends foundry.applications.sidebar.tabs.
_getEntryContextOptions() {
const options = super._getEntryContextOptions();
options.push({
name: 'DAGGERHEART.UI.Sidebar.actorDirectory.duplicateToNewTier',
icon: `<i class="fa-solid fa-arrow-trend-up" inert></i>`,
condition: li => {
const actor = game.actors.get(li.dataset.entryId);
return actor?.type === 'adversary' && actor.system.type !== 'social';
},
callback: async li => {
const actor = game.actors.get(li.dataset.entryId);
if (!actor) throw new Error('Unexpected missing actor');
options.push(
{
label: 'DAGGERHEART.UI.Sidebar.actorDirectory.duplicateToNewTier',
icon: `<i class="fa-solid fa-arrow-trend-up" inert></i>`,
visible: li => {
const actor = game.actors.get(li.dataset.entryId);
return actor?.type === 'adversary' && actor.system.type !== 'social';
},
callback: async li => {
const actor = game.actors.get(li.dataset.entryId);
if (!actor) throw new Error('Unexpected missing actor');
const tiers = [1, 2, 3, 4].filter(t => t !== actor.system.tier);
const content = document.createElement('div');
const select = document.createElement('select');
select.name = 'tier';
select.append(
...tiers.map(t => {
const option = document.createElement('option');
option.value = t;
option.textContent = game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${t}`);
return option;
})
);
content.append(select);
const tiers = [1, 2, 3, 4].filter(t => t !== actor.system.tier);
const content = document.createElement('div');
const select = document.createElement('select');
select.name = 'tier';
select.append(
...tiers.map(t => {
const option = document.createElement('option');
option.value = t;
option.textContent = game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${t}`);
return option;
})
);
content.append(select);
const tier = await foundry.applications.api.Dialog.input({
classes: ['dh-style', 'dialog'],
window: { title: 'DAGGERHEART.UI.Sidebar.actorDirectory.pickTierTitle' },
content,
ok: {
label: 'Create Adversary',
callback: (event, button, dialog) => Number(button.form.elements.tier.value)
const tier = await foundry.applications.api.Dialog.input({
classes: ['dh-style', 'dialog'],
window: { title: 'DAGGERHEART.UI.Sidebar.actorDirectory.pickTierTitle' },
content,
ok: {
label: 'DAGGERHEART.UI.Sidebar.actorDirectory.createAdversary',
callback: (event, button, dialog) => Number(button.form.elements.tier.value)
}
});
if (tier === actor.system.tier) {
ui.notifications.warn('This actor is already at this tier');
} else if (tier) {
const source = actor.system.adjustForTier(tier);
await Actor.create(source);
ui.notifications.info(`Tier ${tier} ${actor.name} created`);
}
});
}
},
{
label: 'DAGGERHEART.UI.Sidebar.actorDirectory.activateParty',
icon: `<i class="fa-regular fa-square"></i>`,
visible: li => {
const actor = game.actors.get(li.dataset.entryId);
return actor && actor.type === 'party' && !actor.system.active;
},
callback: async li => {
const actor = game.actors.get(li.dataset.entryId);
if (!actor) throw new Error('Unexpected missing actor');
if (tier === actor.system.tier) {
ui.notifications.warn('This actor is already at this tier');
} else if (tier) {
const source = actor.system.adjustForTier(tier);
await Actor.create(source);
ui.notifications.info(`Tier ${tier} ${actor.name} created`);
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.ActiveParty, actor.id);
ui.actors.render();
}
}
});
);
return options;
}

View file

@ -31,7 +31,8 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
},
actions: {
selectRefreshable: DaggerheartMenu.#selectRefreshable,
refreshActors: DaggerheartMenu.#refreshActors
refreshActors: DaggerheartMenu.#refreshActors,
createFallCollisionDamage: DaggerheartMenu.#createFallCollisionDamage
}
};
@ -50,6 +51,7 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
const context = await super._prepareContext(options);
context.refreshables = this.refreshSelections;
context.disableRefresh = Object.values(this.refreshSelections).every(x => !x.selected);
context.fallAndCollision = CONFIG.DH.GENERAL.fallAndCollisionDamage;
return context;
}
@ -71,4 +73,22 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
this.refreshSelections = DaggerheartMenu.defaultRefreshSelections();
this.render();
}
static async #createFallCollisionDamage(_event, button) {
const data = CONFIG.DH.GENERAL.fallAndCollisionDamage[button.dataset.key];
const roll = new Roll(data.damageFormula);
await roll.evaluate();
/* class BaseRoll needed to get rendered by foundryRoll.hbs */
const rollJSON = roll.toJSON();
rollJSON.class = 'BaseRoll';
foundry.documents.ChatMessage.implementation.create({
title: game.i18n.localize(data.chatTitle),
author: game.user.id,
speaker: foundry.documents.ChatMessage.implementation.getSpeaker(),
rolls: [rollJSON],
sound: CONFIG.sounds.dice
});
}
}

View file

@ -7,3 +7,4 @@ export { default as DhFearTracker } from './fearTracker.mjs';
export { default as DhHotbar } from './hotbar.mjs';
export { default as DhSceneNavigation } from './sceneNavigation.mjs';
export { ItemBrowser } from './itemBrowser.mjs';
export { default as DhProgress } from './progress.mjs';

View file

@ -1,5 +1,6 @@
import { abilities } from '../../config/actorConfig.mjs';
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import { enrichedDualityRoll } from '../../enrichers/DualityRollEnricher.mjs';
import { enrichedFateRoll, getFateTypeData } from '../../enrichers/FateRollEnricher.mjs';
import { getCommandTarget, rollCommandToJSON } from '../../helpers/utils.mjs';
export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog {
constructor(options) {
@ -21,35 +22,114 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
classes: ['daggerheart']
};
static CHAT_COMMANDS = {
...super.CHAT_COMMANDS,
dr: {
rgx: /^(?:\/dr)((?:\s)[^]*)?/,
fn: (_, match) => {
const argString = match[1]?.trim();
const result = argString ? rollCommandToJSON(argString) : { result: {} };
if (!result) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.dualityParsing'));
return false;
}
const { result: rollCommand, flavor } = result;
const reaction = rollCommand.reaction;
const traitValue = rollCommand.trait?.toLowerCase();
const advantage = rollCommand.advantage
? CONFIG.DH.ACTIONS.advantageState.advantage.value
: rollCommand.disadvantage
? CONFIG.DH.ACTIONS.advantageState.disadvantage.value
: undefined;
const difficulty = rollCommand.difficulty;
const grantResources = rollCommand.grantResources;
const target = getCommandTarget({ allowNull: true });
const title =
(flavor ?? traitValue)
? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(CONFIG.DH.ACTOR.abilities[traitValue].label)
})
: game.i18n.localize('DAGGERHEART.GENERAL.duality');
enrichedDualityRoll({
reaction,
traitValue,
target,
difficulty,
title,
label: game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll'),
actionType: null,
advantage,
grantResources
});
return false;
}
},
fr: {
rgx: /^(?:\/fr)((?:\s)[^]*)?/,
fn: (_, match) => {
const argString = match[1]?.trim();
const result = argString ? rollCommandToJSON(argString) : { result: {} };
if (!result) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateParsing'));
return false;
}
const { result: rollCommand, flavor } = result;
const fateTypeData = getFateTypeData(rollCommand?.type);
if (!fateTypeData)
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing'));
const { value: fateType, label: fateTypeLabel } = fateTypeData;
const target = getCommandTarget({ allowNull: true });
const title = flavor ?? game.i18n.localize('DAGGERHEART.GENERAL.fateRoll');
enrichedFateRoll({
target,
title,
label: fateTypeLabel,
fateType
});
return false;
}
}
};
_getEntryContextOptions() {
return [
...super._getEntryContextOptions(),
// {
// name: 'Reroll',
// icon: '<i class="fa-solid fa-dice"></i>',
// condition: li => {
// const message = game.messages.get(li.dataset.messageId);
// return (game.user.isGM || message.isAuthor) && message.rolls.length > 0;
// },
// callback: li => {
// const message = game.messages.get(li.dataset.messageId);
// new game.system.api.applications.dialogs.RerollDialog(message).render({ force: true });
// }
// },
{
name: game.i18n.localize('DAGGERHEART.UI.ChatLog.rerollDamage'),
label: 'DAGGERHEART.UI.ChatLog.rerollActionRoll',
icon: '<i class="fa-solid fa-dice"></i>',
condition: li => {
visible: li => {
const message = game.messages.get(li.dataset.messageId);
return message.system.hasRoll && (game.user.isGM || message.isAuthor);
},
callback: async li => {
const message = game.messages.get(li.dataset.messageId);
const reroll = await message.rolls[0].reroll({ liveRoll: true });
message.update({ rolls: [reroll] });
}
},
{
label: 'DAGGERHEART.UI.ChatLog.rerollDamage',
icon: '<i class="fa-solid fa-dice"></i>',
visible: li => {
const message = game.messages.get(li.dataset.messageId);
const hasRolledDamage = message.system.hasDamage
? Object.keys(message.system.damage).length > 0
: false;
return (game.user.isGM || message.isAuthor) && hasRolledDamage;
},
callback: li => {
callback: async li => {
const message = game.messages.get(li.dataset.messageId);
new game.system.api.applications.dialogs.RerollDamageDialog(message).render({ force: true });
const update = await message.system.getRerolledDamage();
message.update(update);
}
}
];
@ -69,18 +149,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
html.querySelectorAll('.reroll-button').forEach(element =>
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)
);
html.querySelectorAll('.risk-it-all-button').forEach(element =>
element.addEventListener('click', event => this.riskItAllClearStressAndHitPoints(event, data))
);
@ -175,7 +243,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
action.use(event);
}
async rerollEvent(event, message) {
async rerollEvent(event, messageData) {
event.stopPropagation();
if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
@ -187,206 +255,41 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
if (!confirmed) return;
}
const message = game.messages.get(messageData._id);
const target = event.target.closest('[data-die-index]');
if (target.dataset.type === 'damage') {
game.system.api.dice.DamageRoll.reroll(target, message);
} else {
let originalRoll_parsed = message.rolls.map(roll => JSON.parse(roll))[0];
const rollClass =
game.system.api.dice[
message.type === 'dualityRoll'
? 'DualityRoll'
: target.dataset.type === 'damage'
? 'DHRoll'
: 'D20Roll'
];
if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
const { newRoll, parsedRoll } = await rollClass.reroll(originalRoll_parsed, target, message);
await game.messages.get(message._id).update({
'system.roll': newRoll,
'rolls': [parsedRoll]
const { damageType, part, dice, result } = target.dataset;
const damagePart = message.system.damage[damageType].parts[part];
const { parsedRoll, rerolledDice } = await game.system.api.dice.DamageRoll.reroll(damagePart, dice, result);
const damageParts = message.system.damage[damageType].parts.map((damagePart, index) => {
if (index !== Number(part)) return damagePart;
return {
...damagePart,
total: parsedRoll.total,
dice: rerolledDice
};
});
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
const updateMessage = game.messages.get(message._id);
await updateMessage.update({
[`system.damage.${damageType}`]: {
total: parsedRoll.total,
parts: damageParts
}
});
}
}
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');
} else {
const rerollDice = message.system.roll.dice[target.dataset.dieIndex];
await rerollDice.reroll(`/r1=${rerollDice.total}`, {
liveRoll: {
roll: message.system.roll,
actor: message.system.actionActor,
isReaction: message.system.roll.options.actionType === 'reaction'
}
});
event.target.closest('.group-roll-section').querySelector('.group-roll-content').classList.toggle('closed');
await message.update({
rolls: [message.system.roll.toJSON()]
});
}
}
async riskItAllClearStressAndHitPoints(event, data) {

View file

@ -1,4 +1,6 @@
import { AdversaryBPPerEncounter } from '../../config/encounterConfig.mjs';
import { expireActiveEffects } from '../../helpers/utils.mjs';
import { clearPreviousSpotlight } from '../../macros/spotlightCombatant.mjs';
export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
static DEFAULT_OPTIONS = {
@ -54,7 +56,9 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
async _prepareTrackerContext(context, options) {
await super._prepareTrackerContext(context, options);
const adversaries = context.turns?.filter(x => x.isNPC) ?? [];
const npcs = context.turns?.filter(x => x.isNPC) ?? [];
const adversaries = npcs.filter(x => x.disposition !== CONST.TOKEN_DISPOSITIONS.FRIENDLY);
const friendlies = npcs.filter(x => x.disposition === CONST.TOKEN_DISPOSITIONS.FRIENDLY);
const characters = context.turns?.filter(x => !x.isNPC) ?? [];
const spotlightQueueEnabled = game.settings.get(
CONFIG.DH.id,
@ -73,25 +77,56 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
Object.assign(context, {
actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens,
adversaries,
friendlies,
allCharacters: characters,
characters: characters.filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0),
spotlightRequests
});
}
/**
* Open the dialog used to edit the name of the currently viewed Combat encounter.
* @this {CombatTracker}
* @returns {Promise<void>}
*/
static async #onEditName() {
const combat = this.viewed;
if (!combat || !game.user.isGM) return null;
const field = combat.schema.fields.name;
const inputHTML = field.toFormGroup({}, { name: 'name', value: combat.name, autofocus: true }).outerHTML;
const formData = await foundry.applications.api.DialogV2.input({
window: { icon: 'fa-solid fa-tag', title: 'COMBAT.ACTIONS.EditNameTitle' },
position: { width: 480 },
content: inputHTML
});
await combat.update({ name: formData.name || '' });
}
_getCombatContextOptions() {
return [
{
name: 'COMBAT.ClearMovementHistories',
icon: '<i class="fa-solid fa-shoe-prints"></i>',
condition: () => game.user.isGM && this.viewed?.combatants.size > 0,
callback: () => this.viewed.clearMovementHistories()
label: 'COMBAT.ACTIONS.EditName',
icon: 'fa-solid fa-tag',
visible: () => game.user.isGM && !!this.viewed,
onClick: () => DhCombatTracker.#onEditName.call(this)
},
{
name: 'COMBAT.Delete',
icon: '<i class="fa-solid fa-trash"></i>',
condition: () => game.user.isGM && !!this.viewed,
callback: () => this.viewed.endCombat()
label: 'COMBAT.ACTIONS.LinkToScene',
icon: '<i class="fa-solid fa-link"></i>',
visible: () => game.user.isGM && !this.scene,
onClick: () => this.viewed.toggleSceneLink()
},
{
label: 'COMBAT.ACTIONS.UnlinkFromScene',
icon: '<i class="fa-solid fa-unlink"></i>',
visible: () => game.user.isGM && !!this.scene,
onClick: () => this.viewed.toggleSceneLink()
},
{
label: 'COMBAT.End',
icon: 'fa-solid fa-xmark',
visible: () => game.user.isGM && !!this.viewed,
onClick: () => this.viewed.endCombat()
}
];
}
@ -127,7 +162,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
active: index === combat.turn,
canPing: combatant.sceneId === canvas.scene?.id && game.user.hasPermission('PING_CANVAS'),
type: combatant.actor?.system?.type,
img: await this._getCombatantThumbnail(combatant)
img: await this._getCombatantThumbnail(combatant),
disposition: combatant.token?.disposition
};
turn.css = [turn.active ? 'active' : null, hidden ? 'hide' : null, isDefeated ? 'defeated' : null].filterJoin(
@ -149,13 +185,13 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
}
async setCombatantSpotlight(combatantId) {
const combatant = this.viewed.combatants.get(combatantId);
const update = {
system: {
'spotlight.requesting': false,
'spotlight.requestOrderIndex': 0
}
};
const combatant = this.viewed.combatants.get(combatantId);
const toggleTurn = this.viewed.combatants.contents
.sort(this.viewed._sortCombatants)
@ -177,6 +213,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
if (autoPoints) {
update.system.actionTokens = Math.max(combatant.system.actionTokens - 1, 0);
}
if (combatant.actor) expireActiveEffects(combatant.actor, [CONFIG.DH.GENERAL.activeEffectDurations.act.id]);
}
await this.viewed.update({
@ -184,6 +222,14 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
round: this.viewed.round + 1
});
await combatant.update(update);
if (combatant.token) clearPreviousSpotlight();
}
async clearTurn() {
await this.viewed.update({
turn: null,
round: this.viewed.round + 1
});
}
static async requestSpotlight(_, target) {

View file

@ -1,6 +1,6 @@
import { DhCountdown } from '../../data/countdowns.mjs';
import { waitForDiceSoNice } from '../../helpers/utils.mjs';
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -56,8 +56,8 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio
? 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'
? 'DAGGERHEART.UI.Countdowns.decreasingLoop'
: 'DAGGERHEART.UI.Countdowns.loop'
: null;
const randomizeValid = !new Roll(countdown.progress.startFormula ?? '').isDeterministic;
acc[key] = {
@ -114,7 +114,7 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio
}
await this.data.updateSource(update);
await emitAsGM(GMUpdateEvent.UpdateCountdowns, this.gmSetSetting.bind(this.data), this.data, null, {
await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, this.gmSetSetting.bind(this.data), this.data, null, {
refreshType: RefreshType.Countdown
});
@ -148,11 +148,11 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio
}
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 }
});
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 });
}
@ -233,6 +233,6 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio
}
if (this.editingCountdowns.has(countdownId)) this.editingCountdowns.delete(countdownId);
this.updateSetting({ [`countdowns.-=${countdownId}`]: null });
this.updateSetting({ [`countdowns.${countdownId}`]: _del });
}
}

View file

@ -1,5 +1,5 @@
import { waitForDiceSoNice } from '../../helpers/utils.mjs';
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -21,19 +21,19 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
static DEFAULT_OPTIONS = {
id: 'countdowns',
tag: 'div',
classes: ['daggerheart', 'dh-style', 'countdowns', 'faded-ui'],
classes: ['daggerheart', 'dh-style', 'countdowns'],
window: {
icon: 'fa-solid fa-clock-rotate-left',
frame: true,
frame: false,
title: 'DAGGERHEART.UI.Countdowns.title',
positioned: false,
resizable: false,
minimizable: false
},
actions: {
toggleViewMode: DhCountdowns.#toggleViewMode,
editCountdowns: DhCountdowns.#editCountdowns,
loopCountdown: DhCountdowns.#loopCountdown,
toggleViewMode: DhCountdowns.#onToggleViewMode,
editCountdowns: DhCountdowns.#onEditCountdowns,
loopCountdown: DhCountdowns.#onLoopCountdown,
decreaseCountdown: (_, target) => this.editCountdown(false, target),
increaseCountdown: (_, target) => this.editCountdown(true, target)
},
@ -52,10 +52,6 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
}
};
get element() {
return document.body.querySelector('.daggerheart.dh-style.countdowns');
}
/**@inheritdoc */
async _renderFrame(options) {
const frame = await super._renderFrame(options);
@ -66,19 +62,6 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
if (iconOnly) frame.classList.add('icon-only');
else frame.classList.remove('icon-only');
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;
}
@ -118,8 +101,8 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
? 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'
? 'DAGGERHEART.UI.Countdowns.decreasingLoop'
: 'DAGGERHEART.UI.Countdowns.loop'
: null;
const loopDisabled =
!countdownEditable ||
@ -140,6 +123,8 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
}
static #getPlayerOwnership(user, setting, countdown) {
if (user.isGM) return CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER;
const playerOwnership = countdown.ownership[user.id];
return playerOwnership === undefined || playerOwnership === CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT
? setting.defaultOwnership
@ -162,7 +147,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
return true;
}
static async #toggleViewMode() {
static async #onToggleViewMode() {
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;
@ -173,15 +158,16 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
this.render();
}
static async #editCountdowns() {
static async #onEditCountdowns() {
new game.system.api.applications.ui.CountdownEdit().render(true);
}
static async #loopCountdown(_, target) {
static async #onLoopCountdown(_, target) {
if (!DhCountdowns.canPerformEdit()) return;
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const countdown = settings.countdowns[target.id];
const countdownId = target.closest('[data-countdown]').dataset.countdown;
const countdown = settings.countdowns[countdownId];
let progressMax = countdown.progress.start;
let message = null;
@ -195,17 +181,17 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
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;
? Math.max(Number(progressMax) - 1, 0)
: progressMax;
await waitForDiceSoNice(message);
await settings.updateSource({
[`countdowns.${target.id}.progress`]: {
[`countdowns.${countdownId}.progress`]: {
current: newMax,
start: newMax
}
});
await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
refreshType: RefreshType.Countdown
});
}
@ -214,22 +200,23 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
if (!DhCountdowns.canPerformEdit()) return;
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const countdown = settings.countdowns[target.id];
const countdownId = target.closest('[data-countdown]').dataset.countdown;
const countdown = settings.countdowns[countdownId];
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, {
await settings.updateSource({ [`countdowns.${countdownId}.progress.current`]: newCurrent });
await emitGMUpdate(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,
data: { refreshType: RefreshType.Countdown }
});
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 });
}
@ -278,10 +265,8 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
return acc;
}, {})
};
await emitAsGM(GMUpdateEvent.UpdateCountdowns,
DhCountdowns.gmSetSetting.bind(settings),
settings, null, {
refreshType: RefreshType.Countdown
await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
refreshType: RefreshType.Countdown
});
}

View file

@ -1,3 +1,4 @@
import { getIconVisibleActiveEffects } from '../../helpers/utils.mjs';
import { RefreshType } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -48,11 +49,9 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica
_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));
});
for (const element of this.element?.querySelectorAll('.effect-container a') ?? []) {
element.addEventListener('click', e => this.#onClickEffect(e));
element.addEventListener('contextmenu', e => this.#onClickEffect(e, -1));
}
}
@ -68,11 +67,11 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica
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() ?? [];
? !game.user.isGM
? game.user.character
: null
: canvas.tokens.controlled[0].actor;
return getIconVisibleActiveEffects(actor?.getActiveEffects() ?? []);
};
toggleHidden(token, focused) {
@ -86,11 +85,22 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica
this.render();
}
async removeEffect(event) {
async #onClickEffect(event, delta = 1) {
const element = event.target.closest('.effect-container');
const effects = DhEffectsDisplay.getTokenEffects();
const effect = effects.find(x => x.id === element.dataset.effectId);
await effect.delete();
if (!effect || (delta >= 0 && !effect.system.stacking)) {
return;
}
const maxValue = effect.system.stacking?.max ?? Infinity;
const newValue = Math.clamp((effect.system.stacking?.value ?? 1) + delta, 0, maxValue);
if (newValue > 0) {
await effect.update({ 'system.stacking.value': newValue });
} else {
await effect.delete();
}
this.render();
}
setupHooks() {

View file

@ -1,4 +1,4 @@
import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
import { emitGMUpdate, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -22,7 +22,7 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
tag: 'div',
window: {
frame: true,
title: 'Fear',
title: 'DAGGERHEART.GENERAL.fear',
positioned: true,
resizable: true,
minimizable: false
@ -104,7 +104,7 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
}
async updateFear(value) {
return emitAsGM(
return emitGMUpdate(
GMUpdateEvent.UpdateFear,
game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
value

View file

@ -1,3 +1,4 @@
import { getDocFromElement } from '../../helpers/utils.mjs';
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -37,7 +38,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
tag: 'div',
window: {
frame: true,
title: 'Compendium Browser',
title: 'DAGGERHEART.UI.ItemBrowser.windowTitle',
icon: 'fa-solid fa-book-atlas',
positioned: true,
resizable: true
@ -47,7 +48,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
expandContent: this.expandContent,
resetFilters: this.resetFilters,
sortList: this.sortList,
openSettings: this.openSettings
openSettings: this.openSettings,
viewSheet: this.#onViewSheet
},
position: {
left: 100,
@ -109,8 +111,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
CONFIG.DH.id,
CONFIG.DH.FLAGS[`${this.compendiumBrowserTypeKey}`].position
);
options.position = userPresetPosition ?? ItemBrowser.DEFAULT_OPTIONS.position;
delete options.position.zIndex;
if (!userPresetPosition) {
const width = noFolder === true || lite === true ? 600 : 850;
@ -207,8 +209,23 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
label: game.i18n.localize(col.label)
}));
const splitPath = folderId?.split('.') ?? [];
const { pathLabels } = splitPath.reduce(
(acc, curr) => {
acc.currentPath = !acc.currentPath ? curr : [acc.currentPath, curr].join('.');
if (curr === 'folder') return acc;
const label = foundry.utils.getProperty(this.config, acc.currentPath)?.label;
if (label) acc.pathLabels.push(game.i18n.localize(label));
return acc;
},
{ pathLabels: [], currentPath: '' }
);
this.selectedMenu = {
path: folderId?.split('.') ?? [],
path: splitPath,
pathLabels: pathLabels,
data: {
...folderData,
columns: columns
@ -262,7 +279,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
(await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.description));
}
this.fieldFilter = this._createFieldFilter();
this.fieldFilter = await this._createFieldFilter();
if (this.presets?.filter) {
Object.entries(this.presets.filter).forEach(([k, v]) => {
@ -291,7 +308,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
{
items: this.items,
menu: this.selectedMenu,
formatLabel: this.formatLabel
formatLabel: this.formatLabel,
viewSheet: this.items[0] instanceof Actor
}
);
@ -340,12 +358,12 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
);
}
_createFieldFilter() {
async _createFieldFilter() {
const filters = ItemBrowser.getFolderConfig(this.selectedMenu.data, 'filters');
filters.forEach(f => {
for (const f of filters) {
if (typeof f.field === 'string') f.field = foundry.utils.getProperty(game, f.field);
else if (typeof f.choices === 'function') {
f.choices = f.choices(this.items);
f.choices = await f.choices(this.items);
}
// Clear field label so template uses our custom label parameter
@ -355,7 +373,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
f.name ??= f.key;
f.value = this.presets?.filter?.[f.name]?.value ?? null;
});
}
return filters;
}
@ -552,6 +571,11 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
}
}
static async #onViewSheet(_, target) {
const document = await getDocFromElement(target);
document?.sheet?.render(true);
}
_createDragProcess() {
new foundry.applications.ux.DragDrop.implementation({
dragSelector: '.item-container',
@ -568,7 +592,9 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
const { itemUuid } = event.target.closest('[data-item-uuid]').dataset,
item = await foundry.utils.fromUuid(itemUuid),
dragData = item.toDragData();
event.dataTransfer.setData('text/plain', JSON.stringify(dragData));
event.dataTransfer.setDragImage(event.target.querySelector('img'), 0, 0);
}
_canDragStart() {
@ -588,7 +614,16 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
items: {
folder: 'equipments',
render: {
noFolder: true
folders: [
'equipments',
'ancestries',
'classes',
'subclasses',
'domains',
'communities',
'beastforms'
// excluded: features
]
}
},
compendium: {}

View file

@ -0,0 +1,27 @@
export default class DhProgress {
#notification;
constructor({ max, label = '' }) {
this.max = max;
this.label = label;
this.#notification = ui.notifications.info(this.label, { progress: true });
}
updateMax(newMax) {
this.max = newMax;
}
advance({ by = 1, label = this.label } = {}) {
if (this.value === this.max) return;
this.value = (this.value ?? 0) + Math.abs(by);
this.#notification.update({ message: label, pct: this.value / this.max });
}
close({ label = '' } = {}) {
this.#notification.update({ message: label, pct: 1 });
}
static createMigrationProgress(max = 0) {
return new DhProgress({ max, label: game.i18n.localize('DAGGERHEART.UI.Progress.migrationLabel') });
}
}

View file

@ -1,4 +1,4 @@
import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
import { emitGMUpdate, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
export default class DhSceneNavigation extends foundry.applications.ui.SceneNavigation {
/** @inheritdoc */
@ -31,7 +31,7 @@ export default class DhSceneNavigation extends foundry.applications.ui.SceneNavi
const environments = daggerheartInfo.sceneEnvironments.filter(
x => x && x.testUserPermission(game.user, 'LIMITED')
);
const hasEnvironments = environments.length > 0 && x.isView;
const hasEnvironments = environments.length > 0 && x.active;
return {
...x,
hasEnvironments,
@ -39,9 +39,10 @@ export default class DhSceneNavigation extends foundry.applications.ui.SceneNavi
environments: environments
};
});
context.scenes.active = extendScenes(context.scenes.active);
context.scenes.inactive = extendScenes(context.scenes.inactive);
context.scenes.viewed = context.scenes.viewed ? extendScenes([context.scenes.viewed])[0] : null;
return context;
}
@ -67,7 +68,7 @@ export default class DhSceneNavigation extends foundry.applications.ui.SceneNavi
1
)[0];
newEnvironments.unshift(newFirst);
emitAsGM(
emitGMUpdate(
GMUpdateEvent.UpdateDocument,
scene.update.bind(scene),
{ 'flags.daggerheart.sceneEnvironments': newEnvironments },

View file

@ -1,97 +1,4 @@
/**
* @typedef ContextMenuEntry
* @property {string} name The context menu label. Can be localized.
* @property {string} [icon] A string containing an HTML icon element for the menu item.
* @property {string} [classes] Additional CSS classes to apply to this menu item.
* @property {string} [group] An identifier for a group this entry belongs to.
* @property {ContextMenuJQueryCallback} callback The function to call when the menu item is clicked.
* @property {ContextMenuCondition|boolean} [condition] A function to call or boolean value to determine if this entry
* appears in the menu.
*/
/**
* @callback ContextMenuCondition
* @param {jQuery|HTMLElement} html The element of the context menu entry.
* @returns {boolean} Whether the entry should be rendered in the context menu.
*/
/**
* @callback ContextMenuCallback
* @param {HTMLElement} target The element that the context menu has been triggered for.
* @returns {unknown}
*/
/**
* @callback ContextMenuJQueryCallback
* @param {HTMLElement|jQuery} target The element that the context menu has been triggered for. Will
* either be a jQuery object or an HTMLElement instance, depending
* on how the ContextMenu was configured.
* @returns {unknown}
*/
/**
* @typedef ContextMenuOptions
* @property {string} [eventName="contextmenu"] Optionally override the triggering event which can spawn the menu. If
* the menu is using fixed positioning, this event must be a MouseEvent.
* @property {ContextMenuCallback} [onOpen] A function to call when the context menu is opened.
* @property {ContextMenuCallback} [onClose] A function to call when the context menu is closed.
* @property {boolean} [fixed=false] If true, the context menu is given a fixed position rather than being
* injected into the target.
* @property {boolean} [jQuery=true] If true, callbacks will be passed jQuery objects instead of HTMLElement
* instances.
*/
/**
* @typedef ContextMenuRenderOptions
* @property {Event} [event] The event that triggered the context menu opening.
* @property {boolean} [animate=true] Animate the context menu opening.
*/
/**
* A subclass of ContextMenu.
* @extends {foundry.applications.ux.ContextMenu}
*/
export default class DHContextMenu extends foundry.applications.ux.ContextMenu {
/**
* @param {HTMLElement|jQuery} container - The HTML element that contains the context menu targets.
* @param {string} selector - A CSS selector which activates the context menu.
* @param {ContextMenuEntry[]} menuItems - An Array of entries to display in the menu
* @param {ContextMenuOptions} [options] - Additional options to configure the context menu.
*/
constructor(container, selector, menuItems, options) {
super(container, selector, menuItems, options);
/** @deprecated since v13 until v15 */
this.#jQuery = options.jQuery;
}
/**
* Whether to pass jQuery objects or HTMLElement instances to callback.
* @type {boolean}
*/
#jQuery;
/**@inheritdoc */
activateListeners(menu) {
menu.addEventListener('click', this.#onClickItem.bind(this));
}
/**
* Handle click events on context menu items.
* @param {PointerEvent} event The click event
*/
#onClickItem(event) {
event.preventDefault();
event.stopPropagation();
const element = event.target.closest('.context-item');
if (!element) return;
const item = this.menuItems.find(i => i.element === element);
item?.callback(this.#jQuery ? $(this.target) : this.target, event);
this.close();
}
/* -------------------------------------------- */
/**
* Trigger a context menu event in response to a normal click on a additional options button.
* @param {PointerEvent} event
@ -99,8 +6,11 @@ export default class DHContextMenu extends foundry.applications.ux.ContextMenu {
static triggerContextMenu(event, altSelector) {
event.preventDefault();
event.stopPropagation();
const { clientX, clientY } = event;
const selector = altSelector ?? '[data-item-uuid]';
if (ui.context?.selector === selector) return;
const { clientX, clientY } = event;
const target = event.target.closest(selector) ?? event.currentTarget.closest(selector);
target?.dispatchEvent(
new PointerEvent('contextmenu', {

View file

@ -188,7 +188,7 @@ export default class FilterMenu extends foundry.applications.ux.ContextMenu {
}));
const damageTypeFilter = Object.values(CONFIG.DH.GENERAL.damageTypes).map(({ id, abbreviation }) => ({
group: 'Damage Type', //TODO localize
group: game.i18n.localize('DAGGERHEART.GENERAL.damageType'),
name: game.i18n.localize(abbreviation),
filter: {
field: 'system.damage.type',

View file

@ -1,5 +1,6 @@
export { default as DhMeasuredTemplate } from './measuredTemplate.mjs';
export { default as DhRuler } from './ruler.mjs';
export { default as DhTemplateLayer } from './templateLayer.mjs';
export { default as DhRegion } from './region.mjs';
export { default as DhRegionLayer } from './regionLayer.mjs';
export { default as DhTokenPlaceable } from './token.mjs';
export { default as DhTokenRuler } from './tokenRuler.mjs';

View file

@ -18,7 +18,7 @@ export default class DhMeasuredTemplate extends foundry.canvas.placeables.Measur
static getRangeLabels(distanceValue, settings) {
let result = { distance: distanceValue, units: '' };
if (!settings.enabled) return result;
if (!settings.enabled || !canvas.scene) return result;
const sceneRangeMeasurement = canvas.scene.flags.daggerheart?.rangeMeasurement;
const { disable, custom } = CONFIG.DH.GENERAL.sceneRangeMeasurementSetting;

View file

@ -0,0 +1,12 @@
import DhMeasuredTemplate from './measuredTemplate.mjs';
export default class DhRegion extends foundry.canvas.placeables.Region {
/**@inheritdoc */
_formatMeasuredDistance(distance) {
const range = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement;
if (!range.enabled) return super._formatMeasuredDistance(distance);
const { distance: resultDistance, units } = DhMeasuredTemplate.getRangeLabels(distance, range);
return `${resultDistance} ${units}`;
}
}

View file

@ -0,0 +1,155 @@
export default class DhRegionLayer extends foundry.canvas.layers.RegionLayer {
static prepareSceneControls() {
const sc = foundry.applications.ui.SceneControls;
const { tools, ...rest } = super.prepareSceneControls();
return {
...rest,
tools: {
select: tools.select,
templateMode: tools.templateMode,
rectangle: tools.rectangle,
circle: tools.circle,
ellipse: tools.ellipse,
cone: tools.cone,
inFront: {
name: 'inFront',
order: 7,
title: 'CONTROLS.inFront',
icon: 'fa-solid fa-eye',
toolclip: {
src: 'toolclips/tools/measure-cone.webm',
heading: 'CONTROLS.inFront',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
}
},
ring: { ...tools.ring, order: 8 },
line: { ...tools.line, order: 9 },
emanation: { ...tools.emanation, order: 10 },
polygon: { ...tools.polygon, order: 11 },
hole: { ...tools.hole, order: 12 },
snap: { ...tools.snap, order: 13 },
clear: { ...tools.clear, order: 14 }
}
};
}
/** @inheritDoc */
_isCreationToolActive() {
return this.active && (game.activeTool === 'inFront' || game.activeTool in foundry.data.BaseShapeData.TYPES);
}
_createDragShapeData(event) {
const hole = ui.controls.controls[this.options.name].tools.hole?.active ?? false;
if (game.activeTool === 'inFront') return { type: 'cone', x: 0, y: 0, radius: 0, angle: 180, hole };
const shape = super._createDragShapeData(event);
const token =
shape?.type === 'emanation' && shape.base?.type === 'token'
? this.#findTokenInBounds(event.interactionData.origin)
: null;
if (token) {
shape.base.width = token.width;
shape.base.height = token.height;
event.interactionData.origin = token.getCenterPoint();
}
return shape;
}
async placeRegion(data, options = {}) {
const preConfirm = data => {
const shape = data.document.shapes[0];
const isEmanation = shape.type === 'emanation';
if (isEmanation) {
const token = this.#findTokenInBounds(shape.base.origin);
if (!token) return options.preConfirm?.(data) ?? true;
const shapeData = shape.toObject();
data.document.updateSource({
shapes: [
{
...shapeData,
base: {
...shapeData.base,
height: token.height,
width: token.width,
x: token.x,
y: token.y
}
}
]
});
}
return options?.preConfirm?.(data) ?? true;
};
return await super.placeRegion(data, { ...options, preConfirm });
}
/** Searches for token at origin point, returning null if there are no tokens or multiple overlapping tokens */
#findTokenInBounds(origin) {
const { x, y } = origin;
const gridSize = canvas.grid.size;
const inBounds = canvas.scene.tokens.filter(t => {
return x.between(t.x, t.x + t.width * gridSize) && y.between(t.y, t.y + t.height * gridSize);
});
return inBounds.length === 1 ? inBounds[0] : null;
}
static getTemplateShape({ type, angle, range, direction } = {}) {
const { line, rectangle, inFront, cone, circle, emanation } = CONFIG.DH.GENERAL.templateTypes;
/* Length calculation */
const { grid, distance } = CONFIG.Scene.documentClass.schema.fields.grid.fields;
const sceneGridSize = canvas.scene?.grid.size ?? grid.size.initial;
const sceneGridDistance = canvas.scene?.grid.distance ?? distance.getInitialValue();
const dimensionConstant = sceneGridSize / sceneGridDistance;
const settings = canvas.scene?.rangeSettings;
const rangeNumber = Number(range);
const length = (!Number.isNaN(rangeNumber) ? rangeNumber : settings ? settings[range] : 0) * dimensionConstant;
/*----*/
const shapeData = {
...canvas.mousePosition,
type: type,
direction: direction ?? 0
};
switch (type) {
case rectangle.id:
shapeData.width = length;
shapeData.height = length;
break;
case line.id:
shapeData.length = length;
shapeData.width = 5 * dimensionConstant;
break;
case cone.id:
shapeData.angle = angle ?? CONFIG.MeasuredTemplate.defaults.angle;
shapeData.radius = length;
break;
case inFront.id:
shapeData.angle = '180';
shapeData.radius = length;
shapeData.type = cone.id;
break;
case circle.id:
shapeData.radius = length;
break;
case emanation.id:
shapeData.radius = length;
shapeData.base = {
type: 'token',
x: 0,
y: 0,
width: 1,
height: 1,
shape: game.canvas.grid.isHexagonal ? CONST.TOKEN_SHAPES.ELLIPSE_1 : CONST.TOKEN_SHAPES.RECTANGLE_1
};
break;
}
return shapeData;
}
}

View file

@ -1,116 +0,0 @@
export default class DhTemplateLayer extends foundry.canvas.layers.TemplateLayer {
static prepareSceneControls() {
const sc = foundry.applications.ui.SceneControls;
return {
name: 'templates',
order: 2,
title: 'CONTROLS.GroupMeasure',
icon: 'fa-solid fa-ruler-combined',
visible: game.user.can('TEMPLATE_CREATE'),
onChange: (event, active) => {
if (active) canvas.templates.activate();
},
onToolChange: () => canvas.templates.setAllRenderFlags({ refreshState: true }),
tools: {
circle: {
name: 'circle',
order: 1,
title: 'CONTROLS.MeasureCircle',
icon: 'fa-regular fa-circle',
toolclip: {
src: 'toolclips/tools/measure-circle.webm',
heading: 'CONTROLS.MeasureCircle',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete'])
}
},
cone: {
name: 'cone',
order: 2,
title: 'CONTROLS.MeasureCone',
icon: 'fa-solid fa-angle-left',
toolclip: {
src: 'toolclips/tools/measure-cone.webm',
heading: 'CONTROLS.MeasureCone',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
}
},
inFront: {
name: 'inFront',
order: 3,
title: 'CONTROLS.inFront',
icon: 'fa-solid fa-eye',
toolclip: {
src: 'toolclips/tools/measure-cone.webm',
heading: 'CONTROLS.inFront',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
}
},
rect: {
name: 'rect',
order: 4,
title: 'CONTROLS.MeasureRect',
icon: 'fa-regular fa-square',
toolclip: {
src: 'toolclips/tools/measure-rect.webm',
heading: 'CONTROLS.MeasureRect',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
}
},
ray: {
name: 'ray',
order: 5,
title: 'CONTROLS.MeasureRay',
icon: 'fa-solid fa-up-down',
toolclip: {
src: 'toolclips/tools/measure-ray.webm',
heading: 'CONTROLS.MeasureRay',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
}
},
clear: {
name: 'clear',
order: 6,
title: 'CONTROLS.MeasureClear',
icon: 'fa-solid fa-trash',
visible: game.user.isGM,
onChange: () => canvas.templates.deleteAll(),
button: true
}
},
activeTool: 'circle'
};
}
_onDragLeftStart(event) {
const interaction = event.interactionData;
// Snap the origin to the grid
if (!event.shiftKey) interaction.origin = this.getSnappedPoint(interaction.origin);
// Create a pending MeasuredTemplateDocument
const tool = game.activeTool === 'inFront' ? 'cone' : game.activeTool;
const previewData = {
user: game.user.id,
t: tool,
x: interaction.origin.x,
y: interaction.origin.y,
sort: Math.max(this.getMaxSort() + 1, 0),
distance: 1,
direction: 0,
fillColor: game.user.color || '#FF0000',
hidden: event.altKey
};
const defaults = CONFIG.MeasuredTemplate.defaults;
if (game.activeTool === 'cone') previewData.angle = defaults.angle;
else if (game.activeTool === 'inFront') previewData.angle = 180;
else if (game.activeTool === 'ray') previewData.width = defaults.width * canvas.dimensions.distance;
const cls = foundry.utils.getDocumentClass('MeasuredTemplate');
const doc = new cls(previewData, { parent: canvas.scene });
// Create a preview MeasuredTemplate object
const template = new this.constructor.placeableClass(doc);
doc._object = template;
interaction.preview = this.preview.addChild(template);
template.draw();
}
}

View file

@ -1,3 +1,4 @@
import { getIconVisibleActiveEffects } from '../../helpers/utils.mjs';
import DhMeasuredTemplate from './measuredTemplate.mjs';
export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
@ -9,6 +10,36 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
this.previewHelp ||= this.addChild(this.#drawPreviewHelp());
}
/**@inheritdoc */
_refreshTurnMarker() {
// Should a Turn Marker be active?
const { turnMarker } = this.document;
const markersEnabled =
CONFIG.Combat.settings.turnMarker.enabled && turnMarker.mode !== CONST.TOKEN_TURN_MARKER_MODES.DISABLED;
const spotlighted = game.settings
.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.SpotlightTracker)
.spotlightedTokens.has(this.document.uuid);
const turnIsSet = typeof game.combat?.turn === 'number';
const isTurn = game.combat?.combatant?.tokenId === this.id;
const markerActive = markersEnabled && turnIsSet ? isTurn : spotlighted;
// Activate a Turn Marker
if (markerActive) {
if (!this.turnMarker)
this.turnMarker = this.addChildAt(new foundry.canvas.placeables.tokens.TokenTurnMarker(this), 0);
canvas.tokens.turnMarkers.add(this);
this.turnMarker.draw();
}
// Remove a Turn Marker
else if (this.turnMarker) {
canvas.tokens.turnMarkers.delete(this);
this.turnMarker.destroy();
this.turnMarker = null;
}
}
/** @inheritDoc */
async _drawEffects() {
this.effects.renderable = false;
@ -20,7 +51,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
this.effects.overlay = null;
// Categorize effects
const activeEffects = this.actor?.getActiveEffects() ?? [];
const activeEffects = getIconVisibleActiveEffects(Array.from(this.actor?.allApplicableEffects() ?? []));
const overlayEffect = activeEffects.findLast(e => e.img && e.getFlag?.('core', 'overlay'));
// Draw effects
@ -29,8 +60,8 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
if (!effect.img) continue;
const promise =
effect === overlayEffect
? this._drawOverlay(effect.img, effect.tint)
: this._drawEffect(effect.img, effect.tint);
? this._drawOverlay(effect.img, effect.tint, effect)
: this._drawEffect(effect.img, effect.tint, effect);
promises.push(
promise.then(e => {
if (e) e.zIndex = i;
@ -44,6 +75,39 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
this.renderFlags.set({ refreshEffects: true });
}
/**@inheritdoc */
async _drawEffect(src, tint, effect) {
if (!src) return;
const tex = await foundry.canvas.loadTexture(src, { fallback: 'icons/svg/hazard.svg' });
const icon = new PIXI.Sprite(tex);
icon.tint = tint ?? 0xffffff;
if (effect.system.stacking?.value > 1) {
const stackOverlay = new PIXI.Text(effect.system.stacking.value, {
fill: '#f3c267',
stroke: '#000000',
fontSize: 96,
strokeThickness: 4
});
const nrDigits = Math.floor(Math.log10(effect.system.stacking.value)) + 1;
stackOverlay.y = -8;
/* This does not account for 1:s being much less wide than other digits. I don't think it's desired however as it makes it look jumpy */
stackOverlay.x = icon.width - 8 - nrDigits * 56;
stackOverlay.anchor.set(0, 0);
icon.addChild(stackOverlay);
}
return this.effects.addChild(icon);
}
async _drawOverlay(src, tint, effect) {
const icon = await this._drawEffect(src, tint, effect);
if (icon) icon.alpha = 0.8;
this.effects.overlay = icon ?? null;
return icon;
}
/**
* Returns the distance from this token to another token object.
* This value is corrected to handle alternate token sizes and other grid types
@ -91,15 +155,15 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
const targetEdge = this.#getEdgeBoundary(targetBounds, originPoint, targetPoint);
const adjustedOriginPoint = originEdge
? canvas.grid.getTopLeftPoint({
x: originEdge.x + Math.sign(originPoint.x - originEdge.x),
y: originEdge.y + Math.sign(originPoint.y - originEdge.y)
})
x: originEdge.x + Math.sign(originPoint.x - originEdge.x),
y: originEdge.y + Math.sign(originPoint.y - originEdge.y)
})
: originPoint;
const adjustDestinationPoint = targetEdge
? canvas.grid.getTopLeftPoint({
x: targetEdge.x + Math.sign(targetPoint.x - targetEdge.x),
y: targetEdge.y + Math.sign(targetPoint.y - targetEdge.y)
})
x: targetEdge.x + Math.sign(targetPoint.x - targetEdge.x),
y: targetEdge.y + Math.sign(targetPoint.y - targetEdge.y)
})
: targetPoint;
const distance = canvas.grid.measurePath([
{ ...adjustedOriginPoint, elevation: 0 },
@ -185,9 +249,6 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
/** @inheritDoc */
_drawBar(number, bar, data) {
const val = Number(data.value);
const pct = Math.clamp(val, 0, data.max) / data.max;
// Determine sizing
const { width, height } = this.document.getSize();
const s = canvas.dimensions.uiScale;
@ -195,17 +256,19 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
const bh = 8 * (this.document.height >= 2 ? 1.5 : 1) * s;
// Determine the color to use
const fillColor =
number === 0 ? foundry.utils.Color.fromRGB([1, 0, 0]) : foundry.utils.Color.fromString('#0032b1');
const Color = foundry.utils.Color;
const fillColor = number === 0 ? Color.fromRGB([1, 0, 0]) : Color.fromString('#0032b1');
const emptyColor = Color.fromRGB([0, 0, 0]);
// Draw the bar
const widthUnit = bw / data.max;
// Draw the bar (accounting floating point numbers from bar animations)
const widthUnit = bw / Math.ceil(data.max);
bar.clear().lineStyle(s, 0x000000, 1.0);
const sections = [...Array(data.max).keys()];
for (let mark of sections) {
const sections = [...Array(Math.ceil(data.max)).keys()];
for (const mark of sections) {
const x = mark * widthUnit;
const marked = mark + 1 <= data.value;
const color = marked ? fillColor : foundry.utils.Color.fromRGB([0, 0, 0]);
const marked = mark < Math.ceil(data.value);
const remainder = mark === Math.ceil(data.value) - 1 ? data.value % 1 : 0;
const color = !marked ? emptyColor : remainder ? emptyColor.mix(fillColor, remainder) : fillColor;
if (mark === 0 || mark === sections.length - 1) {
bar.beginFill(color, marked ? 1.0 : 0.5).drawRect(x, 0, widthUnit, bh, 2 * s); // Would like drawRoundedRect, but it's very troublsome with the corners. Leaving for now.
} else {

View file

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

View file

@ -115,3 +115,10 @@ export const advantageState = {
value: 1
}
};
export const areaTypes = {
placed: {
id: 'placed',
label: 'Placed Area'
}
};

View file

@ -70,10 +70,40 @@ export const range = {
}
};
export const groupAttackRange = {
melee: range.melee,
veryClose: range.veryClose,
close: range.close,
far: range.far,
veryFar: range.veryFar
};
/* circle|cone|rect|ray used to be CONST.MEASURED_TEMPLATE_TYPES. Hardcoded for now */
export const templateTypes = {
...CONST.MEASURED_TEMPLATE_TYPES,
EMANATION: 'emanation',
INFRONT: 'inFront'
circle: {
id: 'circle',
label: 'Circle'
},
cone: {
id: 'cone',
label: 'Cone'
},
rectangle: {
id: 'rectangle',
label: 'Rectangle'
},
line: {
id: 'line',
label: 'Line'
},
emanation: {
id: 'emanation',
label: 'Emanation'
},
inFront: {
id: 'inFront',
label: 'In Front'
}
};
export const rangeInclusion = {
@ -241,8 +271,8 @@ export const defaultRestOptions = {
type: 'friendly'
},
damage: {
parts: [
{
parts: {
hitPoints: {
applyTo: healingTypes.hitPoints.id,
value: {
custom: {
@ -251,7 +281,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
}
},
@ -275,8 +305,8 @@ export const defaultRestOptions = {
type: 'self'
},
damage: {
parts: [
{
parts: {
stress: {
applyTo: healingTypes.stress.id,
value: {
custom: {
@ -285,7 +315,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
}
},
@ -310,8 +340,8 @@ export const defaultRestOptions = {
type: 'friendly'
},
damage: {
parts: [
{
parts: {
armor: {
applyTo: healingTypes.armor.id,
value: {
custom: {
@ -320,7 +350,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
}
},
@ -344,8 +374,8 @@ export const defaultRestOptions = {
type: 'self'
},
damage: {
parts: [
{
parts: {
hope: {
applyTo: healingTypes.hope.id,
value: {
custom: {
@ -354,7 +384,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
},
prepareWithFriends: {
@ -368,8 +398,8 @@ export const defaultRestOptions = {
type: 'self'
},
damage: {
parts: [
{
parts: {
hope: {
applyTo: healingTypes.hope.id,
value: {
custom: {
@ -378,7 +408,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
}
},
@ -405,8 +435,8 @@ export const defaultRestOptions = {
type: 'friendly'
},
damage: {
parts: [
{
parts: {
hitPoints: {
applyTo: healingTypes.hitPoints.id,
value: {
custom: {
@ -415,7 +445,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
}
},
@ -439,8 +469,8 @@ export const defaultRestOptions = {
type: 'self'
},
damage: {
parts: [
{
parts: {
stress: {
applyTo: healingTypes.stress.id,
value: {
custom: {
@ -449,7 +479,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
}
},
@ -474,17 +504,17 @@ export const defaultRestOptions = {
type: 'friendly'
},
damage: {
parts: [
{
parts: {
armor: {
applyTo: healingTypes.armor.id,
value: {
custom: {
enabled: true,
formula: '@system.armorScore'
formula: '@system.armorScore.max'
}
}
}
]
}
}
}
},
@ -508,8 +538,8 @@ export const defaultRestOptions = {
type: 'self'
},
damage: {
parts: [
{
parts: {
hope: {
applyTo: healingTypes.hope.id,
value: {
custom: {
@ -518,7 +548,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
},
prepareWithFriends: {
@ -532,8 +562,8 @@ export const defaultRestOptions = {
type: 'self'
},
damage: {
parts: [
{
parts: {
hope: {
applyTo: healingTypes.hope.id,
value: {
custom: {
@ -542,7 +572,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
}
},
@ -704,14 +734,14 @@ const getDiceSoNiceSFX = sfxOptions => {
if (sfxOptions.critical && criticalAnimationData.class) {
return {
specialEffect: criticalAnimationData.class,
options: {}
options: { ...criticalAnimationData.options }
};
}
if (sfxOptions.higher && sfxOptions.data.higher) {
return {
specialEffect: sfxOptions.data.higher.class,
options: {}
options: { ...sfxOptions.data.higher.options }
};
}
@ -943,14 +973,155 @@ export const countdownAppMode = {
export const sceneRangeMeasurementSetting = {
disable: {
id: 'disable',
label: 'Disable Daggerheart Range Measurement'
label: 'DAGGERHEART.CONFIG.SceneRangeMeasurementTypes.disable'
},
default: {
id: 'default',
label: 'Default'
label: 'DAGGERHEART.CONFIG.SceneRangeMeasurementTypes.default'
},
custom: {
id: 'custom',
label: 'Custom'
label: 'DAGGERHEART.CONFIG.SceneRangeMeasurementTypes.custom'
}
};
export const tagTeamRollTypes = {
trait: {
id: 'trait',
label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.trait'
},
ability: {
id: 'ability',
label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.ability'
},
damageAbility: {
id: 'damageAbility',
label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.damageAbility'
}
};
export const baseActiveEffectModes = {
custom: {
id: 'custom',
priority: 0,
label: 'EFFECT.CHANGES.TYPES.custom'
},
multiply: {
id: 'multiply',
priority: 10,
label: 'EFFECT.CHANGES.TYPES.multiply'
},
add: {
id: 'add',
priority: 20,
label: 'EFFECT.CHANGES.TYPES.add'
},
subtract: {
id: 'subtract',
priority: 20,
label: 'EFFECT.CHANGES.TYPES.subtract'
},
downgrade: {
id: 'downgrade',
priority: 30,
label: 'EFFECT.CHANGES.TYPES.downgrade'
},
upgrade: {
id: 'upgrade',
priority: 40,
label: 'EFFECT.CHANGES.TYPES.upgrade'
},
override: {
id: 'override',
priority: 50,
label: 'EFFECT.CHANGES.TYPES.override'
}
};
export const activeEffectModes = {
armor: {
id: 'armor',
priority: 20,
label: 'TYPES.ActiveEffect.armor'
},
...baseActiveEffectModes
};
export const activeEffectArmorInteraction = {
none: { id: 'none', label: 'DAGGERHEART.CONFIG.ArmorInteraction.none.label' },
active: { id: 'active', label: 'DAGGERHEART.CONFIG.ArmorInteraction.active.label' },
inactive: { id: 'inactive', label: 'DAGGERHEART.CONFIG.ArmorInteraction.inactive.label' }
};
export const activeEffectDurations = {
temporary: {
id: 'temporary',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.temporary'
},
act: {
id: 'act',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.act'
},
scene: {
id: 'scene',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.scene'
},
shortRest: {
id: 'shortRest',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.shortRest'
},
longRest: {
id: 'longRest',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.longRest'
},
session: {
id: 'session',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.session'
},
custom: {
id: 'custom',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.custom'
}
};
export const fallAndCollisionDamage = {
veryClose: {
id: 'veryClose',
label: 'DAGGERHEART.CONFIG.fallAndCollision.veryClose.label',
chatTitle: 'DAGGERHEART.CONFIG.fallAndCollision.veryClose.chatTitle',
damageFormula: '1d10 + 3'
},
close: {
id: 'veryClose',
label: 'DAGGERHEART.CONFIG.fallAndCollision.close.label',
chatTitle: 'DAGGERHEART.CONFIG.fallAndCollision.close.chatTitle',
damageFormula: '1d20 + 5'
},
far: {
id: 'veryClose',
label: 'DAGGERHEART.CONFIG.fallAndCollision.far.label',
chatTitle: 'DAGGERHEART.CONFIG.fallAndCollision.far.chatTitle',
damageFormula: '1d100 + 15'
},
collision: {
id: 'veryClose',
label: 'DAGGERHEART.CONFIG.fallAndCollision.collision.label',
chatTitle: 'DAGGERHEART.CONFIG.fallAndCollision.collision.chatTitle',
damageFormula: '1d20 + 5'
}
};
export const simpleDispositions = {
[-1]: {
id: -1,
label: 'TOKEN.DISPOSITION.HOSTILE'
},
[0]: {
id: 0,
label: 'TOKEN.DISPOSITION.NEUTRAL'
},
[1]: {
id: 1,
label: 'TOKEN.DISPOSITION.FRIENDLY'
}
};

View file

@ -1,4 +1,6 @@
export const hooksConfig = {
effectDisplayToggle: 'DHEffectDisplayToggle',
lockedTooltipDismissed: 'DHLockedTooltipDismissed'
lockedTooltipDismissed: 'DHLockedTooltipDismissed',
tagTeamStart: 'DHTagTeamRollStart',
groupRollStart: 'DHGroupRollStart'
};

View file

@ -7,7 +7,12 @@ export const typeConfig = {
},
{
key: 'system.type',
label: 'DAGGERHEART.GENERAL.type'
label: 'DAGGERHEART.GENERAL.type',
format: type => {
if (!type) return '-';
return CONFIG.DH.ACTOR.allAdversaryTypes()[type].label;
}
}
],
filters: [
@ -65,16 +70,65 @@ export const typeConfig = {
}
]
},
environments: {
columns: [
{
key: 'system.tier',
label: 'DAGGERHEART.GENERAL.Tiers.singular'
},
{
key: 'system.type',
label: 'DAGGERHEART.GENERAL.type',
format: type => {
if (!type) return '-';
return CONFIG.DH.ACTOR.environmentTypes[type].label;
}
}
],
filters: [
{
key: 'system.tier',
label: 'DAGGERHEART.GENERAL.Tiers.singular',
field: 'system.api.models.actors.DhEnvironment.schema.fields.tier'
},
{
key: 'system.type',
label: 'DAGGERHEART.GENERAL.type',
field: 'system.api.models.actors.DhEnvironment.schema.fields.type'
},
{
key: 'system.difficulty',
name: 'difficulty.min',
label: 'DAGGERHEART.UI.ItemBrowser.difficultyMin',
field: 'system.api.models.actors.DhEnvironment.schema.fields.difficulty',
operator: 'gte'
},
{
key: 'system.difficulty',
name: 'difficulty.max',
label: 'DAGGERHEART.UI.ItemBrowser.difficultyMax',
field: 'system.api.models.actors.DhEnvironment.schema.fields.difficulty',
operator: 'lte'
}
]
},
items: {
columns: [
{
key: 'type',
label: 'DAGGERHEART.GENERAL.type'
label: 'DAGGERHEART.GENERAL.type',
format: type => (type ? `TYPES.Item.${type}` : '-')
},
{
key: 'system.secondary',
label: 'DAGGERHEART.UI.ItemBrowser.subtype',
format: isSecondary => (isSecondary ? 'secondary' : isSecondary === false ? 'primary' : '-')
format: isSecondary =>
isSecondary
? 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon.short'
: isSecondary === false
? 'DAGGERHEART.ITEMS.Weapon.primaryWeapon.short'
: '-'
},
{
key: 'system.tier',
@ -94,8 +148,8 @@ export const typeConfig = {
key: 'system.secondary',
label: 'DAGGERHEART.UI.ItemBrowser.subtype',
choices: [
{ value: false, label: 'DAGGERHEART.ITEMS.Weapon.primaryWeapon' },
{ value: true, label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon' }
{ value: false, label: 'DAGGERHEART.ITEMS.Weapon.primaryWeapon.full' },
{ value: true, label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon.full' }
]
},
{
@ -166,8 +220,8 @@ export const typeConfig = {
key: 'system.secondary',
label: 'DAGGERHEART.UI.ItemBrowser.subtype',
choices: [
{ value: false, label: 'DAGGERHEART.ITEMS.Weapon.primaryWeapon' },
{ value: true, label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon' }
{ value: false, label: 'DAGGERHEART.ITEMS.Weapon.primaryWeapon.full' },
{ value: true, label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon.full' }
]
},
{
@ -253,11 +307,13 @@ export const typeConfig = {
columns: [
{
key: 'system.type',
label: 'DAGGERHEART.GENERAL.type'
label: 'DAGGERHEART.GENERAL.type',
format: type => (type ? `DAGGERHEART.CONFIG.DomainCardTypes.${type}` : '-')
},
{
key: 'system.domain',
label: 'DAGGERHEART.GENERAL.Domain.single'
label: 'DAGGERHEART.GENERAL.Domain.single',
format: domain => (domain ? CONFIG.DH.DOMAIN.allDomains()[domain].label : '-')
},
{
key: 'system.level',
@ -318,7 +374,14 @@ export const typeConfig = {
},
{
key: 'system.domains',
label: 'DAGGERHEART.GENERAL.Domain.plural'
label: 'DAGGERHEART.GENERAL.Domain.plural',
format: domains => {
const config = CONFIG.DH.DOMAIN.allDomains();
return domains
.map(x => (x ? game.i18n.localize(config[x].label) : null))
.filter(x => x)
.join(', ');
}
}
],
filters: [
@ -362,25 +425,32 @@ export const typeConfig = {
columns: [
{
key: 'system.linkedClass',
label: 'Class',
format: linkedClass => linkedClass?.name ?? 'DAGGERHEART.UI.ItemBrowser.missing'
label: 'TYPES.Item.class',
format: linkedClass =>
foundry.utils.fromUuidSync(linkedClass)?.name ?? 'DAGGERHEART.UI.ItemBrowser.missing'
},
{
key: 'system.spellcastingTrait',
label: 'DAGGERHEART.ITEMS.Subclass.spellcastingTrait'
label: 'DAGGERHEART.ITEMS.Subclass.spellcastingTrait',
format: trait => (trait ? `DAGGERHEART.CONFIG.Traits.${trait}.name` : '-')
}
],
filters: [
{
key: 'system.linkedClass.uuid',
label: 'Class',
choices: items => {
const list = items
.filter(item => item.system.linkedClass)
.map(item => ({
value: item.system.linkedClass.uuid,
label: item.system.linkedClass.name
}));
key: 'system.linkedClass',
label: 'TYPES.Item.class',
choices: async items => {
const list = [];
for (const item of items.filter(item => item.system.linkedClass)) {
const linkedClass = await foundry.utils.fromUuid(item.system.linkedClass);
if (linkedClass) {
list.push({
value: linkedClass.uuid,
label: linkedClass.name
});
}
}
return list.reduce((a, c) => {
if (!a.find(i => i.value === c.value)) a.push(c);
return a;
@ -397,7 +467,8 @@ export const typeConfig = {
},
{
key: 'system.mainTrait',
label: 'DAGGERHEART.GENERAL.Trait.single'
label: 'DAGGERHEART.GENERAL.Trait.single',
format: trait => (trait ? `DAGGERHEART.CONFIG.Traits.${trait}.name` : '-')
}
],
filters: [
@ -533,7 +604,8 @@ export const compendiumConfig = {
id: 'environments',
keys: ['environments'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.environments',
type: ['environment']
type: ['environment'],
listType: 'environments'
},
beastforms: {
id: 'beastforms',

View file

@ -14,8 +14,8 @@ export const armorFeatures = {
type: 'hostile'
},
damage: {
parts: [
{
parts: {
stress: {
applyTo: 'stress',
value: {
custom: {
@ -24,7 +24,7 @@ export const armorFeatures = {
}
}
}
]
}
}
}
]
@ -453,7 +453,7 @@ export const allArmorFeatures = () => {
const feature = homebrewFeatures[key];
const actions = feature.actions.map(action => ({
...action,
effects: action.effects.map(effect => feature.effects.find(x => x.id === effect._id)),
effects: action.effects?.map(effect => feature.effects.find(x => x.id === effect._id)) ?? [],
type: action.type
}));
const actionEffects = actions.flatMap(a => a.effects);
@ -489,15 +489,18 @@ export const weaponFeatures = {
description: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.effects.barrier.description',
img: 'icons/skills/melee/shield-block-bash-blue.webp',
changes: [
{
key: 'system.armorScore',
mode: 2,
value: 'ITEM.@system.tier + 1'
},
{
key: 'system.evasion',
mode: 2,
value: '-1'
},
{
key: 'Armor',
type: 'armor',
typeData: {
type: 'armor',
max: 'ITEM.@system.tier + 1'
}
}
]
}
@ -732,8 +735,8 @@ export const weaponFeatures = {
type: 'hostile'
},
damage: {
parts: [
{
parts: {
stress: {
applyTo: 'stress',
value: {
custom: {
@ -742,7 +745,7 @@ export const weaponFeatures = {
}
}
}
]
}
}
}
],
@ -789,11 +792,6 @@ export const weaponFeatures = {
description: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.description',
img: 'icons/skills/melee/sword-shield-stylized-white.webp',
changes: [
{
key: 'system.armorScore',
mode: 2,
value: '1'
},
{
key: 'system.bonuses.damage.primaryWeapon.bonus',
mode: 2,
@ -808,6 +806,22 @@ export const weaponFeatures = {
type: 'withinRange'
}
}
},
{
name: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.description',
img: 'icons/skills/melee/sword-shield-stylized-white.webp',
changes: [
{
key: 'Armor',
type: 'armor',
value: 0,
typeData: {
type: 'armor',
max: 1
}
}
]
}
]
},
@ -914,8 +928,8 @@ export const weaponFeatures = {
type: 'self'
},
damage: {
parts: [
{
parts: {
hitPoints: {
applyTo: 'hitPoints',
value: {
custom: {
@ -924,7 +938,7 @@ export const weaponFeatures = {
}
}
}
]
}
}
}
]
@ -1191,9 +1205,13 @@ export const weaponFeatures = {
img: 'icons/skills/melee/shield-block-gray-orange.webp',
changes: [
{
key: 'system.armorScore',
mode: 2,
value: 'ITEM.@system.tier'
key: 'Armor',
type: 'armor',
value: 0,
typeData: {
type: 'armor',
max: 'ITEM.@system.tier'
}
}
]
}
@ -1389,7 +1407,7 @@ export const allWeaponFeatures = () => {
const actions = feature.actions.map(action => ({
...action,
effects: action.effects.map(effect => feature.effects.find(x => x.id === effect._id)),
effects: action.effects?.map(effect => feature.effects.find(x => x.id === effect._id)) ?? [],
type: action.type
}));
const actionEffects = actions.flatMap(a => a.effects);

View file

@ -57,15 +57,9 @@ const companionBaseResources = Object.freeze({
stress: {
id: 'stress',
initial: 0,
max: 0,
max: 3,
reverse: true,
label: 'DAGGERHEART.GENERAL.stress'
},
hope: {
id: 'hope',
initial: 0,
reverse: false,
label: 'DAGGERHEART.GENERAL.hope'
}
});

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