Compare commits

..

70 commits
1.6.0 ... main

Author SHA1 Message Date
WBHarry
f1f5102af1 Raised version 2026-03-07 01:33:45 +01:00
WBHarry
a4f8c67707
Added vulnerable condition automation (#1704) 2026-03-07 01:32:36 +01:00
WBHarry
83c3da0130
Fixed so that Rally dice works as a single active effect for bard (#1708) 2026-03-07 00:08:03 +01:00
WBHarry
92d8c2ca18 Moved RefreshFeatures to utils and added a proper description 2026-03-07 00:06:12 +01:00
WBHarry
17aa0680d2
[Fix] 1697 - DamageRolls DiceSoNice (#1706)
* .

* .

* .
2026-03-06 13:07:55 +01:00
WBHarry
5732639391
. (#1705) 2026-03-06 11:07:52 +01:00
WBHarry
9bfe3505bf
[Fix] 1696 - Homebrew Fixes (#1707)
* SettingsActiveEffectConfig was out of date, making it error

* Other two fixes

* .

* .
2026-03-06 11:03:55 +01:00
WBHarry
9cba77ec11
[Fix] 1687 - Item Transfer Without Permissions (#1691)
* Fixed so that transfering items completly aborts if lacking permissions

* .
2026-03-05 22:14:07 +01:00
Carlos Fernandez
0675e1f019
Add recall cost to domain cards in grid view (#1700) 2026-03-05 21:31:49 +01:00
Carlos Fernandez
1212bd01f8
Increase the click area of sidebar and inventory control buttons (#1703) 2026-03-05 21:30:31 +01:00
Psitacus
3267f3f531
fix instances of rolls being called checks (#1702)
Co-authored-by: Psitacus <walther.johnson@ucalgary.ca>
2026-03-05 09:56:35 +01:00
WBHarry
986544a653
[Fix] 1689 - Missing Feature Errors (#1690)
* Fixed so that weaponfeatures and armorFeatures are tolerant of features having been removed

* .
2026-03-04 22:16:11 +10:00
WBHarry
5459581f7f
Fixed styling in firefox (#1692) 2026-03-04 01:10:40 +01:00
WBHarry
0d0b5125ba
[Fix] 1683 - Strange Patterns Explanation (#1693)
* Added an explanation text to Strange Patterns trigger dialog

* Update lang/en.json

Co-authored-by: Chris Ryan <73275196+chrisryan10@users.noreply.github.com>

---------

Co-authored-by: Chris Ryan <73275196+chrisryan10@users.noreply.github.com>
2026-03-02 09:37:33 +01:00
WBHarry
c48842dd2d Fixed error on deleting a sceneEnvironment item 2026-02-26 20:04:59 +01:00
WBHarry
e79ccd34e9
[Fix] 1671 - Compendium Context Menues (#1677)
* Fixed

* .
2026-02-26 11:42:42 +01:00
Carlos Fernandez
4324c3abf2
[Fix] Support elevation in token distance hovering and fix error when overlapping (#1675)
* Support elevation in token distance hovering

* Reduce diffs

* Refine elevation check to handle stacked tokens

* Fix issue with overlapping tokens

* Fix tooltip reporting very close for adjacent diagonal tokens
2026-02-26 11:37:40 +01:00
WBHarry
1b09b44d6c
[Fix] 1676 - Horde Damage Fix (#1678)
* Fixed so that horde damage reduction is only applied to the standard attack

* Changed to just adding 'isStandardAttack' in adversary data prep

* .
2026-02-26 11:32:05 +01:00
Cipher
340abbc98c
Fix incorrect adversary data (ranges, stats, types, names) (#1680)
- Adult Flickerfly: damage dice d10 → d20
- Giant Recruit: stress max 1 → 2
- Hallowed Soldier: stress max 1 → 2
- Jagged Knife Sniper: type standard → ranged
- Minor Demon: add missing melee range
- Oak Treant: type standard → bruiser, attack name → Branch, range → veryClose
- Outer Realms Thrall: range melee → veryClose
- Treant Sapling: add missing melee range
- Young Ice Dragon: range melee → close
- Zombie Legion: attack name Tentacles → Undead Hands

Co-authored-by: Sebastian Will <sebastian.h.will@gmail.com>
2026-02-23 16:34:06 +01:00
WBHarry
56cc16b39a
[Feature] Item Description Enrichment (#1666)
* Added enrichment for Ancestries and Communities

* Fixed remainder

* Bit of padding

* Increased left padding
2026-02-22 21:32:35 +01:00
WBHarry
267de9a8cf
Fixed so that saving custom scene measurements work (#1664) 2026-02-22 14:12:26 +01:00
WBHarry
9296b8fcc2
Fixed so that scars are applied to hope.max during derived data prep (#1673) 2026-02-22 14:10:06 +01:00
WBHarry
ca434d33f1
Fixed so that advantage/disadvantage dice are properly considered when rerolling (#1662) 2026-02-14 13:09:13 +01:00
WBHarry
b64a9002ea
Fixed advantage/disadvantage sources for adversaries and companions (#1659) 2026-02-14 13:07:47 +01:00
WBHarry
472f876ea3 Merge branch 'development' 2026-02-12 22:27:59 +01:00
Carlos Fernandez
7022630316
[PR][Feature] Add support for changing the tier of an adversary (#1503)
* Add support for changing the tier of an adversary

* Move scaling data to actorConfig

* Use a new algorithm using the median average deviation

* Fine tune damage conversion for actions

* Use standard deviation instead and change dialog type

* Use daggerheart style for dialog

* Formatting

* Improve handling of minions and hordes

* Changed to using lookup for Group Attack damage

* Added lookup for Horde feature

* Remove spaces in damage formulas

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>
2026-02-12 22:27:37 +01:00
WBHarry
e0b3d33f80 Added the ability to exclude world compendiums in the Compendium Browser Settings 2026-02-12 19:27:03 +01:00
WBHarry
60cd28ae82 Fixed the fear tracker showing up while supposed to be hidden 2026-02-12 19:03:21 +01:00
WBHarry
12bcd6e34e Fixed ScenEnvironment menu removing sceneEnvironments when used 2026-02-12 18:55:22 +01:00
WBHarry
6cbe770880
[Fix] ActiveEffectConfig Missing Resistances (#1653)
* Fixed so that ActiveEffectConfig uses missing hints and has resistance in the autocomplete list

* Raised version
2026-02-11 23:59:27 +01:00
WBHarry
95d4003045
Fixed so that messages auto expand the description (#1650) 2026-02-11 23:56:35 +01:00
WBHarry
fa19339868 Fixed better sceneNavigation compatability 2026-02-11 23:34:10 +01:00
WBHarry
17ec77a349 Fixed not being able to open the tokenConfig of actor-less tokens 2026-02-11 00:31:45 +01:00
WBHarry
a65514b1c1 Improved Downtime Prepare translation 2026-02-09 14:25:56 +01:00
WBHarry
b23b6c75fb
[Feature] Browser Compendium Handling (#1648)
* Initial version

* .

* Fixed so that CompendiumSetting saving refreshes the CompendiumBrowser for all users

* .

* Improved design

* Fixed max height

* Fixed local reload

* Added GM restriction

* Raised version

* Fixed tooltip

* Raised verison to 1.7.0
2026-02-09 12:42:00 +01:00
WBHarry
7c86417752 Added _applyLegacy parse logic for ActiveEffect 2026-02-09 12:41:13 +01:00
WBHarry
c7431d16a7
Improved the Reaction toggle in dice rolls (#1643) 2026-02-09 01:02:59 +01:00
WBHarry
5413730108 Corrected experience value display to handle negative values aswell 2026-02-09 00:25:18 +01:00
WBHarry
d96e72505a Fixed Armor/Weapon sheet showing double 'Configure Attribution' options 2026-02-08 22:21:36 +01:00
WBHarry
f9f252c7a6
Fixed so that node start can accept escaped spaces in the path (#1649) 2026-02-08 19:22:48 +01:00
WBHarry
78012be6e4 Added delete confirmation to homebrew items 2026-02-08 19:14:04 +01:00
alterNERDtive
4ad8b960b5
fix: adds actions to prepare downtime actions (#1646) 2026-02-08 18:30:32 +01:00
WBHarry
f7e4c5346e
[Fix] ActiveEffect Autocomplete (#1641)
* Added rules and bonuses to ActiveEffect-autocomplete

* .
2026-02-08 18:03:35 +01:00
WBHarry
44131d21a6
[Fix] Beastform Effects (#1635)
* Fixed so that beastform items always have a beastformEffect on them that can't be removed

* Fixed so that you can drag an active effect onto a character
2026-02-08 18:01:30 +01:00
WBHarry
202e624a06
Fixed so that SecondWind has a decreasing resource (#1642) 2026-02-08 18:00:09 +01:00
WBHarry
5e7201bfe9
[Fix]Environment Attack Error (#1647)
* Fixed so that environment attacks don't error

* Fixed for companion aswell
2026-02-08 17:59:08 +01:00
alterNERDtive
cad3f533ad
fix: restricts target amount for downtime actions (#1645) 2026-02-07 23:14:17 +01:00
alterNERDtive
c3653e1b30
feat(dev): adds editorconfig (#1644) 2026-02-07 20:02:28 +01:00
WBHarry
c1f7866594
[Fix] 1633 - ActiveEffect Autocomplete (#1636)
* Improved the autocomplete typing experience

* Made it work. But I hate it.

* Revert "Made it work. But I hate it."

This reverts commit d2fc9fd648.

* Actually nice solution instead O_O
2026-02-06 11:32:33 +01:00
WBHarry
0d2495c143
Fixed fumigation and bold presence (#1638) 2026-02-06 00:36:52 +01:00
WBHarry
cab185df66
Made gridless distances lean towards being in the lower range (#1639) 2026-02-05 16:36:49 -05:00
WBHarry
735ed4c214
[Fix] RollMessage Order (#1626)
* Fixed so that the description message always comes first with the action workflow

* Changed to instead render the description in the roll message

* Made the action config title not get changed in d20rolldialog if it's not a trait roll

* Initial chat message description design change

* Revert "Initial chat message description design change"

This reverts commit f4f5fd6c24.

* .
2026-02-04 07:11:18 +01:00
WBHarry
c8d1ea1460
[Fix] TriggerConfig Expand/Collapse (#1630)
* Fixed so that the angle icon indicating expand/collapse for a trigger config actually flips when clicked

* Fixed styles
2026-02-03 23:31:18 +01:00
WBHarry
c1924534da Fixed a case where SceneConfig could fail to save because flags were not submitted 2026-02-03 23:26:29 +01:00
WBHarry
31c70469ef
[Fix] ActionField UUID for Actor Actions (#1622)
* Fixed so that actionFields correctly grab a uuid for actions that are directly on an actor

* Corrected to logical or

* Update module/data/fields/actionField.mjs

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

---------

Co-authored-by: Carlos Fernandez <CarlosFdez@users.noreply.github.com>
2026-02-02 00:57:17 +01:00
WBHarry
491d921a9b
Made sure item.toChat enriches descriptions (#1625) 2026-02-02 00:40:09 +01:00
WBHarry
668dbdf8f4
Fixed character hopeResource to be set as 'isReversed' again, so damage and healing is applied correctly (#1620) 2026-02-01 01:19:31 +01:00
WBHarry
ab538df3aa
[Fix] 1601 - Template Enrichment (#1609)
* Fixed so param.range is compared as lowerCase

* .
2026-02-01 00:59:50 +01:00
WBHarry
483caa1062 Corrected default DowntimeMoves to have effects defined 2026-01-31 19:46:21 +01:00
WBHarry
22d446f360
Fixed so that homebrew downtime move actions can have effects (#1615) 2026-01-31 19:34:37 +01:00
WBHarry
94efbeada3
Fixed a silly error with Hexgrid (#1608) 2026-01-30 16:05:11 +01:00
Carlos Fernandez
1bc9e07098
[Feature] Show token distance on hover (#1607)
* Show token distance on hover

* Do not show distance hover when ranges variant rule is disabled

* Use range labels function for distance hover

* Fix very far and support feet

* .

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>
2026-01-30 15:58:21 +01:00
WBHarry
b374070809 Raised version 2026-01-28 12:57:10 +01:00
WBHarry
7bf0f0fbee
Fixed an error where extra labels were shown (#1596) 2026-01-28 12:56:32 +01:00
WBHarry
3f4d1cd292
Removed faulty hitPoint cost from rousing speech (#1595) 2026-01-28 11:09:01 +01:00
Nikhil Nagarajan
a18393a9d0
[PR] Loadout Max detection when pulled from compendium (#1589)
* Initial test implementation for solution

* Expanded the existing _preCreate logic

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>
2026-01-28 08:51:00 +01:00
WBHarry
076b7f01fa
[Fix] 1587 - Lightmode Touchups (#1590)
* Fixed hud statuses and tooltips

* Fixed chat

* Fixed radiobuttons being missplaced
2026-01-28 08:49:22 +01:00
WBHarry
bb43cb57dc
[Fix] 1582 - Automation Additions (#1592)
* Added an automation setting for chat command resources

* Added automation settings for wether to have deathmove automation

* .

* Flattened automation structure
2026-01-28 08:47:02 +01:00
WBHarry
0baed9234b
Made sure the advantage section is always shown (#1591) 2026-01-27 19:35:03 -05:00
Nikhil Nagarajan
6321c7c508
[Fix] Enricher fixes for [[fr]] and reaction argument lazy fix (#1586)
* Update utils.mjs

* fixed reaction situation.

* Made change in enricher instead of dialog

* comment clarity
2026-01-27 17:56:42 +01:00
191 changed files with 2987 additions and 950 deletions

3
.editorconfig Normal file
View file

@ -0,0 +1,3 @@
[*]
indent_size = 4
indent_style = spaces

View file

@ -242,6 +242,41 @@ Hooks.on('setup', () => {
systemEffect: true systemEffect: true
})) }))
]; ];
const damageThresholds = ['damageThresholds.major', 'damageThresholds.severe'];
const traits = Object.keys(game.system.api.data.actors.DhCharacter.schema.fields.traits.fields).map(
trait => `traits.${trait}.value`
);
const resistance = Object.values(game.system.api.data.actors.DhCharacter.schema.fields.resistance.fields).flatMap(
type => Object.keys(type.fields).map(x => `resistance.${type.name}.${x}`)
);
const actorCommon = {
bar: ['resources.stress'],
value: [...resistance, 'advantageSources', 'disadvantageSources']
};
CONFIG.Actor.trackableAttributes = {
character: {
bar: [...actorCommon.bar, 'resources.hitPoints', 'resources.hope'],
value: [
...actorCommon.value,
...traits,
...damageThresholds,
'proficiency',
'evasion',
'armorScore',
'scars',
'levelData.level.current'
]
},
adversary: {
bar: [...actorCommon.bar, 'resources.hitPoints'],
value: [...actorCommon.value, ...damageThresholds, 'criticalThreshold', 'difficulty']
},
companion: {
bar: [...actorCommon.bar],
value: [...actorCommon.value, 'evasion', 'levelData.level.current']
}
};
}); });
Hooks.on('ready', async () => { Hooks.on('ready', async () => {
@ -309,7 +344,7 @@ Hooks.on('chatMessage', (_, message) => {
? CONFIG.DH.ACTIONS.advantageState.disadvantage.value ? CONFIG.DH.ACTIONS.advantageState.disadvantage.value
: undefined; : undefined;
const difficulty = rollCommand.difficulty; const difficulty = rollCommand.difficulty;
const grantResources = Boolean(rollCommand.grantResources); const grantResources = rollCommand.grantResources;
const target = getCommandTarget({ allowNull: true }); const target = getCommandTarget({ allowNull: true });
const title = const title =
@ -385,10 +420,7 @@ const updateActorsRangeDependentEffects = async token => {
// Get required distance and special case 5 feet to test adjacency // Get required distance and special case 5 feet to test adjacency
const required = rangeMeasurement[range]; const required = rangeMeasurement[range];
const reverse = type === CONFIG.DH.GENERAL.rangeInclusion.outsideRange.id; const reverse = type === CONFIG.DH.GENERAL.rangeInclusion.outsideRange.id;
const inRange = const inRange = userTarget.distanceTo(token.object) <= required;
required === 5
? userTarget.isAdjacentWith(token.object)
: userTarget.distanceTo(token.object) <= required;
if (reverse ? inRange : !inRange) { if (reverse ? inRange : !inRange) {
enabledEffect = false; enabledEffect = false;
break; break;

View file

@ -192,6 +192,9 @@
}, },
"age": "Age", "age": "Age",
"backgroundQuestions": "Backgrounds", "backgroundQuestions": "Backgrounds",
"burden": {
"ignore": { "label": "Burden: Ignore", "hint": "Ignore burden rules" }
},
"companionFeatures": "Companion Features", "companionFeatures": "Companion Features",
"connections": "Connections", "connections": "Connections",
"contextMenu": { "contextMenu": {
@ -214,6 +217,12 @@
"maxEvasionBonus": "Max Evasion Increase", "maxEvasionBonus": "Max Evasion Increase",
"maxHPBonus": "Max HP Increase", "maxHPBonus": "Max HP Increase",
"pronouns": "Pronouns", "pronouns": "Pronouns",
"roll": {
"guaranteedCritical": {
"label": "Guaranteed Critical",
"hint": "Set to 1 to always roll a critical"
}
},
"story": { "story": {
"backgroundTitle": "Background", "backgroundTitle": "Background",
"characteristics": "Characteristics", "characteristics": "Characteristics",
@ -343,6 +352,12 @@
"requestSpotlight": "Request The Spotlight", "requestSpotlight": "Request The Spotlight",
"openCountdowns": "Countdowns" "openCountdowns": "Countdowns"
}, },
"CompendiumBrowserSettings": {
"title": "Enable Compendiums",
"enableSource": "Enable Source",
"disableSource": "Disable Source",
"worldCompendiums": "World Compendiums"
},
"ContextMenu": { "ContextMenu": {
"disableEffect": "Disable Effect", "disableEffect": "Disable Effect",
"enableEffect": "Enable Effect", "enableEffect": "Enable Effect",
@ -443,9 +458,13 @@
"name": "Clear Stress" "name": "Clear Stress"
}, },
"prepare": { "prepare": {
"description": "Describe how you are preparing for the next day's adventure, then gain a Hope. If you choose to Prepare with one or more members of your party, you may each take two Hope.", "description": "Describe how you are preparing for the next day's adventure, then gain a Hope.",
"name": "Prepare" "name": "Prepare"
}, },
"prepareWithFriends": {
"description": "You prepare with one or more members of your party, and you each gain 2 Hope.",
"name": "Prepare (together)"
},
"repairArmor": { "repairArmor": {
"description": "Describe how you spend time repairing your armor and clear all of its Armor Slots. You may also do this to an ally's armor instead.", "description": "Describe how you spend time repairing your armor and clear all of its Armor Slots. You may also do this to an ally's armor instead.",
"name": "Repair Armor" "name": "Repair Armor"
@ -476,7 +495,11 @@
}, },
"prepare": { "prepare": {
"name": "Prepare", "name": "Prepare",
"description": "Describe how you prepare yourself for the path ahead, then gain a Hope. If you choose to Prepare with one or more members of your party, you each gain 2 Hope." "description": "Describe how you prepare yourself for the path ahead, then gain a Hope."
},
"prepareWithFriends": {
"name": "Prepare (together)",
"description": "You prepare with one or more members of your party, and you each gain 2 Hope."
} }
}, },
"refreshable": { "refreshable": {
@ -1008,7 +1031,8 @@
}, },
"vulnerable": { "vulnerable": {
"name": "Vulnerable", "name": "Vulnerable",
"description": "While a creature is Vulnerable, all rolls targeting them have advantage.\nA creature who is already Vulnerable cant be made to take the condition again." "description": "While a creature is Vulnerable, all rolls targeting them have advantage.\nA creature who is already Vulnerable cant be made to take the condition again.",
"autoAppliedByLabel": "Max Stress"
} }
}, },
"CountdownType": { "CountdownType": {
@ -1143,12 +1167,12 @@
}, },
"far": { "far": {
"name": "Far", "name": "Far",
"description": "means a distance where one can see the appearance of a person or object, but probably not in great detail-- across a small battlefield or down a large corridor. This is usually about 30-100 feet away. While under danger, a PC will likely have to make an Agility check to get here safely. Anything on a battle map that is within the length of a standard piece of paper (~10-11 inches) can usually be considered far.", "description": "means a distance where one can see the appearance of a person or object, but probably not in great detail-- across a small battlefield or down a large corridor. This is usually about 30-100 feet away. While under danger, a PC will likely have to make an Agility roll to get here safely. Anything on a battle map that is within the length of a standard piece of paper (~10-11 inches) can usually be considered far.",
"short": "Far" "short": "Far"
}, },
"veryFar": { "veryFar": {
"name": "Very Far", "name": "Very Far",
"description": "means a distance where you can see the shape of a person or object, but probably not make outany details-- across a large battlefield or down a long street, generally about 100-300 feet away. While under danger, a PC likely has to make an Agility check to get here safely. Anything on a battle map that is beyond far distance, but still within sight of the characters can usually be considered very far.", "description": "means a distance where you can see the shape of a person or object, but probably not make outany details-- across a large battlefield or down a long street, generally about 100-300 feet away. While under danger, a PC likely has to make an Agility roll to get here safely. Anything on a battle map that is beyond far distance, but still within sight of the characters can usually be considered very far.",
"short": "V. Far" "short": "V. Far"
} }
}, },
@ -1271,6 +1295,7 @@
"triggerTexts": { "triggerTexts": {
"strangePatternsContentTitle": "Matched {nr} times.", "strangePatternsContentTitle": "Matched {nr} times.",
"strangePatternsContentSubTitle": "Increase hope and stress to a total of {nr}.", "strangePatternsContentSubTitle": "Increase hope and stress to a total of {nr}.",
"strangePatternsActionExplanation": "Left click to increase, right click to decrease",
"ferocityContent": "Spend 2 Hope to gain {bonus} bonus Evasion until after the next attack against you?", "ferocityContent": "Spend 2 Hope to gain {bonus} bonus Evasion until after the next attack against you?",
"ferocityEffectDescription": "Your evasion is increased by {bonus}. This bonus lasts until after the next attack made against you." "ferocityEffectDescription": "Your evasion is increased by {bonus}. This bonus lasts until after the next attack made against you."
}, },
@ -1840,6 +1865,16 @@
"singular": "Adversary", "singular": "Adversary",
"plural": "Adversaries" "plural": "Adversaries"
}, },
"Attack": {
"hpDamageMultiplier": {
"label": "HP Damage Multiplier",
"hint": "Multiply any damage you deal by this number"
},
"hpDamageTakenMultiplier": {
"label": "HP Damage Taken Multiplier",
"hint": "Multiply any damage dealt to you by this number"
}
},
"Bonuses": { "Bonuses": {
"rest": { "rest": {
"downtimeAction": "Downtime Action", "downtimeAction": "Downtime Action",
@ -2024,16 +2059,40 @@
"reaction": "Reaction Roll" "reaction": "Reaction Roll"
}, },
"Rules": { "Rules": {
"conditionImmunities": {
"hidden": "Condition Immunity: Hidden",
"restrained": "Condition Immunity: Restrained",
"vulnerable": "Condition Immunity: Vulnerable"
},
"damageReduction": { "damageReduction": {
"disabledArmor": { "label": "Disabled Armorslots" },
"increasePerArmorMark": { "increasePerArmorMark": {
"label": "Damage Reduction per Armor Slot", "label": "Damage Reduction per Armor Slot",
"hint": "A used armor slot normally reduces damage by one step. This value increases the number of steps damage is reduced by." "hint": "A used armor slot normally reduces damage by one step. This value increases the number of steps damage is reduced by."
}, },
"magical": {
"label": "Daamge Reduction: Only Magical",
"hint": "Armor can only be used to reduce magical damage"
},
"maxArmorMarkedBonus": "Max Armor Used", "maxArmorMarkedBonus": "Max Armor Used",
"maxArmorMarkedStress": { "maxArmorMarkedStress": {
"label": "Max Armor Used With Stress", "label": "Max Armor Used With Stress",
"hint": "If this value is set you can use up to that much stress to spend additional Armor Marks beyond your normal maximum." "hint": "If this value is set you can use up to that much stress to spend additional Armor Marks beyond your normal maximum."
}, },
"reduceSeverity": {
"magical": {
"label": "Reduce Damage Severity: Magical",
"hint": "Lowers any magical damage received by the set amount of severity degrees"
},
"physical": {
"label": "Reduce Damage Severity: Physical",
"hint": "Lowers any physical damage received by the set amount of severity degrees"
}
},
"physical": {
"label": "Damage Reduction: Only Physical",
"hint": "Armor can only be used to reduce physical damage"
},
"stress": { "stress": {
"any": { "any": {
"label": "Stress Damage Reduction: Any", "label": "Stress Damage Reduction: Any",
@ -2051,6 +2110,12 @@
"label": "Stress Damage Reduction: Minor", "label": "Stress Damage Reduction: Minor",
"hint": "The cost in stress you can pay to reduce minor damage to none." "hint": "The cost in stress you can pay to reduce minor damage to none."
} }
},
"thresholdImmunities": {
"minor": {
"label": "Threshold Immunities: Minor",
"hint": "Automatically ignores minor damage when set to 1"
}
} }
}, },
"attack": { "attack": {
@ -2111,7 +2176,6 @@
"tier4": "tier 4", "tier4": "tier 4",
"domains": "Domains", "domains": "Domains",
"downtime": "Downtime", "downtime": "Downtime",
"itemFeatures": "Item Features",
"roll": "Roll", "roll": "Roll",
"rules": "Rules", "rules": "Rules",
"partyMembers": "Party Members", "partyMembers": "Party Members",
@ -2120,7 +2184,10 @@
"questions": "Questions", "questions": "Questions",
"configuration": "Configuration", "configuration": "Configuration",
"base": "Base", "base": "Base",
"triggers": "Triggers" "triggers": "Triggers",
"deathMoves": "Deathmoves",
"sources": "Sources",
"packs": "Packs"
}, },
"Tiers": { "Tiers": {
"singular": "Tier", "singular": "Tier",
@ -2144,6 +2211,7 @@
"armorSlots": "Armor Slots", "armorSlots": "Armor Slots",
"artistAttribution": "Artwork By: {artist}", "artistAttribution": "Artwork By: {artist}",
"attack": "Attack", "attack": "Attack",
"automation": "Automation",
"basics": "Basics", "basics": "Basics",
"bonus": "Bonus", "bonus": "Bonus",
"burden": "Burden", "burden": "Burden",
@ -2151,6 +2219,7 @@
"continue": "Continue", "continue": "Continue",
"criticalSuccess": "Critical Success", "criticalSuccess": "Critical Success",
"criticalShort": "Critical", "criticalShort": "Critical",
"currentLevel": "Current Level",
"custom": "Custom", "custom": "Custom",
"d20Roll": "D20 Roll", "d20Roll": "D20 Roll",
"damage": "Damage", "damage": "Damage",
@ -2256,6 +2325,7 @@
"single": "Target", "single": "Target",
"plural": "Targets" "plural": "Targets"
}, },
"thingsAndThing": "{things} and {thing}",
"title": "Title", "title": "Title",
"tokenSize": "Token Size", "tokenSize": "Token Size",
"total": "Total", "total": "Total",
@ -2294,7 +2364,8 @@
}, },
"Ancestry": { "Ancestry": {
"primaryFeature": "Primary Feature", "primaryFeature": "Primary Feature",
"secondaryFeature": "Secondary Feature" "secondaryFeature": "Secondary Feature",
"featuresLabel": "Ancestry Features"
}, },
"Armor": { "Armor": {
"baseScore": "Base Score", "baseScore": "Base Score",
@ -2347,7 +2418,12 @@
"evolvedImagePlaceholder": "The image for the form selected for evolution will be used" "evolvedImagePlaceholder": "The image for the form selected for evolution will be used"
}, },
"Class": { "Class": {
"startingEvasionScore": "Starting Evasion Score",
"startingHitPoints": "Starting Hit Points",
"classItems": "Class Items",
"hopeFeatureLabel": "{class}'s Hope Feature",
"hopeFeatures": "Hope Features", "hopeFeatures": "Hope Features",
"classFeature": "Class Feature",
"classFeatures": "Class Features", "classFeatures": "Class Features",
"guide": { "guide": {
"suggestedEquipment": "Suggested Equipments", "suggestedEquipment": "Suggested Equipments",
@ -2360,6 +2436,9 @@
} }
} }
}, },
"Community": {
"featuresLabel": "Community Feature"
},
"Consumable": { "Consumable": {
"consumeOnUse": "Consume On Use", "consumeOnUse": "Consume On Use",
"destroyOnEmpty": "Destroy On Empty" "destroyOnEmpty": "Destroy On Empty"
@ -2375,7 +2454,11 @@
"masteryTitle": "Mastery" "masteryTitle": "Mastery"
}, },
"Subclass": { "Subclass": {
"spellcastingTrait": "Spellcasting Trait" "spellcastingTrait": "Spellcasting Trait",
"spellcastTrait": "Spellcast Trait",
"foundationFeatures": "Foundation Features",
"specializationFeature": "Specialization Feature",
"masteryFeature": "Mastery Feature"
}, },
"Weapon": { "Weapon": {
"weaponType": "Weapon Type", "weaponType": "Weapon Type",
@ -2404,6 +2487,14 @@
"hideAttribution": { "hideAttribution": {
"label": "Hide Attribution" "label": "Hide Attribution"
}, },
"showTokenDistance": {
"label": "Show Token Distance on Hover",
"choices": {
"always": "Always",
"encounters": "Encounters",
"never": "Never"
}
},
"expandedTitle": "Auto-expand Descriptions", "expandedTitle": "Auto-expand Descriptions",
"extendCharacterDescriptions": { "extendCharacterDescriptions": {
"label": "Characters" "label": "Characters"
@ -2466,6 +2557,10 @@
"gm": { "label": "GM" }, "gm": { "label": "GM" },
"players": { "label": "Players" } "players": { "label": "Players" }
}, },
"vulnerableAutomation": {
"label": "Vulnerable Automation",
"hint": "Automatically apply the Vulnerable condition when a actor reaches max stress"
},
"countdownAutomation": { "countdownAutomation": {
"label": "Countdown Automation", "label": "Countdown Automation",
"hint": "Automatically progress countdowns based on their progression settings" "hint": "Automatically progress countdowns based on their progression settings"
@ -2546,6 +2641,8 @@
"resetMovesTitle": "Reset {type} Downtime Moves", "resetMovesTitle": "Reset {type} Downtime Moves",
"resetItemFeaturesTitle": "Reset {type}", "resetItemFeaturesTitle": "Reset {type}",
"resetMovesText": "Are you sure you want to reset?", "resetMovesText": "Are you sure you want to reset?",
"deleteItemTitle": "Delete Homebrew Item",
"deleteItemText": "Are you sure you want to delete the item?",
"FIELDS": { "FIELDS": {
"maxFear": { "label": "Max Fear" }, "maxFear": { "label": "Max Fear" },
"maxHope": { "label": "Max Hope" }, "maxHope": { "label": "Max Hope" },
@ -2714,7 +2811,7 @@
"title": "Domain Card" "title": "Domain Card"
}, },
"dualityRoll": { "dualityRoll": {
"abilityCheckTitle": "{ability} Check" "abilityCheckTitle": "{ability} Roll"
}, },
"effectSummary": { "effectSummary": {
"title": "Effects Applied", "title": "Effects Applied",
@ -2729,7 +2826,7 @@
"selectLeader": "Select a Leader", "selectLeader": "Select a Leader",
"selectMember": "Select a Member", "selectMember": "Select a Member",
"rerollTitle": "Reroll Group Roll", "rerollTitle": "Reroll Group Roll",
"rerollContent": "Are you sure you want to reroll your {trait} check?", "rerollContent": "Are you sure you want to reroll your {trait} roll?",
"rerollTooltip": "Reroll", "rerollTooltip": "Reroll",
"wholePartySelected": "The whole party is selected" "wholePartySelected": "The whole party is selected"
}, },
@ -2777,6 +2874,7 @@
"ItemBrowser": { "ItemBrowser": {
"title": "Daggerheart Compendium Browser", "title": "Daggerheart Compendium Browser",
"hint": "Select a Folder in sidebar to start browsing through the compendium", "hint": "Select a Folder in sidebar to start browsing through the compendium",
"browserSettings": "Browser Settings",
"searchPlaceholder": "Search...", "searchPlaceholder": "Search...",
"columnName": "Name", "columnName": "Name",
"tooltipFilters": "Filters", "tooltipFilters": "Filters",
@ -2894,14 +2992,17 @@
"tokenActorMissing": "{name} is missing an Actor", "tokenActorMissing": "{name} is missing an Actor",
"tokenActorsMissing": "[{names}] missing Actors", "tokenActorsMissing": "[{names}] missing Actors",
"domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used", "domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used",
"knowTheTide": "Know The Tide gained a token" "knowTheTide": "Know The Tide gained a token",
"lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}"
}, },
"Sidebar": { "Sidebar": {
"actorDirectory": { "actorDirectory": {
"tier": "Tier {tier} {type}", "tier": "Tier {tier} {type}",
"character": "Level {level} Character", "character": "Level {level} Character",
"companion": "Level {level} - {partner}", "companion": "Level {level} - {partner}",
"companionNoPartner": "No Partner" "companionNoPartner": "No Partner",
"duplicateToNewTier": "Duplicate to New Tier",
"pickTierTitle": "Pick a new tier for this adversary"
}, },
"daggerheartMenu": { "daggerheartMenu": {
"title": "Daggerheart Menu", "title": "Daggerheart Menu",
@ -2933,7 +3034,7 @@
"rulesOn": "Rules On", "rulesOn": "Rules On",
"rulesOff": "Rules Off", "rulesOff": "Rules Off",
"remainingUses": "Uses refresh on {type}", "remainingUses": "Uses refresh on {type}",
"rightClickExtand": "Right-Click to extand", "rightClickExtend": "Right-Click to extend",
"companionPartnerLevelBlock": "The companion needs an assigned partner to level up.", "companionPartnerLevelBlock": "The companion needs an assigned partner to level up.",
"configureAttribution": "Configure Attribution", "configureAttribution": "Configure Attribution",
"deleteItem": "Delete Item", "deleteItem": "Delete Item",

View file

@ -0,0 +1,143 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class CompendiumBrowserSettings extends HandlebarsApplicationMixin(ApplicationV2) {
constructor() {
super();
this.browserSettings = game.settings
.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings)
.toObject();
}
static DEFAULT_OPTIONS = {
tag: 'div',
classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'compendium-brower-settings'],
window: {
icon: 'fa-solid fa-book',
title: 'DAGGERHEART.APPLICATIONS.CompendiumBrowserSettings.title'
},
position: {
width: 500
},
actions: {
toggleSource: CompendiumBrowserSettings.#toggleSource,
finish: CompendiumBrowserSettings.#finish
}
};
/** @override */
static PARTS = {
packs: {
id: 'packs',
template: 'systems/daggerheart/templates/dialogs/compendiumBrowserSettingsDialog/packs.hbs'
},
footer: { template: 'systems/daggerheart/templates/dialogs/compendiumBrowserSettingsDialog/footer.hbs' }
};
static #browserPackTypes = ['Actor', 'Item'];
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
for (const element of htmlElement.querySelectorAll('.pack-checkbox'))
element.addEventListener('change', this.toggleTypedPack.bind(this));
}
/**@inheritdoc */
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
const excludedSourceData = this.browserSettings.excludedSources;
const excludedPackData = this.browserSettings.excludedPacks;
context.typePackCollections = game.packs.reduce((acc, pack) => {
const { type, label, packageType, packageName: basePackageName, id } = pack.metadata;
if (!CompendiumBrowserSettings.#browserPackTypes.includes(type)) return acc;
const isWorldPack = packageType === 'world';
const packageName = isWorldPack ? 'world' : basePackageName;
const sourceChecked =
!excludedSourceData[packageName] ||
!excludedSourceData[packageName].excludedDocumentTypes.includes(type);
const sourceLabel =
game.modules.get(packageName)?.title ??
(isWorldPack
? game.i18n.localize('DAGGERHEART.APPLICATIONS.CompendiumBrowserSettings.worldCompendiums')
: game.system.title);
if (!acc[type]) acc[type] = { label: game.i18n.localize(`DOCUMENT.${type}s`), sources: {} };
if (!acc[type].sources[packageName])
acc[type].sources[packageName] = { label: sourceLabel, checked: sourceChecked, packs: [] };
const checked = !excludedPackData[id] || !excludedPackData[id].excludedDocumentTypes.includes(type);
acc[type].sources[packageName].packs.push({
pack: id,
type,
label: id === game.system.id ? game.system.title : game.i18n.localize(label),
checked: checked
});
return acc;
}, {});
return context;
}
static #toggleSource(event, button) {
event.stopPropagation();
const { type, source } = button.dataset;
const currentlyExcluded = this.browserSettings.excludedSources[source]
? this.browserSettings.excludedSources[source].excludedDocumentTypes.includes(type)
: false;
if (!this.browserSettings.excludedSources[source])
this.browserSettings.excludedSources[source] = { excludedDocumentTypes: [] };
this.browserSettings.excludedSources[source].excludedDocumentTypes = currentlyExcluded
? this.browserSettings.excludedSources[source].excludedDocumentTypes.filter(x => x !== type)
: [...(this.browserSettings.excludedSources[source]?.excludedDocumentTypes ?? []), type];
const toggleIcon = button.querySelector('a > i');
toggleIcon.classList.toggle('fa-toggle-off');
toggleIcon.classList.toggle('fa-toggle-on');
button.closest('.source-container').querySelector('.checks-container').classList.toggle('collapsed');
}
toggleTypedPack(event) {
event.stopPropagation();
const { type, pack } = event.target.dataset;
const currentlyExcluded = this.browserSettings.excludedPacks[pack]
? this.browserSettings.excludedPacks[pack].excludedDocumentTypes.includes(type)
: false;
if (!this.browserSettings.excludedPacks[pack])
this.browserSettings.excludedPacks[pack] = { excludedDocumentTypes: [] };
this.browserSettings.excludedPacks[pack].excludedDocumentTypes = currentlyExcluded
? this.browserSettings.excludedPacks[pack].excludedDocumentTypes.filter(x => x !== type)
: [...(this.browserSettings.excludedPacks[pack]?.excludedDocumentTypes ?? []), type];
this.render();
}
static async #finish() {
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings);
await settings.updateSource(this.browserSettings);
await game.settings.set(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings,
settings.toObject()
);
this.updated = true;
this.close();
}
static async configure() {
return new Promise(resolve => {
const app = new this();
app.addEventListener('close', () => resolve(app.updated), { once: true });
app.render({ force: true });
});
}
}

View file

@ -16,3 +16,4 @@ export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs';
export { default as GroupRollDialog } from './group-roll-dialog.mjs'; export { default as GroupRollDialog } from './group-roll-dialog.mjs';
export { default as TagTeamDialog } from './tagTeamDialog.mjs'; export { default as TagTeamDialog } from './tagTeamDialog.mjs';
export { default as RiskItAllDialog } from './riskItAllDialog.mjs'; export { default as RiskItAllDialog } from './riskItAllDialog.mjs';
export { default as CompendiumBrowserSettingsDialog } from './CompendiumBrowserSettings.mjs';

View file

@ -54,7 +54,11 @@ export default class AttributionDialog extends HandlebarsApplicationMixin(Applic
const after = label.slice(matchIndex + search.length, label.length); const after = label.slice(matchIndex + search.length, label.length);
const element = document.createElement('li'); const element = document.createElement('li');
element.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`; element.innerHTML =
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
' ',
'&nbsp;'
);
if (item.hint) { if (item.hint) {
element.dataset.tooltip = game.i18n.localize(item.hint); element.dataset.tooltip = game.i18n.localize(item.hint);
} }

View file

@ -165,6 +165,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
} }
if (rest.hasOwnProperty('trait')) { if (rest.hasOwnProperty('trait')) {
this.config.roll.trait = rest.trait; this.config.roll.trait = rest.trait;
if (!this.config.source.item)
this.config.title = game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { this.config.title = game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(abilities[this.config.roll.trait]?.label) ability: game.i18n.localize(abilities[this.config.roll.trait]?.label)
}); });

View file

@ -43,7 +43,7 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
return context; return context;
} }
async handleAvoidDeath() { async handleAvoidDeath(useAutomation) {
const target = this.actor.uuid; const target = this.actor.uuid;
const config = await enrichedFateRoll({ const config = await enrichedFateRoll({
target, target,
@ -53,6 +53,7 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
}); });
if (!config.roll.fate) return; if (!config.roll.fate) return;
if (!useAutomation) return '';
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) {
@ -75,7 +76,7 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
return returnMessage; return returnMessage;
} }
async handleRiskItAll() { async handleRiskItAll(useAutomation) {
const config = await enrichedDualityRoll({ const config = await enrichedDualityRoll({
reaction: true, reaction: true,
traitValue: null, traitValue: null,
@ -90,6 +91,7 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
}); });
if (!config.roll.result) return; if (!config.roll.result) return;
if (!useAutomation) return '';
const clearAllStressAndHitpointsUpdates = [ const clearAllStressAndHitpointsUpdates = [
{ key: 'hitPoints', clear: true }, { key: 'hitPoints', clear: true },
@ -128,7 +130,9 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
return chatMessage; return chatMessage;
} }
async handleBlazeOfGlory() { async handleBlazeOfGlory(useAutomation) {
if (!useAutomation) return '';
this.actor.createEmbeddedDocuments('ActiveEffect', [ this.actor.createEmbeddedDocuments('ActiveEffect', [
{ {
name: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.blazeOfGlory.name'), name: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.blazeOfGlory.name'),
@ -160,19 +164,23 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
let result = ''; let result = '';
const deathMoveAutomation = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Automation
).deathMoveAutomation;
if (CONFIG.DH.GENERAL.deathMoves.blazeOfGlory === this.selectedMove) { if (CONFIG.DH.GENERAL.deathMoves.blazeOfGlory === this.selectedMove) {
result = await this.handleBlazeOfGlory(); result = await this.handleBlazeOfGlory(deathMoveAutomation.blazeOfGlory);
} }
if (CONFIG.DH.GENERAL.deathMoves.avoidDeath === this.selectedMove) { if (CONFIG.DH.GENERAL.deathMoves.avoidDeath === this.selectedMove) {
result = await this.handleAvoidDeath(); result = await this.handleAvoidDeath(deathMoveAutomation.avoidDeath);
} }
if (CONFIG.DH.GENERAL.deathMoves.riskItAll === this.selectedMove) { if (CONFIG.DH.GENERAL.deathMoves.riskItAll === this.selectedMove) {
result = await this.handleRiskItAll(); result = await this.handleRiskItAll(deathMoveAutomation.riskItAll);
} }
if (!result) return; if (result === undefined) return;
const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance) const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
.expandRollMessage?.desc; .expandRollMessage?.desc;
@ -192,7 +200,6 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
description: game.i18n.localize(this.selectedMove.description), description: game.i18n.localize(this.selectedMove.description),
result: result, result: result,
open: autoExpandDescription ? 'open' : '', open: autoExpandDescription ? 'open' : '',
chevron: autoExpandDescription ? 'fa-chevron-up' : 'fa-chevron-down',
showRiskItAllButton: this.showRiskItAllButton, showRiskItAllButton: this.showRiskItAllButton,
riskItAllButtonLabel: this.riskItAllButtonLabel, riskItAllButtonLabel: this.riskItAllButtonLabel,
riskItAllHope: this.riskItAllHope riskItAllHope: this.riskItAllHope

View file

@ -196,6 +196,9 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
.filter(x => x.testUserPermission(game.user, 'LIMITED')) .filter(x => x.testUserPermission(game.user, 'LIMITED'))
.filter(x => x.uuid !== this.actor.uuid); .filter(x => x.uuid !== this.actor.uuid);
const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
.expandRollMessage?.desc;
const cls = getDocumentClass('ChatMessage'); const cls = getDocumentClass('ChatMessage');
const msg = { const msg = {
user: game.user.id, user: game.user.id,
@ -216,7 +219,8 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
actor: { name: this.actor.name, img: this.actor.img }, actor: { name: this.actor.name, img: this.actor.img },
moves: moves, moves: moves,
characters: characters, characters: characters,
selfId: this.actor.uuid selfId: this.actor.uuid,
open: autoExpandDescription ? 'open' : ''
} }
), ),
flags: { flags: {

View file

@ -70,7 +70,11 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
element.appendChild(img); element.appendChild(img);
const label = document.createElement('span'); const label = document.createElement('span');
label.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`; label.innerHTML =
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
' ',
'&nbsp;'
);
element.appendChild(label); element.appendChild(label);
return element; return element;
@ -119,7 +123,11 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
element.appendChild(img); element.appendChild(img);
const label = document.createElement('span'); const label = document.createElement('span');
label.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`; label.innerHTML =
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
' ',
'&nbsp;'
);
element.appendChild(label); element.appendChild(label);
return element; return element;

View file

@ -103,7 +103,11 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
/** @override */ /** @override */
async _processSubmitData(event, form, submitData, options) { async _processSubmitData(event, form, submitData, options) {
submitData.flags.daggerheart = this.daggerheartFlag.toObject(); if (!submitData.flags) submitData.flags = {};
submitData.flags.daggerheart = foundry.utils.mergeObject(
this.daggerheartFlag.toObject(),
submitData.flags.daggerheart
);
submitData.flags.daggerheart.sceneEnvironments = submitData.flags.daggerheart.sceneEnvironments.filter(x => submitData.flags.daggerheart.sceneEnvironments = submitData.flags.daggerheart.sceneEnvironments.filter(x =>
foundry.utils.fromUuidSync(x) foundry.utils.fromUuidSync(x)
); );

View file

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

View file

@ -103,6 +103,12 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
? { id: this.selected.adversaryType, ...this.settings.adversaryTypes[this.selected.adversaryType] } ? { id: this.selected.adversaryType, ...this.settings.adversaryTypes[this.selected.adversaryType] }
: null; : null;
break; break;
case 'downtime':
context.restOptions = {
shortRest: CONFIG.DH.GENERAL.defaultRestOptions.shortRest(),
longRest: CONFIG.DH.GENERAL.defaultRestOptions.longRest()
};
break;
} }
return context; return context;
@ -165,7 +171,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newDowntimeMove'), name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newDowntimeMove'),
img: 'icons/magic/life/cross-worn-green.webp', img: 'icons/magic/life/cross-worn-green.webp',
description: '', description: '',
actions: [] actions: [],
effects: []
} }
}); });
} else if (['armorFeatures', 'weaponFeatures'].includes(type)) { } else if (['armorFeatures', 'weaponFeatures'].includes(type)) {
@ -180,6 +187,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
}); });
} }
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
this.render(); this.render();
} }
@ -220,16 +228,28 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
} }
}); });
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
this.render(); this.render();
} }
static async removeItem(_, target) { static async removeItem(_, target) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize(`DAGGERHEART.SETTINGS.Homebrew.deleteItemTitle`)
},
content: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.deleteItemText')
});
if (!confirmed) return;
const { type, id } = target.dataset; const { type, id } = target.dataset;
const isDowntime = ['shortRest', 'longRest'].includes(type); const isDowntime = ['shortRest', 'longRest'].includes(type);
const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`; const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`;
await this.settings.updateSource({ await this.settings.updateSource({
[`${path}.-=${id}`]: null [`${path}.-=${id}`]: null
}); });
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
this.render(); this.render();
} }

View file

@ -314,7 +314,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
const index = Number.parseInt(button.dataset.index); const index = Number.parseInt(button.dataset.index);
const toggle = (element, codeMirror) => { const toggle = (element, codeMirror) => {
codeMirror.classList.toggle('revealed'); codeMirror.classList.toggle('revealed');
const button = element.querySelector('a > i'); const button = element.querySelector('.expand-trigger > i');
button.classList.toggle('fa-angle-up'); button.classList.toggle('fa-angle-up');
button.classList.toggle('fa-angle-down'); button.classList.toggle('fa-angle-down');
}; };

View file

@ -4,22 +4,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
constructor(options) { constructor(options) {
super(options); super(options);
const ignoredActorKeys = ['config', 'DhEnvironment']; this.changeChoices = DhActiveEffectConfig.getChangeChoices();
this.changeChoices = Object.keys(game.system.api.models.actors).reduce((acc, key) => {
if (!ignoredActorKeys.includes(key)) {
const model = game.system.api.models.actors[key];
const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model);
// As per DHToken._getTrackedAttributesFromSchema, attributes.bar have a max version as well.
const maxAttributes = attributes.bar.map(x => [...x, 'max']);
attributes.value.push(...maxAttributes);
const group = game.i18n.localize(model.metadata.label);
const choices = CONFIG.Token.documentClass
.getTrackedAttributeChoices(attributes, model)
.map(x => ({ ...x, group: group }));
acc.push(...choices);
}
return acc;
}, []);
} }
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
@ -50,6 +35,69 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
} }
}; };
/**
* Get ChangeChoices for the changes autocomplete. Static for use in this class aswell as in settings-active-effect-config.mjs
* @returns {ChangeChoice { value: string, label: string, hint: string, group: string }[]}
*/
static getChangeChoices() {
const ignoredActorKeys = ['config', 'DhEnvironment', 'DhParty'];
const getAllLeaves = (root, group, parentPath = '') => {
const leaves = [];
const rootKey = `${parentPath ? `${parentPath}.` : ''}${root.name}`;
for (const field of Object.values(root.fields)) {
if (field instanceof foundry.data.fields.SchemaField)
leaves.push(...getAllLeaves(field, group, rootKey));
else
leaves.push({
value: `${rootKey}.${field.name}`,
label: game.i18n.localize(field.label),
hint: game.i18n.localize(field.hint),
group
});
}
return leaves;
};
return Object.keys(game.system.api.models.actors).reduce((acc, key) => {
if (ignoredActorKeys.includes(key)) return acc;
const model = game.system.api.models.actors[key];
const group = game.i18n.localize(model.metadata.label);
const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model.metadata.type);
const getTranslations = path => {
if (path === 'resources.hope.max')
return {
label: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxHope.label'),
hint: ''
};
const field = model.schema.getField(path);
return {
label: field ? game.i18n.localize(field.label) : path,
hint: field ? game.i18n.localize(field.hint) : ''
};
};
const bars = attributes.bar.flatMap(x => {
const joined = `${x.join('.')}.max`;
return { value: joined, ...getTranslations(joined), group };
});
const values = attributes.value.flatMap(x => {
const joined = x.join('.');
return { value: joined, ...getTranslations(joined), group };
});
const bonuses = getAllLeaves(model.schema.fields.bonuses, group);
const rules = getAllLeaves(model.schema.fields.rules, group);
acc.push(...bars, ...values, ...rules, ...bonuses);
return acc;
}, []);
}
_attachPartListeners(partId, htmlElement, options) { _attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options); super._attachPartListeners(partId, htmlElement, options);
const changeChoices = this.changeChoices; const changeChoices = this.changeChoices;
@ -68,14 +116,18 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
}, },
render: function (item, search) { render: function (item, search) {
const label = game.i18n.localize(item.label); const label = game.i18n.localize(item.label);
const matchIndex = label.toLowerCase().indexOf(search); const matchIndex = label.toLowerCase().indexOf(search.toLowerCase());
const beforeText = label.slice(0, matchIndex); const beforeText = label.slice(0, matchIndex);
const matchText = label.slice(matchIndex, matchIndex + search.length); const matchText = label.slice(matchIndex, matchIndex + search.length);
const after = label.slice(matchIndex + search.length, label.length); const after = label.slice(matchIndex + search.length, label.length);
const element = document.createElement('li'); const element = document.createElement('li');
element.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`; element.innerHTML =
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
' ',
'&nbsp;'
);
if (item.hint) { if (item.hint) {
element.dataset.tooltip = game.i18n.localize(item.hint); element.dataset.tooltip = game.i18n.localize(item.hint);
} }

View file

@ -7,19 +7,7 @@ export default class SettingActiveEffectConfig extends HandlebarsApplicationMixi
super({}); super({});
this.effect = foundry.utils.deepClone(effect); this.effect = foundry.utils.deepClone(effect);
const ignoredActorKeys = ['config', 'DhEnvironment']; this.changeChoices = game.system.api.applications.sheetConfigs.ActiveEffectConfig.getChangeChoices();
this.changeChoices = Object.keys(game.system.api.models.actors).reduce((acc, key) => {
if (!ignoredActorKeys.includes(key)) {
const model = game.system.api.models.actors[key];
const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model);
const group = game.i18n.localize(model.metadata.label);
const choices = CONFIG.Token.documentClass
.getTrackedAttributeChoices(attributes, model)
.map(x => ({ ...x, group: group }));
acc.push(...choices);
}
return acc;
}, []);
} }
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
@ -103,7 +91,11 @@ export default class SettingActiveEffectConfig extends HandlebarsApplicationMixi
const after = label.slice(matchIndex + search.length, label.length); const after = label.slice(matchIndex + search.length, label.length);
const element = document.createElement('li'); const element = document.createElement('li');
element.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`; element.innerHTML =
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
' ',
'&nbsp;'
);
if (item.hint) { if (item.hint) {
element.dataset.tooltip = game.i18n.localize(item.hint); element.dataset.tooltip = game.i18n.localize(item.hint);
} }

View file

@ -73,9 +73,11 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
return context; return context;
} }
static async updateData(event, element, formData) { static async updateData(_event, _element, formData) {
const data = foundry.utils.expandObject(formData.object); const data = foundry.utils.expandObject(formData.object);
foundry.utils.mergeObject(this.move, data); await this.updateMove({
[`${this.movePath}`]: data
});
this.render(); this.render();
} }
@ -135,9 +137,7 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
} }
); );
await this.settings.updateSource({ [`${this.actionsPath}.${action.id}`]: action }); await this.updateMove({ [`${this.actionsPath}.${action.id}`]: action });
this.move = foundry.utils.getProperty(this.settings, this.movePath);
this.render(); this.render();
} }
@ -150,13 +150,12 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect); await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect);
if (!updatedEffect) return; if (!updatedEffect) return;
await this.settings.updateSource({ await this.updateMove({
[`${this.movePath}.effects`]: this.move.effects.reduce((acc, effect, index) => { [`${this.movePath}.effects`]: this.move.effects.reduce((acc, effect, index) => {
acc.push(index === effectIndex ? { ...updatedEffect, id: effect.id } : effect); acc.push(index === effectIndex ? { ...updatedEffect, id: effect.id } : effect);
return acc; return acc;
}, []) }, [])
}); });
this.move = foundry.utils.getProperty(this.settings, this.movePath);
this.render(); this.render();
} else { } else {
const action = this.move.actions.get(id); const action = this.move.actions.get(id);
@ -171,13 +170,13 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
: existingEffectIndex === -1 : existingEffectIndex === -1
? [...currentEffects, effectData] ? [...currentEffects, effectData]
: currentEffects.with(existingEffectIndex, effectData); : currentEffects.with(existingEffectIndex, effectData);
await this.settings.updateSource({ await this.updateMove({
[`${this.movePath}.effects`]: updatedEffects [`${this.movePath}.effects`]: updatedEffects
}); });
} }
await this.settings.updateSource({ [`${this.actionsPath}.${id}`]: updatedMove }); await this.updateMove({ [`${this.actionsPath}.${id}`]: updatedMove });
this.move = foundry.utils.getProperty(this.settings, this.movePath);
this.render(); this.render();
return updatedEffects; return updatedEffects;
}).render(true); }).render(true);
@ -199,33 +198,36 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
}); });
} }
} }
await this.settings.updateSource({ await this.updateMove({
[this.movePath]: { [this.movePath]: {
effects: move.effects.filter(x => x.id !== id), effects: move.effects.filter(x => x.id !== id),
actions: move.actions actions: move.actions
} }
}); });
} else { } else {
await this.settings.updateSource({ [`${this.actionsPath}.-=${target.dataset.id}`]: null }); await this.updateMove({ [`${this.actionsPath}.-=${target.dataset.id}`]: null });
} }
this.move = foundry.utils.getProperty(this.settings, this.movePath);
this.render(); this.render();
} }
static async addEffect(_, target) { static async addEffect() {
const currentEffects = foundry.utils.getProperty(this.settings, `${this.movePath}.effects`); const currentEffects = foundry.utils.getProperty(this.settings, `${this.movePath}.effects`);
await this.settings.updateSource({
await this.updateMove({
[`${this.movePath}.effects`]: [ [`${this.movePath}.effects`]: [
...currentEffects, ...currentEffects,
game.system.api.data.activeEffects.BaseEffect.getDefaultObject() game.system.api.data.activeEffects.BaseEffect.getDefaultObject()
] ]
}); });
this.move = foundry.utils.getProperty(this.settings, this.movePath);
this.render(); this.render();
} }
async updateMove(update) {
await this.settings.updateSource(update);
this.move = foundry.utils.getProperty(this.settings, this.movePath);
}
static resetMoves() {} static resetMoves() {}
_filterTabs(tabs) { _filterTabs(tabs) {

View file

@ -6,7 +6,6 @@ import DaggerheartMenu from '../../sidebar/tabs/daggerheartMenu.mjs';
import { socketEvent } from '../../../systemRegistration/socket.mjs'; import { socketEvent } from '../../../systemRegistration/socket.mjs';
import GroupRollDialog from '../../dialogs/group-roll-dialog.mjs'; import GroupRollDialog from '../../dialogs/group-roll-dialog.mjs';
import DhpActor from '../../../documents/actor.mjs'; import DhpActor from '../../../documents/actor.mjs';
import DHItem from '../../../documents/item.mjs';
export default class Party extends DHBaseActorSheet { export default class Party extends DHBaseActorSheet {
constructor(options) { constructor(options) {
@ -269,15 +268,6 @@ export default class Party extends DHBaseActorSheet {
).render({ force: true }); ).render({ force: true });
} }
/**
* Get the set of ContextMenu options for Consumable and Loot.
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
* @this {CharacterSheet}
* @protected
*/
static #getItemContextOptions() {
return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true });
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Filter Tracking */ /* Filter Tracking */
/* -------------------------------------------- */ /* -------------------------------------------- */

View file

@ -431,18 +431,18 @@ export default function DHApplicationMixin(Base) {
{ {
name: 'disableEffect', name: 'disableEffect',
icon: 'fa-solid fa-lightbulb', icon: 'fa-solid fa-lightbulb',
condition: target => { condition: element => {
const doc = getDocFromElementSync(target); const target = element.closest('[data-item-uuid]');
return doc && !doc.disabled; return !target.dataset.disabled && target.dataset.itemType !== 'beastform';
}, },
callback: async target => (await getDocFromElement(target)).update({ disabled: true }) callback: async target => (await getDocFromElement(target)).update({ disabled: true })
}, },
{ {
name: 'enableEffect', name: 'enableEffect',
icon: 'fa-regular fa-lightbulb', icon: 'fa-regular fa-lightbulb',
condition: target => { condition: element => {
const doc = getDocFromElementSync(target); const target = element.closest('[data-item-uuid]');
return doc && doc.disabled; return target.dataset.disabled && target.dataset.itemType !== 'beastform';
}, },
callback: async target => (await getDocFromElement(target)).update({ disabled: false }) callback: async target => (await getDocFromElement(target)).update({ disabled: false })
} }
@ -536,6 +536,10 @@ export default function DHApplicationMixin(Base) {
options.push({ options.push({
name: 'CONTROLS.CommonDelete', name: 'CONTROLS.CommonDelete',
icon: 'fa-solid fa-trash', icon: 'fa-solid fa-trash',
condition: element => {
const target = element.closest('[data-item-uuid]');
return target.dataset.itemType !== 'beastform';
},
callback: async (target, event) => { callback: async (target, event) => {
const doc = await getDocFromElement(target); const doc = await getDocFromElement(target);
if (event.shiftKey) return doc.delete(); if (event.shiftKey) return doc.delete();

View file

@ -36,7 +36,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
], ],
dragDrop: [ dragDrop: [
{ dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null }, { dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null },
{ dragSelector: ".currency[data-currency] .drag-handle", dropSelector: null } { dragSelector: '.currency[data-currency] .drag-handle', dropSelector: null }
] ]
}; };
@ -92,7 +92,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
value: context.source.system.gold[key] value: context.source.system.gold[key]
}; };
} }
context.inventory.hasCurrency = Object.values(context.inventory.currencies).some((c) => c.enabled); context.inventory.hasCurrency = Object.values(context.inventory.currencies).some(c => c.enabled);
} }
return context; return context;
@ -270,7 +270,9 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
currency currency
}); });
if (quantity) { if (quantity) {
originActor.update({ [`system.gold.${currency}`]: Math.max(0, originActor.system.gold[currency] - quantity) }); originActor.update({
[`system.gold.${currency}`]: Math.max(0, originActor.system.gold[currency] - quantity)
});
this.document.update({ [`system.gold.${currency}`]: this.document.system.gold[currency] + quantity }); this.document.update({ [`system.gold.${currency}`]: this.document.system.gold[currency] + quantity });
} }
return; return;
@ -292,6 +294,15 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
/* Handling transfer of inventoryItems */ /* Handling transfer of inventoryItems */
if (item.system.metadata.isInventoryItem) { if (item.system.metadata.isInventoryItem) {
if (!this.document.testUserPermission(game.user, 'OWNER', { exact: true })) {
return ui.notifications.error(
game.i18n.format('DAGGERHEART.UI.Notifications.lackingItemTransferPermission', {
user: game.user.name,
target: this.document.name
})
);
}
if (item.system.metadata.isQuantifiable) { if (item.system.metadata.isQuantifiable) {
const actorItem = originActor.items.get(data.originId); const actorItem = originActor.items.get(data.originId);
const quantityTransfered = await game.system.api.applications.dialogs.ItemTransferDialog.configure({ const quantityTransfered = await game.system.api.applications.dialogs.ItemTransferDialog.configure({
@ -300,14 +311,6 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
}); });
if (quantityTransfered) { if (quantityTransfered) {
if (quantityTransfered === actorItem.system.quantity) {
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
} else {
await actorItem.update({
'system.quantity': actorItem.system.quantity - quantityTransfered
});
}
const existingItem = this.document.items.find(x => itemIsIdentical(x, item)); const existingItem = this.document.items.find(x => itemIsIdentical(x, item));
if (existingItem) { if (existingItem) {
await existingItem.update({ await existingItem.update({
@ -325,10 +328,18 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
} }
]); ]);
} }
if (quantityTransfered === actorItem.system.quantity) {
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
} else {
await actorItem.update({
'system.quantity': actorItem.system.quantity - quantityTransfered
});
}
} }
} else { } else {
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
await this.document.createEmbeddedDocuments('Item', [item.toObject()]); await this.document.createEmbeddedDocuments('Item', [item.toObject()]);
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
} }
} }
} }
@ -339,7 +350,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
*/ */
async _onDragStart(event) { async _onDragStart(event) {
// Handle drag/dropping currencies // Handle drag/dropping currencies
const currencyEl = event.currentTarget.closest(".currency[data-currency]"); const currencyEl = event.currentTarget.closest('.currency[data-currency]');
if (currencyEl) { if (currencyEl) {
const currency = currencyEl.dataset.currency; const currency = currencyEl.dataset.currency;
const data = { type: 'Currency', currency, originActor: this.document.uuid }; const data = { type: 'Currency', currency, originActor: this.document.uuid };

View file

@ -1,7 +1,6 @@
export default function ItemAttachmentSheet(Base) { export default function ItemAttachmentSheet(Base) {
return class extends Base { return class extends Base {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
...super.DEFAULT_OPTIONS,
dragDrop: [ dragDrop: [
...(super.DEFAULT_OPTIONS.dragDrop || []), ...(super.DEFAULT_OPTIONS.dragDrop || []),
{ dragSelector: null, dropSelector: '.attachments-section' } { dragSelector: null, dropSelector: '.attachments-section' }

View file

@ -43,4 +43,54 @@ export default class DhActorDirectory extends foundry.applications.sidebar.tabs.
event.dataTransfer.setDragImage(preview, w / 2, h / 2); event.dataTransfer.setDragImage(preview, w / 2, h / 2);
} }
} }
_getEntryContextOptions() {
const options = super._getEntryContextOptions();
options.push({
name: 'DAGGERHEART.UI.Sidebar.actorDirectory.duplicateToNewTier',
icon: `<i class="fa-solid fa-arrow-trend-up" inert></i>`,
condition: li => {
const actor = game.actors.get(li.dataset.entryId);
return actor?.type === 'adversary' && actor.system.type !== 'social';
},
callback: async li => {
const actor = game.actors.get(li.dataset.entryId);
if (!actor) throw new Error('Unexpected missing actor');
const tiers = [1, 2, 3, 4].filter(t => t !== actor.system.tier);
const content = document.createElement('div');
const select = document.createElement('select');
select.name = 'tier';
select.append(
...tiers.map(t => {
const option = document.createElement('option');
option.value = t;
option.textContent = game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${t}`);
return option;
})
);
content.append(select);
const tier = await foundry.applications.api.Dialog.input({
classes: ['dh-style', 'dialog'],
window: { title: 'DAGGERHEART.UI.Sidebar.actorDirectory.pickTierTitle' },
content,
ok: {
label: 'Create Adversary',
callback: (event, button, dialog) => Number(button.form.elements.tier.value)
}
});
if (tier === actor.system.tier) {
ui.notifications.warn('This actor is already at this tier');
} else if (tier) {
const source = actor.system.adjustForTier(tier);
await Actor.create(source);
ui.notifications.info(`Tier ${tier} ${actor.name} created`);
}
}
});
return options;
}
} }

View file

@ -1,4 +1,4 @@
import { refreshIsAllowed } from '../../../helpers/utils.mjs'; import { RefreshFeatures } from '../../../helpers/utils.mjs';
const { HandlebarsApplicationMixin } = foundry.applications.api; const { HandlebarsApplicationMixin } = foundry.applications.api;
const { AbstractSidebarTab } = foundry.applications.sidebar; const { AbstractSidebarTab } = foundry.applications.sidebar;
@ -54,73 +54,6 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
return context; return context;
} }
async getRefreshables(types) {
const refreshedActors = {};
for (let actor of game.actors) {
if (['character', 'adversary'].includes(actor.type) && actor.prototypeToken.actorLink) {
const updates = {};
for (let item of actor.items) {
if (item.system.metadata?.hasResource && refreshIsAllowed(types, item.system.resource?.recovery)) {
if (!refreshedActors[actor.id])
refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() };
refreshedActors[actor.id].refreshed.add(
game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[item.system.resource.recovery].label)
);
if (!updates[item.id]?.system) updates[item.id] = { system: {} };
const increasing =
item.system.resource.progression === CONFIG.DH.ITEM.itemResourceProgression.increasing.id;
updates[item.id].system = {
...updates[item.id].system,
'resource.value': increasing
? 0
: Roll.replaceFormulaData(item.system.resource.max, actor.getRollData())
};
}
if (item.system.metadata?.hasActions) {
const refreshTypes = new Set();
const actions = item.system.actions.filter(action => {
if (refreshIsAllowed(types, action.uses.recovery)) {
refreshTypes.add(action.uses.recovery);
return true;
}
return false;
});
if (actions.length === 0) continue;
if (!refreshedActors[actor.id])
refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() };
refreshedActors[actor.id].refreshed.add(
...refreshTypes.map(type => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[type].label))
);
if (!updates[item.id]?.system) updates[item.id] = { system: {} };
updates[item.id].system = {
...updates[item.id].system,
...actions.reduce(
(acc, action) => {
acc.actions[action.id] = { 'uses.value': 0 };
return acc;
},
{ actions: updates[item.id].system.actions ?? {} }
)
};
}
}
for (let key in updates) {
const update = updates[key];
await actor.items.get(key).update(update);
}
}
}
return refreshedActors;
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Application Clicks Actions */ /* Application Clicks Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -133,30 +66,9 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
static async #refreshActors() { static async #refreshActors() {
const refreshKeys = Object.keys(this.refreshSelections).filter(key => this.refreshSelections[key].selected); const refreshKeys = Object.keys(this.refreshSelections).filter(key => this.refreshSelections[key].selected);
await this.getRefreshables(refreshKeys); await RefreshFeatures(refreshKeys);
const types = refreshKeys.map(x => this.refreshSelections[x].label).join(', ');
ui.notifications.info(
game.i18n.format('DAGGERHEART.UI.Notifications.gmMenuRefresh', {
types: `[${types}]`
})
);
this.refreshSelections = DaggerheartMenu.defaultRefreshSelections(); this.refreshSelections = DaggerheartMenu.defaultRefreshSelections();
const cls = getDocumentClass('ChatMessage');
const msg = {
user: game.user.id,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/refreshMessage.hbs',
{
types: types
}
),
title: game.i18n.localize('DAGGERHEART.UI.Chat.refreshMessage.title'),
speaker: cls.getSpeaker()
};
cls.create(msg);
this.render(); this.render();
} }
} }

View file

@ -34,8 +34,6 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
position: { position: {
width: 222, width: 222,
height: 222 height: 222
// top: "200px",
// left: "120px"
} }
}; };
@ -66,7 +64,7 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
max = this.maxFear, max = this.maxFear,
percent = (current / max) * 100, percent = (current / max) * 100,
isGM = game.user.isGM; isGM = game.user.isGM;
// Return the data for rendering
return { display, current, max, percent, isGM }; return { display, current, max, percent, isGM };
} }

View file

@ -1,3 +1,5 @@
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
/** /**
@ -17,6 +19,15 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
this.config = CONFIG.DH.ITEMBROWSER.compendiumConfig; this.config = CONFIG.DH.ITEMBROWSER.compendiumConfig;
this.presets = {}; this.presets = {};
this.compendiumBrowserTypeKey = 'compendiumBrowserDefault'; this.compendiumBrowserTypeKey = 'compendiumBrowserDefault';
this.setupHooks = Hooks.on(socketEvent.Refresh, ({ refreshType }) => {
if (refreshType === RefreshType.CompendiumBrowser) {
if (this.rendered) {
this.render();
this.loadItems();
}
}
});
} }
/** @inheritDoc */ /** @inheritDoc */
@ -35,7 +46,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
selectFolder: this.selectFolder, selectFolder: this.selectFolder,
expandContent: this.expandContent, expandContent: this.expandContent,
resetFilters: this.resetFilters, resetFilters: this.resetFilters,
sortList: this.sortList sortList: this.sortList,
openSettings: this.openSettings
}, },
position: { position: {
left: 100, left: 100,
@ -157,6 +169,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
context.formatChoices = this.formatChoices; context.formatChoices = this.formatChoices;
context.items = this.items; context.items = this.items;
context.presets = this.presets; context.presets = this.presets;
context.isGM = game.user.isGM;
return context; return context;
} }
@ -214,6 +228,10 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
loadItems() { loadItems() {
let loadTimeout = this.toggleLoader(true); let loadTimeout = this.toggleLoader(true);
const browserSettings = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings
);
const promises = []; const promises = [];
game.packs.forEach(pack => { game.packs.forEach(pack => {
@ -227,7 +245,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
Promise.all(promises).then(async result => { Promise.all(promises).then(async result => {
this.items = ItemBrowser.sortBy( this.items = ItemBrowser.sortBy(
result.flatMap(r => r), result.flatMap(r => r).filter(r => !browserSettings.isEntryExcluded.bind(browserSettings)(r)),
'name' 'name'
); );
@ -512,6 +530,22 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
itemListContainer.replaceChildren(...newOrder); itemListContainer.replaceChildren(...newOrder);
} }
static async openSettings() {
const settingsUpdated = await game.system.api.applications.dialogs.CompendiumBrowserSettingsDialog.configure();
if (settingsUpdated) {
if (this.rendered) {
this.render();
this.loadItems();
}
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.CompendiumBrowser
}
});
}
}
_createDragProcess() { _createDragProcess() {
new foundry.applications.ux.DragDrop.implementation({ new foundry.applications.ux.DragDrop.implementation({
dragSelector: '.item-container', dragSelector: '.item-container',
@ -571,4 +605,9 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
headerActions.append(button); headerActions.append(button);
} }
} }
async close(options = {}) {
Hooks.off(socketEvent.Refresh, this.setupHooks);
await super.close(options);
}
} }

View file

@ -63,7 +63,8 @@ export default class DhSceneNavigation extends foundry.applications.ui.SceneNavi
if (scene.flags.daggerheart.sceneEnvironments[0] !== environment.uuid) { if (scene.flags.daggerheart.sceneEnvironments[0] !== environment.uuid) {
const newEnvironments = scene.flags.daggerheart.sceneEnvironments; const newEnvironments = scene.flags.daggerheart.sceneEnvironments;
const newFirst = newEnvironments.splice( const newFirst = newEnvironments.splice(
newEnvironments.findIndex(x => x === environment.uuid) newEnvironments.findIndex(x => x === environment.uuid),
1
)[0]; )[0];
newEnvironments.unshift(newFirst); newEnvironments.unshift(newFirst);
emitAsGM( emitAsGM(

View file

@ -18,8 +18,9 @@ export default class DhMeasuredTemplate extends foundry.canvas.placeables.Measur
static getRangeLabels(distanceValue, settings) { static getRangeLabels(distanceValue, settings) {
let result = { distance: distanceValue, units: '' }; let result = { distance: distanceValue, units: '' };
const sceneRangeMeasurement = canvas.scene.flags.daggerheart?.rangeMeasurement; if (!settings.enabled) return result;
const sceneRangeMeasurement = canvas.scene.flags.daggerheart?.rangeMeasurement;
const { disable, custom } = CONFIG.DH.GENERAL.sceneRangeMeasurementSetting; const { disable, custom } = CONFIG.DH.GENERAL.sceneRangeMeasurementSetting;
if (sceneRangeMeasurement?.setting === disable.id) { if (sceneRangeMeasurement?.setting === disable.id) {
result.distance = distanceValue; result.distance = distanceValue;
@ -27,31 +28,9 @@ export default class DhMeasuredTemplate extends foundry.canvas.placeables.Measur
return result; return result;
} }
const melee = sceneRangeMeasurement?.setting === custom.id ? sceneRangeMeasurement.melee : settings.melee; const ranges = sceneRangeMeasurement?.setting === custom.id ? sceneRangeMeasurement : settings;
const veryClose = const distanceKey = ['melee', 'veryClose', 'close', 'far'].find(r => ranges[r] >= distanceValue);
sceneRangeMeasurement?.setting === custom.id ? sceneRangeMeasurement.veryClose : settings.veryClose; result.distance = game.i18n.localize(`DAGGERHEART.CONFIG.Range.${distanceKey ?? 'veryFar'}.name`);
const close = sceneRangeMeasurement?.setting === custom.id ? sceneRangeMeasurement.close : settings.close;
const far = sceneRangeMeasurement?.setting === custom.id ? sceneRangeMeasurement.far : settings.far;
if (distanceValue <= melee) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.melee.name');
return result;
}
if (distanceValue <= veryClose) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.veryClose.name');
return result;
}
if (distanceValue <= close) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.close.name');
return result;
}
if (distanceValue <= far) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.far.name');
return result;
}
if (distanceValue > far) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.veryFar.name');
}
return result; return result;
} }
} }

View file

@ -1,3 +1,5 @@
import DhMeasuredTemplate from './measuredTemplate.mjs';
export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
/** @inheritdoc */ /** @inheritdoc */
async _draw(options) { async _draw(options) {
@ -52,30 +54,111 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
if (this === target) return 0; if (this === target) return 0;
const originPoint = this.center; const originPoint = this.center;
const destinationPoint = target.center; const targetPoint = target.center;
const thisBounds = this.bounds;
const targetBounds = target.bounds;
const adjacencyBuffer = canvas.grid.distance * 1.75; // handles diagonals with one square elevation difference
// Figure out the elevation difference.
// This intends to return "grid distance" for adjacent ones, so we add that number if not overlapping.
const sizePerUnit = canvas.grid.size / canvas.grid.distance;
const thisHeight = Math.max(thisBounds.width, thisBounds.height) / sizePerUnit;
const targetHeight = Math.max(targetBounds.width, targetBounds.height) / sizePerUnit;
const thisElevation = [this.document.elevation, this.document.elevation + thisHeight];
const targetElevation = [target.document.elevation, target.document.elevation + targetHeight];
const isSameAltitude =
thisElevation[0] < targetElevation[1] && // bottom of this must be at or below the top of target
thisElevation[1] > targetElevation[0]; // top of this must be at or above the bottom of target
const [lower, higher] = [targetElevation, thisElevation].sort((a, b) => a[1] - b[1]);
const elevation = isSameAltitude ? 0 : higher[0] - lower[1] + canvas.grid.distance;
// Compute for gridless. This version returns circular edge to edge + grid distance, // Compute for gridless. This version returns circular edge to edge + grid distance,
// so that tokens that are touching return 5. // so that tokens that are touching return 5.
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) { if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
const boundsCorrection = canvas.grid.distance / canvas.grid.size; const boundsCorrection = canvas.grid.distance / canvas.grid.size;
const originRadius = (this.bounds.width * boundsCorrection) / 2; const originRadius = (thisBounds.width * boundsCorrection) / 2;
const targetRadius = (target.bounds.width * boundsCorrection) / 2; const targetRadius = (targetBounds.width * boundsCorrection) / 2;
const distance = canvas.grid.measurePath([originPoint, destinationPoint]).distance; const measuredDistance = canvas.grid.measurePath([
return distance - originRadius - targetRadius + canvas.grid.distance; { ...originPoint, elevation: 0 },
{ ...targetPoint, elevation }
]).distance;
const distance = Math.floor(measuredDistance - originRadius - targetRadius + canvas.grid.distance);
return Math.min(distance, distance > adjacencyBuffer ? Infinity : canvas.grid.distance);
} }
// Compute what the closest grid space of each token is, then compute that distance // Compute what the closest grid space of each token is, then compute that distance
const originEdge = this.#getEdgeBoundary(this.bounds, originPoint, destinationPoint); const originEdge = this.#getEdgeBoundary(thisBounds, originPoint, targetPoint);
const targetEdge = this.#getEdgeBoundary(target.bounds, originPoint, destinationPoint); const targetEdge = this.#getEdgeBoundary(targetBounds, originPoint, targetPoint);
const adjustedOriginPoint = canvas.grid.getTopLeftPoint({ const adjustedOriginPoint = originEdge
? 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)
}); })
const adjustDestinationPoint = canvas.grid.getTopLeftPoint({ : originPoint;
x: targetEdge.x + Math.sign(destinationPoint.x - targetEdge.x), const adjustDestinationPoint = targetEdge
y: targetEdge.y + Math.sign(destinationPoint.y - targetEdge.y) ? canvas.grid.getTopLeftPoint({
}); x: targetEdge.x + Math.sign(targetPoint.x - targetEdge.x),
return canvas.grid.measurePath([adjustedOriginPoint, adjustDestinationPoint]).distance; y: targetEdge.y + Math.sign(targetPoint.y - targetEdge.y)
})
: targetPoint;
const distance = canvas.grid.measurePath([
{ ...adjustedOriginPoint, elevation: 0 },
{ ...adjustDestinationPoint, elevation }
]).distance;
return Math.min(distance, distance > adjacencyBuffer ? Infinity : canvas.grid.distance);
}
_onHoverIn(event, options) {
super._onHoverIn(event, options);
// Check if the setting is enabled
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance).showTokenDistance;
if (setting === 'never' || (setting === 'encounters' && !game.combat?.started)) return;
// Check if this token isn't invisible and is actually being hovered
const isTokenValid =
this.visible &&
this.hover &&
!this.isPreview &&
!this.document.isSecret &&
!this.controlled &&
!this.animation;
if (!isTokenValid) return;
// Ensure we have a single controlled token
const originToken = canvas.tokens.controlled[0];
if (!originToken || canvas.tokens.controlled.length > 1) return;
// Determine the actual range
const ranges = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement;
const distanceResult = DhMeasuredTemplate.getRangeLabels(originToken.distanceTo(this), ranges);
const distanceLabel = `${distanceResult.distance} ${distanceResult.units}`.trim();
// Create the element
const element = document.createElement('div');
element.id = 'token-hover-distance';
element.classList.add('waypoint-label', 'last');
const ruler = document.createElement('i');
ruler.classList.add('fa-solid', 'fa-ruler');
element.appendChild(ruler);
const labelEl = document.createElement('span');
labelEl.classList.add('total-measurement');
labelEl.textContent = distanceLabel;
element.appendChild(labelEl);
// Position the element and add to the DOM
const center = this.getCenterPoint();
element.style.setProperty('--transformY', 'calc(-100% - 10px)');
element.style.setProperty('--position-y', `${this.y}px`);
element.style.setProperty('--position-x', `${center.x}px`);
element.style.setProperty('--ui-scale', String(canvas.dimensions.uiScale));
document.querySelector('#token-hover-distance')?.remove();
document.querySelector('#measurement').appendChild(element);
}
_onHoverOut(...args) {
super._onHoverOut(...args);
document.querySelector('#token-hover-distance')?.remove();
} }
/** Returns the point at which a line starting at origin and ending at destination intersects the edge of the bounds */ /** Returns the point at which a line starting at origin and ending at destination intersects the edge of the bounds */
@ -100,11 +183,6 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
return null; return null;
} }
/** Tests if the token is at least adjacent with another, with some leeway for diagonals */
isAdjacentWith(token) {
return this.distanceTo(token) <= canvas.grid.distance * 1.5;
}
/** @inheritDoc */ /** @inheritDoc */
_drawBar(number, bar, data) { _drawBar(number, bar, data) {
const val = Number(data.value); const val = Number(data.value);

View file

@ -494,3 +494,275 @@ export const subclassFeatureLabels = {
2: 'DAGGERHEART.ITEMS.DomainCard.specializationTitle', 2: 'DAGGERHEART.ITEMS.DomainCard.specializationTitle',
3: 'DAGGERHEART.ITEMS.DomainCard.masteryTitle' 3: 'DAGGERHEART.ITEMS.DomainCard.masteryTitle'
}; };
/**
* @typedef {Object} TierData
* @property {number} difficulty
* @property {number} majorThreshold
* @property {number} severeThreshold
* @property {number} hp
* @property {number} stress
* @property {number} attack
* @property {number[]} damage
*/
/**
* @type {Record<string, Record<2 | 3 | 4, TierData>}
* Scaling data used to change an adversary's tier. Each rank is applied incrementally.
*/
export const adversaryScalingData = {
bruiser: {
2: {
difficulty: 2,
majorThreshold: 5,
severeThreshold: 10,
hp: 1,
stress: 2,
attack: 2,
},
3: {
difficulty: 2,
majorThreshold: 7,
severeThreshold: 15,
hp: 1,
stress: 0,
attack: 2,
},
4: {
difficulty: 2,
majorThreshold: 12,
severeThreshold: 25,
hp: 1,
stress: 0,
attack: 2,
}
},
horde: {
2: {
difficulty: 2,
majorThreshold: 5,
severeThreshold: 8,
hp: 2,
stress: 0,
attack: 0,
},
3: {
difficulty: 2,
majorThreshold: 5,
severeThreshold: 12,
hp: 0,
stress: 1,
attack: 1,
},
4: {
difficulty: 2,
majorThreshold: 10,
severeThreshold: 15,
hp: 2,
stress: 0,
attack: 0,
}
},
leader: {
2: {
difficulty: 2,
majorThreshold: 6,
severeThreshold: 10,
hp: 0,
stress: 0,
attack: 1,
},
3: {
difficulty: 2,
majorThreshold: 6,
severeThreshold: 15,
hp: 1,
stress: 0,
attack: 2,
},
4: {
difficulty: 2,
majorThreshold: 12,
severeThreshold: 25,
hp: 1,
stress: 1,
attack: 3,
}
},
minion: {
2: {
difficulty: 2,
majorThreshold: 0,
severeThreshold: 0,
hp: 0,
stress: 0,
attack: 1,
},
3: {
difficulty: 2,
majorThreshold: 0,
severeThreshold: 0,
hp: 0,
stress: 1,
attack: 1,
},
4: {
difficulty: 2,
majorThreshold: 0,
severeThreshold: 0,
hp: 0,
stress: 0,
attack: 1,
}
},
ranged: {
2: {
difficulty: 2,
majorThreshold: 3,
severeThreshold: 6,
hp: 1,
stress: 0,
attack: 1,
},
3: {
difficulty: 2,
majorThreshold: 7,
severeThreshold: 14,
hp: 1,
stress: 1,
attack: 2,
},
4: {
difficulty: 2,
majorThreshold: 5,
severeThreshold: 10,
hp: 1,
stress: 1,
attack: 1,
}
},
skulk: {
2: {
difficulty: 2,
majorThreshold: 3,
severeThreshold: 8,
hp: 1,
stress: 1,
attack: 1,
},
3: {
difficulty: 2,
majorThreshold: 8,
severeThreshold: 12,
hp: 1,
stress: 1,
attack: 1,
},
4: {
difficulty: 2,
majorThreshold: 8,
severeThreshold: 10,
hp: 1,
stress: 1,
attack: 1,
}
},
solo: {
2: {
difficulty: 2,
majorThreshold: 5,
severeThreshold: 10,
hp: 0,
stress: 1,
attack: 2,
},
3: {
difficulty: 2,
majorThreshold: 7,
severeThreshold: 15,
hp: 2,
stress: 1,
attack: 2,
},
4: {
difficulty: 2,
majorThreshold: 12,
severeThreshold: 25,
hp: 0,
stress: 1,
attack: 3,
}
},
standard: {
2: {
difficulty: 2,
majorThreshold: 3,
severeThreshold: 8,
hp: 0,
stress: 0,
attack: 1,
},
3: {
difficulty: 2,
majorThreshold: 7,
severeThreshold: 15,
hp: 1,
stress: 1,
attack: 1,
},
4: {
difficulty: 2,
majorThreshold: 10,
severeThreshold: 15,
hp: 0,
stress: 1,
attack: 1,
}
},
support: {
2: {
difficulty: 2,
majorThreshold: 3,
severeThreshold: 8,
hp: 1,
stress: 1,
attack: 1,
},
3: {
difficulty: 2,
majorThreshold: 7,
severeThreshold: 12,
hp: 0,
stress: 0,
attack: 1,
},
4: {
difficulty: 2,
majorThreshold: 8,
severeThreshold: 10,
hp: 1,
stress: 1,
attack: 1,
}
}
};
/**
* Scaling data used for an adversary's damage.
* Tier 4 is missing certain adversary types and therefore skews upwards.
* We manually set tier 4 data to hopefully lead to better results
*/
export const adversaryExpectedDamage = {
basic: {
1: { mean: 7.321428571428571, deviation: 1.962519002770912 },
2: { mean: 12.444444444444445, deviation: 2.0631069425529676 },
3: { mean: 15.722222222222221, deviation: 2.486565208464823 },
4: { mean: 26, deviation: 5.2 }
},
minion: {
1: { mean: 2.142857142857143, deviation: 1.0690449676496976 },
2: { mean: 5, deviation: 0.816496580927726 },
3: { mean: 6.5, deviation: 2.1213203435596424 },
4: { mean: 11, deviation: 1 }
}
};

View file

@ -202,7 +202,8 @@ export const conditions = () => ({
id: 'vulnerable', id: 'vulnerable',
name: 'DAGGERHEART.CONFIG.Condition.vulnerable.name', name: 'DAGGERHEART.CONFIG.Condition.vulnerable.name',
img: 'icons/magic/control/silhouette-fall-slip-prone.webp', img: 'icons/magic/control/silhouette-fall-slip-prone.webp',
description: 'DAGGERHEART.CONFIG.Condition.vulnerable.description' description: 'DAGGERHEART.CONFIG.Condition.vulnerable.description',
autoApplyFlagId: 'auto-vulnerable'
}, },
hidden: { hidden: {
id: 'hidden', id: 'hidden',
@ -236,6 +237,7 @@ export const defaultRestOptions = {
actionType: 'action', actionType: 'action',
chatDisplay: false, chatDisplay: false,
target: { target: {
amount: 1,
type: 'friendly' type: 'friendly'
}, },
damage: { damage: {
@ -252,7 +254,8 @@ export const defaultRestOptions = {
] ]
} }
} }
} },
effects: []
}, },
clearStress: { clearStress: {
id: 'clearStress', id: 'clearStress',
@ -285,7 +288,8 @@ export const defaultRestOptions = {
] ]
} }
} }
} },
effects: []
}, },
repairArmor: { repairArmor: {
id: 'repairArmor', id: 'repairArmor',
@ -302,6 +306,7 @@ export const defaultRestOptions = {
actionType: 'action', actionType: 'action',
chatDisplay: false, chatDisplay: false,
target: { target: {
amount: 1,
type: 'friendly' type: 'friendly'
}, },
damage: { damage: {
@ -318,7 +323,8 @@ export const defaultRestOptions = {
] ]
} }
} }
} },
effects: []
}, },
prepare: { prepare: {
id: 'prepare', id: 'prepare',
@ -326,7 +332,57 @@ export const defaultRestOptions = {
icon: 'fa-solid fa-dumbbell', icon: 'fa-solid fa-dumbbell',
img: 'icons/skills/trades/academics-merchant-scribe.webp', img: 'icons/skills/trades/academics-merchant-scribe.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepare.description'), description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepare.description'),
actions: {} actions: {
prepare: {
type: 'healing',
systemPath: 'restMoves.shortRest.moves.prepare.actions',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepare.name'),
img: 'icons/skills/trades/academics-merchant-scribe.webp',
actionType: 'action',
chatDisplay: false,
target: {
type: 'self'
},
damage: {
parts: [
{
applyTo: healingTypes.hope.id,
value: {
custom: {
enabled: true,
formula: '1'
}
}
}
]
}
},
prepareWithFriends: {
type: 'healing',
systemPath: 'restMoves.shortRest.moves.prepare.actions',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepareWithFriends.name'),
img: 'icons/skills/trades/academics-merchant-scribe.webp',
actionType: 'action',
chatDisplay: false,
target: {
type: 'self'
},
damage: {
parts: [
{
applyTo: healingTypes.hope.id,
value: {
custom: {
enabled: true,
formula: '2'
}
}
}
]
}
}
},
effects: []
} }
}), }),
longRest: () => ({ longRest: () => ({
@ -345,6 +401,7 @@ export const defaultRestOptions = {
actionType: 'action', actionType: 'action',
chatDisplay: false, chatDisplay: false,
target: { target: {
amount: 1,
type: 'friendly' type: 'friendly'
}, },
damage: { damage: {
@ -361,7 +418,8 @@ export const defaultRestOptions = {
] ]
} }
} }
} },
effects: []
}, },
clearStress: { clearStress: {
id: 'clearStress', id: 'clearStress',
@ -394,7 +452,8 @@ export const defaultRestOptions = {
] ]
} }
} }
} },
effects: []
}, },
repairArmor: { repairArmor: {
id: 'repairArmor', id: 'repairArmor',
@ -411,6 +470,7 @@ export const defaultRestOptions = {
actionType: 'action', actionType: 'action',
chatDisplay: false, chatDisplay: false,
target: { target: {
amount: 1,
type: 'friendly' type: 'friendly'
}, },
damage: { damage: {
@ -427,7 +487,8 @@ export const defaultRestOptions = {
] ]
} }
} }
} },
effects: []
}, },
prepare: { prepare: {
id: 'prepare', id: 'prepare',
@ -435,7 +496,57 @@ export const defaultRestOptions = {
icon: 'fa-solid fa-dumbbell', icon: 'fa-solid fa-dumbbell',
img: 'icons/skills/trades/academics-merchant-scribe.webp', img: 'icons/skills/trades/academics-merchant-scribe.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepare.description'), description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepare.description'),
actions: {} actions: {
prepare: {
type: 'healing',
systemPath: 'restMoves.longRest.moves.prepare.actions',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepare.name'),
img: 'icons/skills/trades/academics-merchant-scribe.webp',
actionType: 'action',
chatDisplay: false,
target: {
type: 'self'
},
damage: {
parts: [
{
applyTo: healingTypes.hope.id,
value: {
custom: {
enabled: true,
formula: '1'
}
}
}
]
}
},
prepareWithFriends: {
type: 'healing',
systemPath: 'restMoves.longRest.moves.prepare.actions',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepareWithFriends.name'),
img: 'icons/skills/trades/academics-merchant-scribe.webp',
actionType: 'action',
chatDisplay: false,
target: {
type: 'self'
},
damage: {
parts: [
{
applyTo: healingTypes.hope.id,
value: {
custom: {
enabled: true,
formula: '2'
}
}
}
]
}
}
},
effects: []
}, },
workOnAProject: { workOnAProject: {
id: 'workOnAProject', id: 'workOnAProject',
@ -443,7 +554,8 @@ export const defaultRestOptions = {
icon: 'fa-solid fa-diagram-project', icon: 'fa-solid fa-diagram-project',
img: 'icons/skills/social/thumbsup-approval-like.webp', img: 'icons/skills/social/thumbsup-approval-like.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.workOnAProject.description'), description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.workOnAProject.description'),
actions: {} actions: {},
effects: []
} }
}) })
}; };

View file

@ -467,9 +467,7 @@ export const allArmorFeatures = () => {
}; };
export const orderedArmorFeatures = () => { export const orderedArmorFeatures = () => {
const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures const allFeatures = allArmorFeatures();
.armorFeatures;
const allFeatures = { ...armorFeatures, ...homebrewFeatures };
const all = Object.keys(allFeatures).map(key => { const all = Object.keys(allFeatures).map(key => {
const feature = allFeatures[key]; const feature = allFeatures[key];
return { return {
@ -1404,9 +1402,7 @@ export const allWeaponFeatures = () => {
}; };
export const orderedWeaponFeatures = () => { export const orderedWeaponFeatures = () => {
const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures const allFeatures = allWeaponFeatures();
.weaponFeatures;
const allFeatures = { ...weaponFeatures, ...homebrewFeatures };
const all = Object.keys(allFeatures).map(key => { const all = Object.keys(allFeatures).map(key => {
const feature = allFeatures[key]; const feature = allFeatures[key];
return { return {

View file

@ -30,6 +30,7 @@ export const gameSettings = {
LastMigrationVersion: 'LastMigrationVersion', LastMigrationVersion: 'LastMigrationVersion',
TagTeamRoll: 'TagTeamRoll', TagTeamRoll: 'TagTeamRoll',
SpotlightRequestQueue: 'SpotlightRequestQueue', SpotlightRequestQueue: 'SpotlightRequestQueue',
CompendiumBrowserSettings: 'CompendiumBrowserSettings'
}; };
export const actionAutomationChoices = { export const actionAutomationChoices = {

View file

@ -3,6 +3,7 @@ export { default as DhCombatant } from './combatant.mjs';
export { default as DhTagTeamRoll } from './tagTeamRoll.mjs'; export { default as DhTagTeamRoll } from './tagTeamRoll.mjs';
export { default as DhRollTable } from './rollTable.mjs'; export { default as DhRollTable } from './rollTable.mjs';
export { default as RegisteredTriggers } from './registeredTriggers.mjs'; export { default as RegisteredTriggers } from './registeredTriggers.mjs';
export { default as CompendiumBrowserSettings } from './compendiumBrowserSettings.mjs';
export * as countdowns from './countdowns.mjs'; export * as countdowns from './countdowns.mjs';
export * as actions from './action/_module.mjs'; export * as actions from './action/_module.mjs';

View file

@ -34,6 +34,20 @@ export default class DHAttackAction extends DHDamageAction {
}; };
} }
get damageFormula() {
const hitPointsPart = this.damage.parts.find(x => x.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
if (!hitPointsPart) return '0';
return hitPointsPart.value.getFormula();
}
get altDamageFormula() {
const hitPointsPart = this.damage.parts.find(x => x.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
if (!hitPointsPart) return '0';
return hitPointsPart.valueAlt.getFormula();
}
async use(event, options) { async use(event, options) {
const result = await super.use(event, options); const result = await super.use(event, options);
if (!result.message) return; if (!result.message) return;

View file

@ -114,9 +114,24 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
* Return Item the action is attached too. * Return Item the action is attached too.
*/ */
get item() { get item() {
if (!this.parent.parent && this.systemPath)
return foundry.utils.getProperty(this.parent, this.systemPath).get(this.id);
return this.parent.parent; return this.parent.parent;
} }
get applyEffects() {
if (this.item.systemPath) {
const itemEffectIds = this.item.effects.map(x => x._id);
const movePathSplit = this.item.systemPath.split('.');
movePathSplit.pop();
const move = foundry.utils.getProperty(this.parent, movePathSplit.join('.'));
return new Collection(itemEffectIds.map(id => [id, move.effects.find(x => x.id === id)]));
}
return this.item.effects;
}
/** /**
* Return the first Actor parent found. * Return the first Actor parent found.
*/ */
@ -125,7 +140,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
? this.item ? this.item
: this.item?.parent instanceof DhpActor : this.item?.parent instanceof DhpActor
? this.item.parent ? this.item.parent
: this.item?.actor; : null;
} }
static getRollType(parent) { static getRollType(parent) {
@ -214,7 +229,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return; if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return;
if (this.chatDisplay) await this.toChat(); if (this.chatDisplay && !config.actionChatMessageHandled) await this.toChat();
return config; return config;
} }
@ -225,9 +240,13 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
* @returns {object} * @returns {object}
*/ */
prepareBaseConfig(event) { prepareBaseConfig(event) {
const isActor = this.item instanceof CONFIG.Actor.documentClass;
const actionTitle = game.i18n.localize(this.name);
const itemTitle = isActor || this.item.name === actionTitle ? '' : `${this.item.name} - `;
const config = { const config = {
event, event,
title: `${this.item instanceof CONFIG.Actor.documentClass ? '' : `${this.item.name}: `}${game.i18n.localize(this.name)}`, title: `${itemTitle}${actionTitle}`,
source: { source: {
item: this.item._id, item: this.item._id,
originItem: this.originItem, originItem: this.originItem,

View file

@ -1,9 +1,12 @@
import DHAdversarySettings from '../../applications/sheets-configs/adversary-settings.mjs'; import DHAdversarySettings from '../../applications/sheets-configs/adversary-settings.mjs';
import { ActionField } from '../fields/actionField.mjs'; import { ActionField } from '../fields/actionField.mjs';
import BaseDataActor, { commonActorRules } from './base.mjs'; import { commonActorRules } from './base.mjs';
import DhCreature from './creature.mjs';
import { resourceField, bonusField } from '../fields/actorField.mjs'; import { resourceField, bonusField } from '../fields/actorField.mjs';
import { calculateExpectedValue, parseTermsFromSimpleFormula } from '../../helpers/utils.mjs';
import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs';
export default class DhpAdversary extends BaseDataActor { export default class DhpAdversary extends DhCreature {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Adversary']; static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Adversary'];
static get metadata() { static get metadata() {
@ -40,7 +43,14 @@ export default class DhpAdversary extends BaseDataActor {
integer: true, integer: true,
label: 'DAGGERHEART.GENERAL.hordeHp' label: 'DAGGERHEART.GENERAL.hordeHp'
}), }),
criticalThreshold: new fields.NumberField({ required: true, integer: true, min: 1, max: 20, initial: 20 }), criticalThreshold: new fields.NumberField({
required: true,
integer: true,
min: 1,
max: 20,
initial: 20,
label: 'DAGGERHEART.ACTIONS.Settings.criticalThreshold'
}),
damageThresholds: new fields.SchemaField({ damageThresholds: new fields.SchemaField({
major: new fields.NumberField({ major: new fields.NumberField({
required: true, required: true,
@ -180,6 +190,10 @@ export default class DhpAdversary extends BaseDataActor {
} }
} }
prepareDerivedData() {
this.attack.roll.isStandardAttack = true;
}
_getTags() { _getTags() {
const tags = [ const tags = [
game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${this.tier}`), game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${this.tier}`),
@ -188,4 +202,211 @@ export default class DhpAdversary extends BaseDataActor {
]; ];
return tags; return tags;
} }
/** Returns source data for this actor adjusted to a new tier, which can be used to create a new actor. */
adjustForTier(tier) {
const source = this.parent.toObject(true);
/** @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
for (const action of Object.values(item.system.actions)) {
if (!action.damage) continue;
// Parse damage, and convert all formula matches in the descriptions to the new damage
try {
const result = this.#adjustActionDamage(action, { ...damageMeta, type: 'action' });
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
*/
#adjustActionDamage(action, damageMeta) {
// The current algorithm only returns a value if there is a single damage part
const hpDamageParts = action.damage.parts.filter(d => d.applyTo === 'hitPoints');
if (hpDamageParts.length !== 1) throw new Error('incorrect number of hp parts');
const result = {};
for (const property of ['value', 'valueAlt']) {
const data = hpDamageParts[0][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

@ -29,17 +29,40 @@ const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) =>
/* Common rules applying to Characters and Adversaries */ /* Common rules applying to Characters and Adversaries */
export const commonActorRules = (extendedData = { damageReduction: {}, attack: { damage: {} } }) => ({ export const commonActorRules = (extendedData = { damageReduction: {}, attack: { damage: {} } }) => ({
conditionImmunities: new fields.SchemaField({ conditionImmunities: new fields.SchemaField({
hidden: new fields.BooleanField({ initial: false }), hidden: new fields.BooleanField({
restrained: new fields.BooleanField({ initial: false }), initial: false,
vulnerable: new fields.BooleanField({ initial: false }) label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.hidden'
}),
restrained: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.restrained'
}),
vulnerable: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.vulnerable'
})
}), }),
damageReduction: new fields.SchemaField({ damageReduction: new fields.SchemaField({
thresholdImmunities: new fields.SchemaField({ thresholdImmunities: new fields.SchemaField({
minor: new fields.BooleanField({ initial: false }) minor: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.thresholdImmunities.minor.label',
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.thresholdImmunities.minor.hint'
})
}), }),
reduceSeverity: new fields.SchemaField({ reduceSeverity: new fields.SchemaField({
magical: new fields.NumberField({ initial: 0, min: 0 }), magical: new fields.NumberField({
physical: new fields.NumberField({ initial: 0, min: 0 }) initial: 0,
min: 0,
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.reduceSeverity.magical.label',
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.reduceSeverity.magical.hint'
}),
physical: new fields.NumberField({
initial: 0,
min: 0,
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.reduceSeverity.physical.label',
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.reduceSeverity.physical.hint'
})
}), }),
...(extendedData.damageReduction ?? {}) ...(extendedData.damageReduction ?? {})
}), }),
@ -49,12 +72,16 @@ export const commonActorRules = (extendedData = { damageReduction: {}, attack: {
hpDamageMultiplier: new fields.NumberField({ hpDamageMultiplier: new fields.NumberField({
required: true, required: true,
nullable: false, nullable: false,
initial: 1 initial: 1,
label: 'DAGGERHEART.GENERAL.Attack.hpDamageMultiplier.label',
hint: 'DAGGERHEART.GENERAL.Attack.hpDamageMultiplier.hint'
}), }),
hpDamageTakenMultiplier: new fields.NumberField({ hpDamageTakenMultiplier: new fields.NumberField({
required: true, required: true,
nullable: false, nullable: false,
initial: 1 initial: 1,
label: 'DAGGERHEART.GENERAL.Attack.hpDamageTakenMultiplier.label',
hint: 'DAGGERHEART.GENERAL.Attack.hpDamageTakenMultiplier.hint'
}), }),
...(extendedData.attack?.damage ?? {}) ...(extendedData.attack?.damage ?? {})
}) })

View file

@ -1,12 +1,13 @@
import { burden } from '../../config/generalConfig.mjs'; import { burden } from '../../config/generalConfig.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import DhLevelData from '../levelData.mjs'; import DhLevelData from '../levelData.mjs';
import BaseDataActor, { commonActorRules } from './base.mjs'; import { commonActorRules } from './base.mjs';
import DhCreature from './creature.mjs';
import { attributeField, resourceField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs'; import { attributeField, resourceField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs';
import { ActionField } from '../fields/actionField.mjs'; import { ActionField } from '../fields/actionField.mjs';
import DHCharacterSettings from '../../applications/sheets-configs/character-settings.mjs'; import DHCharacterSettings from '../../applications/sheets-configs/character-settings.mjs';
export default class DhCharacter extends BaseDataActor { export default class DhCharacter extends DhCreature {
/**@override */ /**@override */
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Character']; static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Character'];
@ -35,14 +36,18 @@ export default class DhCharacter extends BaseDataActor {
'DAGGERHEART.ACTORS.Character.maxHPBonus' 'DAGGERHEART.ACTORS.Character.maxHPBonus'
), ),
stress: resourceField(6, 0, 'DAGGERHEART.GENERAL.stress', true), stress: resourceField(6, 0, 'DAGGERHEART.GENERAL.stress', true),
hope: new fields.SchemaField({ hope: new fields.SchemaField(
{
value: new fields.NumberField({ value: new fields.NumberField({
initial: 2, initial: 2,
min: 0, min: 0,
integer: true, integer: true,
label: 'DAGGERHEART.GENERAL.hope' label: 'DAGGERHEART.GENERAL.hope'
}) }),
}) isReversed: new fields.BooleanField({ initial: false })
},
{ label: 'DAGGERHEART.GENERAL.hope' }
)
}), }),
traits: new fields.SchemaField({ traits: new fields.SchemaField({
agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'), agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'),
@ -127,14 +132,6 @@ export default class DhCharacter extends BaseDataActor {
} }
} }
}), }),
advantageSources: new fields.ArrayField(new fields.StringField(), {
label: 'DAGGERHEART.ACTORS.Character.advantageSources.label',
hint: 'DAGGERHEART.ACTORS.Character.advantageSources.hint'
}),
disadvantageSources: new fields.ArrayField(new fields.StringField(), {
label: 'DAGGERHEART.ACTORS.Character.disadvantageSources.label',
hint: 'DAGGERHEART.ACTORS.Character.disadvantageSources.hint'
}),
levelData: new fields.EmbeddedDataField(DhLevelData), levelData: new fields.EmbeddedDataField(DhLevelData),
bonuses: new fields.SchemaField({ bonuses: new fields.SchemaField({
roll: new fields.SchemaField({ roll: new fields.SchemaField({
@ -221,8 +218,16 @@ export default class DhCharacter extends BaseDataActor {
rules: new fields.SchemaField({ rules: new fields.SchemaField({
...commonActorRules({ ...commonActorRules({
damageReduction: { damageReduction: {
magical: new fields.BooleanField({ initial: false }), magical: new fields.BooleanField({
physical: new fields.BooleanField({ initial: false }), initial: false,
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.magical.label',
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.magical.hint'
}),
physical: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.physical.label',
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.physical.hint'
}),
maxArmorMarked: new fields.SchemaField({ maxArmorMarked: new fields.SchemaField({
value: new fields.NumberField({ value: new fields.NumberField({
required: true, required: true,
@ -252,7 +257,10 @@ export default class DhCharacter extends BaseDataActor {
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.label', label: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.label',
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint' hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint'
}), }),
disabledArmor: new fields.BooleanField({ intial: false }) disabledArmor: new fields.BooleanField({
intial: false,
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.disabledArmor.label'
})
}, },
attack: { attack: {
damage: { damage: {
@ -300,12 +308,14 @@ export default class DhCharacter extends BaseDataActor {
label: 'DAGGERHEART.ACTORS.Character.defaultFearDice' label: 'DAGGERHEART.ACTORS.Character.defaultFearDice'
}) })
}), }),
runeWard: new fields.BooleanField({ initial: false }),
burden: new fields.SchemaField({ burden: new fields.SchemaField({
ignore: new fields.BooleanField() ignore: new fields.BooleanField({ label: 'DAGGERHEART.ACTORS.Character.burden.ignore.label' })
}), }),
roll: new fields.SchemaField({ roll: new fields.SchemaField({
guaranteedCritical: new fields.BooleanField() guaranteedCritical: new fields.BooleanField({
label: 'DAGGERHEART.ACTORS.Character.roll.guaranteedCritical.label',
hint: 'DAGGERHEART.ACTORS.Character.roll.guaranteedCritical.hint'
})
}) })
}) })
}; };
@ -660,7 +670,7 @@ export default class DhCharacter extends BaseDataActor {
}; };
const globalHopeMax = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxHope; const globalHopeMax = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxHope;
this.resources.hope.max = globalHopeMax - this.scars; this.resources.hope.max = globalHopeMax;
this.resources.hitPoints.max += this.class.value?.system?.hitPoints ?? 0; this.resources.hitPoints.max += this.class.value?.system?.hitPoints ?? 0;
/* Companion Related Data */ /* Companion Related Data */
@ -684,6 +694,7 @@ export default class DhCharacter extends BaseDataActor {
} }
} }
this.resources.hope.max -= this.scars;
this.resources.hope.value = Math.min(baseHope, this.resources.hope.max); this.resources.hope.value = Math.min(baseHope, this.resources.hope.max);
this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait; this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait;

View file

@ -1,4 +1,4 @@
import BaseDataActor from './base.mjs'; import DhCreature from './creature.mjs';
import DhLevelData from '../levelData.mjs'; import DhLevelData from '../levelData.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import { ActionField } from '../fields/actionField.mjs'; import { ActionField } from '../fields/actionField.mjs';
@ -6,7 +6,7 @@ import { adjustDice, adjustRange } from '../../helpers/utils.mjs';
import DHCompanionSettings from '../../applications/sheets-configs/companion-settings.mjs'; import DHCompanionSettings from '../../applications/sheets-configs/companion-settings.mjs';
import { resourceField, bonusField } from '../fields/actorField.mjs'; import { resourceField, bonusField } from '../fields/actorField.mjs';
export default class DhCompanion extends BaseDataActor { export default class DhCompanion extends DhCreature {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Companion']; static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Companion'];
/**@inheritdoc */ /**@inheritdoc */
@ -53,9 +53,18 @@ export default class DhCompanion extends BaseDataActor {
), ),
rules: new fields.SchemaField({ rules: new fields.SchemaField({
conditionImmunities: new fields.SchemaField({ conditionImmunities: new fields.SchemaField({
hidden: new fields.BooleanField({ initial: false }), hidden: new fields.BooleanField({
restrained: new fields.BooleanField({ initial: false }), initial: false,
vulnerable: new fields.BooleanField({ initial: false }) label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.hidden'
}),
restrained: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.restrained'
}),
vulnerable: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.vulnerable'
})
}) })
}), }),
attack: new ActionField({ attack: new ActionField({

View file

@ -0,0 +1,61 @@
import BaseDataActor from './base.mjs';
export default class DhCreature extends BaseDataActor {
/**@inheritdoc */
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
advantageSources: new fields.ArrayField(new fields.StringField(), {
label: 'DAGGERHEART.ACTORS.Character.advantageSources.label',
hint: 'DAGGERHEART.ACTORS.Character.advantageSources.hint'
}),
disadvantageSources: new fields.ArrayField(new fields.StringField(), {
label: 'DAGGERHEART.ACTORS.Character.disadvantageSources.label',
hint: 'DAGGERHEART.ACTORS.Character.disadvantageSources.hint'
})
};
}
get isAutoVulnerableActive() {
const vulnerableAppliedByOther = this.parent.effects.some(
x => x.statuses.has('vulnerable') && !x.flags.daggerheart?.autoApplyFlagId
);
return !vulnerableAppliedByOther;
}
async _preUpdate(changes, options, userId) {
const allowed = await super._preUpdate(changes, options, userId);
if (allowed === false) return;
const automationSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
if (
automationSettings.vulnerableAutomation &&
this.parent.type !== 'companion' &&
changes.system?.resources?.stress?.value
) {
const { name, description, img, autoApplyFlagId } = CONFIG.DH.GENERAL.conditions().vulnerable;
const autoEffects = this.parent.effects.filter(
x => x.flags.daggerheart?.autoApplyFlagId === autoApplyFlagId
);
if (changes.system.resources.stress.value >= this.resources.stress.max) {
if (!autoEffects.length)
this.parent.createEmbeddedDocuments('ActiveEffect', [
{
name: game.i18n.localize(name),
description: game.i18n.localize(description),
img: img,
statuses: ['vulnerable'],
flags: { daggerheart: { autoApplyFlagId } }
}
]);
} else if (this.resources.stress.value >= this.resources.stress.max) {
this.parent.deleteEmbeddedDocuments(
'ActiveEffect',
autoEffects.map(x => x.id)
);
}
}
}
}

View file

@ -31,6 +31,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
static defineSchema() { static defineSchema() {
return { return {
title: new fields.StringField(), title: new fields.StringField(),
actionDescription: new fields.HTMLField(),
roll: new fields.ObjectField(), roll: new fields.ObjectField(),
targets: targetsField(), targets: targetsField(),
hasRoll: new fields.BooleanField({ initial: false }), hasRoll: new fields.BooleanField({ initial: false }),

View file

@ -0,0 +1,36 @@
export default class CompendiumBrowserSettings extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
excludedSources: new fields.TypedObjectField(
new fields.SchemaField({
excludedDocumentTypes: new fields.ArrayField(
new fields.StringField({ required: true, choices: CONST.SYSTEM_SPECIFIC_COMPENDIUM_TYPES })
)
})
),
excludedPacks: new fields.TypedObjectField(
new fields.SchemaField({
excludedDocumentTypes: new fields.ArrayField(
new fields.StringField({ required: true, choices: CONST.SYSTEM_SPECIFIC_COMPENDIUM_TYPES })
)
})
)
};
}
isEntryExcluded(item) {
const pack = game.packs.get(item.pack);
if (!pack) return false;
const packageName = pack.metadata.packageType === 'world' ? 'world' : pack.metadata.packageName;
const excludedSourceData = this.excludedSources[packageName];
if (excludedSourceData && excludedSourceData.excludedDocumentTypes.includes(pack.metadata.type)) return true;
const excludedPackData = this.excludedPacks[item.pack];
if (excludedPackData && excludedPackData.excludedDocumentTypes.includes(pack.metadata.type)) return true;
return false;
}
}

View file

@ -68,6 +68,8 @@ export default class DamageField extends fields.SchemaField {
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;
config.damage = damageResult.damage; config.damage = damageResult.damage;
config.message ??= damageConfig.message; config.message ??= damageConfig.message;
} }
@ -107,8 +109,8 @@ export default class DamageField extends fields.SchemaField {
); );
else { else {
const configDamage = foundry.utils.deepClone(config.damage); const configDamage = foundry.utils.deepClone(config.damage);
const hpDamageMultiplier = config.actionActor?.system.rules.attack.damage.hpDamageMultiplier ?? 1; const hpDamageMultiplier = config.actionActor?.system.rules?.attack?.damage?.hpDamageMultiplier ?? 1;
const hpDamageTakenMultiplier = actor.system.rules.attack.damage.hpDamageTakenMultiplier; const hpDamageTakenMultiplier = actor.system.rules?.attack?.damage?.hpDamageTakenMultiplier;
if (configDamage.hitPoints) { if (configDamage.hitPoints) {
for (const part of configDamage.hitPoints.parts) { for (const part of configDamage.hitPoints.parts) {
part.total = Math.ceil(part.total * hpDamageMultiplier * hpDamageTakenMultiplier); part.total = Math.ceil(part.total * hpDamageMultiplier * hpDamageTakenMultiplier);
@ -163,7 +165,8 @@ export default class DamageField extends fields.SchemaField {
if (data.hasRoll && part.resultBased && data.roll.result.duality === -1) return part.valueAlt; if (data.hasRoll && part.resultBased && data.roll.result.duality === -1) return part.valueAlt;
const isAdversary = this.actor.type === 'adversary'; const isAdversary = this.actor.type === 'adversary';
if (isAdversary && this.actor.system.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id) { const isHorde = this.actor.system.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id;
if (isAdversary && isHorde && this.roll?.isStandardAttack) {
const hasHordeDamage = this.actor.effects.find(x => x.type === 'horde'); const hasHordeDamage = this.actor.effects.find(x => x.type === 'horde');
if (hasHordeDamage && !hasHordeDamage.disabled) return part.valueAlt; if (hasHordeDamage && !hasHordeDamage.disabled) return part.valueAlt;
} }

View file

@ -73,7 +73,7 @@ export default class EffectsField extends fields.ArrayField {
}); });
effects.forEach(async e => { effects.forEach(async e => {
const effect = this.item.effects.get(e._id); const effect = (this.item.applyEffects ?? this.item.effects).get(e._id);
if (!token.actor || !effect) return; if (!token.actor || !effect) return;
await EffectsField.applyEffect(effect, token.actor); await EffectsField.applyEffect(effect, token.actor);
}); });
@ -96,7 +96,7 @@ export default class EffectsField extends fields.ArrayField {
content: await foundry.applications.handlebars.renderTemplate( content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/effectSummary.hbs', 'systems/daggerheart/templates/ui/chat/effectSummary.hbs',
{ {
effects: this.effects.map(e => this.item.effects.get(e._id)), effects: this.effects.map(e => (this.item.applyEffects ?? this.item.effects).get(e._id)),
targets: messageTargets targets: messageTargets
} }
) )
@ -123,7 +123,7 @@ export default class EffectsField extends fields.ArrayField {
// Otherwise, create a new effect on the target // Otherwise, create a new effect on the target
const effectData = foundry.utils.mergeObject({ const effectData = foundry.utils.mergeObject({
...effect.toObject(), ...(effect.toObject?.() ?? effect),
disabled: false, disabled: false,
transfer: false, transfer: false,
origin: effect.uuid origin: effect.uuid

View file

@ -152,7 +152,9 @@ export function ActionMixin(Base) {
} }
get uuid() { get uuid() {
return `${this.item.uuid}.${this.documentName}.${this.id}`; const isItem = this.item instanceof game.system.api.documents.DHItem;
const isActor = this.item instanceof game.system.api.documents.DhpActor;
return isItem || isActor ? `${this.item.uuid}.${this.documentName}.${this.id}` : null;
} }
get sheet() { get sheet() {
@ -260,6 +262,9 @@ export function ActionMixin(Base) {
} }
async toChat(origin) { async toChat(origin) {
const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
.expandRollMessage?.desc;
const cls = getDocumentClass('ChatMessage'); const cls = getDocumentClass('ChatMessage');
const systemData = { const systemData = {
title: game.i18n.localize('DAGGERHEART.CONFIG.FeatureForm.action'), title: game.i18n.localize('DAGGERHEART.CONFIG.FeatureForm.action'),
@ -288,7 +293,7 @@ export function ActionMixin(Base) {
system: systemData, system: systemData,
content: await foundry.applications.handlebars.renderTemplate( content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/action.hbs', 'systems/daggerheart/templates/ui/chat/action.hbs',
systemData { ...systemData, open: autoExpandDescription ? 'open' : '' }
), ),
flags: { flags: {
daggerheart: { daggerheart: {

View file

@ -7,16 +7,20 @@ const attributeField = label =>
}); });
const resourceField = (max = 0, initial = 0, label, reverse = false, maxLabel) => const resourceField = (max = 0, initial = 0, label, reverse = false, maxLabel) =>
new fields.SchemaField({ new fields.SchemaField(
{
value: new fields.NumberField({ initial: initial, min: 0, integer: true, label }), value: new fields.NumberField({ initial: initial, min: 0, integer: true, label }),
max: new fields.NumberField({ max: new fields.NumberField({
initial: max, initial: max,
integer: true, integer: true,
label: label:
maxLabel ?? game.i18n.format('DAGGERHEART.GENERAL.maxWithThing', { thing: game.i18n.localize(label) }) maxLabel ??
game.i18n.format('DAGGERHEART.GENERAL.maxWithThing', { thing: game.i18n.localize(label) })
}), }),
isReversed: new fields.BooleanField({ initial: reverse }) isReversed: new fields.BooleanField({ initial: reverse })
}); },
{ label }
);
const stressDamageReductionRule = localizationPath => const stressDamageReductionRule = localizationPath =>
new fields.SchemaField({ new fields.SchemaField({

View file

@ -1,5 +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';
export default class DHAncestry extends BaseDataItem { export default class DHAncestry extends BaseDataItem {
/** @inheritDoc */ /** @inheritDoc */
@ -19,7 +20,6 @@ export default class DHAncestry extends BaseDataItem {
}; };
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/**@override */ /**@override */
@ -42,4 +42,18 @@ export default class DHAncestry extends BaseDataItem {
get secondaryFeature() { get secondaryFeature() {
return this.features.find(x => x.type === CONFIG.DH.ITEM.featureSubTypes.secondary)?.item; return this.features.find(x => x.type === CONFIG.DH.ITEM.featureSubTypes.secondary)?.item;
} }
/**@inheritdoc */
async getDescriptionData() {
const baseDescription = this.description;
const features = await getFeaturesHTMLData(this.features);
if (!features.length) return { prefix: null, value: baseDescription, suffix: null };
const suffix = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/items/description.hbs',
{ label: 'DAGGERHEART.ITEMS.Ancestry.featuresLabel', features }
);
return { prefix: null, value: baseDescription, suffix };
}
} }

View file

@ -23,9 +23,7 @@ export default class DHArmor extends AttachableItem {
armorFeatures: new fields.ArrayField( armorFeatures: new fields.ArrayField(
new fields.SchemaField({ new fields.SchemaField({
value: new fields.StringField({ value: new fields.StringField({
required: true, required: true
choices: CONFIG.DH.ITEM.allArmorFeatures,
blank: true
}), }),
effectIds: new fields.ArrayField(new fields.StringField({ required: true })), effectIds: new fields.ArrayField(new fields.StringField({ required: true })),
actionIds: new fields.ArrayField(new fields.StringField({ required: true })) actionIds: new fields.ArrayField(new fields.StringField({ required: true }))
@ -58,12 +56,11 @@ export default class DHArmor extends AttachableItem {
async getDescriptionData() { async getDescriptionData() {
const baseDescription = this.description; const baseDescription = this.description;
const allFeatures = CONFIG.DH.ITEM.allArmorFeatures(); const allFeatures = CONFIG.DH.ITEM.allArmorFeatures();
const features = this.armorFeatures.map(x => allFeatures[x.value]); const features = this.armorFeatures.map(x => allFeatures[x.value]).filter(x => x);
if (!features.length) return { prefix: null, value: baseDescription, suffix: null };
const prefix = await foundry.applications.handlebars.renderTemplate( const prefix = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/items/armor/description.hbs', 'systems/daggerheart/templates/sheets/items/armor/description.hbs',
{ features } { item: this.parent, features }
); );
return { prefix, value: baseDescription, suffix: null }; return { prefix, value: baseDescription, suffix: null };

View file

@ -253,4 +253,20 @@ export default class DHBeastform extends BaseDataItem {
return false; return false;
} }
_onCreate(_data, _options, userId) {
if (!this.actor && game.user.id === userId) {
const hasBeastformEffect = this.parent.effects.some(x => x.type === 'beastform');
if (!hasBeastformEffect)
this.parent.createEmbeddedDocuments('ActiveEffect', [
{
type: 'beastform',
name: game.i18n.localize('DAGGERHEART.ITEMS.Beastform.beastformEffect'),
img: 'icons/creatures/abilities/paw-print-pair-purple.webp'
}
]);
return;
}
}
} }

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, updateLinkedItemApps } from '../../helpers/utils.mjs'; import { addLinkedItemsDiff, getFeaturesHTMLData, updateLinkedItemApps } from '../../helpers/utils.mjs';
export default class DHClass extends BaseDataItem { export default class DHClass extends BaseDataItem {
/** @inheritDoc */ /** @inheritDoc */
@ -163,4 +163,56 @@ export default class DHClass extends BaseDataItem {
updateLinkedItemApps(options, this.parent.sheet); updateLinkedItemApps(options, this.parent.sheet);
} }
/**@inheritdoc */
async getDescriptionData() {
const baseDescription = this.description;
const getDomainLabel = domain => {
const data = CONFIG.DH.DOMAIN.allDomains()[domain];
return data ? game.i18n.localize(data.label) : '';
};
let domainsLabel = '';
if (this.domains.length) {
if (this.domains.length === 1) domainsLabel = getDomainLabel(this.domains[0]);
else {
const firstDomains = this.domains
.slice(0, this.domains.length - 1)
.map(getDomainLabel)
.join(', ');
const lastDomain = getDomainLabel(this.domains[this.domains.length - 1]);
domainsLabel = game.i18n.format('DAGGERHEART.GENERAL.thingsAndThing', {
things: firstDomains,
thing: lastDomain
});
}
}
const classItems = [];
for (const itemData of this.inventory.choiceB) {
const linkData = [
undefined,
'UUID', // type
itemData.uuid // target
];
const contentLink = await foundry.applications.ux.TextEditor.implementation._createContentLink(linkData);
classItems.push(contentLink.outerHTML);
}
const hopeFeatures = await getFeaturesHTMLData(this.hopeFeatures);
const classFeatures = await getFeaturesHTMLData(this.classFeatures);
const suffix = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/items/class/description.hbs',
{
class: this.parent,
domains: domainsLabel,
classItems,
hopeFeatures,
classFeatures
}
);
return { prefix: null, value: baseDescription, suffix };
}
} }

View file

@ -1,3 +1,4 @@
import { 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';
@ -24,4 +25,17 @@ export default class DHCommunity extends BaseDataItem {
/**@override */ /**@override */
static DEFAULT_ICON = 'systems/daggerheart/assets/icons/documents/items/village.svg'; static DEFAULT_ICON = 'systems/daggerheart/assets/icons/documents/items/village.svg';
/**@inheritdoc */
async getDescriptionData() {
const baseDescription = this.description;
const features = await getFeaturesHTMLData(this.features);
if (!features.length) return { prefix: null, value: baseDescription, suffix: null };
const suffix = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/items/description.hbs',
{ label: 'DAGGERHEART.ITEMS.Community.featuresLabel', features }
);
return { prefix: null, value: baseDescription, suffix };
}
} }

View file

@ -94,8 +94,10 @@ export default class DHDomainCard extends BaseDataItem {
return false; return false;
} }
if (!this.actor.system.loadoutSlot.available) { if (!this.actor.system.loadoutSlot.available && !this.loadoutIgnore) {
data.system.inVault = true; data.system.inVault = true;
await this.updateSource({ inVault: true });
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.loadoutMaxReached'));
} }
} }
} }

View file

@ -1,3 +1,4 @@
import { getFeaturesHTMLData } from '../../helpers/utils.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.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';
@ -89,4 +90,28 @@ export default class DHSubclass extends BaseDataItem {
const allowed = await super._preCreate(data, options, user); const allowed = await super._preCreate(data, options, user);
if (allowed === false) return; if (allowed === false) return;
} }
/**@inheritdoc */
async getDescriptionData() {
const baseDescription = this.description;
const spellcastTrait = this.spellcastingTrait
? game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.spellcastingTrait].label)
: null;
const foundationFeatures = await getFeaturesHTMLData(this.foundationFeatures);
const specializationFeatures = await getFeaturesHTMLData(this.specializationFeatures);
const masteryFeatures = await getFeaturesHTMLData(this.masteryFeatures);
const suffix = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/items/subclass/description.hbs',
{
spellcastTrait,
foundationFeatures,
specializationFeatures,
masteryFeatures
}
);
return { prefix: null, value: baseDescription, suffix };
}
} }

View file

@ -38,9 +38,7 @@ export default class DHWeapon extends AttachableItem {
weaponFeatures: new fields.ArrayField( weaponFeatures: new fields.ArrayField(
new fields.SchemaField({ new fields.SchemaField({
value: new fields.StringField({ value: new fields.StringField({
required: true, required: true
choices: CONFIG.DH.ITEM.allWeaponFeatures,
blank: true
}), }),
effectIds: new fields.ArrayField(new fields.StringField({ required: true })), effectIds: new fields.ArrayField(new fields.StringField({ required: true })),
actionIds: new fields.ArrayField(new fields.StringField({ required: true })) actionIds: new fields.ArrayField(new fields.StringField({ required: true }))
@ -113,13 +111,26 @@ export default class DHWeapon extends AttachableItem {
/**@inheritdoc */ /**@inheritdoc */
async getDescriptionData() { async getDescriptionData() {
const baseDescription = this.description; const baseDescription = this.description;
const tier = game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${this.tier}`);
const trait = game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.attack.roll.trait].label);
const range = game.i18n.localize(`DAGGERHEART.CONFIG.Range.${this.attack.range}.name`);
const damage = Roll.replaceFormulaData(this.attack.damageFormula, this.parent.parent ?? this.parent);
const burden = game.i18n.localize(CONFIG.DH.GENERAL.burden[this.burden].label);
const allFeatures = CONFIG.DH.ITEM.allWeaponFeatures(); const allFeatures = CONFIG.DH.ITEM.allWeaponFeatures();
const features = this.weaponFeatures.map(x => allFeatures[x.value]); const features = this.weaponFeatures.map(x => allFeatures[x.value]).filter(x => x);
if (!features.length) return { prefix: null, value: baseDescription, suffix: null };
const prefix = await foundry.applications.handlebars.renderTemplate( const prefix = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/items/weapon/description.hbs', 'systems/daggerheart/templates/sheets/items/weapon/description.hbs',
{ features } {
features,
tier,
trait,
range,
damage,
burden
}
); );
return { prefix, value: baseDescription, suffix: null }; return { prefix, value: baseDescription, suffix: null };

View file

@ -6,7 +6,12 @@ export default class DhLevelData extends foundry.abstract.DataModel {
return { return {
level: new fields.SchemaField({ level: new fields.SchemaField({
current: new fields.NumberField({ required: true, integer: true, initial: 1 }), current: new fields.NumberField({
required: true,
integer: true,
initial: 1,
label: 'DAGGERHEART.GENERAL.currentLevel'
}),
changed: new fields.NumberField({ required: true, integer: true, initial: 1 }), changed: new fields.NumberField({ required: true, integer: true, initial: 1 }),
bonuses: new fields.TypedObjectField(new fields.NumberField({ integer: true, nullable: false })) bonuses: new fields.TypedObjectField(new fields.NumberField({ integer: true, nullable: false }))
}), }),

View file

@ -75,7 +75,7 @@ export default class RegisteredTriggers extends Map {
unregisterSceneEnvironmentTriggers(flagSystemData) { unregisterSceneEnvironmentTriggers(flagSystemData) {
const sceneData = new game.system.api.data.scenes.DHScene(flagSystemData); const sceneData = new game.system.api.data.scenes.DHScene(flagSystemData);
for (const environment of sceneData.sceneEnvironments) { for (const environment of sceneData.sceneEnvironments) {
if (environment.pack) continue; if (!environment || environment.pack) continue;
this.unregisterItemTriggers(environment.system.features); this.unregisterItemTriggers(environment.system.features);
} }
} }

View file

@ -37,11 +37,30 @@ export default class DhAppearance extends foundry.abstract.DataModel {
extendEnvironmentDescriptions: new BooleanField(), extendEnvironmentDescriptions: new BooleanField(),
extendItemDescriptions: new BooleanField(), extendItemDescriptions: new BooleanField(),
expandRollMessage: new SchemaField({ expandRollMessage: new SchemaField({
desc: new BooleanField(), desc: new BooleanField({ initial: true }),
roll: new BooleanField(), roll: new BooleanField(),
damage: new BooleanField(), damage: new BooleanField(),
target: new BooleanField() target: new BooleanField()
}), }),
showTokenDistance: new StringField({
required: true,
choices: {
always: {
value: 'always',
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.showTokenDistance.choices.always'
},
encounters: {
value: 'encounters',
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.showTokenDistance.choices.encounters'
},
never: {
value: 'never',
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.showTokenDistance.choices.never'
}
},
nullable: false,
initial: 'always'
}),
hideAttribution: new BooleanField(), hideAttribution: new BooleanField(),
showGenericStatusEffects: new BooleanField({ initial: true }) showGenericStatusEffects: new BooleanField({ initial: true })
}; };

View file

@ -18,6 +18,10 @@ export default class DhAutomation extends foundry.abstract.DataModel {
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hopeFear.players.label' label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hopeFear.players.label'
}) })
}), }),
vulnerableAutomation: new fields.BooleanField({
initial: true,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.vulnerableAutomation.label'
}),
countdownAutomation: new fields.BooleanField({ countdownAutomation: new fields.BooleanField({
required: true, required: true,
initial: true, initial: true,
@ -55,6 +59,23 @@ export default class DhAutomation extends foundry.abstract.DataModel {
initial: true, initial: true,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.resourceScrollTexts.label' label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.resourceScrollTexts.label'
}), }),
deathMoveAutomation: new fields.SchemaField({
avoidDeath: new fields.BooleanField({
required: true,
initial: true,
label: 'DAGGERHEART.CONFIG.DeathMoves.avoidDeath.name'
}),
riskItAll: new fields.BooleanField({
required: true,
initial: true,
label: 'DAGGERHEART.CONFIG.DeathMoves.riskItAll.name'
}),
blazeOfGlory: new fields.BooleanField({
required: true,
initial: true,
label: 'DAGGERHEART.CONFIG.DeathMoves.blazeOfGlory.name'
})
}),
defeated: new fields.SchemaField({ defeated: new fields.SchemaField({
enabled: new fields.BooleanField({ enabled: new fields.BooleanField({
required: true, required: true,

View file

@ -12,6 +12,20 @@ const currencyField = (initial, label, icon) =>
icon: new foundry.data.fields.StringField({ required: true, nullable: false, blank: true, initial: icon }) icon: new foundry.data.fields.StringField({ required: true, nullable: false, blank: true, initial: icon })
}); });
const restMoveField = () =>
new foundry.data.fields.SchemaField({
name: new foundry.data.fields.StringField({ required: true }),
icon: new foundry.data.fields.StringField({ required: true }),
img: new foundry.data.fields.FilePathField({
initial: 'icons/magic/life/cross-worn-green.webp',
categories: ['IMAGE'],
base64: false
}),
description: new foundry.data.fields.HTMLField(),
actions: new ActionsField(),
effects: new foundry.data.fields.ArrayField(new foundry.data.fields.ObjectField())
});
export default class DhHomebrew extends foundry.abstract.DataModel { export default class DhHomebrew extends foundry.abstract.DataModel {
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
@ -105,37 +119,11 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
restMoves: new fields.SchemaField({ restMoves: new fields.SchemaField({
longRest: new fields.SchemaField({ longRest: new fields.SchemaField({
nrChoices: new fields.NumberField({ required: true, integer: true, min: 1, initial: 2 }), nrChoices: new fields.NumberField({ required: true, integer: true, min: 1, initial: 2 }),
moves: new fields.TypedObjectField( moves: new fields.TypedObjectField(restMoveField(), { initial: defaultRestOptions.longRest() })
new fields.SchemaField({
name: new fields.StringField({ required: true }),
icon: new fields.StringField({ required: true }),
img: new fields.FilePathField({
initial: 'icons/magic/life/cross-worn-green.webp',
categories: ['IMAGE'],
base64: false
}),
description: new fields.HTMLField(),
actions: new ActionsField()
}),
{ initial: defaultRestOptions.longRest() }
)
}), }),
shortRest: new fields.SchemaField({ shortRest: new fields.SchemaField({
nrChoices: new fields.NumberField({ required: true, integer: true, min: 1, initial: 2 }), nrChoices: new fields.NumberField({ required: true, integer: true, min: 1, initial: 2 }),
moves: new fields.TypedObjectField( moves: new fields.TypedObjectField(restMoveField(), { initial: defaultRestOptions.shortRest() })
new fields.SchemaField({
name: new fields.StringField({ required: true }),
icon: new fields.StringField({ required: true }),
img: new fields.FilePathField({
initial: 'icons/magic/life/cross-worn-green.webp',
categories: ['IMAGE'],
base64: false
}),
description: new fields.HTMLField(),
actions: new ActionsField()
}),
{ initial: defaultRestOptions.shortRest() }
)
}) })
}), }),
domains: new fields.TypedObjectField( domains: new fields.TypedObjectField(

View file

@ -1,4 +1,5 @@
import DamageDialog from '../applications/dialogs/damageDialog.mjs'; import DamageDialog from '../applications/dialogs/damageDialog.mjs';
import { parseRallyDice } from '../helpers/utils.mjs';
import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs'; import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs';
import DHRoll from './dhRoll.mjs'; import DHRoll from './dhRoll.mjs';
@ -33,7 +34,7 @@ export default class DamageRoll extends DHRoll {
static async buildPost(roll, config, message) { static async buildPost(roll, config, message) {
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').applyRollMode({}, config.rollMode); : getDocumentClass('ChatMessage').applyRollMode({}, config.rollMode ?? CONST.DICE_ROLL_MODES.PUBLIC);
if (game.modules.get('dice-so-nice')?.active) { if (game.modules.get('dice-so-nice')?.active) {
const pool = foundry.dice.terms.PoolTerm.fromRolls( const pool = foundry.dice.terms.PoolTerm.fromRolls(
Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll)) Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll))
@ -46,9 +47,14 @@ export default class DamageRoll extends DHRoll {
chatMessage.whisper?.length > 0 ? chatMessage.whisper : null, chatMessage.whisper?.length > 0 ? chatMessage.whisper : null,
chatMessage.blind chatMessage.blind
); );
config.mute = true;
} }
await super.buildPost(roll, config, message); await super.buildPost(roll, config, message);
if (config.source?.message) chatMessage.update({ 'system.damage': config.damage }); if (config.source?.message) {
chatMessage.update({ 'system.damage': config.damage });
if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
}
} }
static unifyDamageRoll(rolls) { static unifyDamageRoll(rolls) {
@ -192,7 +198,7 @@ export default class DamageRoll extends DHRoll {
// Bardic Rally // Bardic Rally
const rallyChoices = config.data?.parent?.appliedEffects.reduce((a, c) => { const rallyChoices = config.data?.parent?.appliedEffects.reduce((a, c) => {
const change = c.changes.find(ch => ch.key === 'system.bonuses.rally'); const change = c.changes.find(ch => ch.key === 'system.bonuses.rally');
if (change) a.push({ value: c.id, label: change.value }); if (change) a.push({ value: c.id, label: parseRallyDice(change.value, c) });
return a; return a;
}, []); }, []);
if (rallyChoices.length) { if (rallyChoices.length) {

View file

@ -96,6 +96,19 @@ export default class DHRoll extends Roll {
} }
static async toMessage(roll, config) { static async toMessage(roll, config) {
const item = config.data.parent?.items?.get?.(config.source.item) ?? null;
const action = item ? item.system.actions.get(config.source.action) : null;
let actionDescription = null;
if (action?.chatDisplay) {
actionDescription = action
? await foundry.applications.ux.TextEditor.implementation.enrichHTML(action.description, {
relativeTo: config.data,
rollData: config.data.getRollData?.() ?? {}
})
: null;
config.actionChatMessageHandled = true;
}
const cls = getDocumentClass('ChatMessage'), const cls = getDocumentClass('ChatMessage'),
msgData = { msgData = {
type: this.messageType, type: this.messageType,
@ -103,7 +116,7 @@ export default class DHRoll extends Roll {
title: roll.title, title: roll.title,
speaker: cls.getSpeaker({ actor: roll.data?.parent }), speaker: cls.getSpeaker({ actor: roll.data?.parent }),
sound: config.mute ? null : CONFIG.sounds.dice, sound: config.mute ? null : CONFIG.sounds.dice,
system: config, system: { ...config, actionDescription },
rolls: [roll] rolls: [roll]
}; };

View file

@ -1,6 +1,6 @@
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 { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs'; import { parseRallyDice, setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
import { getDiceSoNicePresets } from '../config/generalConfig.mjs'; import { getDiceSoNicePresets } from '../config/generalConfig.mjs';
import { ResourceUpdateMap } from '../data/action/baseAction.mjs'; import { ResourceUpdateMap } from '../data/action/baseAction.mjs';
@ -68,7 +68,7 @@ export default class DualityRoll extends D20Roll {
setRallyChoices() { setRallyChoices() {
return this.data?.parent?.appliedEffects.reduce((a, c) => { return this.data?.parent?.appliedEffects.reduce((a, c) => {
const change = c.changes.find(ch => ch.key === 'system.bonuses.rally'); const change = c.changes.find(ch => ch.key === 'system.bonuses.rally');
if (change) a.push({ value: c.id, label: change.value }); if (change) a.push({ value: c.id, label: parseRallyDice(change.value, c) });
return a; return a;
}, []); }, []);
} }
@ -409,7 +409,9 @@ export default class DualityRoll extends D20Roll {
difficulty: message.system.roll.difficulty ? Number(message.system.roll.difficulty) : null difficulty: message.system.roll.difficulty ? Number(message.system.roll.difficulty) : null
} }
}); });
newRoll.extra = newRoll.extra.slice(2);
const extraIndex = newRoll.advantage ? 3 : 2;
newRoll.extra = newRoll.extra.slice(extraIndex);
const tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); const tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);

View file

@ -61,14 +61,15 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
update.img = 'icons/magic/life/heart-cross-blue.webp'; update.img = 'icons/magic/life/heart-cross-blue.webp';
} }
const statuses = Object.keys(data.statuses ?? {});
const immuneStatuses = const immuneStatuses =
data.statuses?.filter( statuses.filter(
status => status =>
this.parent.system.rules?.conditionImmunities && this.parent.system.rules?.conditionImmunities &&
this.parent.system.rules.conditionImmunities[status] this.parent.system.rules.conditionImmunities[status]
) ?? []; ) ?? [];
if (immuneStatuses.length > 0) { if (immuneStatuses.length > 0) {
update.statuses = data.statuses.filter(x => !immuneStatuses.includes(x)); update.statuses = statuses.filter(x => !immuneStatuses.includes(x));
const conditions = CONFIG.DH.GENERAL.conditions(); const conditions = CONFIG.DH.GENERAL.conditions();
const scrollingTexts = immuneStatuses.map(status => ({ const scrollingTexts = immuneStatuses.map(status => ({
text: game.i18n.format('DAGGERHEART.ACTIVEEFFECT.immuneStatusText', { text: game.i18n.format('DAGGERHEART.ACTIVEEFFECT.immuneStatusText', {
@ -113,6 +114,11 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
super.applyField(model, change, field); super.applyField(model, change, field);
} }
_applyLegacy(actor, change, changes) {
change.value = DhActiveEffect.getChangeValue(actor, change, change.effect);
super._applyLegacy(actor, change, changes);
}
/** */ /** */
static getChangeValue(model, change, effect) { static getChangeValue(model, change, effect) {
let value = change.value; let value = change.value;

View file

@ -934,10 +934,23 @@ export default class DhpActor extends Actor {
/** Get active effects */ /** Get active effects */
getActiveEffects() { getActiveEffects() {
const conditions = CONFIG.DH.GENERAL.conditions();
const statusMap = new Map(foundry.CONFIG.statusEffects.map(status => [status.id, status])); const statusMap = new Map(foundry.CONFIG.statusEffects.map(status => [status.id, status]));
const autoVulnerableActive = this.system.isAutoVulnerableActive;
return this.effects return this.effects
.filter(x => !x.disabled) .filter(x => !x.disabled)
.reduce((acc, effect) => { .reduce((acc, effect) => {
/* Could be generalized if needed. Currently just related to Vulnerable */
const isAutoVulnerableEffect =
effect.flags.daggerheart?.autoApplyFlagId === conditions.vulnerable.autoApplyFlagId;
if (isAutoVulnerableEffect) {
if (!autoVulnerableActive) return acc;
effect.appliedBy = game.i18n.localize('DAGGERHEART.CONFIG.Condition.vulnerable.autoAppliedByLabel');
effect.isLockedCondition = true;
effect.condition = 'vulnerable';
}
acc.push(effect); acc.push(effect);
const currentStatusActiveEffects = acc.filter( const currentStatusActiveEffects = acc.filter(

View file

@ -110,6 +110,8 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
} else if (s.classList.contains('damage-section')) } else if (s.classList.contains('damage-section'))
s.classList.toggle('expanded', autoExpandRoll.damage); s.classList.toggle('expanded', autoExpandRoll.damage);
else if (s.classList.contains('target-section')) s.classList.toggle('expanded', autoExpandRoll.target); else if (s.classList.contains('target-section')) s.classList.toggle('expanded', autoExpandRoll.target);
else if (s.classList.contains('description-section'))
s.classList.toggle('expanded', autoExpandRoll.desc);
}); });
if (itemDesc && autoExpandRoll.desc) itemDesc.setAttribute('open', ''); if (itemDesc && autoExpandRoll.desc) itemDesc.setAttribute('open', '');
} }

View file

@ -185,7 +185,10 @@ export default class DHItem extends foundry.documents.Item {
tags: this._getTags() tags: this._getTags()
}, },
actions: item.system.actionsList, actions: item.system.actionsList,
description: this.system.description description: await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.system.description, {
relativeTo: this.parent,
rollData: this.parent?.getRollData() ?? {}
})
}; };
const msg = { const msg = {

View file

@ -1,78 +1,30 @@
export default class DHToken extends CONFIG.Token.documentClass { export default class DHToken extends CONFIG.Token.documentClass {
/** /**@inheritdoc */
* Inspect the Actor data model and identify the set of attributes which could be used for a Token Bar. static getTrackedAttributeChoices(attributes, typeKey) {
* @param {object} attributes The tracked attributes which can be chosen from
* @returns {object} A nested object of attribute choices to display
*/
static getTrackedAttributeChoices(attributes, model) {
attributes = attributes || this.getTrackedAttributes(); attributes = attributes || this.getTrackedAttributes();
const barGroup = game.i18n.localize('TOKEN.BarAttributes'); const barGroup = game.i18n.localize('TOKEN.BarAttributes');
const valueGroup = game.i18n.localize('TOKEN.BarValues'); const valueGroup = game.i18n.localize('TOKEN.BarValues');
const actorModel = typeKey ? game.system.api.data.actors[`Dh${typeKey.capitalize()}`] : null;
const getLabel = path => {
const label = actorModel?.schema.getField(path)?.label;
return label ? game.i18n.localize(label) : path;
};
const bars = attributes.bar.map(v => { const bars = attributes.bar.map(v => {
const a = v.join('.'); const a = v.join('.');
const modelLabel = model ? game.i18n.localize(model.schema.getField(`${a}.value`).label) : null; return { group: barGroup, value: a, label: getLabel(a) };
return { group: barGroup, value: a, label: modelLabel ? modelLabel : a };
}); });
bars.sort((a, b) => a.label.compare(b.label)); bars.sort((a, b) => a.value.compare(b.value));
const invalidAttributes = [ const values = attributes.value.map(v => {
'gold',
'levelData',
'actions',
'biography',
'class',
'multiclass',
'companion',
'notes',
'partner',
'description',
'impulses',
'tier',
'type'
];
const values = attributes.value.reduce((acc, v) => {
const a = v.join('.'); const a = v.join('.');
if (invalidAttributes.some(x => a.startsWith(x))) return acc; return { group: valueGroup, value: a, label: getLabel(a) };
});
const field = model ? model.schema.getField(a) : null;
const modelLabel = field ? game.i18n.localize(field.label) : null;
const hint = field ? game.i18n.localize(field.hint) : null;
acc.push({ group: valueGroup, value: a, label: modelLabel ? modelLabel : a, hint: hint });
return acc;
}, []);
values.sort((a, b) => a.label.compare(b.label));
values.sort((a, b) => a.value.compare(b.value));
return bars.concat(values); return bars.concat(values);
} }
static _getTrackedAttributesFromSchema(schema, _path = []) {
const attributes = { bar: [], value: [] };
for (const [name, field] of Object.entries(schema.fields)) {
const p = _path.concat([name]);
if (field instanceof foundry.data.fields.NumberField) attributes.value.push(p);
if (field instanceof foundry.data.fields.BooleanField && field.options.isAttributeChoice)
attributes.value.push(p);
if (field instanceof foundry.data.fields.StringField) attributes.value.push(p);
if (field instanceof foundry.data.fields.ArrayField) attributes.value.push(p);
const isSchema = field instanceof foundry.data.fields.SchemaField;
const isModel = field instanceof foundry.data.fields.EmbeddedDataField;
if (isSchema || isModel) {
const schema = isModel ? field.model.schema : field;
const isBar = schema.has && schema.has('value') && schema.has('max');
if (isBar) attributes.bar.push(p);
else {
const inner = this.getTrackedAttributes(schema, p);
attributes.bar.push(...inner.bar);
attributes.value.push(...inner.value);
}
}
}
return attributes;
}
_shouldRecordMovementHistory() { _shouldRecordMovementHistory() {
return false; return false;
} }
@ -269,7 +221,7 @@ export default class DHToken extends CONFIG.Token.documentClass {
// Hexagon symmetry // Hexagon symmetry
if (columns) { if (columns) {
const rowData = BaseToken.#getHexagonalShape(height, width, shape, false); const rowData = DHToken.#getHexagonalShape(height, width, shape, false);
if (!rowData) return null; if (!rowData) return null;
// Transpose the offsets/points of the shape in row orientation // Transpose the offsets/points of the shape in row orientation

View file

@ -86,19 +86,22 @@ export const enrichedDualityRoll = async (
{ reaction, traitValue, target, difficulty, title, label, advantage, grantResources, customConfig }, { reaction, traitValue, target, difficulty, title, label, advantage, grantResources, customConfig },
event event
) => { ) => {
const shouldGrantResources = grantResources === undefined ? true : grantResources;
const config = { const config = {
event: event ?? {}, event: event ?? {},
title: title, title: title,
headerTitle: label, headerTitle: label,
actionType: reaction ? 'reaction' : null,
roll: { roll: {
trait: traitValue && target ? traitValue : null, trait: traitValue && target ? traitValue : null,
difficulty: difficulty, difficulty: difficulty,
advantage, advantage
type: reaction ? 'reaction' : null // type: reaction ? 'reaction' : null //not needed really but keeping it for troubleshooting
}, },
skips: { skips: {
resources: !grantResources, resources: !shouldGrantResources,
triggers: !grantResources triggers: !shouldGrantResources
}, },
type: 'trait', type: 'trait',
hasRoll: true, hasRoll: true,

View file

@ -4,6 +4,7 @@ export default function DhTemplateEnricher(match, _options) {
const params = parseInlineParams(match[1]); const params = parseInlineParams(match[1]);
const { type, angle = CONFIG.MeasuredTemplate.defaults.angle, inline = false } = params; const { type, angle = CONFIG.MeasuredTemplate.defaults.angle, inline = false } = params;
const direction = Number(params.direction) || 0; const direction = Number(params.direction) || 0;
params.range = params.range?.toLowerCase();
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(

View file

@ -12,7 +12,7 @@ export function rollCommandToJSON(text) {
const flavor = flavorMatch ? flavorMatch[1] : null; const flavor = flavorMatch ? flavorMatch[1] : null;
// Match key="quoted string" OR key=unquotedValue // Match key="quoted string" OR key=unquotedValue
const PAIR_RE = /(\w+)=("(?:[^"\\]|\\.)*"|\S+)/g; const PAIR_RE = /(\w+)\s*=\s*("(?:[^"\\]|\\.)*"|[^\]\}\s]+)/g; //updated regex to allow escaped quotes in quoted strings and avoid matching closing brackets/braces
const result = {}; const result = {};
for (const [, key, raw] of text.matchAll(PAIR_RE)) { for (const [, key, raw] of text.matchAll(PAIR_RE)) {
let value; let value;
@ -119,8 +119,8 @@ export const tagifyElement = (element, baseOptions, onChange, tagifyOptions = {}
}), }),
maxTags: typeof maxTags === 'function' ? maxTags() : maxTags, maxTags: typeof maxTags === 'function' ? maxTags() : maxTags,
dropdown: { dropdown: {
searchKeys: ['value', 'name'],
mapValueTo: 'name', mapValueTo: 'name',
searchKeys: ['value'],
enabled: 0, enabled: 0,
maxItems: 100, maxItems: 100,
closeOnSelect: true, closeOnSelect: true,
@ -472,7 +472,7 @@ export function refreshIsAllowed(allowedTypes, typeToCheck) {
case CONFIG.DH.GENERAL.refreshTypes.scene.id: case CONFIG.DH.GENERAL.refreshTypes.scene.id:
case CONFIG.DH.GENERAL.refreshTypes.session.id: case CONFIG.DH.GENERAL.refreshTypes.session.id:
case CONFIG.DH.GENERAL.refreshTypes.longRest.id: case CONFIG.DH.GENERAL.refreshTypes.longRest.id:
return allowedTypes.includes(typeToCheck); return allowedTypes.includes?.(typeToCheck) ?? allowedTypes.has(typeToCheck);
case CONFIG.DH.GENERAL.refreshTypes.shortRest.id: case CONFIG.DH.GENERAL.refreshTypes.shortRest.id:
return allowedTypes.some( return allowedTypes.some(
x => x =>
@ -495,3 +495,183 @@ export function htmlToText(html) {
return tempDivElement.textContent || tempDivElement.innerText || ''; return tempDivElement.textContent || tempDivElement.innerText || '';
} }
export async function getFeaturesHTMLData(features) {
const result = [];
for (const feature of features) {
if (feature) {
const base = feature.item ?? feature;
const item = base.system ? base : await foundry.utils.fromUuid(base.uuid);
const itemDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
item.system.description
);
result.push({ label: item.name, description: itemDescription });
}
}
return result;
}
/**
* Given a simple flavor-less formula with only +/- operators, returns a list of damage partial terms.
* All subtracted terms become negative terms.
* If there are no dice, it returns 0d1 for that term.
*/
export function parseTermsFromSimpleFormula(formula) {
const roll = formula instanceof Roll ? formula : new Roll(formula);
// Parse from right to left so that when we hit an operator, we already have the term.
return roll.terms.reduceRight((result, term) => {
// Ignore + terms, we assume + by default
if (term.expression === ' + ') return result;
// - terms modify the last term we parsed
if (term.expression === ' - ') {
const termToModify = result[0];
if (termToModify) {
if (termToModify.bonus) termToModify.bonus *= -1;
if (termToModify.dice) termToModify.dice *= -1;
}
return result;
}
result.unshift({
bonus: term instanceof foundry.dice.terms.NumericTerm ? term.number : 0,
diceQuantity: term instanceof foundry.dice.terms.Die ? term.number : 0,
faces: term.faces ?? 1
});
return result;
}, []);
}
/**
* Calculates the expectede value from a formula or the results of parseTermsFromSimpleFormula.
* @returns {number} the average result of rolling the given dice
*/
export function calculateExpectedValue(formulaOrTerms) {
const terms = Array.isArray(formulaOrTerms)
? formulaOrTerms
: typeof formulaOrTerms === 'string'
? parseTermsFromSimpleFormula(formulaOrTerms)
: [formulaOrTerms];
return terms.reduce((r, t) => r + (t.bonus ?? 0) + (t.diceQuantity ? (t.diceQuantity * (t.faces + 1)) / 2 : 0), 0);
}
export function parseRallyDice(value, effect) {
const legacyStartsWithPrefix = value.toLowerCase().startsWith('d');
const workingValue = legacyStartsWithPrefix ? value.slice(1) : value;
const dataParsedValue = itemAbleRollParse(workingValue, effect.parent);
return `d${game.system.api.documents.DhActiveEffect.effectSafeEval(dataParsedValue)}`;
}
/**
* Refreshes character and/or adversary resources.
* @param { string[] } refreshTypes Which type of features to refresh using IDs from CONFIG.DH.GENERAL.refreshTypes
* @param { string[] = ['character', 'adversary'] } actorTypes Which actor types should refresh their features. Defaults to character and adversary.
* @param { boolean = true } sendRefreshMessage If a chat message should be created detailing the refresh
* @return { Actor[] } The actors that had their features refreshed
*/
export async function RefreshFeatures(
refreshTypes = [],
actorTypes = ['character', 'adversary'],
sendNotificationMessage = true,
sendRefreshMessage = true
) {
const refreshedActors = {};
for (let actor of game.actors) {
if (actorTypes.includes(actor.type) && actor.prototypeToken.actorLink) {
const updates = {};
for (let item of actor.items) {
if (
item.system.metadata?.hasResource &&
refreshIsAllowed(refreshTypes, item.system.resource?.recovery)
) {
if (!refreshedActors[actor.id])
refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() };
refreshedActors[actor.id].refreshed.add(
game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[item.system.resource.recovery].label)
);
if (!updates[item.id]?.system) updates[item.id] = { system: {} };
const increasing =
item.system.resource.progression === CONFIG.DH.ITEM.itemResourceProgression.increasing.id;
updates[item.id].system = {
...updates[item.id].system,
'resource.value': increasing
? 0
: game.system.api.documents.DhActiveEffect.effectSafeEval(
Roll.replaceFormulaData(item.system.resource.max, actor.getRollData())
)
};
}
if (item.system.metadata?.hasActions) {
const usedTypes = new Set();
const actions = item.system.actions.filter(action => {
if (refreshIsAllowed(refreshTypes, action.uses.recovery)) {
usedTypes.add(action.uses.recovery);
return true;
}
return false;
});
if (actions.length === 0) continue;
if (!refreshedActors[actor.id])
refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() };
refreshedActors[actor.id].refreshed.add(
...usedTypes.map(type => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[type].label))
);
if (!updates[item.id]?.system) updates[item.id] = { system: {} };
updates[item.id].system = {
...updates[item.id].system,
...actions.reduce(
(acc, action) => {
acc.actions[action.id] = { 'uses.value': 0 };
return acc;
},
{ actions: updates[item.id].system.actions ?? {} }
)
};
}
}
for (let key in updates) {
const update = updates[key];
await actor.items.get(key).update(update);
}
}
}
const types = refreshTypes.map(x => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[x].label)).join(', ');
if (sendNotificationMessage) {
ui.notifications.info(
game.i18n.format('DAGGERHEART.UI.Notifications.gmMenuRefresh', {
types: `[${types}]`
})
);
}
if (sendRefreshMessage) {
const cls = getDocumentClass('ChatMessage');
const msg = {
user: game.user.id,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/refreshMessage.hbs',
{
types: types
}
),
title: game.i18n.localize('DAGGERHEART.UI.Chat.refreshMessage.title'),
speaker: cls.getSpeaker()
};
cls.create(msg);
}
return refreshedActors;
}

View file

@ -39,6 +39,7 @@ export const preloadHandlebarsTemplates = async function () {
'systems/daggerheart/templates/dialogs/downtime/activities.hbs', 'systems/daggerheart/templates/dialogs/downtime/activities.hbs',
'systems/daggerheart/templates/dialogs/dice-roll/costSelection.hbs', 'systems/daggerheart/templates/dialogs/dice-roll/costSelection.hbs',
'systems/daggerheart/templates/ui/chat/parts/roll-part.hbs', 'systems/daggerheart/templates/ui/chat/parts/roll-part.hbs',
'systems/daggerheart/templates/ui/chat/parts/description-part.hbs',
'systems/daggerheart/templates/ui/chat/parts/damage-part.hbs', 'systems/daggerheart/templates/ui/chat/parts/damage-part.hbs',
'systems/daggerheart/templates/ui/chat/parts/target-part.hbs', 'systems/daggerheart/templates/ui/chat/parts/target-part.hbs',
'systems/daggerheart/templates/ui/chat/parts/button-part.hbs', 'systems/daggerheart/templates/ui/chat/parts/button-part.hbs',

View file

@ -7,7 +7,7 @@ import {
DhHomebrewSettings, DhHomebrewSettings,
DhVariantRuleSettings DhVariantRuleSettings
} from '../applications/settings/_module.mjs'; } from '../applications/settings/_module.mjs';
import { DhTagTeamRoll } from '../data/_module.mjs'; import { CompendiumBrowserSettings, DhTagTeamRoll } from '../data/_module.mjs';
export const registerDHSettings = () => { export const registerDHSettings = () => {
registerMenuSettings(); registerMenuSettings();
@ -126,7 +126,7 @@ const registerNonConfigSettings = () => {
type: Number, type: Number,
default: 0, default: 0,
onChange: () => { onChange: () => {
if (ui.resources) ui.resources.render({ force: true }); if (ui.resources) ui.resources.render();
ui.combat.render({ force: true }); ui.combat.render({ force: true });
} }
}); });
@ -142,6 +142,12 @@ const registerNonConfigSettings = () => {
config: false, config: false,
type: DhTagTeamRoll type: DhTagTeamRoll
}); });
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings, {
scope: 'client',
config: false,
type: CompendiumBrowserSettings
});
}; };
/** /**

View file

@ -38,7 +38,8 @@ export const RefreshType = {
Countdown: 'DhCoundownRefresh', Countdown: 'DhCoundownRefresh',
TagTeamRoll: 'DhTagTeamRollRefresh', TagTeamRoll: 'DhTagTeamRollRefresh',
EffectsDisplay: 'DhEffectsDisplayRefresh', EffectsDisplay: 'DhEffectsDisplayRefresh',
Scene: 'DhSceneRefresh' Scene: 'DhSceneRefresh',
CompendiumBrowser: 'DhCompendiumBrowserRefresh'
}; };
export const registerSocketHooks = () => { export const registerSocketHooks = () => {

View file

@ -82,7 +82,7 @@
"enabled": false "enabled": false
}, },
"flatMultiplier": 3, "flatMultiplier": 3,
"dice": "d10", "dice": "d20",
"bonus": null, "bonus": null,
"multiplier": "flat" "multiplier": "flat"
}, },

View file

@ -246,7 +246,7 @@
"name": "Group Attack", "name": "Group Attack",
"type": "feature", "type": "feature",
"system": { "system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 4 physical damage each. Combine this damage.</p>", "description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"resource": null, "resource": null,
"actions": { "actions": {
"vgguNWz8vG8aoLXR": { "vgguNWz8vG8aoLXR": {

View file

@ -218,10 +218,10 @@
}, },
"items": [ "items": [
{ {
"name": "Horde (1d6+3)", "name": "Horde",
"type": "feature", "type": "feature",
"system": { "system": {
"description": "<p>When the @Lookup[@name] has marked half or more of their HP, their standard attack deals <strong>1d6+3</strong> physical damage instead.</p>", "description": "<p>When the @Lookup[@name] has marked half or more of their HP, their standard attack deals <strong>@Lookup[@system.attack.altDamageFormula]</strong> physical damage instead.</p>",
"resource": null, "resource": null,
"actions": {}, "actions": {},
"originItemType": null, "originItemType": null,

View file

@ -239,7 +239,7 @@
"name": "Group Attack", "name": "Group Attack",
"type": "feature", "type": "feature",
"system": { "system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 6 physical damage each. Combine this damage.</p>", "description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"resource": null, "resource": null,
"actions": { "actions": {
"cbAvPSIhwBMBTI3D": { "cbAvPSIhwBMBTI3D": {

View file

@ -239,7 +239,7 @@
"name": "Group Attack", "name": "Group Attack",
"type": "feature", "type": "feature",
"system": { "system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all Cult @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 5 physical damage each. Combine this damage.</p>", "description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all Cult @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"resource": null, "resource": null,
"actions": { "actions": {
"EH1preaTWBD4rOvx": { "EH1preaTWBD4rOvx": {

View file

@ -224,10 +224,10 @@
}, },
"items": [ "items": [
{ {
"name": "Horde (2d4+1)", "name": "Horde",
"type": "feature", "type": "feature",
"system": { "system": {
"description": "<p>When the @Lookup[@name] has marked half or more of their HP, their standard attack deals <strong>2d4+1</strong> physical damage instead.</p>", "description": "<p>When the @Lookup[@name] has marked half or more of their HP, their standard attack deals <strong>@Lookup[@system.attack.altDamageFormula]</strong> physical damage instead.</p>",
"resource": null, "resource": null,
"actions": {}, "actions": {},
"originItemType": null, "originItemType": null,

View file

@ -218,10 +218,10 @@
}, },
"items": [ "items": [
{ {
"name": "Horde (2d4+1)", "name": "Horde",
"type": "feature", "type": "feature",
"system": { "system": {
"description": "<p>When the @Lookup[@name] have marked half or more of their HP, their standard attack deals <strong>2d4+1</strong> physical damage instead.</p>", "description": "<p>When the @Lookup[@name] have marked half or more of their HP, their standard attack deals <strong>@Lookup[@system.attack.altDamageFormula]</strong> physical damage instead.</p>",
"resource": null, "resource": null,
"actions": {}, "actions": {},
"originItemType": null, "originItemType": null,

View file

@ -239,7 +239,7 @@
"name": "Group Attack", "name": "Group Attack",
"type": "feature", "type": "feature",
"system": { "system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 5 physical damage each. Combine this damage.</p>", "description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"resource": null, "resource": null,
"actions": { "actions": {
"vXHZVb0Y7Hqu3uso": { "vXHZVb0Y7Hqu3uso": {

View file

@ -317,7 +317,7 @@
"name": "Group Attack", "name": "Group Attack",
"type": "feature", "type": "feature",
"system": { "system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 12 physical damage each. Combine this damage.</p>", "description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"resource": null, "resource": null,
"actions": { "actions": {
"QHNRSEQmqOcaoXq4": { "QHNRSEQmqOcaoXq4": {

View file

@ -229,7 +229,7 @@
"_id": "9RduwBLYcBaiouYk", "_id": "9RduwBLYcBaiouYk",
"img": "icons/creatures/magical/humanoid-silhouette-aliens-green.webp", "img": "icons/creatures/magical/humanoid-silhouette-aliens-green.webp",
"system": { "system": {
"description": "<p>When the @Lookup[@name] have marked half or more of their HP, their standard attack deals <strong>1d4+1</strong> physical damage instead.</p>", "description": "<p>When the @Lookup[@name] have marked half or more of their HP, their standard attack deals <strong>@Lookup[@system.attack.altDamageFormula]</strong> physical damage instead.</p>",
"resource": null, "resource": null,
"actions": {}, "actions": {},
"originItemType": null, "originItemType": null,

View file

@ -248,7 +248,7 @@
"_id": "fsaBlCjTdq1jM23G", "_id": "fsaBlCjTdq1jM23G",
"img": "icons/creatures/abilities/tail-strike-bone-orange.webp", "img": "icons/creatures/abilities/tail-strike-bone-orange.webp",
"system": { "system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 1 physical damage each. Combine this damage.</p>", "description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"resource": null, "resource": null,
"actions": { "actions": {
"q8chow47nQLR9qeF": { "q8chow47nQLR9qeF": {

View file

@ -55,7 +55,7 @@
"max": 1 "max": 1
}, },
"stress": { "stress": {
"max": 1 "max": 2
} }
}, },
"attack": { "attack": {
@ -239,7 +239,7 @@
"name": "Group Attack", "name": "Group Attack",
"type": "feature", "type": "feature",
"system": { "system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 5 physical damage each. Combine this damage.</p>", "description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"resource": null, "resource": null,
"actions": { "actions": {
"DjbPQowW1OdBD9Zn": { "DjbPQowW1OdBD9Zn": {

View file

@ -55,7 +55,7 @@
"max": 1 "max": 1
}, },
"stress": { "stress": {
"max": 1 "max": 2
} }
}, },
"attack": { "attack": {
@ -294,7 +294,7 @@
"name": "Group Attack", "name": "Group Attack",
"type": "feature", "type": "feature",
"system": { "system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 10 physical damage each. Combine this damage.</p>", "description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"resource": null, "resource": null,
"actions": { "actions": {
"eo7J0v1B5zPHul1M": { "eo7J0v1B5zPHul1M": {

View file

@ -248,7 +248,7 @@
"_id": "1k5TmQIAunM7Bv32", "_id": "1k5TmQIAunM7Bv32",
"img": "icons/creatures/abilities/tail-strike-bone-orange.webp", "img": "icons/creatures/abilities/tail-strike-bone-orange.webp",
"system": { "system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name] within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 2 physical damage each. Combine this damage.</p>", "description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name] within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"resource": null, "resource": null,
"actions": { "actions": {
"aoQDb2m32NDxE6ZP": { "aoQDb2m32NDxE6ZP": {

View file

@ -33,7 +33,7 @@
"reduction": 0 "reduction": 0
} }
}, },
"type": "standard", "type": "ranged",
"notes": "", "notes": "",
"hordeHp": 1, "hordeHp": 1,
"experiences": { "experiences": {

View file

@ -104,6 +104,7 @@
] ]
}, },
"type": "attack", "type": "attack",
"range": "melee",
"chatDisplay": false "chatDisplay": false
}, },
"attribution": { "attribution": {

View file

@ -242,7 +242,7 @@
"_id": "K08WlZwGqzEo4idT", "_id": "K08WlZwGqzEo4idT",
"img": "icons/creatures/abilities/tail-strike-bone-orange.webp", "img": "icons/creatures/abilities/tail-strike-bone-orange.webp",
"system": { "system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 4 physical damage each. Combine this damage.</p>", "description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"resource": null, "resource": null,
"actions": { "actions": {
"xTMNAHcoErKuR6TZ": { "xTMNAHcoErKuR6TZ": {

View file

@ -33,7 +33,7 @@
"reduction": 0 "reduction": 0
} }
}, },
"type": "standard", "type": "bruiser",
"notes": "", "notes": "",
"hordeHp": 1, "hordeHp": 1,
"experiences": {}, "experiences": {},
@ -66,12 +66,12 @@
"tier": 3, "tier": 3,
"description": "<p>A sturdy animate old-growth tree.</p>", "description": "<p>A sturdy animate old-growth tree.</p>",
"attack": { "attack": {
"name": "Attack", "name": "Branch",
"roll": { "roll": {
"type": "attack", "type": "attack",
"bonus": 2 "bonus": 2
}, },
"range": "close", "range": "veryClose",
"damage": { "damage": {
"parts": [ "parts": [
{ {

View file

@ -97,7 +97,7 @@
}, },
"img": "icons/creatures/claws/claw-talons-yellow-red.webp", "img": "icons/creatures/claws/claw-talons-yellow-red.webp",
"type": "attack", "type": "attack",
"range": "melee", "range": "veryClose",
"chatDisplay": false "chatDisplay": false
}, },
"attribution": { "attribution": {
@ -239,7 +239,7 @@
"name": "Group Attack", "name": "Group Attack",
"type": "feature", "type": "feature",
"system": { "system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 11 physical damage each. Combine this damage.</p>", "description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"resource": null, "resource": null,
"actions": { "actions": {
"tvQetauskZoHDR5y": { "tvQetauskZoHDR5y": {

View file

@ -229,7 +229,7 @@
"_id": "Q7DRbWjHl64CNwag", "_id": "Q7DRbWjHl64CNwag",
"img": "icons/creatures/magical/humanoid-silhouette-aliens-green.webp", "img": "icons/creatures/magical/humanoid-silhouette-aliens-green.webp",
"system": { "system": {
"description": "<p>When the @Lookup[@name] have marked half or more of their HP, their standard attack deals <strong>1d4+1</strong> physical damage instead.</p>", "description": "<p>When the @Lookup[@name] have marked half or more of their HP, their standard attack deals <strong>@Lookup[@system.attack.altDamageFormula]</strong> physical damage instead.</p>",
"resource": null, "resource": null,
"actions": {}, "actions": {},
"originItemType": null, "originItemType": null,

View file

@ -242,7 +242,7 @@
"_id": "R9vrwFNl5BD1YXJo", "_id": "R9vrwFNl5BD1YXJo",
"img": "icons/creatures/abilities/tail-strike-bone-orange.webp", "img": "icons/creatures/abilities/tail-strike-bone-orange.webp",
"system": { "system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 2 physical damage each. Combine this damage.</p>", "description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"resource": null, "resource": null,
"actions": { "actions": {
"DJBNtd3hWjwsjPwq": { "DJBNtd3hWjwsjPwq": {

View file

@ -242,7 +242,7 @@
"_id": "CQZQiEiRH70Br5Ge", "_id": "CQZQiEiRH70Br5Ge",
"img": "icons/creatures/abilities/tail-strike-bone-orange.webp", "img": "icons/creatures/abilities/tail-strike-bone-orange.webp",
"system": { "system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 3 physical damage each. Combine this damage.</p>", "description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"resource": null, "resource": null,
"actions": { "actions": {
"ghgFZskDiizJDjcn": { "ghgFZskDiizJDjcn": {

View file

@ -242,7 +242,7 @@
"_id": "wl9KKEpVWDBu62hU", "_id": "wl9KKEpVWDBu62hU",
"img": "icons/creatures/abilities/tail-strike-bone-orange.webp", "img": "icons/creatures/abilities/tail-strike-bone-orange.webp",
"system": { "system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 1 physical damage each. Combine this damage.</p>", "description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"resource": null, "resource": null,
"actions": { "actions": {
"Sz55uB8xkoNytLwJ": { "Sz55uB8xkoNytLwJ": {

View file

@ -223,7 +223,7 @@
"_id": "9Zuu892SO5NmtI4w", "_id": "9Zuu892SO5NmtI4w",
"img": "icons/creatures/magical/humanoid-silhouette-aliens-green.webp", "img": "icons/creatures/magical/humanoid-silhouette-aliens-green.webp",
"system": { "system": {
"description": "<p>When the @Lookup[@name] has marked half or more of their HP, their standard attack deals <strong>1d4+1</strong> physical damage instead.</p>", "description": "<p>When the @Lookup[@name] has marked half or more of their HP, their standard attack deals <strong>@Lookup[@system.attack.altDamageFormula]</strong> physical damage instead.</p>",
"resource": null, "resource": null,
"actions": {}, "actions": {},
"originItemType": null, "originItemType": null,

View file

@ -254,12 +254,12 @@
}, },
"items": [ "items": [
{ {
"name": "Horde (1d4+2)", "name": "Horde",
"type": "feature", "type": "feature",
"_id": "4dSzqtYvH385r9Ng", "_id": "4dSzqtYvH385r9Ng",
"img": "icons/creatures/magical/humanoid-silhouette-aliens-green.webp", "img": "icons/creatures/magical/humanoid-silhouette-aliens-green.webp",
"system": { "system": {
"description": "<p>When the @Lookup[@name] has marked half or more of their HP, their standard attack deals <strong>1d4+2</strong> physical damage instead.</p>", "description": "<p>When the @Lookup[@name] has marked half or more of their HP, their standard attack deals <strong>@Lookup[@system.attack.altDamageFormula]</strong> physical damage instead.</p>",
"resource": null, "resource": null,
"actions": {}, "actions": {},
"originItemType": null, "originItemType": null,

View file

@ -281,7 +281,7 @@
"_id": "WiobzuyvJ46zfsOv", "_id": "WiobzuyvJ46zfsOv",
"img": "icons/creatures/abilities/tail-strike-bone-orange.webp", "img": "icons/creatures/abilities/tail-strike-bone-orange.webp",
"system": { "system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 2 physical damage each. Combine this damage.</p>", "description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"resource": null, "resource": null,
"actions": { "actions": {
"ZC5pKIb9N82vgMWu": { "ZC5pKIb9N82vgMWu": {

View file

@ -97,8 +97,8 @@
] ]
}, },
"type": "attack", "type": "attack",
"chatDisplay": false, "range": "melee",
"range": "" "chatDisplay": false
}, },
"attribution": { "attribution": {
"source": "Daggerheart SRD", "source": "Daggerheart SRD",
@ -239,7 +239,7 @@
"name": "Group Attack", "name": "Group Attack",
"type": "feature", "type": "feature",
"system": { "system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 8 physical damage each. Combine this damage.</p>", "description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"resource": null, "resource": null,
"actions": { "actions": {
"euP8VA4wvfsCpwN1": { "euP8VA4wvfsCpwN1": {

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