Compare commits

..

76 commits
2.2.5 ... 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
275 changed files with 3973 additions and 2857 deletions

View file

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

2
.gitattributes vendored Normal file
View file

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

1
.gitignore vendored
View file

@ -6,3 +6,4 @@ Build
build build
foundry foundry
styles/daggerheart.css 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

@ -66,6 +66,10 @@ You can find the documentation here: https://github.com/Foundryborne/daggerheart
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: ## Disclaimer:
**Daggerheart System** **Daggerheart System**

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

24
daggerheart.d.ts vendored
View file

@ -1,8 +1,11 @@
import '@client/global.mjs'; import '@client/global.mjs';
import '@common/global.mjs';
import '@common/primitives/global.mjs';
import Canvas from '@client/canvas/board.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 // Foundry's use of `Object.assign(globalThis) means many globally available objects are not read as such
// This declare global hopefully fixes that // 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 { declare global {
/** /**
* A simple event framework used throughout Foundry Virtual Tabletop. * A simple event framework used throughout Foundry Virtual Tabletop.
@ -12,9 +15,28 @@ declare global {
class Hooks extends foundry.helpers.Hooks {} class Hooks extends foundry.helpers.Hooks {}
const fromUuid = foundry.utils.fromUuid; const fromUuid = foundry.utils.fromUuid;
const fromUuidSync = foundry.utils.fromUuidSync; 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 * The singleton game canvas
*/ */
const canvas: 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

@ -196,6 +196,11 @@ Hooks.once('init', () => {
makeDefault: true, makeDefault: true,
label: sheetLabel('TYPES.Actor.environment') 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, { Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Party, {
types: ['party'], types: ['party'],
makeDefault: true, makeDefault: true,
@ -441,3 +446,33 @@ Hooks.on('canvasTearDown', canvas => {
Hooks.on('canvasReady', canas => { Hooks.on('canvasReady', canas => {
game.system.registeredTriggers.registerSceneTriggers(canvas.scene); 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();
}
});
});

View file

@ -1,14 +1,101 @@
import globals from 'globals'; import globals from 'globals';
import { defineConfig } from 'eslint/config'; import { defineConfig, globalIgnores } from 'eslint/config';
import prettier from 'eslint-plugin-prettier'; 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([ export default defineConfig([
{ files: ['**/*.{js,mjs,cjs}'], languageOptions: { globals: globals.browser } }, globalIgnores(['foundry/**/*', 'build/**/*']),
{ plugins: { prettier } }, {
files: ['gulpfile.js', 'postcss.config.js'],
languageOptions: { globals: globals.node }
},
{ {
files: ['**/*.{js,mjs,cjs}'], 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: { rules: {
'prettier/prettier': 'error' '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 // Less configuration
var gulp = require('gulp'); var gulp = require('gulp');
var less = require('gulp-less'); var less = require('gulp-less');
var sourcemaps = require('gulp-sourcemaps');
gulp.task('less', function (cb) { gulp.task('less', function (cb) {
gulp.src('styles/daggerheart.less').pipe(less()).on('error', console.error.bind(console)).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(); cb();
}); });

View file

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

View file

@ -23,6 +23,7 @@
"companion": "Companion", "companion": "Companion",
"adversary": "Adversary", "adversary": "Adversary",
"environment": "Environment", "environment": "Environment",
"npc": "NPC",
"party": "Party" "party": "Party"
} }
}, },
@ -333,6 +334,11 @@
}, },
"newAdversary": "New Adversary" "newAdversary": "New Adversary"
}, },
"NPC": {
"FIELDS": {
"motives": { "label": "Motives" }
}
},
"Party": { "Party": {
"Subtitle": { "Subtitle": {
"character": "{community} {ancestry} | {subclass} {class}", "character": "{community} {ancestry} | {subclass} {class}",
@ -705,19 +711,13 @@
}, },
"PendingReactionsDialog": { "PendingReactionsDialog": {
"title": "Pending Reaction Rolls Found", "title": "Pending Reaction Rolls Found",
"unfinishedRolls": "Some Tokens still need to roll their Reaction Roll.", "unfinishedRolls": "Some Tokens have not finished their Reaction Rolls.",
"confirmation": "Are you sure you want to continue ?", "warning": "Unfinished reaction rolls will be considered as failed.",
"warning": "Undone reaction rolls will be considered as failed" "confirmation": "Are you sure you want to continue?"
}, },
"ReactionRoll": { "ReactionRoll": {
"title": "Reaction Roll: {trait}" "title": "Reaction Roll: {trait}"
}, },
"RerollDialog": {
"title": "Reroll",
"damageTitle": "Reroll Damage",
"deselectDiceNotification": "Deselect one of the selected dice first",
"acceptCurrentRolls": "Accept Current Rolls"
},
"ResourceDice": { "ResourceDice": {
"title": "{name} Resource", "title": "{name} Resource",
"rerollDice": "Reroll Dice" "rerollDice": "Reroll Dice"
@ -778,7 +778,9 @@
"title": "Group Roll" "title": "Group Roll"
}, },
"TokenConfig": { "TokenConfig": {
"actorSizeUsed": "Actor size is set, determining the dimensions" "actorSizeUsed": "Actor size is set, determining the dimensions",
"tokenSize": "Token Size",
"sizeCategory": "Size Category"
} }
}, },
"CLASS": { "CLASS": {
@ -2565,10 +2567,11 @@
"tokenImg": { "label": "Token Image" }, "tokenImg": { "label": "Token Image" },
"tokenRingImg": { "label": "Subject Texture" }, "tokenRingImg": { "label": "Subject Texture" },
"tokenSize": { "tokenSize": {
"placeholder": "Using character dimensions", "placeholder": "Token Size",
"disabledPlaceholder": "Set by character size", "disabledPlaceholder": "Token Size",
"height": { "label": "Height" }, "height": { "label": "Height" },
"width": { "label": "Width" }, "width": { "label": "Width" },
"depth": { "label": "Depth" },
"scale": { "label": "Token Scale" } "scale": { "label": "Token Scale" }
}, },
"evolved": { "evolved": {
@ -3094,6 +3097,7 @@
} }
}, },
"ChatLog": { "ChatLog": {
"rerollActionRoll": "Reroll Action",
"rerollDamage": "Reroll Damage", "rerollDamage": "Reroll Damage",
"assignTagRoll": "Assign as Tag Roll" "assignTagRoll": "Assign as Tag Roll"
}, },

View file

@ -154,8 +154,8 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
v.active = this.tabGroups[v.group] v.active = this.tabGroups[v.group]
? this.tabGroups[v.group] === v.id ? this.tabGroups[v.group] === v.id
: this.tabGroups.primary !== 'equipment' : this.tabGroups.primary !== 'equipment'
? v.active ? v.active
: false; : false;
v.cssClass = v.active ? 'active' : ''; v.cssClass = v.active ? 'active' : '';
switch (v.id) { switch (v.id) {
@ -211,9 +211,9 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
context.suggestedTraits = this.setup.class.system context.suggestedTraits = this.setup.class.system
? Object.keys(this.setup.class.system.characterGuide.suggestedTraits).map(traitKey => { ? Object.keys(this.setup.class.system.characterGuide.suggestedTraits).map(traitKey => {
const trait = this.setup.class.system.characterGuide.suggestedTraits[traitKey]; const trait = this.setup.class.system.characterGuide.suggestedTraits[traitKey];
return `${game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${traitKey}.short`)} ${trait > 0 ? `+${trait}` : trait}`; return `${game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${traitKey}.short`)} ${trait > 0 ? `+${trait}` : trait}`;
}) })
: []; : [];
context.traits = { context.traits = {
values: Object.keys(this.setup.traits).map(traitKey => { values: Object.keys(this.setup.traits).map(traitKey => {
@ -450,7 +450,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
if (equipment.includes(type)) if (equipment.includes(type))
presets.filter = { presets.filter = {
'system.tier': { key: 'system.tier', value: 1 }, 'system.tier': { key: 'system.tier', value: 1 },
'type': { key: 'type', value: type } type: { key: 'type', value: type }
}; };
ui.compendiumBrowser.open(presets); ui.compendiumBrowser.open(presets);

View file

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

View file

@ -10,7 +10,6 @@ export { default as ImageSelectDialog } from './imageSelectDialog.mjs';
export { default as ItemTransferDialog } from './itemTransfer.mjs'; export { default as ItemTransferDialog } from './itemTransfer.mjs';
export { default as MulticlassChoiceDialog } from './multiclassChoiceDialog.mjs'; export { default as MulticlassChoiceDialog } from './multiclassChoiceDialog.mjs';
export { default as OwnershipSelection } from './ownershipSelection.mjs'; export { default as OwnershipSelection } from './ownershipSelection.mjs';
export { default as RerollDamageDialog } from './rerollDamageDialog.mjs';
export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs'; export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs';
export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs'; export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs';
export { default as TagTeamDialog } from './tagTeamDialog.mjs'; export { default as TagTeamDialog } from './tagTeamDialog.mjs';

View file

@ -175,14 +175,14 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.disadvantage = advantage === -1; this.disadvantage = advantage === -1;
this.config.roll.advantage = this.config.roll.advantage === advantage ? 0 : advantage; this.config.roll.advantage = this.config.roll.advantage === advantage ? 0 : advantage;
if (this.config.roll.advantage === 0) return this.render();
if (this.config.roll.advantage === 1 && this.config.data.rules.roll.defaultAdvantageDice) { const defaultFaces =
const faces = Number.parseInt(this.config.data.rules.roll.defaultAdvantageDice); this.config.roll.advantage === 1
this.roll.advantageFaces = Number.isNaN(faces) ? this.roll.advantageFaces : faces; ? this.config.data.rules.roll.defaultAdvantageDice
} else if (this.config.roll.advantage === -1 && this.config.data.rules.roll.defaultDisadvantageDice) { : this.config.data.rules.roll.defaultDisadvantageDice;
const faces = Number.parseInt(this.config.data.rules.roll.defaultDisadvantageDice); const faces = Number.parseInt(defaultFaces);
this.roll.advantageFaces = Number.isNaN(faces) ? this.roll.advantageFaces : faces; this.roll.advantageFaces = Number.isNaN(faces) ? this.roll.advantageFaces : faces;
}
this.render(); this.render();
} }
@ -196,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.indexOf(this.config.costs.find(c => c.extKey === button.dataset.key)) > -1
? this.config.costs.filter(x => x.extKey !== button.dataset.key) ? this.config.costs.filter(x => x.extKey !== button.dataset.key)
: [ : [
...this.config.costs, ...this.config.costs,
{ {
extKey: button.dataset.key, extKey: button.dataset.key,
key: this.config?.data?.parent?.isNPC ? 'fear' : 'hope', key: this.config?.data?.parent?.isNPC ? 'fear' : 'hope',
value: 1, value: 1,
name: this.config.data?.system.experiences?.[button.dataset.key]?.name name: this.config.data?.system.experiences?.[button.dataset.key]?.name
} }
]; ];
this.render(); this.render();
} }
@ -213,8 +213,8 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.config.actionType = this.reactionOverride this.config.actionType = this.reactionOverride
? 'reaction' ? 'reaction'
: this.config.actionType === 'reaction' : this.config.actionType === 'reaction'
? 'action' ? 'action'
: this.config.actionType; : this.config.actionType;
this.render(); this.render();
} }
} }

View file

@ -138,13 +138,13 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
const stressReductionStress = this.availableStressReductions const stressReductionStress = this.availableStressReductions
? stressReductions.reduce((acc, red) => acc + red.cost, 0) ? stressReductions.reduce((acc, red) => acc + red.cost, 0)
: 0; : 0;
const stress = this.actor.system.resources.stress;
context.stress = context.stress =
selectedStressMarks.length > 0 || this.availableStressReductions selectedStressMarks.length > 0 || this.availableStressReductions
? { ? {
value: value: stress.value + selectedStressMarks.length + stressReductionStress,
this.actor.system.resources.stress.value + selectedStressMarks.length + stressReductionStress, max: stress.max
max: this.actor.system.resources.stress.max }
}
: null; : null;
context.maxArmorUsed = maxArmorUsed; context.maxArmorUsed = maxArmorUsed;

View file

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

View file

@ -259,10 +259,10 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
const resetValue = increasing const resetValue = increasing
? 0 ? 0
: feature.system.resource.max : feature.system.resource.max
? new Roll( ? new Roll(
Roll.replaceFormulaData(feature.system.resource.max, this.actor.getRollData()) Roll.replaceFormulaData(feature.system.resource.max, this.actor.getRollData())
).evaluateSync().total ).evaluateSync().total
: 0; : 0;
await feature.update({ 'system.resource.value': resetValue }); await feature.update({ 'system.resource.value': resetValue });
} }

View file

@ -106,7 +106,12 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.isGM = game.user.isGM; context.isGM = game.user.isGM;
context.isEditable = this.getIsEditable(); 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.fields = this.party.system.schema.fields.groupRoll.fields;
context.data = this.party.system.groupRoll; context.data = this.party.system.groupRoll;
context.traitOptions = CONFIG.DH.ACTOR.abilities; context.traitOptions = CONFIG.DH.ACTOR.abilities;
@ -162,8 +167,8 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
partContext.groupRoll = { partContext.groupRoll = {
totalLabel: leader?.rollData totalLabel: leader?.rollData
? game.i18n.format('DAGGERHEART.GENERAL.withThing', { ? game.i18n.format('DAGGERHEART.GENERAL.withThing', {
thing: leader.roll.totalLabel thing: leader.roll.totalLabel
}) })
: null, : null,
totalDualityClass: leader?.roll?.isCritical ? 'critical' : leader?.roll?.withHope ? 'hope' : 'fear', totalDualityClass: leader?.roll?.isCritical ? 'critical' : leader?.roll?.withHope ? 'hope' : 'fear',
total: leaderTotal + modifierTotal, total: leaderTotal + modifierTotal,
@ -265,13 +270,6 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
]; ];
} }
getIsEditable() {
return 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);
});
}
groupRollRefresh = ({ refreshType, action, parts }) => { groupRollRefresh = ({ refreshType, action, parts }) => {
if (refreshType !== RefreshType.GroupRoll) return; if (refreshType !== RefreshType.GroupRoll) return;
@ -358,8 +356,6 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
}); });
if (!result) return; if (!result) return;
// todo: move logic to actor.rollTrait() or actor.diceRoll()
if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
const rollData = result.messageRoll.toJSON(); const rollData = result.messageRoll.toJSON();
delete rollData.options.messageRoll; delete rollData.options.messageRoll;

View file

@ -1,280 +0,0 @@
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);
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; 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 max = itemAbleRollParse(this.item.system.resource.max, this.actor, this.item);
const diceFormula = `${max}${this.item.system.resource.dieFaces}`; const diceFormula = `${max}${this.item.system.resource.dieFaces}`;
const roll = await new Roll(diceFormula).evaluate(); 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.rollValues = roll.terms[0].results.map(x => ({ value: x.result, used: false }));
this.resetUsed = true; this.resetUsed = true;

View file

@ -116,7 +116,12 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
async _prepareContext(_options) { async _prepareContext(_options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.isEditable = this.getIsEditable(); context.isEditable =
game.user.isGM ||
this.party.system.partyMembers.some(actor => {
const selected = Boolean(this.party.system.tagTeam.members[actor.id]);
return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER);
});
context.fields = this.party.system.schema.fields.tagTeam.fields; context.fields = this.party.system.schema.fields.tagTeam.fields;
context.data = this.party.system.tagTeam; context.data = this.party.system.tagTeam;
context.rollTypes = CONFIG.DH.GENERAL.tagTeamRollTypes; context.rollTypes = CONFIG.DH.GENERAL.tagTeamRollTypes;
@ -179,57 +184,56 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
} }
if (Object.keys(this.party.system.tagTeam.members).includes(partId)) { if (Object.keys(this.party.system.tagTeam.members).includes(partId)) {
const data = this.party.system.tagTeam.members[partId]; const data = await this.#prepareMemberContext(partId);
const actor = game.actors.get(partId); partContext.hasDamage |= Boolean(data?.damage);
partContext.members[partId] = data;
const rollOptions = [];
const damageRollOptions = [];
for (const item of actor.items) {
if (item.system.metadata.hasActions) {
const actions = [...item.system.actions, ...(item.system.attack ? [item.system.attack] : [])];
for (const action of actions) {
if (action.hasRoll) {
const actionItem = {
value: action.uuid,
label: action.name,
group: item.name,
baseAction: action.baseAction
};
if (action.hasDamage) damageRollOptions.push(actionItem);
else rollOptions.push(actionItem);
}
}
}
}
const selectedRoll = Object.values(this.party.system.tagTeam.members).find(member => member.selected);
const critSelected = !selectedRoll
? undefined
: (selectedRoll?.rollData?.options?.roll?.isCritical ?? false);
const damage = data.rollData?.options?.damage;
partContext.hasDamage |= Boolean(damage);
const critHitPointsDamage = await this.getCriticalDamage(damage);
partContext.members[partId] = {
...data,
roll: data.roll,
isEditable: actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER),
key: partId,
readyToRoll: Boolean(data.rollChoice),
hasRolled: Boolean(data.rollData),
rollOptions,
damageRollOptions,
damage: damage,
critDamage: critHitPointsDamage,
useCritDamage: critSelected || (critSelected === undefined && data.rollData?.options?.roll?.isCritical)
};
} }
return partContext; return partContext;
} }
async #prepareMemberContext(partId) {
const data = this.party.system.tagTeam.members[partId] ?? {};
const actor = game.actors.get(partId);
if (!actor) console.error(`Failed to get actor ${partId}`);
const rollOptions = [];
const damageRollOptions = [];
for (const item of actor?.items ?? []) {
if (!item.system.metadata.hasActions) continue;
const actions = [...item.system.actions, ...(item.system.attack ? [item.system.attack] : [])];
for (const action of actions) {
if (action.hasRoll) {
const collection = action.hasDamage ? damageRollOptions : rollOptions;
collection.push({
value: action.uuid,
label: action.name,
group: item.name,
baseAction: action.baseAction
});
}
}
}
const selectedRoll = Object.values(this.party.system.tagTeam.members).find(member => member.selected);
const critSelected = !selectedRoll ? undefined : (selectedRoll?.rollData?.options?.roll?.isCritical ?? false);
const damage = data.rollData?.options?.damage;
return {
...data,
roll: data.roll,
isEditable: actor?.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER),
key: partId,
readyToRoll: Boolean(data.rollChoice),
hasRolled: Boolean(data.rollData),
rollOptions,
damageRollOptions,
damage: damage,
critDamage: await this.getCriticalDamage(damage),
useCritDamage: critSelected || (critSelected === undefined && data.rollData?.options?.roll?.isCritical)
};
}
getUpdatingParts(target) { getUpdatingParts(target) {
const { initialization, rollSelection, result } = this.constructor.PARTS; const { initialization, rollSelection, result } = this.constructor.PARTS;
const isInitialization = this.tabGroups.application === initialization.id; const isInitialization = this.tabGroups.application === initialization.id;
@ -273,13 +277,6 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
); );
} }
getIsEditable() {
return this.party.system.partyMembers.some(actor => {
const selected = Boolean(this.party.system.tagTeam.members[actor.id]);
return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER);
});
}
tagTeamRefresh = ({ refreshType, action, parts }) => { tagTeamRefresh = ({ refreshType, action, parts }) => {
if (refreshType !== RefreshType.TagTeamRoll) return; if (refreshType !== RefreshType.TagTeamRoll) return;
@ -434,8 +431,6 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
if (!result) return; if (!result) return;
if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
const rollData = result.messageRoll.toJSON(); const rollData = result.messageRoll.toJSON();
delete rollData.options.messageRoll; delete rollData.options.messageRoll;
this.updatePartyData( this.updatePartyData(
@ -651,42 +646,50 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
} }
async getJoinedRoll({ overrideIsCritical, displayVersion } = {}) { async getJoinedRoll({ overrideIsCritical, displayVersion } = {}) {
const memberValues = Object.values(this.party.system.tagTeam.members); try {
const selectedRoll = memberValues.find(x => x.selected); const memberValues = Object.values(this.party.system.tagTeam.members);
let baseMainRoll = selectedRoll ?? memberValues[0]; const selectedRoll = memberValues.find(x => x.selected);
let baseSecondaryRoll = selectedRoll const baseMainRoll = selectedRoll ?? memberValues[0];
? memberValues.find(x => !x.selected) const baseSecondaryRoll = selectedRoll
: memberValues.length > 1 ? memberValues.find(x => !x.selected)
? memberValues[1] : memberValues.length > 1
: null; ? memberValues[1]
: null;
if (!baseMainRoll?.rollData || !baseSecondaryRoll) return null; if (!baseMainRoll?.rollData || !baseSecondaryRoll) return null;
const mainRoll = new MemberData(baseMainRoll.toObject()); const mainRoll = new MemberData(baseMainRoll.toObject());
const secondaryRollData = new MemberData(baseSecondaryRoll.toObject()).rollData; const secondaryRollData = new MemberData(baseSecondaryRoll.toObject()).rollData;
const systemData = mainRoll.rollData.options; const systemData = mainRoll.rollData.options;
const isCritical = overrideIsCritical ?? systemData.roll.isCritical; const isCritical = overrideIsCritical ?? systemData.roll.isCritical;
if (isCritical) systemData.damage = await this.getCriticalDamage(systemData.damage); if (isCritical) systemData.damage = await this.getCriticalDamage(systemData.damage);
if (secondaryRollData?.options.hasDamage) { if (secondaryRollData?.options.hasDamage) {
const secondaryDamage = (displayVersion ? overrideIsCritical : isCritical) const secondaryDamage = (displayVersion ? overrideIsCritical : isCritical)
? await this.getCriticalDamage(secondaryRollData.options.damage) ? await this.getCriticalDamage(secondaryRollData.options.damage)
: secondaryRollData.options.damage; : secondaryRollData.options.damage;
if (systemData.damage) { if (systemData.damage) {
for (const key in secondaryDamage) { for (const [key, damage] of Object.entries(secondaryDamage ?? {})) {
const damage = secondaryDamage[key]; if (key in systemData.damage) {
systemData.damage[key].formula = [systemData.damage[key].formula, damage.formula] systemData.damage[key].formula = [systemData.damage[key]?.formula, damage.formula]
.filter(x => x) .filter(x => x)
.join(' + '); .join(' + ');
systemData.damage[key].total += damage.total; systemData.damage[key].total += damage.total;
systemData.damage[key].parts.push(...damage.parts); systemData.damage[key].parts.push(...damage.parts);
} else {
systemData.damage[key] = damage;
}
}
} else {
systemData.damage = secondaryDamage;
} }
} else {
systemData.damage = secondaryDamage;
} }
}
return mainRoll; return mainRoll;
} catch (err) {
console.error(err);
return null;
}
} }
static async #onCancelRoll(_event, _button, options = { confirm: true }) { static async #onCancelRoll(_event, _button, options = { confirm: true }) {

View file

@ -50,11 +50,11 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
).showGenericStatusEffects; ).showGenericStatusEffects;
context.genericStatusEffects = useGeneric context.genericStatusEffects = useGeneric
? Object.keys(context.statusEffects).reduce((acc, key) => { ? Object.keys(context.statusEffects).reduce((acc, key) => {
const effect = context.statusEffects[key]; const effect = context.statusEffects[key];
if (!effect.systemEffect) acc[key] = effect; if (!effect.systemEffect) acc[key] = effect;
return acc; return acc;
}, {}) }, {})
: null; : null;
context.hasCompanion = this.actor.system.companion; context.hasCompanion = this.actor.system.companion;
@ -68,11 +68,11 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
const warning = const warning =
tokensWithoutActors.length === 1 tokensWithoutActors.length === 1
? game.i18n.format('DAGGERHEART.UI.Notifications.tokenActorMissing', { ? game.i18n.format('DAGGERHEART.UI.Notifications.tokenActorMissing', {
name: tokensWithoutActors[0].name name: tokensWithoutActors[0].name
}) })
: game.i18n.format('DAGGERHEART.UI.Notifications.tokenActorsMissing', { : 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 const tokens = canvas.tokens.controlled
.filter(t => t.actor && !DHTokenHUD.#nonCombatTypes.includes(t.actor.type)) .filter(t => t.actor && !DHTokenHUD.#nonCombatTypes.includes(t.actor.type))
@ -124,7 +124,9 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
const animationDuration = 500; const animationDuration = 500;
const scene = game.scenes.get(game.user.viewedScene); 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 */ /* 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)); const activeTokens = actors.flatMap(member =>
member.getDependentTokens({ scenes: scene }).filter(x => x._id && !x._destroyed)
);
const { x: actorX, y: actorY } = this.document; const { x: actorX, y: actorY } = this.document;
if (activeTokens.length > 0) { if (activeTokens.length > 0) {
for (let token of activeTokens) { for (let token of activeTokens) {
@ -172,8 +174,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
nonZeroIndex === sideMiddle nonZeroIndex === sideMiddle
? 0 ? 0
: nonZeroIndex < sideMiddle : nonZeroIndex < sideMiddle
? -nonZeroIndex ? -nonZeroIndex
: nonZeroIndex - sideMiddle; : nonZeroIndex - sideMiddle;
return { x: actorX - sizeX * distance, y: actorY - sizeY * distanceCoefficient }; return { x: actorX - sizeX * distance, y: actorY - sizeY * distanceCoefficient };
} else if (index < side + inbetween) { } else if (index < side + inbetween) {
const inbetweenIndex = nonZeroIndex - side; const inbetweenIndex = nonZeroIndex - side;
@ -181,8 +183,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
inbetweenIndex === inbetweenMiddle inbetweenIndex === inbetweenMiddle
? 0 ? 0
: inbetweenIndex < inbetweenMiddle : inbetweenIndex < inbetweenMiddle
? -inbetweenIndex ? -inbetweenIndex
: inbetweenIndex - inbetweenMiddle; : inbetweenIndex - inbetweenMiddle;
return { x: actorX + sizeX * distanceCoefficient, y: actorY + sizeY * distance }; return { x: actorX + sizeX * distanceCoefficient, y: actorY + sizeY * distance };
} else if (index < 2 * side + inbetween) { } else if (index < 2 * side + inbetween) {
const sideIndex = nonZeroIndex - side - inbetween; const sideIndex = nonZeroIndex - side - inbetween;
@ -190,8 +192,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
sideIndex === sideMiddle sideIndex === sideMiddle
? 0 ? 0
: sideIndex < sideMiddle : sideIndex < sideMiddle
? sideIndex ? sideIndex
: -(sideIndex - sideMiddle); : -(sideIndex - sideMiddle);
return { x: actorX + sizeX * distance, y: actorY + sizeY * distanceCoefficient }; return { x: actorX + sizeX * distance, y: actorY + sizeY * distanceCoefficient };
} else { } else {
const inbetweenIndex = nonZeroIndex - 2 * side - inbetween; const inbetweenIndex = nonZeroIndex - 2 * side - inbetween;
@ -199,8 +201,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
inbetweenIndex === inbetweenMiddle inbetweenIndex === inbetweenMiddle
? 0 ? 0
: inbetweenIndex < inbetweenMiddle : inbetweenIndex < inbetweenMiddle
? inbetweenIndex ? inbetweenIndex
: -(inbetweenIndex - inbetweenMiddle); : -(inbetweenIndex - inbetweenMiddle);
return { x: actorX - sizeX * distanceCoefficient, y: actorY + sizeY * distance }; return { x: actorX - sizeX * distanceCoefficient, y: actorY + sizeY * distance };
} }
}) })

View file

@ -210,9 +210,9 @@ export default class DhCharacterLevelUp extends LevelUpBase {
achievementExperiences = level.achievements.experiences achievementExperiences = level.achievements.experiences
? Object.values(level.achievements.experiences).reduce((acc, experience) => { ? Object.values(level.achievements.experiences).reduce((acc, experience) => {
if (experience.name) acc.push(experience); if (experience.name) acc.push(experience);
return acc; return acc;
}, []) }, [])
: []; : [];
} }
@ -315,15 +315,15 @@ export default class DhCharacterLevelUp extends LevelUpBase {
: null; : null;
advancement[choiceKey] = multiclassItem advancement[choiceKey] = multiclassItem
? { ? {
...multiclassItem.toObject(), ...multiclassItem.toObject(),
domain: checkbox.secondaryData.domain domain: checkbox.secondaryData.domain
? game.i18n.localize( ? game.i18n.localize(
CONFIG.DH.DOMAIN.allDomains()[checkbox.secondaryData.domain] CONFIG.DH.DOMAIN.allDomains()[checkbox.secondaryData.domain]
.label .label
) )
: null, : null,
subclass: subclass ? subclass.name : null subclass: subclass ? subclass.name : null
} }
: {}; : {};
break; break;
} }

View file

@ -77,9 +77,9 @@ export default class DhCompanionLevelUp extends BaseLevelUp {
achievementExperiences = level.achievements.experiences achievementExperiences = level.achievements.experiences
? Object.values(level.achievements.experiences).reduce((acc, experience) => { ? Object.values(level.achievements.experiences).reduce((acc, experience) => {
if (experience.name) acc.push(experience); if (experience.name) acc.push(experience);
return acc; return acc;
}, []) }, [])
: []; : [];
} }
context.achievements = { context.achievements = {
@ -155,15 +155,15 @@ export default class DhCompanionLevelUp extends BaseLevelUp {
vicious: { vicious: {
damage: advancement.vicious?.damage damage: advancement.vicious?.damage
? { ? {
old: actorDamageDice, old: actorDamageDice,
new: advancement.vicious.damage new: advancement.vicious.damage
} }
: null, : null,
range: advancement.vicious?.range range: advancement.vicious?.range
? { ? {
old: game.i18n.localize(`DAGGERHEART.CONFIG.Range.${actorRange}.name`), old: game.i18n.localize(`DAGGERHEART.CONFIG.Range.${actorRange}.name`),
new: game.i18n.localize(advancement.vicious.range.label) new: game.i18n.localize(advancement.vicious.range.label)
} }
: null : null
}, },
simple: advancement.simple ?? {} 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.tabs.advancements.progress = { selected: selections, max: currentLevel.maxSelections };
context.showTabs = this.tabGroups.primary !== 'summary'; context.showTabs = this.tabGroups.primary !== 'summary';
break; 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; return context;
@ -358,14 +172,14 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const experienceIncreaseTagify = htmlElement.querySelector('.levelup-experience-increases'); const experienceIncreaseTagify = htmlElement.querySelector('.levelup-experience-increases');
if (experienceIncreaseTagify) { if (experienceIncreaseTagify) {
const allExperiences = { const allExperiences = {
...this.actor.system.experiences,
...Object.values(this.levelup.levels).reduce((acc, level) => { ...Object.values(this.levelup.levels).reduce((acc, level) => {
for (const key of Object.keys(level.achievements.experiences)) { for (const key of Object.keys(level.achievements.experiences)) {
acc[key] = level.achievements.experiences[key]; acc[key] = level.achievements.experiences[key];
} }
return acc; return acc;
}, {}) }, {}),
...this.actor.system.experiences
}; };
tagifyElement( tagifyElement(
experienceIncreaseTagify, experienceIncreaseTagify,
@ -384,37 +198,35 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
this._dragDrop.forEach(d => d.bind(htmlElement)); this._dragDrop.forEach(d => d.bind(htmlElement));
} }
tagifyUpdate = tagifyUpdate = type => async (_, { option, removed }) => {
type => const updatePath = Object.keys(this.levelup.levels[this.levelup.currentLevel].choices).reduce(
async (_, { option, removed }) => { (acc, choiceKey) => {
const updatePath = Object.keys(this.levelup.levels[this.levelup.currentLevel].choices).reduce( const choice = this.levelup.levels[this.levelup.currentLevel].choices[choiceKey];
(acc, choiceKey) => { Object.keys(choice).forEach(checkboxNr => {
const choice = this.levelup.levels[this.levelup.currentLevel].choices[choiceKey]; const checkbox = choice[checkboxNr];
Object.keys(choice).forEach(checkboxNr => { if (
const checkbox = choice[checkboxNr]; choiceKey === type &&
if ( (removed ? checkbox.data.includes(option) : checkbox.data.length < checkbox.amount)
choiceKey === type && ) {
(removed ? checkbox.data.includes(option) : checkbox.data.length < checkbox.amount) acc = `levels.${this.levelup.currentLevel}.choices.${choiceKey}.${checkboxNr}.data`;
) { }
acc = `levels.${this.levelup.currentLevel}.choices.${choiceKey}.${checkboxNr}.data`; });
}
});
return acc; return acc;
}, },
null null
); );
if (!updatePath) { if (!updatePath) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.noSelectionsLeft')); ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.noSelectionsLeft'));
return; return;
} }
const currentData = foundry.utils.getProperty(this.levelup, updatePath); const currentData = foundry.utils.getProperty(this.levelup, updatePath);
const updatedData = removed ? currentData.filter(x => x !== option) : [...currentData, option]; const updatedData = removed ? currentData.filter(x => x !== option) : [...currentData, option];
await this.levelup.updateSource({ [updatePath]: updatedData }); await this.levelup.updateSource({ [updatePath]: updatedData });
this.render(); this.render();
}; };
static async updateForm(event, _, formData) { static async updateForm(event, _, formData) {
const { levelup } = foundry.utils.expandObject(formData.object); const { levelup } = foundry.utils.expandObject(formData.object);
@ -593,10 +405,10 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const domainCards = this.levelup.levels[this.levelup.currentLevel].achievements.domainCards; const domainCards = this.levelup.levels[this.levelup.currentLevel].achievements.domainCards;
const illegalDomainCards = option.secondaryData.domain const illegalDomainCards = option.secondaryData.domain
? Object.keys(domainCards) ? Object.keys(domainCards)
.map(key => ({ ...domainCards[key], key })) .map(key => ({ ...domainCards[key], key }))
.filter( .filter(
x => x.uuid && foundry.utils.fromUuidSync(x.uuid).system.domain === option.secondaryData.domain x => x.uuid && foundry.utils.fromUuidSync(x.uuid).system.domain === option.secondaryData.domain
) )
: []; : [];
illegalDomainCards.forEach(card => { illegalDomainCards.forEach(card => {
update[`levels.${this.levelup.currentLevel}.achievements.domainCards.${card.key}.uuid`] = null; update[`levels.${this.levelup.currentLevel}.achievements.domainCards.${card.key}.uuid`] = null;

View file

@ -1,6 +1,5 @@
import { DhHomebrew } from '../../data/settings/_module.mjs'; import { DhHomebrew } from '../../data/settings/_module.mjs';
import { Resource } from '../../data/settings/Homebrew.mjs'; import { Resource } from '../../data/settings/Homebrew.mjs';
import { slugify } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -112,7 +111,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
switch (partId) { switch (partId) {
case 'domains': 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 const enrichedDescription = selectedDomain
? await foundry.applications.ux.TextEditor.implementation.enrichHTML(selectedDomain.description) ? await foundry.applications.ux.TextEditor.implementation.enrichHTML(selectedDomain.description)
: null; : null;
@ -252,8 +251,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
const configTitle = isDowntime const configTitle = isDowntime
? game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.downtimeMove') ? game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.downtimeMove')
: type === 'armorFeatures' : type === 'armorFeatures'
? game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.armorFeature') ? game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.armorFeature')
: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.weaponFeature'); : game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.weaponFeature');
const editedBase = await game.system.api.applications.sheetConfigs.SettingFeatureConfig.configure( const editedBase = await game.system.api.applications.sheetConfigs.SettingFeatureConfig.configure(
configTitle, configTitle,
@ -403,12 +402,12 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
const domainName = button.form.elements.domainName.value; const domainName = button.form.elements.domainName.value;
if (!domainName) return; if (!domainName) return;
const newSlug = slugify(domainName); const newSlug = domainName.slugify();
const existingDomains = [ const existingDomains = [
...Object.values(this.settings.domains), ...Object.values(this.settings.domains),
...Object.values(CONFIG.DH.DOMAIN.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')); ui.notifications.warn(game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.domains.duplicateDomain'));
return; return;
} }
@ -529,7 +528,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
const identifier = button.form.elements.identifier.value; const identifier = button.form.elements.identifier.value;
if (!identifier) return; if (!identifier) return;
const sluggedIdentifier = slugify(identifier); const sluggedIdentifier = identifier.slugify();
await this.settings.updateSource({ await this.settings.updateSource({
[`resources.${actorType}.resources.${sluggedIdentifier}`]: Resource.getDefaultResourceData(identifier) [`resources.${actorType}.resources.${sluggedIdentifier}`]: Resource.getDefaultResourceData(identifier)

View file

@ -2,6 +2,7 @@ export { default as ActionConfig } from './action-config.mjs';
export { default as ActionSettingsConfig } from './action-settings-config.mjs'; export { default as ActionSettingsConfig } from './action-settings-config.mjs';
export { default as CharacterSettings } from './character-settings.mjs'; export { default as CharacterSettings } from './character-settings.mjs';
export { default as AdversarySettings } from './adversary-settings.mjs'; export { default as AdversarySettings } from './adversary-settings.mjs';
export { default as NPCSettings } from './npc-settings.mjs';
export { default as CompanionSettings } from './companion-settings.mjs'; export { default as CompanionSettings } from './companion-settings.mjs';
export { default as SettingFeatureConfig } from './setting-feature-config.mjs'; export { default as SettingFeatureConfig } from './setting-feature-config.mjs';
export { default as EnvironmentSettings } from './environment-settings.mjs'; export { default as EnvironmentSettings } from './environment-settings.mjs';

View file

@ -204,7 +204,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
}; };
} }
if (this.action.parent.metadata?.isQuantifiable) { if (this.action.parent.metadata?.isInventoryItem) {
options.quantity = { options.quantity = {
label: 'DAGGERHEART.GENERAL.itemQuantity', label: 'DAGGERHEART.GENERAL.itemQuantity',
group: 'Global' group: 'Global'

View file

@ -41,7 +41,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
* @returns {ChangeChoice { value: string, label: string, hint: string, group: string }[]} * @returns {ChangeChoice { value: string, label: string, hint: string, group: string }[]}
*/ */
static getChangeChoices() { static getChangeChoices() {
const ignoredActorKeys = ['config', 'DhEnvironment', 'DhParty']; const ignoredActorKeys = ['config', 'DhEnvironment', 'DhParty', 'DhNPC'];
const getAllLeaves = (root, group, parentPath = '') => { const getAllLeaves = (root, group, parentPath = '') => {
const leaves = []; const leaves = [];
@ -175,6 +175,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
const partContext = await super._preparePartContext(partId, context); const partContext = await super._preparePartContext(partId, context);
switch (partId) { switch (partId) {
case 'details': case 'details':
partContext.isItemEffect = partContext.isItemEffect || this.options.isSetting;
const useGeneric = game.settings.get( const useGeneric = game.settings.get(
CONFIG.DH.id, CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.appearance CONFIG.DH.SETTINGS.gameSettings.appearance

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

@ -168,8 +168,8 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
updatedEffects = deleteEffect updatedEffects = deleteEffect
? currentEffects.filter(x => x.id !== effectData.id) ? currentEffects.filter(x => x.id !== effectData.id)
: existingEffectIndex === -1 : existingEffectIndex === -1
? [...currentEffects, effectData] ? [...currentEffects, effectData]
: currentEffects.with(existingEffectIndex, effectData); : currentEffects.with(existingEffectIndex, effectData);
await this.updateMove({ await this.updateMove({
[`${this.movePath}.effects`]: updatedEffects [`${this.movePath}.effects`]: updatedEffects
}); });
@ -235,9 +235,9 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
return this.hasEffects return this.hasEffects
? tabs ? tabs
: Object.keys(tabs).reduce((acc, key) => { : Object.keys(tabs).reduce((acc, key) => {
if (key !== 'effects') acc[key] = tabs[key]; if (key !== 'effects') acc[key] = tabs[key];
return acc; return acc;
}, {}); }, {});
} }
/** @override */ /** @override */

View file

@ -2,4 +2,5 @@ export { default as Adversary } from './adversary.mjs';
export { default as Character } from './character.mjs'; export { default as Character } from './character.mjs';
export { default as Companion } from './companion.mjs'; export { default as Companion } from './companion.mjs';
export { default as Environment } from './environment.mjs'; export { default as Environment } from './environment.mjs';
export { default as NPC } from './npc.mjs';
export { default as Party } from './party.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"]', dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
dropSelector: null dropSelector: null
} }
],
contextMenus: [
{
handler: DHBaseActorSheet.getBaseAttackContextOptions,
selector: '[data-item-uuid][data-type="attack"]',
options: {
parentClassHooks: false,
fixed: true
}
}
] ]
}; };

View file

@ -57,6 +57,22 @@ export default class CharacterSheet extends DHBaseActorSheet {
} }
], ],
contextMenus: [ 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, handler: CharacterSheet.#getDomainCardContextOptions,
selector: '[data-item-uuid][data-type="domainCard"]', selector: '[data-item-uuid][data-type="domainCard"]',
@ -176,6 +192,9 @@ export default class CharacterSheet extends DHBaseActorSheet {
for (const input of form.querySelectorAll('input:not([type=search]), .editor.prosemirror')) { for (const input of form.querySelectorAll('input:not([type=search]), .editor.prosemirror')) {
input.disabled = disabled; input.disabled = disabled;
} }
for (const element of form.querySelectorAll('.input[contenteditable]')) {
element.classList.toggle('disabled', disabled);
}
} }
/** @inheritDoc */ /** @inheritDoc */
@ -319,6 +338,56 @@ export default class CharacterSheet extends DHBaseActorSheet {
/* Context Menu */ /* 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. * 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 * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
@ -335,7 +404,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
const doc = getDocFromElementSync(target); const doc = getDocFromElementSync(target);
return doc?.isOwner && doc.system.inVault; return doc?.isOwner && doc.system.inVault;
}, },
callback: async target => { onClick: async (_, target) => {
const doc = await getDocFromElement(target); const doc = await getDocFromElement(target);
const actorLoadout = doc.actor.system.loadoutSlot; const actorLoadout = doc.actor.system.loadoutSlot;
if (actorLoadout.available) return doc.update({ 'system.inVault': false }); if (actorLoadout.available) return doc.update({ 'system.inVault': false });
@ -349,7 +418,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
const doc = getDocFromElementSync(target); const doc = getDocFromElementSync(target);
return doc?.isOwner && doc.system.inVault; return doc?.isOwner && doc.system.inVault;
}, },
callback: async (target, event) => { onClick: async (event, target) => {
const doc = await getDocFromElement(target); const doc = await getDocFromElement(target);
const actorLoadout = doc.actor.system.loadoutSlot; const actorLoadout = doc.actor.system.loadoutSlot;
if (!actorLoadout.available) { if (!actorLoadout.available) {
@ -388,7 +457,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
const doc = getDocFromElementSync(target); const doc = getDocFromElementSync(target);
return doc?.isOwner && !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 => ({ ].map(option => ({
...option, ...option,
@ -414,7 +483,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
const doc = getDocFromElementSync(target); const doc = getDocFromElementSync(target);
return doc.isOwner && 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)
}, },
{ {
label: 'unequip', label: 'unequip',
@ -423,7 +492,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
const doc = getDocFromElementSync(target); const doc = getDocFromElementSync(target);
return doc.isOwner && 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 => ({ ].map(option => ({
...option, ...option,
@ -716,11 +785,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
filter: filter:
key === 'subclasses' key === 'subclasses'
? { ? {
'system.linkedClass.uuid': { 'system.linkedClass.uuid': {
key: 'system.linkedClass.uuid', key: 'system.linkedClass.uuid',
value: this.document.system.class.value._stats.compendiumSource value: this.document.system.class.value?._stats.compendiumSource
} }
} }
: undefined, : undefined,
render: { render: {
noFolder: true noFolder: true
@ -984,7 +1053,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
game.tooltip.activate(target, { game.tooltip.activate(target, {
html, html,
locked: true, locked: true,
cssClass: 'bordered-tooltip', cssClass: 'bordered-tooltip dh-style',
direction: 'DOWN' direction: 'DOWN'
}); });
@ -1080,7 +1149,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
game.tooltip.activate(target, { game.tooltip.activate(target, {
html, html,
locked: true, locked: true,
cssClass: 'bordered-tooltip', cssClass: 'bordered-tooltip dh-style',
direction: 'DOWN', direction: 'DOWN',
noOffset: true noOffset: true
}); });

View file

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

@ -26,7 +26,6 @@ export default class Party extends DHBaseActorSheet {
actions: { actions: {
openDocument: Party.#openDocument, openDocument: Party.#openDocument,
deletePartyMember: Party.#deletePartyMember, deletePartyMember: Party.#deletePartyMember,
deleteItem: Party.#deleteItem,
toggleHope: Party.#toggleHope, toggleHope: Party.#toggleHope,
toggleHitPoints: Party.#toggleHitPoints, toggleHitPoints: Party.#toggleHitPoints,
toggleStress: Party.#toggleStress, toggleStress: Party.#toggleStress,
@ -48,11 +47,6 @@ export default class Party extends DHBaseActorSheet {
template: 'systems/daggerheart/templates/sheets/actors/party/party-members.hbs', template: 'systems/daggerheart/templates/sheets/actors/party/party-members.hbs',
scrollable: [''] scrollable: ['']
}, },
/* NOT YET IMPLEMENTED */
// projects: {
// template: 'systems/daggerheart/templates/sheets/actors/party/projects.hbs',
// scrollable: ['']
// },
inventory: { inventory: {
template: 'systems/daggerheart/templates/sheets/actors/party/inventory.hbs', template: 'systems/daggerheart/templates/sheets/actors/party/inventory.hbs',
scrollable: ['.tab.inventory .items-section'] scrollable: ['.tab.inventory .items-section']
@ -63,19 +57,13 @@ export default class Party extends DHBaseActorSheet {
/** @inheritdoc */ /** @inheritdoc */
static TABS = { static TABS = {
primary: { primary: {
tabs: [ tabs: [{ id: 'partyMembers' }, { id: 'inventory' }, { id: 'notes' }],
{ id: 'partyMembers' },
/* NOT YET IMPLEMENTED */
// { id: 'projects' },
{ id: 'inventory' },
{ id: 'notes' }
],
initial: 'partyMembers', initial: 'partyMembers',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' 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']; static DICE_ROLL_ACTOR_TYPES = ['character'];
async _onRender(context, options) { async _onRender(context, options) {
@ -174,9 +162,9 @@ export default class Party extends DHBaseActorSheet {
difficulty: actor.system.difficulty, difficulty: actor.system.difficulty,
traits: actor.system.traits traits: actor.system.traits
? traits.map(t => ({ ? traits.map(t => ({
label: game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${t}.short`), label: game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${t}.short`),
value: actor.system.traits[t].value value: actor.system.traits[t].value
})) }))
: null, : null,
weapons weapons
}); });
@ -318,7 +306,7 @@ export default class Party extends DHBaseActorSheet {
static async downtimeMoveQuery({ actorId, downtimeType }) { static async downtimeMoveQuery({ actorId, downtimeType }) {
const actor = await foundry.utils.fromUuid(actorId); 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({ new game.system.api.applications.dialogs.Downtime(actor, downtimeType === 'shortRest').render({
force: true force: true
}); });
@ -509,23 +497,4 @@ export default class Party extends DHBaseActorSheet {
const newMembersList = currentMembers.filter(uuid => uuid !== doc.uuid); const newMembersList = currentMembers.filter(uuid => uuid !== doc.uuid);
await this.document.update({ 'system.partyMembers': newMembersList }); await this.document.update({ 'system.partyMembers': newMembersList });
} }
static async #deleteItem(event, target) {
const doc = await getDocFromElement(target.closest('.inventory-item'));
if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize('TYPES.Actor.party'),
name: doc.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: doc.name })
});
if (!confirmed) return;
}
this.document.deleteEmbeddedDocuments('Item', [doc.id]);
}
} }

View file

@ -89,7 +89,7 @@ export default function DHApplicationMixin(Base) {
classes: ['daggerheart', 'sheet', 'dh-style'], classes: ['daggerheart', 'sheet', 'dh-style'],
actions: { actions: {
triggerContextMenu: DHSheetV2.#triggerContextMenu, triggerContextMenu: DHSheetV2.#triggerContextMenu,
createDoc: DHSheetV2.#createDoc, createDoc: DHSheetV2.#onCreateDoc,
editDoc: DHSheetV2.#editDoc, editDoc: DHSheetV2.#editDoc,
deleteDoc: DHSheetV2.#deleteDoc, deleteDoc: DHSheetV2.#deleteDoc,
toChat: DHSheetV2.#toChat, toChat: DHSheetV2.#toChat,
@ -97,8 +97,8 @@ export default function DHApplicationMixin(Base) {
viewItem: DHSheetV2.#viewItem, viewItem: DHSheetV2.#viewItem,
toggleEffect: DHSheetV2.#toggleEffect, toggleEffect: DHSheetV2.#toggleEffect,
toggleExtended: DHSheetV2.#toggleExtended, toggleExtended: DHSheetV2.#toggleExtended,
addNewItem: DHSheetV2.#addNewItem, addNewItem: DHSheetV2.#onAddNewItem,
browseItem: DHSheetV2.#browseItem, browseItem: DHSheetV2.#onBrowseItem,
editAttribution: DHSheetV2.#editAttribution editAttribution: DHSheetV2.#editAttribution
}, },
contextMenus: [ contextMenus: [
@ -424,7 +424,7 @@ export default function DHApplicationMixin(Base) {
const target = element.closest('[data-item-uuid]'); const target = element.closest('[data-item-uuid]');
return !target.dataset.disabled && target.dataset.itemType !== 'beastform'; 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 })
}, },
{ {
label: 'enableEffect', label: 'enableEffect',
@ -433,7 +433,7 @@ export default function DHApplicationMixin(Base) {
const target = element.closest('[data-item-uuid]'); const target = element.closest('[data-item-uuid]');
return target.dataset.disabled && target.dataset.itemType !== 'beastform'; 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 => ({ ].map(option => ({
...option, ...option,
@ -478,7 +478,9 @@ export default function DHApplicationMixin(Base) {
(doc?.isOwner && (!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 });
}
} }
]; ];
@ -493,7 +495,7 @@ export default function DHApplicationMixin(Base) {
!foundry.utils.isEmpty(doc?.damage?.parts); !foundry.utils.isEmpty(doc?.damage?.parts);
return doc?.isOwner && hasDamage; return doc?.isOwner && hasDamage;
}, },
callback: async (target, event) => { onClick: async (event, target) => {
const doc = await getDocFromElement(target), const doc = await getDocFromElement(target),
action = doc?.system?.attack ?? doc; action = doc?.system?.attack ?? doc;
const config = action.prepareConfig(event); const config = action.prepareConfig(event);
@ -513,7 +515,7 @@ export default function DHApplicationMixin(Base) {
const doc = getDocFromElementSync(target); const doc = getDocFromElementSync(target);
return doc?.isOwner && !(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)
}); });
} }
@ -521,7 +523,7 @@ export default function DHApplicationMixin(Base) {
options.push({ options.push({
label: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat', label: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
icon: 'fa-solid fa-message', 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) if (deletable)
@ -533,7 +535,7 @@ export default function DHApplicationMixin(Base) {
const doc = getDocFromElementSync(target); const doc = getDocFromElementSync(target);
return doc?.isOwner !== false && target.dataset.itemType !== 'beastform'; return doc?.isOwner !== false && target.dataset.itemType !== 'beastform';
}, },
callback: async (target, event) => { onClick: async (event, target) => {
const doc = await getDocFromElement(target); const doc = await getDocFromElement(target);
if (event.shiftKey) return doc.delete(); if (event.shiftKey) return doc.delete();
else return doc.deleteDialog(); else return doc.deleteDialog();
@ -639,7 +641,7 @@ export default function DHApplicationMixin(Base) {
/* Application Clicks Actions */ /* Application Clicks Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */
static async #addNewItem(event, target) { static async #onAddNewItem(event, target) {
const createChoice = await foundry.applications.api.DialogV2.wait({ const createChoice = await foundry.applications.api.DialogV2.wait({
classes: ['dh-style', 'two-big-buttons'], classes: ['dh-style', 'two-big-buttons'],
buttons: [ buttons: [
@ -658,11 +660,11 @@ export default function DHApplicationMixin(Base) {
if (!createChoice) return; if (!createChoice) return;
if (createChoice === 'browse') return DHSheetV2.#browseItem.call(this, event, target); if (createChoice === 'browse') return DHSheetV2.#onBrowseItem.call(this, event, target);
else return DHSheetV2.#createDoc.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 type = target.dataset.compendium ?? target.dataset.type;
const presets = { const presets = {
@ -713,19 +715,19 @@ export default function DHApplicationMixin(Base) {
* Create an embedded document. * Create an embedded document.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #createDoc(event, target) { static async #onCreateDoc(event, target) {
const { documentClass, type, inVault, disabled } = target.dataset; const { documentClass, type, inVault, disabled } = target.dataset;
const parentIsItem = this.document.documentName === 'Item'; const parentIsItem = this.document.documentName === 'Item';
const featureOnCharacter = this.document.parent?.type === 'character' && type === 'feature'; const featureOnCharacter = this.document.parent?.type === 'character' && type === 'feature';
const parent = featureOnCharacter const parent = featureOnCharacter
? this.document.parent ? this.document.parent
: parentIsItem && documentClass === 'Item' : parentIsItem && documentClass === 'Item'
? type === 'action' ? type === 'action'
? this.document.system ? this.document.system
: null : null
: this.document; : this.document;
let systemData = null; let systemData = {};
if (featureOnCharacter) { if (featureOnCharacter) {
systemData = { systemData = {
originItemType: this.document.type, originItemType: this.document.type,
@ -738,15 +740,18 @@ export default function DHApplicationMixin(Base) {
const data = { const data = {
name: cls.defaultName({ type, parent }), name: cls.defaultName({ type, parent }),
type type,
system: systemData
}; };
if (systemData) data.system = systemData;
if (inVault) data['system.inVault'] = true;
if (disabled) data.disabled = true; if (disabled) data.disabled = true;
if (type === 'domainCard' && parent?.system.domains?.length) {
data.system.domain = parent.system.domains[0]; 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 }); const doc = await cls.create(data, { parent, renderSheet: !event.shiftKey });

View file

@ -166,6 +166,15 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
} }
} }
/** 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 */ /* Context Menu */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -180,6 +189,43 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true }); return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true });
} }
/**
* 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 */ /* Application Listener Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -331,7 +377,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
action: 'update', action: 'update',
documentName: 'Item', documentName: 'Item',
parent: targetActor, parent: targetActor,
updates: [{ '_id': existing.id, 'system.quantity': existing.system.quantity + quantity }] updates: [{ _id: existing.id, 'system.quantity': existing.system.quantity + quantity }]
}); });
} else { } else {
const itemsToCreate = []; const itemsToCreate = [];
@ -364,7 +410,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
action: 'update', action: 'update',
documentName: 'Item', documentName: 'Item',
parent: originActor, parent: originActor,
updates: [{ '_id': item.id, 'system.quantity': item.system.quantity - quantity }] updates: [{ _id: item.id, 'system.quantity': item.system.quantity - quantity }]
}); });
} }

View file

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

View file

@ -13,8 +13,8 @@ export default class DhActorDirectory extends foundry.applications.sidebar.tabs.
return document.type === 'adversary' return document.type === 'adversary'
? game.i18n.localize(adversaryTypes[document.system.type]?.label ?? 'TYPES.Actor.adversary') ? game.i18n.localize(adversaryTypes[document.system.type]?.label ?? 'TYPES.Actor.adversary')
: document.type === 'environment' : document.type === 'environment'
? game.i18n.localize(environmentTypes[document.system.type]?.label ?? 'TYPES.Actor.environment') ? game.i18n.localize(environmentTypes[document.system.type]?.label ?? 'TYPES.Actor.environment')
: null; : null;
}; };
} }

View file

@ -41,8 +41,8 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
const advantage = rollCommand.advantage const advantage = rollCommand.advantage
? CONFIG.DH.ACTIONS.advantageState.advantage.value ? CONFIG.DH.ACTIONS.advantageState.advantage.value
: rollCommand.disadvantage : rollCommand.disadvantage
? CONFIG.DH.ACTIONS.advantageState.disadvantage.value ? CONFIG.DH.ACTIONS.advantageState.disadvantage.value
: undefined; : undefined;
const difficulty = rollCommand.difficulty; const difficulty = rollCommand.difficulty;
const grantResources = rollCommand.grantResources; const grantResources = rollCommand.grantResources;
@ -50,8 +50,8 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
const title = const title =
(flavor ?? traitValue) (flavor ?? traitValue)
? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { ? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(SYSTEM.ACTOR.abilities[traitValue].label) ability: game.i18n.localize(CONFIG.DH.ACTOR.abilities[traitValue].label)
}) })
: game.i18n.localize('DAGGERHEART.GENERAL.duality'); : game.i18n.localize('DAGGERHEART.GENERAL.duality');
enrichedDualityRoll({ enrichedDualityRoll({
@ -103,6 +103,19 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
_getEntryContextOptions() { _getEntryContextOptions() {
return [ return [
...super._getEntryContextOptions(), ...super._getEntryContextOptions(),
{
label: 'DAGGERHEART.UI.ChatLog.rerollActionRoll',
icon: '<i class="fa-solid fa-dice"></i>',
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', label: 'DAGGERHEART.UI.ChatLog.rerollDamage',
icon: '<i class="fa-solid fa-dice"></i>', icon: '<i class="fa-solid fa-dice"></i>',
@ -113,9 +126,10 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
: false; : false;
return (game.user.isGM || message.isAuthor) && hasRolledDamage; return (game.user.isGM || message.isAuthor) && hasRolledDamage;
}, },
callback: li => { callback: async li => {
const message = game.messages.get(li.dataset.messageId); 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);
} }
} }
]; ];

View file

@ -84,19 +84,49 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
}); });
} }
/**
* 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() { _getCombatContextOptions() {
return [ return [
{ {
label: 'COMBAT.ClearMovementHistories', label: 'COMBAT.ACTIONS.EditName',
icon: '<i class="fa-solid fa-shoe-prints"></i>', icon: 'fa-solid fa-tag',
visible: () => game.user.isGM && this.viewed?.combatants.size > 0, visible: () => game.user.isGM && !!this.viewed,
callback: () => this.viewed.clearMovementHistories() onClick: () => DhCombatTracker.#onEditName.call(this)
}, },
{ {
label: 'COMBAT.Delete', label: 'COMBAT.ACTIONS.LinkToScene',
icon: '<i class="fa-solid fa-trash"></i>', 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, visible: () => game.user.isGM && !!this.viewed,
callback: () => this.viewed.endCombat() onClick: () => this.viewed.endCombat()
} }
]; ];
} }
@ -133,7 +163,7 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
canPing: combatant.sceneId === canvas.scene?.id && game.user.hasPermission('PING_CANVAS'), canPing: combatant.sceneId === canvas.scene?.id && game.user.hasPermission('PING_CANVAS'),
type: combatant.actor?.system?.type, type: combatant.actor?.system?.type,
img: await this._getCombatantThumbnail(combatant), img: await this._getCombatantThumbnail(combatant),
disposition: combatant.token.disposition disposition: combatant.token?.disposition
}; };
turn.css = [turn.active ? 'active' : null, hidden ? 'hide' : null, isDefeated ? 'defeated' : null].filterJoin( turn.css = [turn.active ? 'active' : null, hidden ? 'hide' : null, isDefeated ? 'defeated' : null].filterJoin(

View file

@ -56,8 +56,8 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio
? countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id ? countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id
? 'DAGGERHEART.UI.Countdowns.increasingLoop' ? 'DAGGERHEART.UI.Countdowns.increasingLoop'
: countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id : countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id
? 'DAGGERHEART.UI.Countdowns.decreasingLoop' ? 'DAGGERHEART.UI.Countdowns.decreasingLoop'
: 'DAGGERHEART.UI.Countdowns.loop' : 'DAGGERHEART.UI.Countdowns.loop'
: null; : null;
const randomizeValid = !new Roll(countdown.progress.startFormula ?? '').isDeterministic; const randomizeValid = !new Roll(countdown.progress.startFormula ?? '').isDeterministic;
acc[key] = { acc[key] = {
@ -148,11 +148,11 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio
} }
async gmSetSetting(data) { async gmSetSetting(data) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data), await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data);
game.socket.emit(`system.${CONFIG.DH.id}`, { game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh, action: socketEvent.Refresh,
data: { refreshType: RefreshType.Countdown } data: { refreshType: RefreshType.Countdown }
}); });
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown }); Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
} }

View file

@ -31,9 +31,9 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
minimizable: false minimizable: false
}, },
actions: { actions: {
toggleViewMode: DhCountdowns.#toggleViewMode, toggleViewMode: DhCountdowns.#onToggleViewMode,
editCountdowns: DhCountdowns.#editCountdowns, editCountdowns: DhCountdowns.#onEditCountdowns,
loopCountdown: DhCountdowns.#loopCountdown, loopCountdown: DhCountdowns.#onLoopCountdown,
decreaseCountdown: (_, target) => this.editCountdown(false, target), decreaseCountdown: (_, target) => this.editCountdown(false, target),
increaseCountdown: (_, target) => this.editCountdown(true, target) increaseCountdown: (_, target) => this.editCountdown(true, target)
}, },
@ -101,8 +101,8 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
? countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id ? countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id
? 'DAGGERHEART.UI.Countdowns.increasingLoop' ? 'DAGGERHEART.UI.Countdowns.increasingLoop'
: countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id : countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id
? 'DAGGERHEART.UI.Countdowns.decreasingLoop' ? 'DAGGERHEART.UI.Countdowns.decreasingLoop'
: 'DAGGERHEART.UI.Countdowns.loop' : 'DAGGERHEART.UI.Countdowns.loop'
: null; : null;
const loopDisabled = const loopDisabled =
!countdownEditable || !countdownEditable ||
@ -147,7 +147,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
return true; return true;
} }
static async #toggleViewMode() { static async #onToggleViewMode() {
const currentMode = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode); const currentMode = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode);
const appMode = CONFIG.DH.GENERAL.countdownAppMode; const appMode = CONFIG.DH.GENERAL.countdownAppMode;
const newMode = currentMode === appMode.textIcon ? appMode.iconOnly : appMode.textIcon; const newMode = currentMode === appMode.textIcon ? appMode.iconOnly : appMode.textIcon;
@ -158,15 +158,16 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
this.render(); this.render();
} }
static async #editCountdowns() { static async #onEditCountdowns() {
new game.system.api.applications.ui.CountdownEdit().render(true); new game.system.api.applications.ui.CountdownEdit().render(true);
} }
static async #loopCountdown(_, target) { static async #onLoopCountdown(_, target) {
if (!DhCountdowns.canPerformEdit()) return; if (!DhCountdowns.canPerformEdit()) return;
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); 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 progressMax = countdown.progress.start;
let message = null; let message = null;
@ -180,12 +181,12 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id
? Number(progressMax) + 1 ? Number(progressMax) + 1
: countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id : countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id
? Math.max(Number(progressMax) - 1, 0) ? Math.max(Number(progressMax) - 1, 0)
: progressMax; : progressMax;
await waitForDiceSoNice(message); await waitForDiceSoNice(message);
await settings.updateSource({ await settings.updateSource({
[`countdowns.${target.id}.progress`]: { [`countdowns.${countdownId}.progress`]: {
current: newMax, current: newMax,
start: newMax start: newMax
} }
@ -199,22 +200,23 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
if (!DhCountdowns.canPerformEdit()) return; if (!DhCountdowns.canPerformEdit()) return;
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); 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 const newCurrent = increase
? Math.min(countdown.progress.current + 1, countdown.progress.start) ? Math.min(countdown.progress.current + 1, countdown.progress.start)
: Math.max(countdown.progress.current - 1, 0); : Math.max(countdown.progress.current - 1, 0);
await settings.updateSource({ [`countdowns.${target.id}.progress.current`]: newCurrent }); await settings.updateSource({ [`countdowns.${countdownId}.progress.current`]: newCurrent });
await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, { await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
refreshType: RefreshType.Countdown refreshType: RefreshType.Countdown
}); });
} }
static async gmSetSetting(data) { static async gmSetSetting(data) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data), await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data);
game.socket.emit(`system.${CONFIG.DH.id}`, { game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh, action: socketEvent.Refresh,
data: { refreshType: RefreshType.Countdown } data: { refreshType: RefreshType.Countdown }
}); });
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown }); Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
} }

View file

@ -67,10 +67,10 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica
const actor = token const actor = token
? token.actor ? token.actor
: canvas.tokens.controlled.length === 0 : canvas.tokens.controlled.length === 0
? !game.user.isGM ? !game.user.isGM
? game.user.character ? game.user.character
: null : null
: canvas.tokens.controlled[0].actor; : canvas.tokens.controlled[0].actor;
return getIconVisibleActiveEffects(actor?.getActiveEffects() ?? []); return getIconVisibleActiveEffects(actor?.getActiveEffects() ?? []);
}; };

View file

@ -1,3 +1,4 @@
import { getDocFromElement } from '../../helpers/utils.mjs';
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -47,7 +48,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
expandContent: this.expandContent, expandContent: this.expandContent,
resetFilters: this.resetFilters, resetFilters: this.resetFilters,
sortList: this.sortList, sortList: this.sortList,
openSettings: this.openSettings openSettings: this.openSettings,
viewSheet: this.#onViewSheet
}, },
position: { position: {
left: 100, left: 100,
@ -306,7 +308,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
{ {
items: this.items, items: this.items,
menu: this.selectedMenu, menu: this.selectedMenu,
formatLabel: this.formatLabel formatLabel: this.formatLabel,
viewSheet: this.items[0] instanceof Actor
} }
); );
@ -568,6 +571,11 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
} }
} }
static async #onViewSheet(_, target) {
const document = await getDocFromElement(target);
document?.sheet?.render(true);
}
_createDragProcess() { _createDragProcess() {
new foundry.applications.ux.DragDrop.implementation({ new foundry.applications.ux.DragDrop.implementation({
dragSelector: '.item-container', dragSelector: '.item-container',
@ -606,7 +614,16 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
items: { items: {
folder: 'equipments', folder: 'equipments',
render: { render: {
noFolder: true folders: [
'equipments',
'ancestries',
'classes',
'subclasses',
'domains',
'communities',
'beastforms'
// excluded: features
]
} }
}, },
compendium: {} compendium: {}

View file

@ -155,15 +155,15 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
const targetEdge = this.#getEdgeBoundary(targetBounds, originPoint, targetPoint); const targetEdge = this.#getEdgeBoundary(targetBounds, originPoint, targetPoint);
const adjustedOriginPoint = originEdge const adjustedOriginPoint = originEdge
? canvas.grid.getTopLeftPoint({ ? canvas.grid.getTopLeftPoint({
x: originEdge.x + Math.sign(originPoint.x - originEdge.x), x: originEdge.x + Math.sign(originPoint.x - originEdge.x),
y: originEdge.y + Math.sign(originPoint.y - originEdge.y) y: originEdge.y + Math.sign(originPoint.y - originEdge.y)
}) })
: originPoint; : originPoint;
const adjustDestinationPoint = targetEdge const adjustDestinationPoint = targetEdge
? canvas.grid.getTopLeftPoint({ ? canvas.grid.getTopLeftPoint({
x: targetEdge.x + Math.sign(targetPoint.x - targetEdge.x), x: targetEdge.x + Math.sign(targetPoint.x - targetEdge.x),
y: targetEdge.y + Math.sign(targetPoint.y - targetEdge.y) y: targetEdge.y + Math.sign(targetPoint.y - targetEdge.y)
}) })
: targetPoint; : targetPoint;
const distance = canvas.grid.measurePath([ const distance = canvas.grid.measurePath([
{ ...adjustedOriginPoint, elevation: 0 }, { ...adjustedOriginPoint, elevation: 0 },

View file

@ -70,6 +70,49 @@ 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: { items: {
columns: [ columns: [
{ {
@ -84,8 +127,8 @@ export const typeConfig = {
isSecondary isSecondary
? 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon.short' ? 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon.short'
: isSecondary === false : isSecondary === false
? 'DAGGERHEART.ITEMS.Weapon.primaryWeapon.short' ? 'DAGGERHEART.ITEMS.Weapon.primaryWeapon.short'
: '-' : '-'
}, },
{ {
key: 'system.tier', key: 'system.tier',
@ -400,10 +443,12 @@ export const typeConfig = {
const list = []; const list = [];
for (const item of items.filter(item => item.system.linkedClass)) { for (const item of items.filter(item => item.system.linkedClass)) {
const linkedClass = await foundry.utils.fromUuid(item.system.linkedClass); const linkedClass = await foundry.utils.fromUuid(item.system.linkedClass);
list.push({ if (linkedClass) {
value: linkedClass.uuid, list.push({
label: linkedClass.name value: linkedClass.uuid,
}); label: linkedClass.name
});
}
} }
return list.reduce((a, c) => { return list.reduce((a, c) => {
@ -559,7 +604,8 @@ export const compendiumConfig = {
id: 'environments', id: 'environments',
keys: ['environments'], keys: ['environments'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.environments', label: 'DAGGERHEART.UI.ItemBrowser.folders.environments',
type: ['environment'] type: ['environment'],
listType: 'environments'
}, },
beastforms: { beastforms: {
id: 'beastforms', id: 'beastforms',

View file

@ -75,7 +75,12 @@ export default class DHAttackAction extends DHDamageAction {
const useAltDamage = this.actor?.effects?.find(x => x.type === 'horde')?.active; const useAltDamage = this.actor?.effects?.find(x => x.type === 'horde')?.active;
for (const { value, valueAlt, type } of damage.parts) { for (const { value, valueAlt, type } of damage.parts) {
const usedValue = useAltDamage ? valueAlt : value; const usedValue = useAltDamage ? valueAlt : value;
const str = Roll.replaceFormulaData(usedValue.getFormula(), this.actor?.getRollData() ?? {}); const damageString = Roll.replaceFormulaData(usedValue.getFormula(), this.actor?.getRollData() ?? {});
const str = damageString
? damageString
: game.i18n.format('DAGGERHEART.GENERAL.missingX', {
x: game.i18n.localize('DAGGERHEART.GENERAL.damage')
});
const icons = Array.from(type) const icons = Array.from(type)
.map(t => CONFIG.DH.GENERAL.damageTypes[t]?.icon) .map(t => CONFIG.DH.GENERAL.damageTypes[t]?.icon)

View file

@ -144,14 +144,18 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return this.item instanceof DhpActor return this.item instanceof DhpActor
? this.item ? this.item
: this.item?.parent instanceof DhpActor : this.item?.parent instanceof DhpActor
? this.item.parent ? this.item.parent
: null; : null;
} }
/** Returns true if the action is usable */ /**
* Returns true if the action is usable.
* An action is usable on any actor type. For example, an adversary might have a base attack action.
*/
get usable() { get usable() {
const actor = this.actor; const actor = this.actor;
return this.isOwner && actor?.type === 'character'; const pack = actor?.pack ? game.packs.get(actor.pack) : null;
return !pack?.locked && this.isOwner;
} }
static getRollType(parent) { static getRollType(parent) {
@ -219,7 +223,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
* @returns {object} * @returns {object}
*/ */
async use(event, configOptions = {}) { async use(event, configOptions = {}) {
if (!this.actor) throw new Error("An Action can't be used outside of an Actor context."); if (!this.actor) throw new Error('An Action can\'t be used outside of an Actor context.');
let config = this.prepareConfig(event, configOptions); let config = this.prepareConfig(event, configOptions);
if (!config) return; if (!config) return;
@ -296,17 +300,17 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
const groupAttackTokens = this.damage.groupAttack const groupAttackTokens = this.damage.groupAttack
? game.system.api.fields.ActionFields.DamageField.getGroupAttackTokens( ? game.system.api.fields.ActionFields.DamageField.getGroupAttackTokens(
this.actor.id, this.actor.id,
this.damage.groupAttack this.damage.groupAttack
) )
: null; : null;
config.damageOptions = { config.damageOptions = {
groupAttack: this.damage.groupAttack groupAttack: this.damage.groupAttack
? { ? {
numAttackers: Math.max(groupAttackTokens.length, 1), numAttackers: Math.max(groupAttackTokens.length, 1),
range: this.damage.groupAttack range: this.damage.groupAttack
} }
: null : null
}; };
} }

View file

@ -36,7 +36,7 @@ export default class DhCountdownAction extends DHBaseAction {
/** @inheritDoc */ /** @inheritDoc */
static migrateData(source) { static migrateData(source) {
for (const countdown of source.countdown) { for (const countdown of Object.values(source.countdown)) {
if (countdown.progress.max) { if (countdown.progress.max) {
countdown.progress.startFormula = countdown.progress.max; countdown.progress.startFormula = countdown.progress.max;
countdown.progress.start = 1; countdown.progress.start = 1;

View file

@ -35,6 +35,7 @@ export default class BeastformEffect extends BaseEffect {
static migrateData(source) { static migrateData(source) {
if (!source.characterTokenData.tokenSize.height) source.characterTokenData.tokenSize.height = 1; if (!source.characterTokenData.tokenSize.height) source.characterTokenData.tokenSize.height = 1;
if (!source.characterTokenData.tokenSize.width) source.characterTokenData.tokenSize.width = 1; if (!source.characterTokenData.tokenSize.width) source.characterTokenData.tokenSize.width = 1;
if (!source.characterTokenData.tokenSize.depth) source.characterTokenData.tokenSize.depth = 1;
return super.migrateData(source); return super.migrateData(source);
} }
@ -52,7 +53,8 @@ export default class BeastformEffect extends BaseEffect {
if (this.parent.parent.type === 'character') { if (this.parent.parent.type === 'character') {
const baseUpdate = { const baseUpdate = {
height: this.characterTokenData.tokenSize.height, height: this.characterTokenData.tokenSize.height,
width: this.characterTokenData.tokenSize.width width: this.characterTokenData.tokenSize.width,
depth: this.characterTokenData.tokenSize.depth
}; };
const update = { const update = {
...baseUpdate, ...baseUpdate,
@ -88,13 +90,13 @@ export default class BeastformEffect extends BaseEffect {
...baseUpdate, ...baseUpdate,
x, x,
y, y,
'texture': { texture: {
enabled: this.characterTokenData.usesDynamicToken, enabled: this.characterTokenData.usesDynamicToken,
src: token.flags.daggerheart?.beastformTokenImg ?? this.characterTokenData.tokenImg, src: token.flags.daggerheart?.beastformTokenImg ?? this.characterTokenData.tokenImg,
scaleX: this.characterTokenData.tokenSize.scale, scaleX: this.characterTokenData.tokenSize.scale,
scaleY: this.characterTokenData.tokenSize.scale scaleY: this.characterTokenData.tokenSize.scale
}, },
'ring': { ring: {
subject: { subject: {
texture: texture:
token.flags.daggerheart?.beastformSubjectTexture ?? this.characterTokenData.tokenRingImg token.flags.daggerheart?.beastformSubjectTexture ?? this.characterTokenData.tokenRingImg

View file

@ -166,10 +166,10 @@ export default class ArmorChange extends foundry.abstract.DataModel {
value: value:
change.type === 'armor' change.type === 'armor'
? { ? {
...change.value, ...change.value,
current: Math.min(change.value.current, newMax), current: Math.min(change.value.current, newMax),
max: newMax max: newMax
} }
: change.value : change.value
})) }))
]; ];

View file

@ -1,15 +1,17 @@
import DhCharacter from './character.mjs'; import DhCharacter from './character.mjs';
import DhCompanion from './companion.mjs'; import DhCompanion from './companion.mjs';
import DhAdversary from './adversary.mjs'; import DhAdversary from './adversary.mjs';
import DhNPC from './npc.mjs';
import DhEnvironment from './environment.mjs'; import DhEnvironment from './environment.mjs';
import DhParty from './party.mjs'; import DhParty from './party.mjs';
export { DhCharacter, DhCompanion, DhAdversary, DhEnvironment, DhParty }; export { DhCharacter, DhCompanion, DhAdversary, DhNPC, DhEnvironment, DhParty };
export const config = { export const config = {
character: DhCharacter, character: DhCharacter,
companion: DhCompanion, companion: DhCompanion,
adversary: DhAdversary, adversary: DhAdversary,
npc: DhNPC,
environment: DhEnvironment, environment: DhEnvironment,
party: DhParty party: DhParty
}; };

View file

@ -3,8 +3,7 @@ import { ActionField } from '../fields/actionField.mjs';
import { commonActorRules } from './base.mjs'; import { commonActorRules } from './base.mjs';
import DhCreature from './creature.mjs'; import DhCreature from './creature.mjs';
import { bonusField } from '../fields/actorField.mjs'; import { bonusField } from '../fields/actorField.mjs';
import { calculateExpectedValue, parseTermsFromSimpleFormula } from '../../helpers/utils.mjs'; import { getTierAdjustedAdversary } from './tierAdjustment.mjs';
import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs';
export default class DhpAdversary extends DhCreature { export default class DhpAdversary extends DhCreature {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Adversary']; static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Adversary'];
@ -206,205 +205,6 @@ export default class DhpAdversary extends DhCreature {
/** Returns source data for this actor adjusted to a new tier, which can be used to create a new actor. */ /** Returns source data for this actor adjusted to a new tier, which can be used to create a new actor. */
adjustForTier(tier) { adjustForTier(tier) {
const source = this.parent.toObject(true); const source = this.parent.toObject(true);
return getTierAdjustedAdversary(source, tier);
/** @type {(2 | 3 | 4)[]} */
const tiers = new Array(Math.abs(tier - this.tier))
.fill(0)
.map((_, idx) => idx + Math.min(tier, this.tier) + 1);
if (tier < this.tier) tiers.reverse();
const typeData = adversaryScalingData[source.system.type] ?? adversaryScalingData[source.system.standard];
const tierEntries = tiers.map(t => ({ tier: t, ...typeData[t] }));
// Apply simple tier changes
const scale = tier > this.tier ? 1 : -1;
for (const entry of tierEntries) {
source.system.difficulty += scale * entry.difficulty;
source.system.damageThresholds.major += scale * entry.majorThreshold;
source.system.damageThresholds.severe += scale * entry.severeThreshold;
source.system.resources.hitPoints.max += scale * entry.hp;
source.system.resources.stress.max += scale * entry.stress;
source.system.attack.roll.bonus += scale * entry.attack;
}
// Get the mean and standard deviation of expected damage in the previous and new tier
// The data we have is for attack scaling, but we reuse this for action scaling later
const expectedDamageData = adversaryExpectedDamage[source.system.type] ?? adversaryExpectedDamage.basic;
const damageMeta = {
currentDamageRange: { tier: source.system.tier, ...expectedDamageData[source.system.tier] },
newDamageRange: { tier, ...expectedDamageData[tier] },
type: 'attack'
};
// Update damage of base attack
try {
this.#adjustActionDamage(source.system.attack, damageMeta);
} catch (err) {
ui.notifications.warn('Failed to convert attack damage of adversary');
console.error(err);
}
// Update damage of each item action, making sure to also update the description if possible
const damageRegex = /@Damage\[([^\[\]]*)\]({[^}]*})?/g;
for (const item of source.items) {
// Replace damage inlines with new formulas
for (const withDescription of [item.system, ...Object.values(item.system.actions)]) {
withDescription.description = withDescription.description.replace(damageRegex, (match, inner) => {
const { value: formula } = parseInlineParams(inner);
if (!formula || !type) return match;
try {
const adjusted = this.#calculateAdjustedDamage(formula, { ...damageMeta, type: 'action' });
const newFormula = [
adjusted.diceQuantity ? `${adjusted.diceQuantity}d${adjusted.faces}` : null,
adjusted.bonus
]
.filter(p => !!p)
.join('+');
return match.replace(formula, newFormula);
} catch {
return match;
}
});
}
// Update damage in item actions
// Parse damage, and convert all formula matches in the descriptions to the new damage
for (const action of Object.values(item.system.actions)) {
try {
const result = this.#adjustActionDamage(action, { ...damageMeta, type: 'action' });
if (!result) continue;
for (const { previousFormula, formula } of Object.values(result)) {
const oldFormulaRegexp = new RegExp(
previousFormula.replace(' ', '').replace('+', '(?:\\s)?\\+(?:\\s)?')
);
item.system.description = item.system.description.replace(oldFormulaRegexp, formula);
action.description = action.description.replace(oldFormulaRegexp, formula);
}
} catch (err) {
ui.notifications.warn(`Failed to convert action damage for item ${item.name}`);
console.error(err);
}
}
}
// Finally set the tier of the source data, now that everything is complete
source.system.tier = tier;
return source;
}
/**
* Converts a damage object to a new damage range
* @returns {{ diceQuantity: number; faces: number; bonus: number }} the adjusted result as a combined term
* @throws error if the formula is the wrong type
*/
#calculateAdjustedDamage(formula, { currentDamageRange, newDamageRange, type }) {
const terms = parseTermsFromSimpleFormula(formula);
const flatTerms = terms.filter(t => t.diceQuantity === 0);
const diceTerms = terms.filter(t => t.diceQuantity > 0);
if (flatTerms.length > 1 || diceTerms.length > 1) {
throw new Error('invalid formula for conversion');
}
const value = {
...(diceTerms[0] ?? { diceQuantity: 0, faces: 1 }),
bonus: flatTerms[0]?.bonus ?? 0
};
const previousExpected = calculateExpectedValue(value);
if (previousExpected === 0) return value; // nothing to do
const dieSizes = [4, 6, 8, 10, 12, 20];
const steps = newDamageRange.tier - currentDamageRange.tier;
const increasing = steps > 0;
const deviation = (previousExpected - currentDamageRange.mean) / currentDamageRange.deviation;
const expected = Math.max(1, newDamageRange.mean + newDamageRange.deviation * deviation);
// If this was just a flat number, convert to the expected damage and exit
if (value.diceQuantity === 0) {
value.bonus = Math.round(expected);
return value;
}
const getExpectedDie = () => calculateExpectedValue({ diceQuantity: 1, faces: value.faces }) || 1;
const getBaseAverage = () => calculateExpectedValue({ ...value, bonus: 0 });
// Check the number of base overages over the expected die. In the end, if the bonus inflates too much, we add a die
const baseOverages = Math.floor(value.bonus / getExpectedDie());
// Prestep. Change number of dice for attacks, bump up/down for actions
// We never bump up to d20, though we might bump down from it
if (type === 'attack') {
const minimum = increasing ? value.diceQuantity : 0;
value.diceQuantity = Math.max(minimum, newDamageRange.tier);
} else {
const currentIdx = dieSizes.indexOf(value.faces);
value.faces = dieSizes[Math.clamp(currentIdx + steps, 0, 4)];
}
value.bonus = Math.round(expected - getBaseAverage());
// Attempt to handle negative values.
// If we can do it with only step downs, do so. Otherwise remove tier dice, and try again
if (value.bonus < 0) {
let stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity);
const currentIdx = dieSizes.indexOf(value.faces);
// If step downs alone don't suffice, change the flat modifier, then calculate steps required again
// If this isn't sufficient, the result will be slightly off. This is unlikely to happen
if (type !== 'attack' && stepsRequired > currentIdx && value.diceQuantity > 0) {
value.diceQuantity -= increasing ? 1 : Math.abs(steps);
value.bonus = Math.round(expected - getBaseAverage());
if (value.bonus >= 0) return value; // complete
}
stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity);
value.faces = dieSizes[Math.max(0, currentIdx - stepsRequired)];
value.bonus = Math.max(0, Math.round(expected - getBaseAverage()));
}
// If value is really high, we add a number of dice based on the number of overages
// This attempts to preserve a similar amount of variance when increasing an action
const overagesToRemove = Math.floor(value.bonus / getExpectedDie()) - baseOverages;
if (type !== 'attack' && increasing && overagesToRemove > 0) {
value.diceQuantity += overagesToRemove;
value.bonus = Math.round(expected - getBaseAverage());
}
return value;
}
/**
* Updates damage to reflect a specific value.
* @throws if damage structure is invalid for conversion
* @returns the converted formula and value as a simplified term, or null if it doesn't deal HP damage
*/
#adjustActionDamage(action, damageMeta) {
if (!action.damage?.parts.hitPoints) return null;
const result = {};
for (const property of ['value', 'valueAlt']) {
const data = action.damage.parts.hitPoints[property];
const previousFormula = data.custom.enabled
? data.custom.formula
: [data.flatMultiplier ? `${data.flatMultiplier}${data.dice}` : 0, data.bonus ?? 0]
.filter(p => !!p)
.join('+');
const value = this.#calculateAdjustedDamage(previousFormula, damageMeta);
const formula = [value.diceQuantity ? `${value.diceQuantity}d${value.faces}` : null, value.bonus]
.filter(p => !!p)
.join('+');
if (value.diceQuantity) {
data.custom.enabled = false;
data.bonus = value.bonus;
data.dice = `d${value.faces}`;
data.flatMultiplier = value.diceQuantity;
} else if (!value.diceQuantity) {
data.custom.enabled = true;
data.custom.formula = formula;
}
result[property] = { previousFormula, formula, value };
}
return result;
} }
} }

View file

@ -315,8 +315,8 @@ export default class DhCharacter extends DhCreature {
return currentLevel === 1 return currentLevel === 1
? 1 ? 1
: Object.values(game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers).find( : Object.values(game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers).find(
tier => currentLevel >= tier.levels.start && currentLevel <= tier.levels.end tier => currentLevel >= tier.levels.start && currentLevel <= tier.levels.end
).tier; ).tier;
} }
get ancestry() { get ancestry() {
@ -520,20 +520,20 @@ export default class DhCharacter extends DhCreature {
if (armorSource.type === 'armor') { if (armorSource.type === 'armor') {
armorUpdates[armorSource.parent.id].updates.push({ armorUpdates[armorSource.parent.id].updates.push({
'_id': armorSource.id, _id: armorSource.id,
'system.armor.current': armorSource.system.armor.current + usedArmorChange 'system.armor.current': armorSource.system.armor.current + usedArmorChange
}); });
} else { } else {
effectUpdates[armorSource.parent.id].updates.push({ effectUpdates[armorSource.parent.id].updates.push({
'_id': armorSource.id, _id: armorSource.id,
'system.changes': armorSource.system.changes.map(change => ({ 'system.changes': armorSource.system.changes.map(change => ({
...change, ...change,
value: value:
change.type === 'armor' change.type === 'armor'
? { ? {
...change.value, ...change.value,
current: armorSource.system.armorChange.value.current + usedArmorChange current: armorSource.system.armorChange.value.current + usedArmorChange
} }
: change.value : change.value
})) }))
}); });
@ -577,6 +577,8 @@ export default class DhCharacter extends DhCreature {
communityFeatures = [], communityFeatures = [],
classFeatures = [], classFeatures = [],
subclassFeatures = [], subclassFeatures = [],
multiclassFeatures = [],
multiclassSubclassFeatures = [],
companionFeatures = [], companionFeatures = [],
features = []; features = [];
@ -586,9 +588,9 @@ export default class DhCharacter extends DhCreature {
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.community.id) { } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.community.id) {
communityFeatures.push(item); communityFeatures.push(item);
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.class.id) { } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.class.id) {
classFeatures.push(item); (item.system.multiclassOrigin ? multiclassFeatures : classFeatures).push(item);
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.subclass.id) { } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.subclass.id) {
subclassFeatures.push(item); (item.system.multiclassOrigin ? multiclassSubclassFeatures : subclassFeatures).push(item);
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.companion.id) { } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.companion.id) {
companionFeatures.push(item); companionFeatures.push(item);
} else if (item.type === 'feature' && !item.system.type) { } else if (item.type === 'feature' && !item.system.type) {
@ -617,6 +619,24 @@ export default class DhCharacter extends DhCreature {
type: 'subclass', type: 'subclass',
values: subclassFeatures values: subclassFeatures
}, },
...(multiclassFeatures.length
? {
multiclassFeatures: {
title: `${game.i18n.localize('DAGGERHEART.GENERAL.multiclass')} - ${this.multiclass.value?.name}`,
type: 'multiclass',
values: multiclassFeatures
}
}
: {}),
...(multiclassSubclassFeatures.length
? {
multiclassSubclassFeatures: {
title: `${game.i18n.localize('DAGGERHEART.GENERAL.multiclass')} ${game.i18n.localize('TYPES.Item.subclass')} - ${this.multiclass.subclass?.name}`,
type: 'multiclassSubclass',
values: multiclassSubclassFeatures
}
}
: {}),
companionFeatures: { companionFeatures: {
title: game.i18n.localize('DAGGERHEART.ACTORS.Character.companionFeatures'), title: game.i18n.localize('DAGGERHEART.ACTORS.Character.companionFeatures'),
type: 'companion', type: 'companion',
@ -639,8 +659,8 @@ export default class DhCharacter extends DhCreature {
(this.primaryWeapon && this.secondaryWeapon) (this.primaryWeapon && this.secondaryWeapon)
? burden.twoHanded.value ? burden.twoHanded.value
: this.primaryWeapon || this.secondaryWeapon : this.primaryWeapon || this.secondaryWeapon
? burden.oneHanded.value ? burden.oneHanded.value
: null; : null;
} }
get deathMoveViable() { get deathMoveViable() {
@ -706,8 +726,8 @@ export default class DhCharacter extends DhCreature {
currentLevel === 1 currentLevel === 1
? null ? null
: Object.values(game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers).find( : Object.values(game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers).find(
tier => currentLevel >= tier.levels.start && currentLevel <= tier.levels.end tier => currentLevel >= tier.levels.start && currentLevel <= tier.levels.end
).tier; ).tier;
if (game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto) { if (game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto) {
for (let levelKey in this.levelData.levelups) { for (let levelKey in this.levelData.levelups) {
const level = this.levelData.levelups[levelKey]; const level = this.levelData.levelups[levelKey];
@ -840,12 +860,13 @@ export default class DhCharacter extends DhCreature {
const newHopeMax = this.resources.hope.max + diff; const newHopeMax = this.resources.hope.max + diff;
const newHopeValue = Math.min(newHopeMax, this.resources.hope.value); const newHopeValue = Math.min(newHopeMax, this.resources.hope.value);
if (newHopeValue != this.resources.hope.value) { if (newHopeValue != this.resources.hope.value) {
if (!changes.system.resources.hope) changes.system.resources.hope = { value: 0 }; changes.system = foundry.utils.mergeObject(changes.system ?? {}, {
resources: {
changes.system.resources.hope = { hope: {
...changes.system.resources.hope, value: (changes.system?.resources?.hope?.value ?? 0) + newHopeMax
value: changes.system.resources.hope.value + newHopeValue }
}; }
});
} }
} }

43
module/data/actor/npc.mjs Normal file
View file

@ -0,0 +1,43 @@
import DHNPCSettings from '../../applications/sheets-configs/npc-settings.mjs';
import BaseDataActor from './base.mjs';
export default class DhpNPC extends BaseDataActor {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.NPC'];
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {
label: 'TYPES.Actor.npc',
type: 'npc',
settingSheet: DHNPCSettings,
hasResistances: false,
hasAttribution: true
});
}
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
difficulty: new fields.NumberField({
nullable: true,
initial: null,
integer: true,
label: 'DAGGERHEART.GENERAL.difficulty'
}),
description: new fields.HTMLField({ label: 'DAGGERHEART.GENERAL.description' }),
motives: new fields.StringField(),
notes: new fields.HTMLField()
};
}
/**@inheritdoc */
static DEFAULT_ICON = 'systems/daggerheart/assets/icons/documents/actors/drama-masks.svg';
get features() {
return this.parent.items.filter(x => x.type === 'feature');
}
isItemValid(source) {
return super.isItemValid(source) || source.type === 'feature';
}
}

View file

@ -0,0 +1,219 @@
import { calculateExpectedValue, parseTermsFromSimpleFormula } from '../../helpers/utils.mjs';
import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs';
import { parseInlineParams } from '../../enrichers/parser.mjs';
export function getTierAdjustedAdversary(source, tier) {
const currentTier = source.tier ?? 1;
/** @type {(2 | 3 | 4)[]} */
const tiers = new Array(Math.abs(tier - currentTier))
.fill(0)
.map((_, idx) => idx + Math.min(tier, currentTier) + 1);
if (tier < currentTier) tiers.reverse();
const typeData = adversaryScalingData[source.system.type] ?? adversaryScalingData[source.system.standard];
const tierEntries = tiers.map(t => ({ tier: t, ...typeData[t] }));
// Apply simple tier changes
const scale = tier > currentTier ? 1 : -1;
for (const entry of tierEntries) {
source.system.difficulty += scale * entry.difficulty;
source.system.damageThresholds.major += scale * entry.majorThreshold;
source.system.damageThresholds.severe += scale * entry.severeThreshold;
source.system.resources.hitPoints.max += scale * entry.hp;
source.system.resources.stress.max += scale * entry.stress;
source.system.attack.roll.bonus += scale * entry.attack;
}
// Get the mean and standard deviation of expected damage in the previous and new tier
// The data we have is for attack scaling, but we reuse this for action scaling later
const expectedDamageData = adversaryExpectedDamage[source.system.type] ?? adversaryExpectedDamage.basic;
const damageMeta = {
currentDamageRange: { tier: source.system.tier, ...expectedDamageData[source.system.tier] },
newDamageRange: { tier, ...expectedDamageData[tier] }
};
// Store initial attack damage for abilities that have you deal a "standard attack"
const initialAttack = {
type: source.system.attack.damage?.parts.hitPoints?.type?.toSorted(),
value: getDamagePartsFormula(source.system.attack.damage?.parts.hitPoints?.value)
};
// Update damage of base attack.
try {
const damage = source.system.attack.damage;
if (!damage?.parts.hitPoints) throw new Error('Unexpected missing attack in adversary');
for (const property of ['value', 'valueAlt']) {
const data = damage.parts.hitPoints[property];
const previousFormula = getDamagePartsFormula(data);
const { value, formula } = calculateAdjustedDamage(previousFormula, 'attack', damageMeta);
applyAdjustedDamage(data, value, formula);
}
} catch (err) {
ui.notifications.warn('Failed to convert attack damage of adversary');
console.error(err);
}
// Update damage of each item action, making sure to also update the description if possible
const damageRegex = /@Damage\[([^\[\]]*)\]({[^}]*})?/g;
for (const item of source.items) {
// Replace damage inlines with new formulas. Keep a record for a specific check later
const descriptionFormulas = [];
for (const withDescription of [item.system, ...Object.values(item.system.actions)]) {
withDescription.description = withDescription.description.replace(damageRegex, (match, inner) => {
const { value: formula } = parseInlineParams(inner, { first: 'value' });
if (!formula) return match;
try {
const newFormula = calculateAdjustedDamage(formula, 'action', damageMeta)?.formula;
descriptionFormulas.push(formula);
return match.replace(formula, newFormula);
} catch {
return match;
}
});
}
// Update damage in item actions and convert all formula matches in the descriptions to the new damage
for (const action of Object.values(item.system.actions)) {
if (!action.damage?.parts.hitPoints) continue;
try {
// Apply conversions and save a record. If it matches attack damage *and* Its not in the description, use attack conversion instead
const result = [];
for (const property of ['value', 'valueAlt']) {
const { [property]: data, type: damageType } = action.damage.parts.hitPoints;
const previousFormula = getDamagePartsFormula(data);
const isActuallyAttack =
previousFormula === initialAttack.value &&
foundry.utils.equals(damageType.toSorted(), initialAttack.type) &&
!descriptionFormulas.includes(previousFormula);
const type = isActuallyAttack ? 'attack' : 'action';
const { value, formula } = calculateAdjustedDamage(previousFormula, type, damageMeta);
applyAdjustedDamage(data, value, formula);
result.push({ previousFormula, formula });
}
// Override text in the description with those values
for (const { previousFormula, formula } of Object.values(result)) {
const oldFormulaRegexp = new RegExp(
previousFormula.replace(' ', '').replace('+', '(?:\\s)?\\+(?:\\s)?')
);
item.system.description = item.system.description.replace(oldFormulaRegexp, formula);
action.description = action.description.replace(oldFormulaRegexp, formula);
}
} catch (err) {
ui.notifications.warn(`Failed to convert action damage for item ${item.name}`);
console.error(err);
}
}
}
// Finally set the tier of the source data, now that everything is complete
source.system.tier = tier;
return source;
}
/**
* Converts a damage object to a new damage range
* @returns {{ diceQuantity: number; faces: number; bonus: number }} the adjusted result as a combined term
* @throws error if the formula is the wrong type
*/
function calculateAdjustedDamage(formula, type, { currentDamageRange, newDamageRange }) {
const terms = parseTermsFromSimpleFormula(formula);
const flatTerms = terms.filter(t => t.diceQuantity === 0);
const diceTerms = terms.filter(t => t.diceQuantity > 0);
if (flatTerms.length > 1 || diceTerms.length > 1) {
throw new Error('invalid formula for conversion');
}
const value = {
...(diceTerms[0] ?? { diceQuantity: 0, faces: 1 }),
bonus: flatTerms[0]?.bonus ?? 0
};
const previousExpected = calculateExpectedValue(value);
if (previousExpected === 0) return value; // nothing to do
const dieSizes = [4, 6, 8, 10, 12, 20];
const steps = newDamageRange.tier - currentDamageRange.tier;
const increasing = steps > 0;
const deviation = (previousExpected - currentDamageRange.mean) / currentDamageRange.deviation;
const expected = Math.max(1, newDamageRange.mean + newDamageRange.deviation * deviation);
// If this was just a flat number, convert to the expected damage and exit
if (value.diceQuantity === 0) {
value.bonus = Math.round(expected);
return value;
}
const getExpectedDie = () => calculateExpectedValue({ diceQuantity: 1, faces: value.faces }) || 1;
const getBaseAverage = () => calculateExpectedValue({ ...value, bonus: 0 });
// Check the number of base overages over the expected die. In the end, if the bonus inflates too much, we add a die
const baseOverages = Math.floor(value.bonus / getExpectedDie());
// Prestep. Change number of dice for attacks, bump up/down for actions
// We never bump up to d20, though we might bump down from it
if (type === 'attack') {
const minimum = increasing ? value.diceQuantity : 0;
value.diceQuantity = Math.max(minimum, newDamageRange.tier);
} else {
const currentIdx = dieSizes.indexOf(value.faces);
value.faces = dieSizes[Math.clamp(currentIdx + steps, 0, 4)];
}
value.bonus = Math.round(expected - getBaseAverage());
// Attempt to handle negative values.
// If we can do it with only step downs, do so. Otherwise remove tier dice, and try again
if (value.bonus < 0) {
let stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity);
const currentIdx = dieSizes.indexOf(value.faces);
// If step downs alone don't suffice, change the flat modifier, then calculate steps required again
// If this isn't sufficient, the result will be slightly off. This is unlikely to happen
if (type !== 'attack' && stepsRequired > currentIdx && value.diceQuantity > 0) {
value.diceQuantity -= increasing ? 1 : Math.abs(steps);
value.bonus = Math.round(expected - getBaseAverage());
if (value.bonus >= 0) return value; // complete
}
stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity);
value.faces = dieSizes[Math.max(0, currentIdx - stepsRequired)];
value.bonus = Math.max(0, Math.round(expected - getBaseAverage()));
}
// If value is really high, we add a number of dice based on the number of overages
// This attempts to preserve a similar amount of variance when increasing an action
const overagesToRemove = Math.floor(value.bonus / getExpectedDie()) - baseOverages;
if (type !== 'attack' && increasing && overagesToRemove > 0) {
value.diceQuantity += overagesToRemove;
value.bonus = Math.round(expected - getBaseAverage());
}
const newFormula = [value.diceQuantity ? `${value.diceQuantity}d${value.faces}` : null, value.bonus]
.filter(p => !!p)
.join('+');
return { value, formula: newFormula };
}
function getDamagePartsFormula(data) {
return data.custom.enabled
? data.custom.formula
: [data.flatMultiplier ? `${data.flatMultiplier}${data.dice}` : 0, data.bonus ?? 0].filter(p => !!p).join('+');
}
/**
* Updates damage to reflect a specific value.
* @throws if damage structure is invalid for conversion
* @returns the converted formula and value as a simplified term, or null if it doesn't deal HP damage
*/
function applyAdjustedDamage(diceData, value, formula) {
if (value.diceQuantity) {
diceData.custom.enabled = false;
diceData.bonus = value.bonus;
diceData.dice = `d${value.faces}`;
diceData.flatMultiplier = value.diceQuantity;
} else if (!value.diceQuantity) {
diceData.custom.enabled = true;
diceData.custom.formula = formula;
}
}

View file

@ -1,3 +1,5 @@
import { triggerChatRollFx } from '../../helpers/utils.mjs';
const fields = foundry.data.fields; const fields = foundry.data.fields;
const targetsField = () => const targetsField = () =>
@ -130,6 +132,35 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
}); });
} }
/* TODO: Change how damage data is stored somehow to enable better rerolling */
async getRerolledDamage() {
if (!this.damage) return;
const rerolls = [];
const update = { system: { damage: {} } };
for (const partKey in this.damage) {
const part = this.damage[partKey];
const testRoll = Roll.fromData(part.parts[0].roll);
const rerolled = await testRoll.reroll();
rerolls.push(rerolled);
if (!update.system.damage[partKey]) update.system.damage[partKey] = { parts: [part.parts[0]] };
const partData = update.system.damage[partKey].parts[0];
update.system.damage[partKey].total = rerolled.total;
partData.modifierTotal = rerolled.terms.reduce((acc, x) => {
if (x.isDeterministic && !x.operator) acc += x.total;
return acc;
}, 0);
partData.dice = rerolled.dice.map(d => ({ ...d.toJSON(), dice: d.denomination }));
partData.total = rerolled.total;
partData.roll = rerolled.toJSON();
}
await triggerChatRollFx(rerolls);
return update;
}
registerTargetHook() { registerTargetHook() {
if (!this.parent.isAuthor || !this.hasTarget) return; if (!this.parent.isAuthor || !this.hasTarget) return;
if (this.targetMode && this.parent.targetHook !== null) { if (this.targetMode && this.parent.targetHook !== null) {

View file

@ -22,12 +22,12 @@ export class DhCompanionLevelup extends foundry.abstract.DataModel {
const initialAchievements = i === tier.levels.start ? tier.initialAchievements : {}; const initialAchievements = i === tier.levels.start ? tier.initialAchievements : {};
const experiences = initialAchievements.experience const experiences = initialAchievements.experience
? [...Array(initialAchievements.experience.nr).keys()].reduce((acc, _) => { ? [...Array(initialAchievements.experience.nr).keys()].reduce((acc, _) => {
acc[foundry.utils.randomID()] = { acc[foundry.utils.randomID()] = {
name: '', name: '',
modifier: initialAchievements.experience.modifier modifier: initialAchievements.experience.modifier
}; };
return acc; return acc;
}, {}) }, {})
: {}; : {};
const currentChoices = pcLevelData.levelups[i]?.selections?.length; const currentChoices = pcLevelData.levelups[i]?.selections?.length;
@ -302,9 +302,9 @@ export class DhLevelupLevel extends foundry.abstract.DataModel {
experiences: levelData.achievements?.experiences ?? achievements.experiences ?? {}, experiences: levelData.achievements?.experiences ?? achievements.experiences ?? {},
domainCards: levelData.achievements?.domainCards domainCards: levelData.achievements?.domainCards
? levelData.achievements.domainCards.reduce((acc, card, index) => { ? levelData.achievements.domainCards.reduce((acc, card, index) => {
acc[index] = { ...card }; acc[index] = { ...card };
return acc; return acc;
}, {}) }, {})
: (achievements.domainCards ?? {}), : (achievements.domainCards ?? {}),
proficiency: levelData.achievements?.proficiency ?? achievements.proficiency ?? null proficiency: levelData.achievements?.proficiency ?? achievements.proficiency ?? null
}, },

View file

@ -11,11 +11,13 @@ export default class CompendiumBrowserSettings extends foundry.abstract.DataMode
}) })
), ),
excludedPacks: new fields.TypedObjectField( excludedPacks: new fields.TypedObjectField(
new fields.SchemaField({ new fields.TypedObjectField(
excludedDocumentTypes: new fields.ArrayField( new fields.SchemaField({
new fields.StringField({ required: true, choices: CONST.SYSTEM_SPECIFIC_COMPENDIUM_TYPES }) excludedDocumentTypes: new fields.ArrayField(
) new fields.StringField({ required: true, choices: CONST.SYSTEM_SPECIFIC_COMPENDIUM_TYPES })
}) )
})
)
) )
}; };
} }
@ -28,7 +30,7 @@ export default class CompendiumBrowserSettings extends foundry.abstract.DataMode
const excludedSourceData = this.excludedSources[packageName]; const excludedSourceData = this.excludedSources[packageName];
if (excludedSourceData && excludedSourceData.excludedDocumentTypes.includes(pack.metadata.type)) return true; if (excludedSourceData && excludedSourceData.excludedDocumentTypes.includes(pack.metadata.type)) return true;
const excludedPackData = this.excludedPacks[item.pack]; const excludedPackData = this.excludedPacks[packageName]?.[pack.metadata.name];
if (excludedPackData && excludedPackData.excludedDocumentTypes.includes(pack.metadata.type)) return true; if (excludedPackData && excludedPackData.excludedDocumentTypes.includes(pack.metadata.type)) return true;
return false; return false;

View file

@ -77,11 +77,11 @@ export class DhCountdown extends foundry.abstract.DataModel {
static defaultCountdown(type, playerHidden) { static defaultCountdown(type, playerHidden) {
const ownership = playerHidden const ownership = playerHidden
? game.users.reduce((acc, user) => { ? game.users.reduce((acc, user) => {
if (!user.isGM) { if (!user.isGM) {
acc[user.id] = CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE; acc[user.id] = CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE;
} }
return acc; return acc;
}, {}) }, {})
: undefined; : undefined;
return { return {
@ -102,8 +102,8 @@ export class DhCountdown extends foundry.abstract.DataModel {
value: user.isGM value: user.isGM
? CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER ? CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER
: this.ownership.players[user.id] && this.ownership.players[user.id].type !== -1 : this.ownership.players[user.id] && this.ownership.players[user.id].type !== -1
? this.ownership.players[user.id].type ? this.ownership.players[user.id].type
: this.ownership.default, : this.ownership.default,
isGM: user.isGM isGM: user.isGM
}; };

View file

@ -105,8 +105,8 @@ export default class BeastformField extends fields.SchemaField {
baseSize === 'custom' baseSize === 'custom'
? 'custom' ? 'custom'
: (Object.keys(CONFIG.DH.ACTOR.tokenSize).find( : (Object.keys(CONFIG.DH.ACTOR.tokenSize).find(
x => CONFIG.DH.ACTOR.tokenSize[x].value === CONFIG.DH.ACTOR.tokenSize[baseSize].value + 1 x => CONFIG.DH.ACTOR.tokenSize[x].value === CONFIG.DH.ACTOR.tokenSize[baseSize].value + 1
) ?? baseSize); ) ?? baseSize);
formData.system.tokenSize = { formData.system.tokenSize = {
...evolvedData.form.system.tokenSize, ...evolvedData.form.system.tokenSize,
size: evolvedSize size: evolvedSize

View file

@ -103,7 +103,7 @@ export default class CostField extends fields.ArrayField {
static calcCosts(costs) { static calcCosts(costs) {
const resources = CostField.getResources.call(this, costs); const resources = CostField.getResources.call(this, costs);
let filteredCosts = costs; let filteredCosts = costs;
if (this.parent?.metadata.isQuantifiable && this.parent.consumeOnUse === false) { if (this.parent?.isInventoryItem && this.parent.consumeOnUse === false) {
filteredCosts = filteredCosts.filter(c => c.key !== 'quantity'); filteredCosts = filteredCosts.filter(c => c.key !== 'quantity');
} }
@ -116,8 +116,8 @@ export default class CostField extends fields.ArrayField {
c.key === 'fear' c.key === 'fear'
? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) ? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear)
: resources[c.key].isReversed : resources[c.key].isReversed
? resources[c.key].max - resources[c.key].value ? resources[c.key].max - resources[c.key].value
: resources[c.key].value; : resources[c.key].value;
if (c.scalable) c.maxStep = Math.floor((c.max - c.value) / c.step); if (c.scalable) c.maxStep = Math.floor((c.max - c.value) / c.step);
return c; return c;
}); });
@ -149,8 +149,8 @@ export default class CostField extends fields.ArrayField {
!resources[c.key] !resources[c.key]
? a ? a
: a && resources[c.key].isReversed : a && resources[c.key].isReversed
? resources[c.key].value + (c.total ?? c.value) <= resources[c.key].max ? resources[c.key].value + (c.total ?? c.value) <= resources[c.key].max
: resources[c.key]?.value >= (c.total ?? c.value), : resources[c.key]?.value >= (c.total ?? c.value),
true true
); );
} }

View file

@ -87,11 +87,11 @@ export default class CountdownField extends fields.ArrayField {
CONFIG.DH.id, CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Countdowns, CONFIG.DH.SETTINGS.gameSettings.Countdowns,
countdownSetting.toObject() countdownSetting.toObject()
), );
game.socket.emit(`system.${CONFIG.DH.id}`, { game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh, action: socketEvent.Refresh,
data: { refreshType: RefreshType.Countdown } data: { refreshType: RefreshType.Countdown }
}); });
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown }); Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
}, },
data, data,

View file

@ -72,9 +72,6 @@ export default class DamageField extends fields.SchemaField {
damageConfig.source.message = messageId; damageConfig.source.message = messageId;
damageConfig.directDamage = !!damageConfig.source?.message; damageConfig.directDamage = !!damageConfig.source?.message;
// if(damageConfig.source?.message && game.modules.get('dice-so-nice')?.active)
// await game.dice3d.waitFor3DAnimationByMessageID(damageConfig.source.message);
const damageResult = await CONFIG.Dice.daggerheart.DamageRoll.build(damageConfig); const damageResult = await CONFIG.Dice.daggerheart.DamageRoll.build(damageConfig);
if (!damageResult) return false; if (!damageResult) return false;
if (damageResult.actionChatMessageHandled) config.actionChatMessageHandled = true; if (damageResult.actionChatMessageHandled) config.actionChatMessageHandled = true;

View file

@ -1,5 +1,3 @@
import { emitGMUpdate, GMUpdateEvent } from '../../../systemRegistration/socket.mjs';
const fields = foundry.data.fields; const fields = foundry.data.fields;
export default class EffectsField extends fields.ArrayField { export default class EffectsField extends fields.ArrayField {
@ -34,8 +32,7 @@ export default class EffectsField extends fields.ArrayField {
} }
if (EffectsField.getAutomation() || force) { if (EffectsField.getAutomation() || force) {
targets ??= (message.system?.targets ?? config.targets).filter(t => !config.hasRoll || t.hit); targets ??= (message.system?.targets ?? config.targets).filter(t => !config.hasRoll || t.hit);
await emitGMUpdate(GMUpdateEvent.UpdateEffect, EffectsField.applyEffects.bind(this), targets, this.uuid); EffectsField.applyEffects.call(this, targets);
// EffectsField.applyEffects.call(this, config.targets.filter(t => !config.hasRoll || t.hit));
} }
} }
@ -59,16 +56,16 @@ export default class EffectsField extends fields.ArrayField {
if (!token) return; if (!token) return;
const messageToken = token.document ?? token; const messageToken = token.document ?? token;
const conditionImmunities = messageToken.actor.system.rules.conditionImmunities ?? {}; const conditionImmunities = messageToken.actor.system.rules?.conditionImmunities ?? {};
messageTargets.push({ messageTargets.push({
token: messageToken, token: messageToken,
conditionImmunities: Object.values(conditionImmunities).some(x => x) conditionImmunities: Object.values(conditionImmunities).some(x => x)
? game.i18n.format('DAGGERHEART.UI.Chat.effectSummary.immunityTo', { ? game.i18n.format('DAGGERHEART.UI.Chat.effectSummary.immunityTo', {
immunities: Object.keys(conditionImmunities) immunities: Object.keys(conditionImmunities)
.filter(x => conditionImmunities[x]) .filter(x => conditionImmunities[x])
.map(x => game.i18n.localize(conditions[x].name)) .map(x => game.i18n.localize(conditions[x].name))
.join(', ') .join(', ')
}) })
: null : null
}); });

View file

@ -69,11 +69,11 @@ export default class SaveField extends fields.SchemaField {
game.user === actor.owner game.user === actor.owner
? SaveField.rollSave.call(this, actor, event) ? SaveField.rollSave.call(this, actor, event)
: actor.owner.query('reactionRoll', { : actor.owner.query('reactionRoll', {
actionId: this.uuid, actionId: this.uuid,
actorId: actor.uuid, actorId: actor.uuid,
event, event,
message message
}); });
const result = await rollSave; const result = await rollSave;
await SaveField.updateSaveMessage.call(this, result, message, target.id); await SaveField.updateSaveMessage.call(this, result, message, target.id);
subResolve(); subResolve();
@ -97,8 +97,8 @@ export default class SaveField extends fields.SchemaField {
const title = actor.isNPC const title = actor.isNPC
? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll') ? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll')
: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { : game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(abilities[this.save.trait]?.label) ability: game.i18n.localize(abilities[this.save.trait]?.label)
}), }),
rollConfig = { rollConfig = {
event, event,
title, title,

View file

@ -1,4 +1,4 @@
import { itemAbleRollParse } from '../../../helpers/utils.mjs'; import { itemAbleRollParse, triggerChatRollFx } from '../../../helpers/utils.mjs';
import FormulaField from '../formulaField.mjs'; import FormulaField from '../formulaField.mjs';
const fields = foundry.data.fields; const fields = foundry.data.fields;
@ -40,7 +40,7 @@ export default class DHSummonField extends fields.ArrayField {
const roll = new Roll(itemAbleRollParse(summon.count, this.actor, this.item)); const roll = new Roll(itemAbleRollParse(summon.count, this.actor, this.item));
await roll.evaluate(); await roll.evaluate();
const count = roll.total; const count = roll.total;
if (!roll.isDeterministic && game.modules.get('dice-so-nice')?.active) rolls.push(roll); if (!roll.isDeterministic) rolls.push(roll);
const actor = await DHSummonField.getWorldActor(await foundry.utils.fromUuid(summon.actorUUID)); const actor = await DHSummonField.getWorldActor(await foundry.utils.fromUuid(summon.actorUUID));
/* Extending summon data in memory so it's available in actionField.toChat. Think it's harmless, but ugly. Could maybe find a better way. */ /* Extending summon data in memory so it's available in actionField.toChat. Think it's harmless, but ugly. Could maybe find a better way. */
@ -56,7 +56,7 @@ export default class DHSummonField extends fields.ArrayField {
} }
} }
if (rolls.length) await Promise.all(rolls.map(roll => game.dice3d.showForRoll(roll, game.user, true))); if (rolls.length) await triggerChatRollFx(rolls);
this.actor.sheet?.minimize(); this.actor.sheet?.minimize();
DHSummonField.handleSummon(summonData, this.actor); DHSummonField.handleSummon(summonData, this.actor);

View file

@ -1,6 +1,6 @@
import BaseDataItem from './base.mjs'; import BaseDataItem from './base.mjs';
import ItemLinkFields from '../../data/fields/itemLinkFields.mjs'; import ItemLinkFields from '../../data/fields/itemLinkFields.mjs';
import { getFeaturesHTMLData } from '../../helpers/utils.mjs'; import { fromUuids, getFeaturesHTMLData } from '../../helpers/utils.mjs';
export default class DHAncestry extends BaseDataItem { export default class DHAncestry extends BaseDataItem {
/** @inheritDoc */ /** @inheritDoc */
@ -45,6 +45,10 @@ export default class DHAncestry extends BaseDataItem {
/**@inheritdoc */ /**@inheritdoc */
async getDescriptionData() { async getDescriptionData() {
// Preload all ancestry features for acquisition from the cache
// todo: make feature acquisition async and replace feature helpers for methods
await fromUuids(this._source.features.map(f => f.item));
const baseDescription = this.description; const baseDescription = this.description;
const features = await getFeaturesHTMLData(this.features); const features = await getFeaturesHTMLData(this.features);

View file

@ -141,16 +141,6 @@ export default class DHArmor extends AttachableItem {
} }
} }
_onUpdate(a, b, c) {
super._onUpdate(a, b, c);
if (this.actor?.type === 'character') {
for (const party of this.actor.parties) {
party.render();
}
}
}
/** @inheritDoc */ /** @inheritDoc */
static migrateDocumentData(source) { static migrateDocumentData(source) {
if (!source.system.armor) { if (!source.system.armor) {

View file

@ -51,7 +51,8 @@ export default class DHBeastform extends BaseDataItem {
}), }),
scale: new fields.NumberField({ nullable: false, min: 0.2, max: 3, step: 0.05, initial: 1 }), scale: new fields.NumberField({ nullable: false, min: 0.2, max: 3, step: 0.05, initial: 1 }),
height: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }), height: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }),
width: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }) width: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }),
depth: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true })
}), }),
mainTrait: new fields.StringField({ mainTrait: new fields.StringField({
required: true, required: true,
@ -192,7 +193,8 @@ export default class DHBeastform extends BaseDataItem {
tokenSize: { tokenSize: {
scale: this.parent.parent.prototypeToken.texture.scaleX, scale: this.parent.parent.prototypeToken.texture.scaleX,
height: this.parent.parent.prototypeToken.height, height: this.parent.parent.prototypeToken.height,
width: this.parent.parent.prototypeToken.width width: this.parent.parent.prototypeToken.width,
depth: this.parent.parent.prototypeToken.depth
} }
}, },
advantageOn: this.advantageOn, advantageOn: this.advantageOn,
@ -206,15 +208,17 @@ export default class DHBeastform extends BaseDataItem {
const autoTokenSize = const autoTokenSize =
this.tokenSize.size !== 'custom' this.tokenSize.size !== 'custom'
? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes[ ? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes[
this.tokenSize.size this.tokenSize.size
] ]
: null; : null;
const width = autoTokenSize ?? this.tokenSize.width; const width = autoTokenSize ?? this.tokenSize.width;
const height = autoTokenSize ?? this.tokenSize.height; const height = autoTokenSize ?? this.tokenSize.height;
const depth = autoTokenSize ?? this.tokenSize.depth;
const prototypeTokenUpdate = { const prototypeTokenUpdate = {
height, height,
width, width,
depth,
texture: { texture: {
src: this.tokenImg, src: this.tokenImg,
scaleX: this.tokenSize.scale, scaleX: this.tokenSize.scale,

View file

@ -2,7 +2,7 @@ import BaseDataItem from './base.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
import ItemLinkFields from '../fields/itemLinkFields.mjs'; import ItemLinkFields from '../fields/itemLinkFields.mjs';
import { addLinkedItemsDiff, getFeaturesHTMLData, updateLinkedItemApps } from '../../helpers/utils.mjs'; import { addLinkedItemsDiff, fromUuids, getFeaturesHTMLData, updateLinkedItemApps } from '../../helpers/utils.mjs';
export default class DHClass extends BaseDataItem { export default class DHClass extends BaseDataItem {
/** @inheritDoc */ /** @inheritDoc */
@ -73,15 +73,16 @@ export default class DHClass extends BaseDataItem {
const uuids = [this.parent.uuid, this.parent._stats?.compendiumSource].filter(u => !!u); const uuids = [this.parent.uuid, this.parent._stats?.compendiumSource].filter(u => !!u);
const subclasses = game.items.filter(x => x.type === 'subclass' && uuids.includes(x.system.linkedClass)); const subclasses = game.items.filter(x => x.type === 'subclass' && uuids.includes(x.system.linkedClass));
for (const pack of game.packs) { for (const pack of game.packs) {
const packIds = [];
const indexes = await pack.getIndex({ fields: ['system.linkedClass'] }); const indexes = await pack.getIndex({ fields: ['system.linkedClass'] });
for (const index of indexes) { for (const index of indexes) {
if (index.type !== 'subclass') continue; if (index.type !== 'subclass') continue;
if (!uuids.includes(index.system?.linkedClass)) continue; if (!uuids.includes(index.system?.linkedClass)) continue;
if (subclasses.find(x => x.uuid === index.uuid)) continue; if (subclasses.find(x => x.uuid === index.uuid)) continue;
packIds.push(index._id);
const subclass = await foundry.utils.fromUuid(index.uuid);
subclasses.push(subclass);
} }
if (packIds.length > 0) subclasses.push(...(await pack.getDocuments({ _id__in: packIds })));
} }
return subclasses; return subclasses;
@ -216,6 +217,10 @@ export default class DHClass extends BaseDataItem {
classItems.push(contentLink.outerHTML); classItems.push(contentLink.outerHTML);
} }
// Preload all class features for acquisition from the cache
// todo: make feature acquisition async and replace feature helpers for methods
await fromUuids(this._source.features.map(f => f.item));
const hopeFeatures = await getFeaturesHTMLData(this.hopeFeatures); const hopeFeatures = await getFeaturesHTMLData(this.hopeFeatures);
const classFeatures = await getFeaturesHTMLData(this.classFeatures); const classFeatures = await getFeaturesHTMLData(this.classFeatures);

View file

@ -1,4 +1,4 @@
import { getFeaturesHTMLData } from '../../helpers/utils.mjs'; import { fromUuids, getFeaturesHTMLData } from '../../helpers/utils.mjs';
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
import BaseDataItem from './base.mjs'; import BaseDataItem from './base.mjs';
@ -27,6 +27,10 @@ export default class DHCommunity extends BaseDataItem {
/**@inheritdoc */ /**@inheritdoc */
async getDescriptionData() { async getDescriptionData() {
// Preload all community features for acquisition from the cache
// todo: make feature acquisition async and replace feature helpers for methods
await fromUuids(this._source.features);
const baseDescription = this.description; const baseDescription = this.description;
const features = await getFeaturesHTMLData(this.features); const features = await getFeaturesHTMLData(this.features);

View file

@ -1,5 +1,4 @@
import { getFeaturesHTMLData } from '../../helpers/utils.mjs'; import { fromUuids, getFeaturesHTMLData } from '../../helpers/utils.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import ItemLinkFields from '../fields/itemLinkFields.mjs'; import ItemLinkFields from '../fields/itemLinkFields.mjs';
import BaseDataItem from './base.mjs'; import BaseDataItem from './base.mjs';
@ -56,38 +55,30 @@ export default class DHSubclass extends BaseDataItem {
if (allowed === false) return; if (allowed === false) return;
if (this.actor?.type === 'character') { if (this.actor?.type === 'character') {
const dataUuid = data.uuid ?? data._stats.compendiumSource ?? `Item.${data._id}`; const { value: actorClass, subclass: existingSubclass } = this.actor.system.class;
if (this.actor.system.class.subclass) { const { value: multiclass, subclass: existingMultisubclass } = this.actor.system.multiclass;
if (this.actor.system.multiclass.subclass) { if (!actorClass && !multiclass) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassesAlreadyPresent')); ui.notifications.warn('DAGGERHEART.UI.Notifications.missingClass', { localize: true });
return false; return false;
} else { }
const multiclass = this.actor.items.find(x => x.type === 'class' && x.system.isMulticlass); if (existingSubclass && existingMultisubclass) {
if (!multiclass) { ui.notifications.warn('DAGGERHEART.UI.Notifications.subclassesAlreadyPresent', { localize: true });
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.missingMulticlass')); return false;
return false; }
} if (existingSubclass && !multiclass) {
ui.notifications.warn('DAGGERHEART.UI.Notifications.missingMulticlass', { localize: true });
return false;
}
if (multiclass.system.subclasses.every(x => x.uuid !== dataUuid)) { const match = [multiclass, actorClass].find(
ui.notifications.error( c => c && (c._stats.compendiumSource ?? c.uuid) === this.linkedClass
game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInMulticlass') );
); if (!match) {
return false; const key = multiclass ? 'subclassNotInMulticlass' : 'subclassNotInClass';
} ui.notifications.warn(`DAGGERHEART.UI.Notifications.${key}`, { localize: true });
return false;
await this.updateSource({ isMulticlass: true }); } else if (match.system.isMulticlass) {
} await this.updateSource({ isMulticlass: true });
} else {
const actorClass = this.actor.items.find(x => x.type === 'class' && !x.system.isMulticlass);
if (!actorClass) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.missingClass'));
return false;
}
if ((await actorClass.system.fetchSubclasses()).every(x => x.uuid !== dataUuid)) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInClass'));
return false;
}
} }
} }
} }
@ -99,6 +90,11 @@ export default class DHSubclass extends BaseDataItem {
const spellcastTrait = this.spellcastingTrait const spellcastTrait = this.spellcastingTrait
? game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.spellcastingTrait].label) ? game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.spellcastingTrait].label)
: null; : null;
// Preload all subclass features for acquisition from the cache
// todo: make feature acquisition async and replace feature helpers for methods
await fromUuids(this._source.features.map(f => f.item));
const foundationFeatures = await getFeaturesHTMLData(this.foundationFeatures); const foundationFeatures = await getFeaturesHTMLData(this.foundationFeatures);
const specializationFeatures = await getFeaturesHTMLData(this.specializationFeatures); const specializationFeatures = await getFeaturesHTMLData(this.specializationFeatures);
const masteryFeatures = await getFeaturesHTMLData(this.masteryFeatures); const masteryFeatures = await getFeaturesHTMLData(this.masteryFeatures);

View file

@ -19,12 +19,12 @@ export class DhLevelup extends foundry.abstract.DataModel {
const initialAchievements = i === tier.levels.start ? tier.initialAchievements : {}; const initialAchievements = i === tier.levels.start ? tier.initialAchievements : {};
const experiences = initialAchievements.experience const experiences = initialAchievements.experience
? [...Array(initialAchievements.experience.nr).keys()].reduce((acc, _) => { ? [...Array(initialAchievements.experience.nr).keys()].reduce((acc, _) => {
acc[foundry.utils.randomID()] = { acc[foundry.utils.randomID()] = {
name: '', name: '',
modifier: initialAchievements.experience.modifier modifier: initialAchievements.experience.modifier
}; };
return acc; return acc;
}, {}) }, {})
: {}; : {};
const domainCards = [...Array(tier.domainCardByLevel).keys()].reduce((acc, _) => { const domainCards = [...Array(tier.domainCardByLevel).keys()].reduce((acc, _) => {
@ -298,9 +298,9 @@ export class DhLevelupLevel extends foundry.abstract.DataModel {
experiences: levelData.achievements?.experiences ?? achievements.experiences ?? {}, experiences: levelData.achievements?.experiences ?? achievements.experiences ?? {},
domainCards: levelData.achievements?.domainCards domainCards: levelData.achievements?.domainCards
? levelData.achievements.domainCards.reduce((acc, card, index) => { ? levelData.achievements.domainCards.reduce((acc, card, index) => {
acc[index] = { ...card }; acc[index] = { ...card };
return acc; return acc;
}, {}) }, {})
: (achievements.domainCards ?? {}), : (achievements.domainCards ?? {}),
proficiency: levelData.achievements?.proficiency ?? achievements.proficiency ?? null proficiency: levelData.achievements?.proficiency ?? achievements.proficiency ?? null
}, },

View file

@ -1,4 +1,5 @@
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
import { triggerChatRollFx } from '../helpers/utils.mjs';
import DHRoll from './dhRoll.mjs'; import DHRoll from './dhRoll.mjs';
export default class D20Roll extends DHRoll { export default class D20Roll extends DHRoll {
@ -224,4 +225,15 @@ export default class D20Roll extends DHRoll {
resetFormula() { resetFormula() {
return (this._formula = this.constructor.getFormula(this.terms)); return (this._formula = this.constructor.getFormula(this.terms));
} }
async reroll(options) {
const result = await super.reroll(options);
if (this instanceof game.system.api.dice.DualityRoll) return result;
if (options?.liveRoll) {
await triggerChatRollFx([result]);
}
return result;
}
} }

View file

@ -1,5 +1,5 @@
import DamageDialog from '../applications/dialogs/damageDialog.mjs'; import DamageDialog from '../applications/dialogs/damageDialog.mjs';
import { parseRallyDice } from '../helpers/utils.mjs'; import { parseRallyDice, triggerChatRollFx } from '../helpers/utils.mjs';
import DHRoll from './dhRoll.mjs'; import DHRoll from './dhRoll.mjs';
export default class DamageRoll extends DHRoll { export default class DamageRoll extends DHRoll {
@ -18,7 +18,12 @@ export default class DamageRoll extends DHRoll {
if (config.evaluate !== false) for (const roll of config.roll) await roll.roll.evaluate(); if (config.evaluate !== false) for (const roll of config.roll) await roll.roll.evaluate();
roll._evaluated = true; roll._evaluated = true;
const parts = config.roll.map(r => this.postEvaluate(r));
const parts = [];
for (const roll of config.roll) {
parts.push(this.postEvaluate(roll));
roll.roll = JSON.stringify(roll.roll.toJSON());
}
config.damage = this.unifyDamageRoll(parts); config.damage = this.unifyDamageRoll(parts);
} }
@ -38,25 +43,24 @@ export default class DamageRoll extends DHRoll {
const chatMessage = config.source?.message const chatMessage = config.source?.message
? ui.chat.collection.get(config.source.message) ? ui.chat.collection.get(config.source.message)
: getDocumentClass('ChatMessage').applyMode({}, config.rollMode ?? 'public'); : getDocumentClass('ChatMessage').applyMode({}, config.rollMode ?? 'public');
const diceRolls = [];
if (game.modules.get('dice-so-nice')?.active) { if (game.modules.get('dice-so-nice')?.active) {
const pool = foundry.dice.terms.PoolTerm.fromRolls(
Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll))
),
diceRoll = Roll.fromTerms([pool]);
await game.dice3d.showForRoll(
diceRoll,
game.user,
true,
chatMessage.whisper?.length > 0 ? chatMessage.whisper : null,
chatMessage.blind
);
config.mute = true; config.mute = true;
const pool = foundry.dice.terms.PoolTerm.fromRolls(
Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll))
);
diceRolls.push(Roll.fromTerms([pool]));
} }
await triggerChatRollFx(diceRolls, {
whisper: chatMessage.whisper?.length > 0 ? chatMessage.whisper : null,
blind: chatMessage.blind
});
await super.buildPost(roll, config, message); await super.buildPost(roll, config, message);
if (config.source?.message) { if (config.source?.message) {
chatMessage.update({ 'system.damage': config.damage }); chatMessage.update({ 'system.damage': config.damage });
if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
} }
} }
@ -319,9 +323,10 @@ export default class DamageRoll extends DHRoll {
const newIndex = parsedDiceTerms[dice].results.length; const newIndex = parsedDiceTerms[dice].results.length;
await term.reroll(`/r1=${termResult.result}`); await term.reroll(`/r1=${termResult.result}`);
const diceRolls = [];
if (game.modules.get('dice-so-nice')?.active) { if (game.modules.get('dice-so-nice')?.active) {
const newResult = parsedDiceTerms[dice].results[newIndex]; const newResult = parsedDiceTerms[dice].results[newIndex];
const diceSoNiceRoll = { diceRolls.push({
_evaluated: true, _evaluated: true,
dice: [ dice: [
new foundry.dice.terms.Die({ new foundry.dice.terms.Die({
@ -332,11 +337,10 @@ export default class DamageRoll extends DHRoll {
}) })
], ],
options: { appearance: {} } options: { appearance: {} }
}; });
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
} }
await triggerChatRollFx(diceRolls);
await parsedRoll.evaluate(); await parsedRoll.evaluate();
const results = parsedRoll.dice[dice].results.map(result => ({ const results = parsedRoll.dice[dice].results.map(result => ({

View file

@ -1,4 +1,5 @@
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
import { triggerChatRollFx } from '../helpers/utils.mjs';
export default class DHRoll extends Roll { export default class DHRoll extends Roll {
baseTerms = []; baseTerms = [];
@ -36,6 +37,7 @@ export default class DHRoll extends Roll {
static async buildConfigure(config = {}, message = {}) { static async buildConfigure(config = {}, message = {}) {
config.hooks = [...this.getHooks(), '']; config.hooks = [...this.getHooks(), ''];
config.dialog ??= {}; config.dialog ??= {};
config.damageOptions ??= {};
for (const hook of config.hooks) { for (const hook of config.hooks) {
if (Hooks.call(`${CONFIG.DH.id}.preRoll${hook.capitalize()}`, config, message) === false) return null; if (Hooks.call(`${CONFIG.DH.id}.preRoll${hook.capitalize()}`, config, message) === false) return null;
@ -75,9 +77,7 @@ export default class DHRoll extends Roll {
} }
if (config.skips?.createMessage) { if (config.skips?.createMessage) {
if (game.modules.get('dice-so-nice')?.active) { await triggerChatRollFx([roll]);
await game.dice3d.showForRoll(roll, game.user, true);
}
} else if (!config.source?.message) { } else if (!config.source?.message) {
config.message = await this.toMessage(roll, config); config.message = await this.toMessage(roll, config);
} }
@ -85,6 +85,7 @@ export default class DHRoll extends Roll {
static postEvaluate(roll, config = {}) { static postEvaluate(roll, config = {}) {
return { return {
...roll.options.roll,
total: roll.total, total: roll.total,
formula: roll.formula, formula: roll.formula,
dice: roll.dice.map(d => ({ dice: roll.dice.map(d => ({
@ -103,9 +104,9 @@ export default class DHRoll extends Roll {
if (action?.chatDisplay) { if (action?.chatDisplay) {
actionDescription = action actionDescription = action
? await foundry.applications.ux.TextEditor.implementation.enrichHTML(action.description, { ? await foundry.applications.ux.TextEditor.implementation.enrichHTML(action.description, {
relativeTo: config.data, relativeTo: config.data,
rollData: config.data.getRollData?.() ?? {} rollData: config.data.getRollData?.() ?? {}
}) })
: null; : null;
config.actionChatMessageHandled = true; config.actionChatMessageHandled = true;
} }

View file

@ -1,4 +1,4 @@
import { ResourceUpdateMap } from '../../data/action/baseAction.mjs'; import { updateResourcesForDualityReroll } from '../helpers.mjs';
export default class DualityDie extends foundry.dice.terms.Die { export default class DualityDie extends foundry.dice.terms.Die {
constructor(options) { constructor(options) {
@ -12,24 +12,6 @@ export default class DualityDie extends foundry.dice.terms.Die {
return roll.withHope ? 1 : roll.withFear ? -1 : 0; return roll.withHope ? 1 : roll.withFear ? -1 : 0;
} }
#updateResources(oldDuality, newDuality, actor) {
const { hopeFear } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
if (game.user.isGM ? !hopeFear.gm : !hopeFear.players) return;
const updates = [];
const hope = (newDuality >= 0 ? 1 : 0) - (oldDuality >= 0 ? 1 : 0);
const stress = (newDuality === 0 ? 1 : 0) - (oldDuality === 0 ? 1 : 0);
const fear = (newDuality === -1 ? 1 : 0) - (oldDuality === -1 ? 1 : 0);
if (hope !== 0) updates.push({ key: 'hope', value: hope, total: -1 * hope, enabled: true });
if (stress !== 0) updates.push({ key: 'stress', value: -1 * stress, total: stress, enabled: true });
if (fear !== 0) updates.push({ key: 'fear', value: fear, total: -1 * fear, enabled: true });
const resourceUpdates = new ResourceUpdateMap(actor);
resourceUpdates.addResources(updates);
resourceUpdates.updateResources();
}
async reroll(modifier, options) { async reroll(modifier, options) {
const oldDuality = this.#getDualityState(options.liveRoll.roll); const oldDuality = this.#getDualityState(options.liveRoll.roll);
await super.reroll(modifier, options); await super.reroll(modifier, options);
@ -57,7 +39,7 @@ export default class DualityDie extends foundry.dice.terms.Die {
if (options.liveRoll.isReaction) return; if (options.liveRoll.isReaction) return;
const newDuality = this.#getDualityState(options.liveRoll.roll); const newDuality = this.#getDualityState(options.liveRoll.roll);
this.#updateResources(oldDuality, newDuality, options.liveRoll.actor); updateResourcesForDualityReroll(oldDuality, newDuality, options.liveRoll.actor);
} }
} }

View file

@ -1,6 +1,8 @@
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
import D20Roll from './d20Roll.mjs'; import D20Roll from './d20Roll.mjs';
import { parseRallyDice, setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs'; import { parseRallyDice, setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
import { getDiceSoNicePresets } from '../config/generalConfig.mjs';
import { updateResourcesForDualityReroll } from './helpers.mjs';
export default class DualityRoll extends D20Roll { export default class DualityRoll extends D20Roll {
_advantageNumber = 1; _advantageNumber = 1;
@ -107,10 +109,10 @@ export default class DualityRoll extends D20Roll {
const label = this.guaranteedCritical const label = this.guaranteedCritical
? 'DAGGERHEART.GENERAL.guaranteedCriticalSuccess' ? 'DAGGERHEART.GENERAL.guaranteedCriticalSuccess'
: this.isCritical : this.isCritical
? 'DAGGERHEART.GENERAL.criticalSuccess' ? 'DAGGERHEART.GENERAL.criticalSuccess'
: this.withHope : this.withHope
? 'DAGGERHEART.GENERAL.hope' ? 'DAGGERHEART.GENERAL.hope'
: 'DAGGERHEART.GENERAL.fear'; : 'DAGGERHEART.GENERAL.fear';
return game.i18n.localize(label); return game.i18n.localize(label);
} }
@ -130,32 +132,34 @@ export default class DualityRoll extends D20Roll {
} }
createBaseDice() { createBaseDice() {
if ( this.terms = [this.terms[0], this.terms[1], this.terms[2]];
this.dice[0] instanceof game.system.api.dice.diceTypes.HopeDie &&
this.dice[1] instanceof game.system.api.dice.diceTypes.FearDie
) {
this.terms = [this.terms[0], this.terms[1], this.terms[2]];
return;
}
this.terms[0] = new game.system.api.dice.diceTypes.HopeDie({ this.terms[0] = new game.system.api.dice.diceTypes.HopeDie({
faces: this.data.rules.dualityRoll?.defaultHopeDice ?? 12 faces: this.terms[0]?.faces ?? this.data.rules.dualityRoll?.defaultHopeDice ?? 12
}); });
this.terms[1] = new foundry.dice.terms.OperatorTerm({ operator: '+' }); this.terms[1] = new foundry.dice.terms.OperatorTerm({ operator: '+' });
this.terms[2] = new game.system.api.dice.diceTypes.FearDie({ this.terms[2] = new game.system.api.dice.diceTypes.FearDie({
faces: this.data.rules.dualityRoll?.defaultFearDice ?? 12 faces: this.terms[2]?.faces ?? this.data.rules.dualityRoll?.defaultFearDice ?? 12
}); });
} }
applyAdvantage() { applyAdvantage() {
if (this.hasAdvantage || this.hasDisadvantage) { const advDieClass = this.hasAdvantage
const dieFaces = this.advantageFaces, ? game.system.api.dice.diceTypes.AdvantageDie
advDie = new foundry.dice.terms.Die({ faces: dieFaces, number: this.advantageNumber }); : this.hasDisadvantage
if (this.advantageNumber > 1) advDie.modifiers = ['kh']; ? game.system.api.dice.diceTypes.DisadvantageDie
this.terms.push( : null;
new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }), if (advDieClass) {
advDie const advDie = new advDieClass({ faces: this.advantageFaces, number: this.advantageNumber });
); if (this.terms.length < 4) {
if (this.advantageNumber > 1) advDie.modifiers = ['kh'];
this.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }),
advDie
);
} else {
this.terms[4] = advDie;
}
} }
if (this.rallyFaces) if (this.rallyFaces)
this.terms.push( this.terms.push(
@ -380,4 +384,40 @@ export default class DualityRoll extends D20Roll {
if (currentCombatant?.actorId == config.data.id) ui.combat.setCombatantSpotlight(currentCombatant.id); if (currentCombatant?.actorId == config.data.id) ui.combat.setCombatantSpotlight(currentCombatant.id);
} }
} }
async reroll(options) {
const oldDuality = this.withHope ? 1 : this.withFear ? -1 : 0;
const rerolled = DualityRoll.fromData((await super.reroll(options)).toJSON());
if (options?.liveRoll) {
if (game.modules.get('dice-so-nice')?.active) {
const diceAppearance = await getDiceSoNicePresets(
rerolled,
rerolled.dHope.denomination,
rerolled.dFear.denomination
);
rerolled.dHope.options.appearance = diceAppearance.hope.appearance;
rerolled.dFear.options.appearance = diceAppearance.fear.appearance;
if (rerolled.dAdvantage) rerolled.dAdvantage.options.appearance = diceAppearance.advantage.appearance;
if (rerolled.dDisadvantage)
rerolled.dDisadvantage.options.appearance = diceAppearance.disadvantage.appearance;
await game.dice3d.showForRoll(rerolled, game.user, true);
} else {
foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
}
if (this.options.actionType === 'reaction') return;
const newDuality = rerolled.withHope ? 1 : rerolled.withFear ? -1 : 0;
const actor = await foundry.utils.fromUuid(this.options.source.actor);
updateResourcesForDualityReroll(oldDuality, newDuality, actor);
}
return rerolled;
}
fromJSON(json) {
return super.fromJSON(json);
}
} }

19
module/dice/helpers.mjs Normal file
View file

@ -0,0 +1,19 @@
import { ResourceUpdateMap } from '../data/action/baseAction.mjs';
export function updateResourcesForDualityReroll(oldDuality, newDuality, actor) {
const { hopeFear } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
if (game.user.isGM ? !hopeFear.gm : !hopeFear.players) return;
const updates = [];
const hope = (newDuality >= 0 ? 1 : 0) - (oldDuality >= 0 ? 1 : 0);
const stress = (newDuality === 0 ? 1 : 0) - (oldDuality === 0 ? 1 : 0);
const fear = (newDuality === -1 ? 1 : 0) - (oldDuality === -1 ? 1 : 0);
if (hope !== 0) updates.push({ key: 'hope', value: hope, total: -1 * hope, enabled: true });
if (stress !== 0) updates.push({ key: 'stress', value: -1 * stress, total: stress, enabled: true });
if (fear !== 0) updates.push({ key: 'fear', value: fear, total: -1 * fear, enabled: true });
const resourceUpdates = new ResourceUpdateMap(actor);
resourceUpdates.addResources(updates);
resourceUpdates.updateResources();
}

View file

@ -175,8 +175,8 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
return model instanceof documentClass return model instanceof documentClass
? model ? model
: model.parent : model.parent
? this.#resolveParentDocument(model.parent, documentClass) ? this.#resolveParentDocument(model.parent, documentClass)
: null; : null;
} }
static getChangeValue(model, change, effect) { static getChangeValue(model, change, effect) {

View file

@ -65,6 +65,11 @@ export default class DhpActor extends Actor {
}; };
} }
static createDialog(data, createOptions, options, renderOptions) {
options.classes = [options.classes ?? [], 'actor-create'].flat(); // handled in hook
return super.createDialog(data, createOptions, options, renderOptions);
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritDoc */ /** @inheritDoc */
@ -109,13 +114,33 @@ export default class DhpActor extends Actor {
}); });
} }
if (this.type === 'npc') {
Object.assign(update, {
prototypeToken: {
disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY
}
});
}
this.updateSource(update); this.updateSource(update);
} }
/** Perform a render, debounced in order to prevent overloading repeat render requests */
renderDebounced = foundry.utils.debounce(options => {
return this.render(options);
}, 10);
_onUpdateDescendantDocuments(parent, collection, documents, changes, options, userId) {
super._onUpdateDescendantDocuments(parent, collection, documents, changes, options, userId);
for (const party of this.parties) {
party.renderDebounced({ parts: ['partyMembers'] });
}
}
_onUpdate(changes, options, userId) { _onUpdate(changes, options, userId) {
super._onUpdate(changes, options, userId); super._onUpdate(changes, options, userId);
for (const party of this.parties) { for (const party of this.parties) {
party.render({ parts: ['partyMembers'] }); party.renderDebounced({ parts: ['partyMembers'] });
} }
} }
@ -134,17 +159,20 @@ export default class DhpActor extends Actor {
_onDelete(options, userId) { _onDelete(options, userId) {
super._onDelete(options, userId); super._onDelete(options, userId);
for (const party of this.parties) { for (const party of this.parties) {
party.render({ parts: ['partyMembers'] }); party.renderDebounced({ parts: ['partyMembers'] });
} }
} }
async updateLevel(newLevel) { async updateLevel(newLevel) {
if (!['character', 'companion'].includes(this.type) || newLevel === this.system.levelData.level.changed) return; if (!['character', 'companion'].includes(this.type) || newLevel === this.system.levelData.level.changed) return;
const tiers = Object.values(game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers);
const maxLevel = tiers.reduce((acc, tier) => Math.max(acc, tier.levels.end), 0);
const multiclassMinLevel = Math.min(
maxLevel,
...tiers.filter(t => t.options.multiclass).map(t => t.levels.start)
);
if (newLevel > this.system.levelData.level.current) { if (newLevel > this.system.levelData.level.current) {
const maxLevel = Object.values(
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers
).reduce((acc, tier) => Math.max(acc, tier.levels.end), 0);
if (newLevel > maxLevel) { if (newLevel > maxLevel) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.tooHighLevel')); ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.tooHighLevel'));
} }
@ -219,18 +247,19 @@ export default class DhpActor extends Actor {
this.system.multiclass.subclass.update({ 'system.featureState': subclassFeatureState.multiclass }); this.system.multiclass.subclass.update({ 'system.featureState': subclassFeatureState.multiclass });
} }
if (multiclass) { // Remove multiclass if we're removing a multiclass feature or if we're below the multiclass minimum level
const multiclassItem = this.items.find(x => x.uuid === multiclass.itemUuid); // Multclasses cannot be manually removed on the sheet, so this allows recovering in the case of errors
const multiclassFeatures = this.items.filter( if (multiclass || newLevel < multiclassMinLevel) {
x => x.system.originItemType === 'class' && x.system.multiclassOrigin const multiclassItems = this.items.filter(
); x =>
const subclassFeatures = this.items.filter( x.uuid === multiclass?.itemUuid ||
x => x.system.originItemType === 'subclass' && x.system.multiclassOrigin x.system.isMulticlass ||
(['class', 'subclass'].includes(x.system.originItemType) && x.system.multiclassOrigin)
); );
this.deleteEmbeddedDocuments( this.deleteEmbeddedDocuments(
'Item', 'Item',
[multiclassItem, ...multiclassFeatures, ...subclassFeatures].map(x => x.id) multiclassItems.map(x => x.id)
); );
this.update({ this.update({
@ -269,6 +298,7 @@ export default class DhpActor extends Actor {
async levelUp(levelupData) { async levelUp(levelupData) {
const levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto; const levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto;
const getStatsWithSource = document => ({ ...(document._stats ?? {}), compendiumSource: document.uuid });
const levelups = {}; const levelups = {};
for (var levelKey of Object.keys(levelupData)) { for (var levelKey of Object.keys(levelupData)) {
@ -381,8 +411,8 @@ export default class DhpActor extends Actor {
const embeddedItem = await this.createEmbeddedDocuments('Item', [ const embeddedItem = await this.createEmbeddedDocuments('Item', [
{ {
...multiclassData, ...multiclassData,
uuid: multiclassItem.uuid, uuid: multiclassItem.uuid, // todo: replace with setting an id and using keepId
_stats: multiclassItem._stats, _stats: getStatsWithSource(multiclassItem),
system: { system: {
...multiclassData.system, ...multiclassData.system,
features: multiclassData.system.features.filter(x => x.type !== 'hope'), features: multiclassData.system.features.filter(x => x.type !== 'hope'),
@ -395,8 +425,8 @@ export default class DhpActor extends Actor {
await this.createEmbeddedDocuments('Item', [ await this.createEmbeddedDocuments('Item', [
{ {
...subclassData, ...subclassData,
uuid: subclassItem.uuid, uuid: subclassItem.uuid, // todo: replace with setting an id and using keepId
_stats: subclassItem._stats, _stats: getStatsWithSource(subclassItem),
system: { system: {
...subclassData.system, ...subclassData.system,
isMulticlass: true isMulticlass: true
@ -416,8 +446,8 @@ export default class DhpActor extends Actor {
const embeddedItem = await this.createEmbeddedDocuments('Item', [ const embeddedItem = await this.createEmbeddedDocuments('Item', [
{ {
...cardData, ...cardData,
uuid: cardItem.uuid, uuid: cardItem.uuid, // todo: replace with setting an id and using keepId
_stats: cardItem._stats, _stats: getStatsWithSource(cardItem),
system: { system: {
...cardData.system, ...cardData.system,
inVault: true inVault: true
@ -438,8 +468,7 @@ export default class DhpActor extends Actor {
const embeddedItem = await this.createEmbeddedDocuments('Item', [ const embeddedItem = await this.createEmbeddedDocuments('Item', [
{ {
...cardData, ...cardData,
uuid: cardItem.uuid, _stats: getStatsWithSource(cardItem),
_stats: cardItem._stats,
system: { system: {
...cardData.system, ...cardData.system,
inVault: true inVault: true

View file

@ -9,9 +9,9 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
actor && this.isContentVisible actor && this.isContentVisible
? actor ? actor
: { : {
img: this.author.avatar ? this.author.avatar : 'icons/svg/mystery-man.svg', img: this.author.avatar ? this.author.avatar : 'icons/svg/mystery-man.svg',
name: '' name: ''
}; };
/* We can change to fully implementing the renderHTML function if needed, instead of augmenting it. */ /* We can change to fully implementing the renderHTML function if needed, instead of augmenting it. */
const html = await super.renderHTML({ actor: actorData, author: this.author }); const html = await super.renderHTML({ actor: actorData, author: this.author });
@ -183,7 +183,11 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
if (pendingingSaves.length) { if (pendingingSaves.length) {
const confirm = await foundry.applications.api.DialogV2.confirm({ const confirm = await foundry.applications.api.DialogV2.confirm({
window: { title: game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.title') }, window: { title: game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.title') },
content: `<p>${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.unfinishedRolls')}</p><p>${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.confirmation')}</p><p><i>${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.warning')}</i></p>` content: `
<p>${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.unfinishedRolls')}</p>
<p><i>${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.warning')}</i></p>
<p>${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.confirmation')}</p>
`
}); });
if (!confirm) return; if (!confirm) return;
} }
@ -247,8 +251,24 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
const targets = this.filterPermTargets(this.system.hitTargets), const targets = this.filterPermTargets(this.system.hitTargets),
config = foundry.utils.deepClone(this.system); config = foundry.utils.deepClone(this.system);
config.event = event; config.event = event;
if (targets.length === 0) if (targets.length === 0)
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm')); return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm'));
else if (config.hasSave) {
const pendingingSaves = targets.filter(t => t.saved.success === null);
if (pendingingSaves.length) {
const confirm = await foundry.applications.api.DialogV2.confirm({
window: { title: game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.title') },
content: `
<p>${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.unfinishedRolls')}</p>
<p><i>${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.warning')}</i></p>
<p>${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.confirmation')}</p>
`
});
if (!confirm) return;
}
}
this.consumeOnSuccess(); this.consumeOnSuccess();
this.system.action?.workflow.get('effects')?.execute(config, targets, true); this.system.action?.workflow.get('effects')?.execute(config, targets, true);
} }
@ -270,14 +290,14 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
behaviors: behaviors:
effects.length > 0 effects.length > 0
? [ ? [
{ {
name: game.i18n.localize('TYPES.RegionBehavior.applyActiveEffect'), name: game.i18n.localize('TYPES.RegionBehavior.applyActiveEffect'),
type: 'applyActiveEffect', type: 'applyActiveEffect',
system: { system: {
effects: effects effects: effects
} }
} }
] ]
: [], : [],
displayMeasurements: true, displayMeasurements: true,
locked: false, locked: false,

View file

@ -73,13 +73,16 @@ export default class DHItem extends foundry.documents.Item {
/** Returns true if the item can be used */ /** Returns true if the item can be used */
get usable() { get usable() {
const actor = this.actor; const actor = this.actor;
const actionsList = this.system.actionsList; const pack = actor?.pack ? game.packs.get(actor.pack) : null;
return this.isOwner && actor?.type === 'character' && (actionsList?.size || actionsList?.length); const hasActions = this.system.actionsList?.size || this.system.actionsList?.length;
const isValidType = actor?.type === 'character' || this.type === 'feature';
return !pack?.locked && this.isOwner && isValidType && hasActions;
} }
/** @inheritdoc */ /** @inheritdoc */
static async createDialog(data = {}, createOptions = {}, options = {}) { static async createDialog(data = {}, createOptions = {}, options = {}) {
const { folders, types, template, context = {}, ...dialogOptions } = options; const { folders, types, template, context = {}, ...dialogOptions } = options;
dialogOptions.classes = [options.classes ?? [], 'item-create'].flat(); // handled in hook
if (types?.length === 0) { if (types?.length === 0) {
throw new Error('The array of sub-types to restrict to must not be empty.'); throw new Error('The array of sub-types to restrict to must not be empty.');
@ -95,8 +98,8 @@ export default class DHItem extends foundry.documents.Item {
isInventoryItem === true isInventoryItem === true
? 'Inventory Items' //TODO localize ? 'Inventory Items' //TODO localize
: isInventoryItem === false : isInventoryItem === false
? 'Character Items' //TODO localize ? 'Character Items' //TODO localize
: 'Other'; //TODO localize : 'Other'; //TODO localize
return { value: type, label, group }; return { value: type, label, group };
} }

View file

@ -20,7 +20,7 @@ export default class DhScene extends Scene {
const prototype = tokenDoc.actor?.prototypeToken ?? tokenDoc; const prototype = tokenDoc.actor?.prototypeToken ?? tokenDoc;
this.#sizeSyncBatch.set(tokenDoc.id, { this.#sizeSyncBatch.set(tokenDoc.id, {
size: tokenSize, size: tokenSize,
prototypeSize: { width: prototype.width, height: prototype.height }, prototypeSize: { width: prototype.width, height: prototype.height, depth: prototype.depth },
position: { x: tokenDoc.x, y: tokenDoc.y, elevation: tokenDoc.elevation } position: { x: tokenDoc.x, y: tokenDoc.y, elevation: tokenDoc.elevation }
}); });
this.#processSyncBatch(); this.#processSyncBatch();
@ -36,11 +36,13 @@ export default class DhScene extends Scene {
const tokenSize = tokenSizes[size]; const tokenSize = tokenSizes[size];
const width = size !== CONFIG.DH.ACTOR.tokenSize.custom.id ? tokenSize : prototypeSize.width; const width = size !== CONFIG.DH.ACTOR.tokenSize.custom.id ? tokenSize : prototypeSize.width;
const height = size !== CONFIG.DH.ACTOR.tokenSize.custom.id ? tokenSize : prototypeSize.height; const height = size !== CONFIG.DH.ACTOR.tokenSize.custom.id ? tokenSize : prototypeSize.height;
const depth = size !== CONFIG.DH.ACTOR.tokenSize.custom.id ? tokenSize : prototypeSize.depth;
const updatedPosition = DHToken.getSnappedPositionInSquareGrid(this.grid, position, width, height); const updatedPosition = DHToken.getSnappedPositionInSquareGrid(this.grid, position, width, height);
return { return {
_id, _id,
width, width,
height, height,
depth,
...updatedPosition ...updatedPosition
}; };
}); });

View file

@ -66,7 +66,8 @@ export default class DHToken extends CONFIG.Token.documentClass {
if (tokenSize && actor.system.size !== CONFIG.DH.ACTOR.tokenSize.custom.id) { if (tokenSize && actor.system.size !== CONFIG.DH.ACTOR.tokenSize.custom.id) {
document.updateSource({ document.updateSource({
width: tokenSize, width: tokenSize,
height: tokenSize height: tokenSize,
depth: tokenSize
}); });
} }
} }
@ -90,7 +91,7 @@ export default class DHToken extends CONFIG.Token.documentClass {
) { ) {
const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes; const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes;
const tokenSize = tokenSizes[update.system.size]; const tokenSize = tokenSizes[update.system.size];
if (tokenSize !== this.width || tokenSize !== this.height) { if (tokenSize !== this.width || tokenSize !== this.height || tokenSize !== this.depth) {
this.parent?.syncTokenDimensions(this, update.system.size); this.parent?.syncTokenDimensions(this, update.system.size);
} }
} }
@ -323,7 +324,7 @@ export default class DHToken extends CONFIG.Token.documentClass {
} }
let x = 0.5 * bottom; let x = 0.5 * bottom;
let y = 0.25; let y = 0.25;
for (let k = width - bottom; k--; ) { for (let k = width - bottom; k--;) {
points.push(x, y); points.push(x, y);
x += 0.5; x += 0.5;
y -= 0.25; y -= 0.25;
@ -332,7 +333,7 @@ export default class DHToken extends CONFIG.Token.documentClass {
y += 0.25; y += 0.25;
} }
points.push(x, y); points.push(x, y);
for (let k = bottom; k--; ) { for (let k = bottom; k--;) {
y += 0.5; y += 0.5;
points.push(x, y); points.push(x, y);
x += 0.5; x += 0.5;
@ -340,14 +341,14 @@ export default class DHToken extends CONFIG.Token.documentClass {
points.push(x, y); points.push(x, y);
} }
y += 0.5; y += 0.5;
for (let k = top; k--; ) { for (let k = top; k--;) {
points.push(x, y); points.push(x, y);
x -= 0.5; x -= 0.5;
y += 0.25; y += 0.25;
points.push(x, y); points.push(x, y);
y += 0.5; y += 0.5;
} }
for (let k = width - top; k--; ) { for (let k = width - top; k--;) {
points.push(x, y); points.push(x, y);
x -= 0.5; x -= 0.5;
y += 0.25; y += 0.25;
@ -356,7 +357,7 @@ export default class DHToken extends CONFIG.Token.documentClass {
y -= 0.25; y -= 0.25;
} }
points.push(x, y); points.push(x, y);
for (let k = top; k--; ) { for (let k = top; k--;) {
y -= 0.5; y -= 0.5;
points.push(x, y); points.push(x, y);
x -= 0.5; x -= 0.5;
@ -364,7 +365,7 @@ export default class DHToken extends CONFIG.Token.documentClass {
points.push(x, y); points.push(x, y);
} }
y -= 0.5; y -= 0.5;
for (let k = bottom; k--; ) { for (let k = bottom; k--;) {
points.push(x, y); points.push(x, y);
x += 0.5; x += 0.5;
y -= 0.25; y -= 0.25;

View file

@ -15,6 +15,7 @@ export default class DhTokenManager {
if (tokenSize && actor.system.size !== CONFIG.DH.ACTOR.tokenSize.custom.id) { if (tokenSize && actor.system.size !== CONFIG.DH.ACTOR.tokenSize.custom.id) {
tokenData.width = tokenSize; tokenData.width = tokenSize;
tokenData.height = tokenSize; tokenData.height = tokenSize;
tokenData.depth = tokenSize;
} }
} }

View file

@ -3,7 +3,6 @@ import { AdversaryBPPerEncounter, BaseBPPerEncounter } from '../config/encounter
export default class DhTooltipManager extends foundry.helpers.interaction.TooltipManager { export default class DhTooltipManager extends foundry.helpers.interaction.TooltipManager {
#wide = false; #wide = false;
#bordered = false; #bordered = false;
#active = false;
async activate(element, options = {}) { async activate(element, options = {}) {
const { TextEditor } = foundry.applications.ux; const { TextEditor } = foundry.applications.ux;

View file

@ -1,7 +1,7 @@
import { parseInlineParams } from './parser.mjs'; import { parseInlineParams } from './parser.mjs';
export default function DhDamageEnricher(match, _options) { export default function DhDamageEnricher(match, _options) {
const { value, type, inline } = parseInlineParams(match[1]); const { value, type, inline } = parseInlineParams(match[1], { first: 'value' });
if (!value || !type) return match[0]; if (!value || !type) return match[0];
return getDamageMessage(value, type, inline, match[0]); return getDamageMessage(value, type, inline, match[0]);
} }
@ -59,7 +59,7 @@ export const renderDamageButton = async event => {
{ {
formula: value, formula: value,
applyTo: CONFIG.DH.GENERAL.healingTypes.hitPoints.id, applyTo: CONFIG.DH.GENERAL.healingTypes.hitPoints.id,
type: type damageTypes: type
} }
] ]
}; };

View file

@ -15,8 +15,8 @@ function getDualityMessage(roll, flavor) {
(roll?.trait (roll?.trait
? game.i18n.format('DAGGERHEART.GENERAL.rollWith', { roll: trait }) ? game.i18n.format('DAGGERHEART.GENERAL.rollWith', { roll: trait })
: roll?.reaction : roll?.reaction
? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll') ? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll')
: game.i18n.localize('DAGGERHEART.GENERAL.duality')); : game.i18n.localize('DAGGERHEART.GENERAL.duality'));
const dataLabel = trait const dataLabel = trait
? game.i18n.localize(abilities[roll.trait].label) ? game.i18n.localize(abilities[roll.trait].label)
@ -25,14 +25,14 @@ function getDualityMessage(roll, flavor) {
const advantage = roll?.advantage const advantage = roll?.advantage
? CONFIG.DH.ACTIONS.advantageState.advantage.value ? CONFIG.DH.ACTIONS.advantageState.advantage.value
: roll?.disadvantage : roll?.disadvantage
? CONFIG.DH.ACTIONS.advantageState.disadvantage.value ? CONFIG.DH.ACTIONS.advantageState.disadvantage.value
: undefined; : undefined;
const advantageLabel = const advantageLabel =
advantage === CONFIG.DH.ACTIONS.advantageState.advantage.value advantage === CONFIG.DH.ACTIONS.advantageState.advantage.value
? 'Advantage' ? 'Advantage'
: advantage === CONFIG.DH.ACTIONS.advantageState.disadvantage.value : advantage === CONFIG.DH.ACTIONS.advantageState.disadvantage.value
? 'Disadvantage' ? 'Disadvantage'
: undefined; : undefined;
const dualityElement = document.createElement('span'); const dualityElement = document.createElement('span');
dualityElement.innerHTML = ` dualityElement.innerHTML = `

View file

@ -8,8 +8,8 @@ export default function DhTemplateEnricher(match, _options) {
const range = const range =
params.range && Number.isNaN(Number(params.range)) params.range && Number.isNaN(Number(params.range))
? Object.values(CONFIG.DH.GENERAL.templateRanges).find( ? Object.values(CONFIG.DH.GENERAL.templateRanges).find(
x => x.id.toLowerCase() === params.range || x.short === params.range x => x.id.toLowerCase() === params.range || x.short === params.range
)?.id )?.id
: params.range; : params.range;
if (!CONFIG.DH.GENERAL.templateTypes[type] || !range) return match[0]; if (!CONFIG.DH.GENERAL.templateTypes[type] || !range) return match[0];

View file

@ -8,7 +8,7 @@ export function parseInlineParams(paramString, { first } = {}) {
const parts = paramString.split('|').map(x => x.trim()); const parts = paramString.split('|').map(x => x.trim());
const params = {}; const params = {};
for (const [idx, param] of parts.entries()) { for (const [idx, param] of parts.entries()) {
if (first && idx === 0) { if (first && idx === 0 && !param.includes(':')) {
params[first] = param; params[first] = param;
} else { } else {
const parts = param.split(':'); const parts = param.split(':');

View file

@ -108,9 +108,9 @@ export const tagifyElement = (element, baseOptions, onChange, tagifyOptions = {}
const options = Array.isArray(baseOptions) const options = Array.isArray(baseOptions)
? baseOptions ? baseOptions
: Object.keys(baseOptions).map(optionKey => ({ : Object.keys(baseOptions).map(optionKey => ({
...baseOptions[optionKey], ...baseOptions[optionKey],
id: optionKey id: optionKey
})); }));
const tagifyElement = new Tagify(element, { const tagifyElement = new Tagify(element, {
tagTextProp: 'name', tagTextProp: 'name',
@ -449,14 +449,9 @@ export async function createEmbeddedItemsWithEffects(actor, baseData) {
effects: data.effects?.map(effect => effect.toObject()) effects: data.effects?.map(effect => effect.toObject())
}); });
} }
await actor.createEmbeddedDocuments('Item', effectData); await actor.createEmbeddedDocuments('Item', effectData);
} }
export const slugify = name => {
return name.toLowerCase().replaceAll(' ', '-').replaceAll('.', '');
};
export function shuffleArray(array) { export function shuffleArray(array) {
let currentIndex = array.length; let currentIndex = array.length;
while (currentIndex != 0) { while (currentIndex != 0) {
@ -610,8 +605,8 @@ export function calculateExpectedValue(formulaOrTerms) {
const terms = Array.isArray(formulaOrTerms) const terms = Array.isArray(formulaOrTerms)
? formulaOrTerms ? formulaOrTerms
: typeof formulaOrTerms === 'string' : typeof formulaOrTerms === 'string'
? parseTermsFromSimpleFormula(formulaOrTerms) ? parseTermsFromSimpleFormula(formulaOrTerms)
: [formulaOrTerms]; : [formulaOrTerms];
return terms.reduce((r, t) => r + (t.bonus ?? 0) + (t.diceQuantity ? (t.diceQuantity * (t.faces + 1)) / 2 : 0), 0); return terms.reduce((r, t) => r + (t.bonus ?? 0) + (t.diceQuantity ? (t.diceQuantity * (t.faces + 1)) / 2 : 0), 0);
} }
@ -661,8 +656,8 @@ export async function RefreshFeatures(
'resource.value': increasing 'resource.value': increasing
? 0 ? 0
: game.system.api.documents.DhActiveEffect.effectSafeEval( : game.system.api.documents.DhActiveEffect.effectSafeEval(
Roll.replaceFormulaData(item.system.resource.max, actor.getRollData()) Roll.replaceFormulaData(item.system.resource.max, actor.getRollData())
) )
}; };
} }
if (item.system.metadata?.hasActions) { if (item.system.metadata?.hasActions) {
@ -869,3 +864,58 @@ export function camelize(str) {
}) })
.replace(/\s+/g, ''); .replace(/\s+/g, '');
} }
/** Bulk load a list of documents using uuids. Returns the documents in the same order */
export async function fromUuids(uuids) {
// Set up base entries. Each step works on a sublist of these objects
const entries = uuids.map(uuid => ({
uuid,
parsed: foundry.utils.parseUuid(uuid),
value: foundry.utils.fromUuidSync(uuid)
}));
// Handle missing uuids for embedded documents first
// A value may be index data, so we check if its a document
const packEmbeddedEntries = entries.filter(
e =>
!(e.value instanceof Document) &&
e.parsed &&
e.parsed.collection instanceof foundry.documents.collections.CompendiumCollection &&
e.parsed.embedded.length > 0
);
await Promise.all(
packEmbeddedEntries.map(async e => {
e.value = await fromUuid(e.uuid);
return true;
})
);
// Handle missing top level pack stuff, by batching per pack
const missingTopLevel = entries.filter(e => !(e.value instanceof Document) && e.value?.pack);
for (const packGroup of Object.values(Object.groupBy(missingTopLevel, e => e.value.pack))) {
const pack = game.packs.get(packGroup[0].value.pack);
if (!pack) continue;
const ids = packGroup.map(p => p.parsed?.id).filter(id => !!id);
const documents = await pack.getDocuments({ _id__in: ids });
for (const p of packGroup) {
p.value = documents.find(d => d.id === p.parsed.id) ?? p.value;
}
}
return entries.map(e => e.value);
}
/**
* Triggers DiceSoNice rolls or dice roll audio for rolls. Not used for duality rolls.
* @param { Roll[] } rolls
* @return { void }
*/
export async function triggerChatRollFx(rolls, options = { whisper: false, blind: false }) {
const { whisper, blind } = options;
if (game.modules.get('dice-so-nice')?.active) {
const rerollPromises = rolls.map(roll => game.dice3d.showForRoll(roll, game.user, true, whisper, blind));
await Promise.allSettled(rerollPromises);
} else {
foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
}
}

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