Compare commits

..

50 commits
main ... 2.0.2

Author SHA1 Message Date
WBHarry
f6b3c297bd Merge branch 'main' into release 2026-04-03 23:09:50 +02:00
WBHarry
cd93246358 Merge branch 'main' into release 2026-04-02 00:38:31 +02:00
WBHarry
3a4b66f487 Merge branch 'main' into release 2026-03-27 08:27:10 +01:00
WBHarry
cdf159b4a7 Merge branch 'main' into release 2026-03-23 01:13:44 +01:00
WBHarry
413a37483c Merge branch 'main' into release 2026-03-17 22:45:41 +01:00
WBHarry
c5e21d9d92 Merge branch 'main' into release 2026-03-16 01:32:48 +01:00
WBHarry
10c0b6b51e Merge branch 'main' into release 2026-03-15 11:44:45 +01:00
WBHarry
652a554c9a Merge branch 'main' into release 2026-03-13 01:29:02 +01:00
WBHarry
c0ed5fe697 Merge branch 'main' into release 2026-03-13 00:19:34 +01:00
WBHarry
ff65abe09b Merge branch 'main' into release 2026-03-07 01:33:55 +01:00
WBHarry
ec7a7b378d Merge branch 'main' into release 2026-02-26 12:47:44 +01:00
WBHarry
8e34356905 Merge branch 'main' into release 2026-02-12 22:30:17 +01:00
WBHarry
35bceac520 Merge branch 'main' into release 2026-02-09 12:43:51 +01:00
WBHarry
436acb0617 Merge branch 'main' into release 2026-02-04 00:24:56 +01:00
WBHarry
1d114633f5 Merge branch 'main' into release 2026-02-02 02:07:29 +01:00
WBHarry
1c70b46639 Merge branch 'main' into release 2026-02-01 01:20:57 +01:00
WBHarry
ae38245877 Merge branch 'main' into release 2026-01-28 12:57:18 +01:00
WBHarry
af5d3d4568 Merge branch 'main' into release 2026-01-25 17:07:47 +01:00
WBHarry
6b5c1ff965 Merge branch 'main' into release 2026-01-25 16:22:39 +01:00
WBHarry
42a22a49f0 Merge branch 'main' into release 2026-01-23 11:52:22 +01:00
WBHarry
da77c2a190 Merge branch 'main' into release 2026-01-17 01:42:52 +01:00
WBHarry
68decf0b57 Merge branch 'main' into release 2026-01-16 21:48:15 +01:00
WBHarry
4f0670cc35 Merge branch 'main' into release 2026-01-16 10:16:12 +01:00
WBHarry
b346ce6766 Merge branch 'main' into release 2026-01-16 10:06:17 +01:00
WBHarry
dddd0581f7 Merge branch 'main' into release 2026-01-15 10:16:38 +01:00
WBHarry
83329fac46 Merge branch 'main' into release 2026-01-10 00:23:09 +01:00
WBHarry
ee0b7b2792 Merge branch 'main' into release 2026-01-09 17:49:14 +01:00
WBHarry
4d062a6892 Merge branch 'main' into release 2025-12-31 04:52:34 +01:00
WBHarry
487c1fd9a2 Merge branch 'main' into release 2025-12-29 14:02:53 +01:00
WBHarry
3aa5cd806a Merge branch 'main' into release 2025-12-27 18:17:36 +01:00
WBHarry
2e93b79633 Merge branch 'main' into release 2025-12-24 03:06:02 +01:00
WBHarry
244dbd4902 Merge branch 'main' into release 2025-12-24 01:18:51 +01:00
WBHarry
c7aed6825a Merge branch 'main' into release 2025-12-13 23:06:23 +01:00
WBHarry
9cb5112b62 Merge branch 'main' into release 2025-12-08 02:35:06 +01:00
WBHarry
81b6f7fc51 Merge branch 'main' into release 2025-12-07 00:54:06 +01:00
WBHarry
828fffd552 Merge branch 'main' into release 2025-11-26 09:47:07 +01:00
WBHarry
fc5626ac47 Merge branch 'main' into release 2025-11-25 00:52:11 +01:00
WBHarry
2e62545aa7 Merge branch 'main' into release 2025-11-23 15:41:24 +01:00
WBHarry
b09c712dd5 Merging main 2025-11-20 11:48:58 +01:00
WBHarry
ca4336bd39 Merge branch 'main' into release 2025-11-17 16:55:14 +01:00
WBHarry
77ac11c522 Merge branch 'main' into release 2025-11-17 10:17:50 +01:00
WBHarry
50311679a5 Merge branch 'main' into release 2025-11-11 22:15:30 +01:00
WBHarry
3a7bcd1b0a Merge branch 'main' into release 2025-11-11 18:04:23 +01:00
WBHarry
511e4bd644 Merge branch 'main' into release 2025-11-11 16:23:35 +01:00
WBHarry
395820513b Merge branch 'main' into release 2025-11-11 16:06:03 +01:00
WBHarry
3566ea3fd3 Merge branch 'main' into release 2025-08-26 20:32:04 +02:00
WBHarry
29d502fb97 Merge branch 'main' into release 2025-08-24 21:11:38 +02:00
WBHarry
685a25d25a Merge branch 'main' into release 2025-08-22 01:47:03 +02:00
WBHarry
dd045b3df7 Merge branch 'main' into release 2025-08-19 20:58:05 +02:00
WBHarry
0aabcec340 Raised version 2025-08-19 18:56:30 +02:00
268 changed files with 2854 additions and 7180 deletions

View file

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

View file

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

View file

@ -35,7 +35,7 @@ jobs:
env:
version: ${{steps.get_version.outputs.version-without-v}}
url: https://github.com/${{github.repository}}
manifest: https://raw.githubusercontent.com/${{github.repository}}/v14/system.json
manifest: https://raw.githubusercontent.com/${{github.repository}}/main/system.json
download: https://github.com/${{github.repository}}/releases/download/${{github.event.release.tag_name}}/system.zip
# Create a zip file with all files required by the module to add to the release

View file

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

View file

@ -32,11 +32,6 @@ CONFIG.Dice.daggerheart = {
FateRoll: FateRoll
};
CONFIG.RegionBehavior.dataModels = {
...CONFIG.RegionBehavior.dataModels,
...data.regionBehaviors
};
Object.assign(CONFIG.Dice.termTypes, dice.diceTypes);
CONFIG.Actor.documentClass = documents.DhpActor;
@ -348,20 +343,7 @@ Hooks.on(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, async data => {
}
});
Hooks.on(CONFIG.DH.HOOKS.hooksConfig.groupRollStart, async data => {
if (data.openForAllPlayers && data.partyId) {
const party = game.actors.get(data.partyId);
if (!party) return;
const dialog = new game.system.api.applications.dialogs.GroupRollDialog(party);
dialog.tabGroups.application = 'groupRoll';
await dialog.render({ force: true });
}
});
const updateActorsRangeDependentEffects = async token => {
if (!token) return;
const rangeMeasurement = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.variantRules

View file

@ -1,14 +0,0 @@
import globals from 'globals';
import { defineConfig } from 'eslint/config';
import prettier from 'eslint-plugin-prettier';
export default defineConfig([
{ files: ['**/*.{js,mjs,cjs}'], languageOptions: { globals: globals.browser } },
{ plugins: { prettier } },
{
files: ['**/*.{js,mjs,cjs}'],
rules: {
'prettier/prettier': 'error'
}
}
]);

View file

@ -74,7 +74,9 @@
"name": "Summon",
"tooltip": "Create tokens in the scene.",
"error": "You do not have permission to summon tokens or there is no active scene.",
"invalidDrop": "You can only drop Actor entities to summon."
"invalidDrop": "You can only drop Actor entities to summon.",
"chatMessageTitle": "Test2",
"chatMessageHeaderTitle": "Summoning"
},
"transform": {
"name": "Transform",
@ -109,18 +111,11 @@
"customFormula": "Custom Formula",
"formula": "Formula"
},
"area": {
"sectionTitle": "Areas",
"shape": "Shape",
"size": "Size"
},
"displayInChat": "Display in chat",
"deleteTriggerTitle": "Delete Trigger",
"deleteTriggerContent": "Are you sure you want to delete the {trigger} trigger?",
"advantageState": "Advantage State",
"damageOnSave": "Damage on Save",
"useDefaultItemValues": "Use default Item values",
"itemDamageIsUsed": "Item Damage Is Used"
"damageOnSave": "Damage on Save"
},
"RollField": {
"diceRolling": {
@ -135,8 +130,7 @@
"attackModifier": "Attack Modifier",
"attackName": "Attack Name",
"criticalThreshold": "Critical Threshold",
"includeBase": { "label": "Use Item Damage" },
"groupAttack": { "label": "Group Attack" },
"includeBase": { "label": "Include Item Damage" },
"multiplier": "Multiplier",
"saveHint": "Set a default Trait to enable Reaction Roll. It can be changed later in Reaction Roll Dialog.",
"resultBased": {
@ -168,8 +162,7 @@
"rangeDependence": {
"title": "Range Dependence"
},
"stacking": { "title": "Stacking" },
"targetDispositions": "Affected Dispositions"
"stacking": { "title": "Stacking" }
},
"RangeDependance": {
"hint": "Settings for an optional distance at which this effect should activate",
@ -216,13 +209,7 @@
"type": { "label": "Type" }
},
"hordeDamage": "Horde Damage",
"horderHp": "Horde/HP",
"adversaryReactionRoll": {
"headerTitle": "Adversary Reaction Roll"
}
},
"Base": {
"CannotAddType": "Cannot add {itemType} items to {actorType} actors."
"horderHp": "Horde/HP"
},
"Character": {
"advantageSources": {
@ -247,8 +234,6 @@
},
"defaultHopeDice": "Default Hope Dice",
"defaultFearDice": "Default Fear Dice",
"defaultAdvantageDice": "Default Advantage Dice",
"defaultDisadvantageDice": "Default Disadvantage Dice",
"disadvantageSources": {
"label": "Disadvantage Sources",
"hint": "Add single words or short text as reminders and hints of what a character has disadvantage on."
@ -333,22 +318,6 @@
}
},
"newAdversary": "New Adversary"
},
"Party": {
"Subtitle": {
"character": "{community} {ancestry} | {subclass} {class}",
"companion": "Companion of {partner}"
},
"RemoveConfirmation": {
"title": "Remove member {name}",
"text": "Are you sure you want to remove {name} from the party?"
},
"Thresholds": {
"minor": "MIN",
"major": "MAJ",
"severe": "SEV"
},
"triggerRestContent": "This will trigger a dialog to players make their downtime moves. Are you sure?"
}
},
"APPLICATIONS": {
@ -384,7 +353,7 @@
"selectSecondaryWeapon": "Select Secondary Weapon",
"selectSubclass": "Select Subclass",
"setupSkipTitle": "Skipping Character Setup",
"setupSkipContent": "You are skipping the Character Setup by adding this manually. The character setup is the blinking button in the top-right. Are you sure you want to continue?",
"setupSkipContent": "You are skipping the Character Setup by adding this manually. The character setup is the blinking arrows in the top-right. Are you sure you want to continue?",
"startingItems": "Starting Items",
"story": "Story",
"storyExplanation": "Select which background and connection prompts you want to copy into your character's background.",
@ -482,10 +451,6 @@
"defaultOwnershipTooltip": "The default player ownership of countdowns",
"hideNewCountdowns": "Hide New Countdowns"
},
"CreateItemDialog": {
"createItem": "Create Item",
"browseCompendium": "Browse Compendium"
},
"DaggerheartMenu": {
"title": "GM Tools",
"refreshFeatures": "Refresh Features",
@ -700,12 +665,6 @@
"noPlayers": "No players to assign ownership to",
"default": "Default Ownership"
},
"PendingReactionsDialog": {
"title": "Pending Reaction Rolls Found",
"unfinishedRolls": "Some Tokens still need to roll their Reaction Roll.",
"confirmation": "Are you sure you want to continue ?",
"warning": "Undone reaction rolls will be considered as failed"
},
"ReactionRoll": {
"title": "Reaction Roll: {trait}"
},
@ -731,7 +690,7 @@
"FIELDS": {
"initiator": {
"memberId": { "label": "Initiating Character" },
"cost": { "label": "Hope Cost" }
"cost": { "label": "Initiation Cost" }
}
},
"leaderTitle": "Initiating Character",
@ -758,17 +717,6 @@
"selectRoll": "Select which roll value to be used for the Tag Team"
}
},
"GroupRollSelect": {
"title": "Group Roll",
"aidingCharacters": "Aiding Characters",
"leader": "Leader",
"leaderRoll": "Leader Roll",
"openDialogForAll": "Open Dialog For All",
"startGroupRoll": "Start Group Roll",
"finishGroupRoll": "Finish Group Roll",
"cancelConfirmTitle": "Cancel Group Roll",
"cancelConfirmText": "Are you sure you want to cancel the Group Roll? This will close it for all other players too."
},
"TokenConfig": {
"actorSizeUsed": "Actor size is set, determining the dimensions"
}
@ -789,11 +737,6 @@
"session": "Next Session",
"custom": "Custom"
},
"ActionAutomationChoices": {
"never": "Never",
"showDialog": "Show Dialog Only",
"always": "Always"
},
"AdversaryTrait": {
"relentless": {
"name": "Relentless",
@ -1323,11 +1266,6 @@
"on": "On",
"onWithToggle": "On With Toggle"
},
"SceneRangeMeasurementTypes": {
"disable": "Disable Daggerheart Range Measurement",
"default": "Default",
"custom": "Custom"
},
"SelectAction": {
"selectType": "Select Action Type",
"selectAction": "Action Selection"
@ -2027,10 +1965,6 @@
"hint": "Multiply any damage dealt to you by this number"
}
},
"Battlepoints": {
"full": "Battlepoints",
"short": "BP"
},
"Bonuses": {
"rest": {
"downtimeAction": "Downtime Action",
@ -2441,13 +2375,10 @@
"maxWithThing": "Max {thing}",
"missingDragDropThing": "Drop {thing} here",
"multiclass": "Multiclass",
"name": "Name",
"newCategory": "New Category",
"newThing": "New {thing}",
"next": "Next",
"none": "None",
"noTarget": "No current target",
"optionalThing": "Optional {thing}",
"partner": "Partner",
"player": {
"single": "Player",
@ -2474,11 +2405,9 @@
"rollDamage": "Roll Damage",
"rollWith": "{roll} Roll",
"save": "Save",
"saveSettings": "Save Settings",
"scalable": "Scalable",
"scars": "Scars",
"situationalBonus": "Situational Bonus",
"searchPlaceholder": "Search...",
"spent": "Spent",
"step": "Step",
"stress": "Stress",
@ -2629,14 +2558,8 @@
},
"Weapon": {
"weaponType": "Weapon Type",
"primaryWeapon": {
"full": "Primary Weapon",
"short": "Primary"
},
"secondaryWeapon": {
"full": "Secondary Weapon",
"short": "Secondary"
}
"primaryWeapon": "Primary Weapon",
"secondaryWeapon": "Secondary Weapon"
}
},
"MACROS": {
@ -2890,10 +2813,6 @@
}
},
"Keybindings": {
"partySheet": {
"name": "Toggle Party Sheet",
"hint": "Open or close the active party's sheet"
},
"spotlight": {
"name": "Spotlight Combatant",
"hint": "Move the spotlight to a hovered or selected token that's present in an active encounter"
@ -2938,7 +2857,6 @@
"system": "Dice Preset",
"font": "Font",
"critical": "Duality Critical Animation",
"muted": "Muted",
"diceAppearance": "Dice Appearance",
"animations": "Animations",
"defaultAnimations": "Set Animations As Player Defaults",
@ -3047,6 +2965,18 @@
"immunityTo": "Immunity: {immunities}"
},
"featureTitle": "Class Feature",
"groupRoll": {
"title": "Group Roll",
"leader": "Leader",
"partyTeam": "Party Team",
"team": "Team",
"selectLeader": "Select a Leader",
"selectMember": "Select a Member",
"rerollTitle": "Reroll Group Roll",
"rerollContent": "Are you sure you want to reroll your {trait} roll?",
"rerollTooltip": "Reroll",
"wholePartySelected": "The whole party is selected"
},
"healingRoll": {
"title": "Heal - {damage}",
"heal": "Heal",
@ -3064,9 +2994,6 @@
"resourceRoll": {
"playerMessage": "{user} rerolled their {name}"
},
"saveRoll": {
"reactionRollAllTargets": "Reaction Roll All Targets"
},
"tagTeam": {
"title": "Tag Team",
"membersTitle": "Members"
@ -3095,9 +3022,9 @@
},
"ItemBrowser": {
"title": "Daggerheart Compendium Browser",
"windowTitle": "Compendium Browser",
"hint": "Select a Folder in sidebar to start browsing through the compendium",
"browserSettings": "Browser Settings",
"searchPlaceholder": "Search...",
"columnName": "Name",
"tooltipFilters": "Filters",
"tooltipErase": "Erase",
@ -3133,7 +3060,7 @@
"weapons": "Weapons",
"armors": "Armors",
"consumables": "Consumables",
"loots": "Loot"
"loots": "Loots"
}
},
"Notifications": {
@ -3215,8 +3142,7 @@
"tokenActorsMissing": "[{names}] missing Actors",
"domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used",
"knowTheTide": "Know The Tide gained a token",
"lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}",
"noTokenTargeted": "No token is targeted"
"lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}"
},
"Progress": {
"migrationLabel": "Performing system migration. Please wait and do not close Foundry."
@ -3228,9 +3154,6 @@
"companion": "Level {level} - {partner}",
"companionNoPartner": "No Partner",
"duplicateToNewTier": "Duplicate to New Tier",
"activateParty": "Make Active Party",
"partyIsActive": "Active",
"createAdversary": "Create Adversary",
"pickTierTitle": "Pick a new tier for this adversary"
},
"daggerheartMenu": {
@ -3242,7 +3165,6 @@
"Tooltip": {
"disableEffect": "Disable Effect",
"enableEffect": "Enable Effect",
"edit": "Edit",
"openItemWorld": "Open Item World",
"openActorWorld": "Open Actor World",
"sendToChat": "Send to Chat",

View file

@ -11,10 +11,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
this.character = character;
this.setup = {
traits: Object.keys(this.character.system.traits).reduce((acc, key) => {
acc[key] = { value: null };
return acc;
}, {}),
traits: this.character.system.traits,
ancestryName: {
primary: '',
secondary: ''
@ -380,10 +377,8 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
];
return Object.values(this.setup.traits).reduce((acc, x) => {
const index = traitCompareArray.indexOf(x.value);
if (index === -1) return acc;
traitCompareArray.splice(index, 1);
acc += 1;
acc += index !== -1;
return acc;
}, 0);
}

View file

@ -13,7 +13,7 @@ export { default as OwnershipSelection } from './ownershipSelection.mjs';
export { default as RerollDamageDialog } from './rerollDamageDialog.mjs';
export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs';
export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs';
export { default as GroupRollDialog } from './group-roll-dialog.mjs';
export { default as TagTeamDialog } from './tagTeamDialog.mjs';
export { default as GroupRollDialog } from './groupRollDialog.mjs';
export { default as RiskItAllDialog } from './riskItAllDialog.mjs';
export { default as CompendiumBrowserSettingsDialog } from './CompendiumBrowserSettings.mjs';

View file

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

View file

@ -123,10 +123,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.advantage = this.config.roll?.advantage;
context.disadvantage = this.config.roll?.disadvantage;
context.diceOptions = CONFIG.DH.GENERAL.diceTypes;
context.dieFaces = CONFIG.DH.GENERAL.dieFaces.reduce((acc, face) => {
acc[face] = `d${face}`;
return acc;
}, {});
context.isLite = this.config.roll?.lite;
context.extraFormula = this.config.extraFormula;
context.formula = this.roll.constructFormula(this.config);
@ -156,7 +152,9 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
}
if (this.config.uses) this.config.uses = foundry.utils.mergeObject(this.config.uses, rest.uses);
if (rest.roll?.dice) {
this.roll = foundry.utils.mergeObject(this.roll, rest.roll.dice);
Object.entries(rest.roll.dice).forEach(([key, value]) => {
this.roll[key] = value;
});
}
if (rest.hasOwnProperty('trait')) {
this.config.roll.trait = rest.trait;
@ -175,15 +173,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.disadvantage = advantage === -1;
this.config.roll.advantage = this.config.roll.advantage === advantage ? 0 : advantage;
if (this.config.roll.advantage === 1 && this.config.data.rules.roll.defaultAdvantageDice) {
const faces = Number.parseInt(this.config.data.rules.roll.defaultAdvantageDice);
this.roll.advantageFaces = Number.isNaN(faces) ? this.roll.advantageFaces : faces;
} else if (this.config.roll.advantage === -1 && this.config.data.rules.roll.defaultDisadvantageDice) {
const faces = Number.parseInt(this.config.data.rules.roll.defaultDisadvantageDice);
this.roll.advantageFaces = Number.isNaN(faces) ? this.roll.advantageFaces : faces;
}
this.render();
}

View file

@ -22,7 +22,6 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
},
actions: {
toggleSelectedEffect: this.toggleSelectedEffect,
updateGroupAttack: this.updateGroupAttack,
toggleCritical: this.toggleCritical,
submitRoll: this.submitRoll
},
@ -65,40 +64,15 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
context.hasSelectedEffects = Boolean(Object.keys(this.selectedEffects).length);
context.selectedEffects = this.selectedEffects;
context.damageOptions = this.config.damageOptions;
context.rangeOptions = CONFIG.DH.GENERAL.groupAttackRange;
return context;
}
static updateRollConfiguration(_event, _, formData) {
const data = foundry.utils.expandObject(formData.object);
foundry.utils.mergeObject(this.config.roll, data.roll);
foundry.utils.mergeObject(this.config.modifiers, data.modifiers);
this.config.selectedMessageMode = data.selectedMessageMode;
const { ...rest } = foundry.utils.expandObject(formData.object);
foundry.utils.mergeObject(this.config.roll, rest.roll);
foundry.utils.mergeObject(this.config.modifiers, rest.modifiers);
this.config.selectedMessageMode = rest.selectedMessageMode;
if (data.damageOptions) {
const numAttackers = data.damageOptions.groupAttack?.numAttackers;
if (typeof numAttackers !== 'number' || numAttackers % 1 !== 0) {
data.damageOptions.groupAttack.numAttackers = null;
}
foundry.utils.mergeObject(this.config.damageOptions, data.damageOptions);
}
this.render();
}
static updateGroupAttack() {
const targets = Array.from(game.user.targets);
if (targets.length === 0)
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.noTokenTargeted'));
const actorId = this.roll.data.parent.id;
const range = this.config.damageOptions.groupAttack.range;
const groupAttackTokens = game.system.api.fields.ActionFields.DamageField.getGroupAttackTokens(actorId, range);
this.config.damageOptions.groupAttack.numAttackers = groupAttackTokens.length;
this.render();
}

View file

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

View file

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

View file

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

View file

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

View file

@ -20,7 +20,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
owned: member.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)
}));
this.initiator = { cost: 3 };
this.initiator = null;
this.openForAllPlayers = true;
this.tabGroups.application = Object.keys(party.system.tagTeam.members).length
@ -115,12 +115,6 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
async _onRender(context, options) {
await super._onRender(context, options);
// if (this.element.querySelector('.roll-selection')) {
// for (const element of this.element.querySelectorAll('.team-member-container')) {
// element.classList.add('select-padding');
// }
// }
if (this.element.querySelector('.team-container')) return;
const initializationPart = this.element.querySelector('.initialization-container');
initializationPart.insertAdjacentHTML('afterend', '<div class="team-container"></div>');
@ -139,10 +133,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
context.members = {};
context.allHaveRolled = Object.keys(this.party.system.tagTeam.members).every(key => {
const data = this.party.system.tagTeam.members[key];
const hasRolled = Boolean(data.rollData);
if (!hasRolled) return false;
return !data.rollData.options.hasDamage || Boolean(data.rollData.options.damage);
return Boolean(data.rollData);
});
return context;
@ -375,7 +366,8 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
let rollIsSelected = false;
for (const member of Object.values(members)) {
const rollFinished = Boolean(member.rollData);
const damageFinished = member.rollData?.options?.hasDamage ? Boolean(member.rollData.options.damage) : true;
const damageFinished =
member.rollData?.options?.hasDamage !== undefined ? member.rollData.options.damage : true;
rollsAreFinished = rollsAreFinished && rollFinished && damageFinished;
rollIsSelected = rollIsSelected || member.selected;

View file

@ -122,14 +122,15 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
async toggleClowncar(actors) {
const animationDuration = 500;
const scene = game.scenes.get(game.user.viewedScene);
/* getDependentTokens returns already removed tokens with id = null. Need to filter that until it's potentially fixed from Foundry */
const activeTokens = actors.flatMap(member => member.getDependentTokens({ scenes: scene }).filter(x => x._id));
const activeTokens = actors.flatMap(member => member.getActiveTokens());
const { x: actorX, y: actorY } = this.document;
if (activeTokens.length > 0) {
for (let token of activeTokens) {
await token.update({ x: actorX, y: actorY, alpha: 0 }, { animation: { duration: animationDuration } });
setTimeout(() => token.delete(), animationDuration);
await token.document.update(
{ x: actorX, y: actorY, alpha: 0 },
{ animation: { duration: animationDuration } }
);
setTimeout(() => token.document.delete(), animationDuration);
}
} else {
const activeScene = game.scenes.find(x => x.id === game.user.viewedScene);
@ -139,16 +140,11 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
tokenData.push(data.toObject());
}
const viewedLevel = game.scenes.get(game.user.viewedScene).levels.get(game.user.viewedLevel);
const elevation = this.actor.token?.elevation ?? viewedLevel.elevation.bottom;
const newTokens = await activeScene.createEmbeddedDocuments(
'Token',
tokenData.map(tokenData => ({
...tokenData,
alpha: 0,
level: viewedLevel,
elevation: elevation,
x: actorX,
y: actorY
}))

View file

@ -118,13 +118,8 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
break;
case 'footer':
partContext.buttons = [
{
type: 'button',
action: 'reset',
icon: 'fa-solid fa-arrow-rotate-left',
label: game.i18n.localize('SETTINGS.UI.ACTIONS.Reset')
},
{ type: 'submit', icon: 'fa-solid fa-floppy-disk', label: game.i18n.localize('EDITOR.Save') }
{ type: 'button', action: 'reset', icon: 'fa-solid fa-arrow-rotate-left', label: 'Reset' },
{ type: 'submit', icon: 'fa-solid fa-floppy-disk', label: 'Save Changes' }
];
break;
}

View file

@ -264,9 +264,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
key = event.target.closest('[data-key]').dataset.key;
if (!this.action[key]) return;
const value = key === 'areas' ? { name: this.action.item.name } : {};
data[key].push(this.action.defaultValues[key] ?? value);
data[key].push(this.action.defaultValues[key] ?? {});
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}

View file

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

View file

@ -31,35 +31,21 @@ export default class DHActionSettingsConfig extends DHActionBaseConfig {
}
static async addEffect(_event) {
const { areaIndex } = event.target.dataset;
if (!this.action.effects) return;
const effectData = game.system.api.data.activeEffects.BaseEffect.getDefaultObject({ transfer: false });
const effectData = game.system.api.data.activeEffects.BaseEffect.getDefaultObject();
const data = this.action.toObject();
this.sheetUpdate(data, effectData);
this.effects = [...this.effects, effectData];
if (areaIndex !== undefined) data.areas[areaIndex].effects.push(effectData.id);
else data.effects.push({ _id: effectData.id });
data.effects.push({ _id: effectData.id });
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static removeEffect(event, button) {
if (!this.action.effects) return;
const { areaIndex, index } = button.dataset;
let effectId = null;
if (areaIndex !== undefined) {
effectId = this.action.areas[areaIndex].effects[index];
const data = this.action.toObject();
data.areas[areaIndex].effects.splice(index, 1);
this.constructor.updateForm.call(this, null, null, { object: foundry.utils.flattenObject(data) });
} else {
const index = button.dataset.index,
effectId = this.action.effects[index]._id;
this.constructor.removeElement.call(this, event, button);
}
this.constructor.removeElement.bind(this)(event, button);
this.sheetUpdate(
this.action.toObject(),
this.effects.find(x => x.id === effectId),

View file

@ -217,8 +217,8 @@ export default class AdversarySheet extends DHBaseActorSheet {
static #reactionRoll(event) {
const config = {
event,
title: game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll'),
headerTitle: game.i18n.localize('DAGGERHEART.ACTORS.Adversary.adversaryReactionRoll.headerTitle'),
title: `Reaction Roll: ${this.actor.name}`,
headerTitle: 'Adversary Reaction Roll',
roll: {
type: 'trait'
},

View file

@ -12,6 +12,8 @@ export default class CharacterSheet extends DHBaseActorSheet {
static DEFAULT_OPTIONS = {
classes: ['character'],
position: { width: 850, height: 800 },
/* Foundry adds disabled to all buttons and inputs if editPermission is missing. This is not desired. */
editPermission: CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER,
actions: {
toggleVault: CharacterSheet.#toggleVault,
rollAttribute: CharacterSheet.#rollAttribute,
@ -66,7 +68,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
}
},
{
handler: CharacterSheet.#getEquipmentContextOptions,
handler: CharacterSheet.#getEquipamentContextOptions,
selector: '[data-item-uuid][data-type="armor"], [data-item-uuid][data-type="weapon"]',
options: {
parentClassHooks: false,
@ -168,16 +170,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
return applicationOptions;
}
/** @inheritdoc */
_toggleDisabled(disabled) {
// Overriden to only disable text inputs by default.
// Everything else is done by checking @root.editable in the sheet
const form = this.form;
for (const input of form.querySelectorAll('input:not([type=search]), .editor.prosemirror')) {
input.disabled = disabled;
}
}
/** @inheritDoc */
async _onRender(context, options) {
await super._onRender(context, options);
@ -323,11 +315,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
/**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */
const options = [
{
label: 'toLoadout',
name: 'toLoadout',
icon: 'fa-solid fa-arrow-up',
visible: target => {
condition: target => {
const doc = getDocFromElementSync(target);
return doc?.isOwner && doc.system.inVault;
return doc && doc.system.inVault;
},
callback: async target => {
const doc = await getDocFromElement(target);
@ -337,11 +329,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
}
},
{
label: 'recall',
name: 'recall',
icon: 'fa-solid fa-bolt-lightning',
visible: target => {
condition: target => {
const doc = getDocFromElementSync(target);
return doc?.isOwner && doc.system.inVault;
return doc && doc.system.inVault;
},
callback: async (target, event) => {
const doc = await getDocFromElement(target);
@ -376,17 +368,17 @@ export default class CharacterSheet extends DHBaseActorSheet {
}
},
{
label: 'toVault',
name: 'toVault',
icon: 'fa-solid fa-arrow-down',
visible: target => {
condition: target => {
const doc = getDocFromElementSync(target);
return doc?.isOwner && !doc.system.inVault;
return doc && !doc.system.inVault;
},
callback: async target => (await getDocFromElement(target)).update({ 'system.inVault': true })
}
].map(option => ({
...option,
label: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.label}`,
name: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.name}`,
icon: `<i class="${option.icon}"></i>`
}));
@ -399,29 +391,29 @@ export default class CharacterSheet extends DHBaseActorSheet {
* @this {CharacterSheet}
* @protected
*/
static #getEquipmentContextOptions() {
static #getEquipamentContextOptions() {
const options = [
{
label: 'equip',
name: 'equip',
icon: 'fa-solid fa-hands',
visible: target => {
condition: target => {
const doc = getDocFromElementSync(target);
return doc.isOwner && doc && !doc.system.equipped;
return doc && !doc.system.equipped;
},
callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target)
},
{
label: 'unequip',
name: 'unequip',
icon: 'fa-solid fa-hands',
visible: target => {
condition: target => {
const doc = getDocFromElementSync(target);
return doc.isOwner && doc && doc.system.equipped;
return doc && doc.system.equipped;
},
callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target)
}
].map(option => ({
...option,
label: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.label}`,
name: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.name}`,
icon: `<i class="${option.icon}"></i>`
}));

View file

@ -1,9 +1,10 @@
import DHBaseActorSheet from '../api/base-actor.mjs';
import { getDocFromElement, sortBy } from '../../../helpers/utils.mjs';
import { getDocFromElement } from '../../../helpers/utils.mjs';
import { ItemBrowser } from '../../ui/itemBrowser.mjs';
import FilterMenu from '../../ux/filter-menu.mjs';
import DaggerheartMenu from '../../sidebar/tabs/daggerheartMenu.mjs';
import { socketEvent } from '../../../systemRegistration/socket.mjs';
import GroupRollDialog from '../../dialogs/group-roll-dialog.mjs';
import DhpActor from '../../../documents/actor.mjs';
export default class Party extends DHBaseActorSheet {
@ -17,14 +18,13 @@ export default class Party extends DHBaseActorSheet {
static DEFAULT_OPTIONS = {
classes: ['party'],
position: {
width: 600,
width: 550,
height: 900
},
window: {
resizable: true
},
actions: {
openDocument: Party.#openDocument,
deletePartyMember: Party.#deletePartyMember,
deleteItem: Party.#deleteItem,
toggleHope: Party.#toggleHope,
@ -45,6 +45,10 @@ export default class Party extends DHBaseActorSheet {
header: { template: 'systems/daggerheart/templates/sheets/actors/party/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
partyMembers: { template: 'systems/daggerheart/templates/sheets/actors/party/party-members.hbs' },
resources: {
template: 'systems/daggerheart/templates/sheets/actors/party/resources.hbs',
scrollable: ['']
},
/* NOT YET IMPLEMENTED */
// projects: {
// template: 'systems/daggerheart/templates/sheets/actors/party/projects.hbs',
@ -62,6 +66,7 @@ export default class Party extends DHBaseActorSheet {
primary: {
tabs: [
{ id: 'partyMembers' },
{ id: 'resources' },
/* NOT YET IMPLEMENTED */
// { id: 'projects' },
{ id: 'inventory' },
@ -91,8 +96,6 @@ export default class Party extends DHBaseActorSheet {
case 'header':
await this._prepareHeaderContext(context, options);
break;
case 'partyMembers':
await this._prepareMembersContext(context, options);
case 'notes':
await this._prepareNotesContext(context, options);
break;
@ -116,60 +119,6 @@ export default class Party extends DHBaseActorSheet {
relativeTo: this.document
});
context.tagTeamActive = Boolean(this.document.system.tagTeam.initiator);
context.groupRollActive = Boolean(this.document.system.groupRoll.leader);
}
async _prepareMembersContext(context, _options) {
context.partyMembers = [];
const traits = ['agility', 'strength', 'finesse', 'instinct', 'presence', 'knowledge'];
for (const actor of this.document.system.partyMembers) {
const weapons = [];
if (actor.type === 'character') {
if (actor.system.usedUnarmed) {
weapons.push(actor.system.usedUnarmed);
}
const equipped = actor.items.filter(i => i.system.equipped && i.type === 'weapon');
weapons.push(...sortBy(equipped, i => (i.system.secondary ? 1 : 0)));
}
context.partyMembers.push({
uuid: actor.uuid,
img: actor.img,
name: actor.name,
subtitle: (() => {
if (!['character', 'companion'].includes(actor.type)) {
return game.i18n.format(`TYPES.Actor.${actor.type}`);
}
const { value: classItem, subclass } = actor.system.class ?? {};
const partner = actor.system.partner;
const ancestry = actor.system.ancestry;
const community = actor.system.community;
if (partner || (classItem && subclass && ancestry && community)) {
return game.i18n.format(`DAGGERHEART.ACTORS.Party.Subtitle.${actor.type}`, {
class: classItem?.name,
subclass: subclass?.name,
partner: partner?.name,
ancestry: ancestry?.name,
community: community?.name
});
}
})(),
type: actor.type,
resources: actor.system.resources,
armorScore: actor.system.armorScore,
damageThresholds: actor.system.damageThresholds,
evasion: actor.system.evasion,
difficulty: actor.system.difficulty,
traits: actor.system.traits
? traits.map(t => ({
label: game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${t}.short`),
value: actor.system.traits[t].value
}))
: null,
weapons
});
}
}
/**
@ -200,12 +149,6 @@ export default class Party extends DHBaseActorSheet {
}
}
static async #openDocument(_, target) {
const uuid = target.dataset.uuid;
const document = await foundry.utils.fromUuid(uuid);
document?.sheet?.render(true);
}
/**
* Toggles a hope resource value.
* @type {ApplicationClickAction}
@ -247,7 +190,7 @@ export default class Party extends DHBaseActorSheet {
* @type {ApplicationClickAction}
*/
static async #toggleArmorSlot(_, target) {
const actor = await foundry.utils.fromUuid(target.dataset.actorId);
const actor = game.actors.get(target.dataset.actorId);
const { value, max } = actor.system.armorScore;
const inputValue = Number.parseInt(target.dataset.value);
const newValue = value >= inputValue ? inputValue - 1 : inputValue;
@ -288,7 +231,7 @@ export default class Party extends DHBaseActorSheet {
title: game.i18n.localize(`DAGGERHEART.APPLICATIONS.Downtime.${button.dataset.type}.title`),
icon: button.dataset.type === 'shortRest' ? 'fa-solid fa-utensils' : 'fa-solid fa-bed'
},
content: game.i18n.localize('DAGGERHEART.ACTORS.Party.triggerRestContent'),
content: 'This will trigger a dialog to players make their downtime moves, are you sure?',
classes: ['daggerheart', 'dialog', 'dh-style']
});
@ -318,7 +261,9 @@ export default class Party extends DHBaseActorSheet {
}
static async #groupRoll(_params) {
new game.system.api.applications.dialogs.GroupRollDialog(this.document).render({ force: true });
new GroupRollDialog(
this.document.system.partyMembers.filter(x => Party.DICE_ROLL_ACTOR_TYPES.includes(x.type))
).render({ force: true });
}
/* -------------------------------------------- */
@ -480,23 +425,25 @@ export default class Party extends DHBaseActorSheet {
}
static async #deletePartyMember(event, target) {
const doc = await foundry.utils.fromUuid(target.closest('[data-uuid]')?.dataset.uuid);
const doc = await getDocFromElement(target.closest('.inventory-item'));
if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.ACTORS.Party.RemoveConfirmation.title', {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize('TYPES.Actor.adversary'),
name: doc.name
})
},
content: game.i18n.format('DAGGERHEART.ACTORS.Party.RemoveConfirmation.text', { name: doc.name })
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: doc.name })
});
if (!confirmed) return;
}
const currentMembers = this.document.system.partyMembers.map(x => x.uuid);
const newMembersList = currentMembers.filter(uuid => uuid !== doc.uuid);
await this.document.update({ 'system.partyMembers': newMembersList });
const newMemberdList = currentMembers.filter(uuid => uuid !== doc.uuid);
await this.document.update({ 'system.partyMembers': newMemberdList });
}
static async #deleteItem(event, target) {

View file

@ -418,18 +418,18 @@ export default function DHApplicationMixin(Base) {
/**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */
const options = [
{
label: 'disableEffect',
name: 'disableEffect',
icon: 'fa-solid fa-lightbulb',
visible: element => {
condition: element => {
const target = element.closest('[data-item-uuid]');
return !target.dataset.disabled && target.dataset.itemType !== 'beastform';
},
callback: async target => (await getDocFromElement(target)).update({ disabled: true })
},
{
label: 'enableEffect',
name: 'enableEffect',
icon: 'fa-regular fa-lightbulb',
visible: element => {
condition: element => {
const target = element.closest('[data-item-uuid]');
return target.dataset.disabled && target.dataset.itemType !== 'beastform';
},
@ -437,7 +437,7 @@ export default function DHApplicationMixin(Base) {
}
].map(option => ({
...option,
label: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.label}`,
name: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.name}`,
icon: `<i class="${option.icon}"></i>`
}));
@ -468,14 +468,14 @@ export default function DHApplicationMixin(Base) {
_getContextMenuCommonOptions({ usable = false, toChat = false, deletable = true }) {
const options = [
{
label: 'CONTROLS.CommonEdit',
name: 'CONTROLS.CommonEdit',
icon: 'fa-solid fa-pen-to-square',
visible: target => {
condition: target => {
const { dataset } = target.closest('[data-item-uuid]');
const doc = getDocFromElementSync(target);
return (
(!dataset.noCompendiumEdit && !doc) ||
(doc?.isOwner && (!doc?.hasOwnProperty('systemPath') || doc?.inCollection))
(doc && (!doc?.hasOwnProperty('systemPath') || doc?.inCollection))
);
},
callback: async target => (await getDocFromElement(target)).sheet.render({ force: true })
@ -484,14 +484,14 @@ export default function DHApplicationMixin(Base) {
if (usable) {
options.unshift({
label: 'DAGGERHEART.GENERAL.damage',
name: 'DAGGERHEART.GENERAL.damage',
icon: 'fa-solid fa-explosion',
visible: target => {
condition: target => {
const doc = getDocFromElementSync(target);
const hasDamage =
return (
!foundry.utils.isEmpty(doc?.system?.attack?.damage.parts) ||
!foundry.utils.isEmpty(doc?.damage?.parts);
return doc?.isOwner && hasDamage;
!foundry.utils.isEmpty(doc?.damage?.parts)
);
},
callback: async (target, event) => {
const doc = await getDocFromElement(target),
@ -507,11 +507,11 @@ export default function DHApplicationMixin(Base) {
});
options.unshift({
label: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem',
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem',
icon: 'fa-solid fa-burst',
visible: target => {
condition: target => {
const doc = getDocFromElementSync(target);
return doc?.isOwner && !(doc.type === 'domainCard' && doc.system.inVault);
return doc && !(doc.type === 'domainCard' && doc.system.inVault);
},
callback: async (target, event) => (await getDocFromElement(target)).use(event)
});
@ -519,19 +519,18 @@ export default function DHApplicationMixin(Base) {
if (toChat)
options.push({
label: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
icon: 'fa-solid fa-message',
callback: async target => (await getDocFromElement(target)).toChat(this.document.uuid)
});
if (deletable)
options.push({
label: 'CONTROLS.CommonDelete',
name: 'CONTROLS.CommonDelete',
icon: 'fa-solid fa-trash',
visible: element => {
condition: element => {
const target = element.closest('[data-item-uuid]');
const doc = getDocFromElementSync(target);
return doc?.isOwner && target.dataset.itemType !== 'beastform';
return target.dataset.itemType !== 'beastform';
},
callback: async (target, event) => {
const doc = await getDocFromElement(target);
@ -645,12 +644,12 @@ export default function DHApplicationMixin(Base) {
buttons: [
{
action: 'create',
label: game.i18n.localize('DAGGERHEART.APPLICATIONS.CreateItemDialog.createItem'),
label: 'Create Item',
icon: 'fa-solid fa-plus'
},
{
action: 'browse',
label: game.i18n.localize('DAGGERHEART.APPLICATIONS.CreateItemDialog.browseCompendium'),
label: 'Browse Compendium',
icon: 'fa-solid fa-book'
}
]

View file

@ -73,7 +73,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
.hideAttribution;
// Prepare inventory data
if (this.document.system.metadata.hasInventory) {
if (['party', 'character'].includes(this.document.type)) {
context.inventory = {
currencies: {},
weapons: this.document.itemTypes.weapon.sort((a, b) => a.sort - b.sort),
@ -283,7 +283,11 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
async _onDropItem(event, item) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const originActor = item.actor;
if (!originActor || originActor.uuid === this.document.uuid || !this.document.system.metadata.hasInventory) {
if (
item.actor?.uuid === this.document.uuid ||
!originActor ||
!['character', 'party'].includes(this.document.type)
) {
return super._onDropItem(event, item);
}
@ -298,79 +302,47 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
);
}
// Perform the actual transfer, showing a dialog when doing it
const availableQuantity = Math.max(1, item.system.quantity);
const actorItem = originActor.items.get(data.originId) ?? item;
if (availableQuantity > 1) {
const quantityTransferred = await game.system.api.applications.dialogs.ItemTransferDialog.configure({
if (item.system.metadata.isQuantifiable) {
const actorItem = originActor.items.get(data.originId);
const quantityTransfered = await game.system.api.applications.dialogs.ItemTransferDialog.configure({
item,
targetActor: this.document
});
return this.#transferItem(actorItem, quantityTransferred);
if (quantityTransfered) {
const existingItem = this.document.items.find(x => itemIsIdentical(x, item));
if (existingItem) {
await existingItem.update({
'system.quantity': existingItem.system.quantity + quantityTransfered
});
} else {
const createData = item.toObject();
await this.document.createEmbeddedDocuments('Item', [
{
...createData,
system: {
...createData.system,
quantity: quantityTransfered
}
}
]);
}
if (quantityTransfered === actorItem.system.quantity) {
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
} else {
await actorItem.update({
'system.quantity': actorItem.system.quantity - quantityTransfered
});
}
}
} else {
return this.#transferItem(actorItem, availableQuantity);
await this.document.createEmbeddedDocuments('Item', [item.toObject()]);
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
}
}
}
/**
* Helper to perform the actual transfer of an item to this actor, including stack/unstack logic based on target quantifiability.
* Make sure item is the actor item before calling this method or there will be issues
*/
async #transferItem(item, quantity) {
const originActor = item.actor;
const targetActor = this.document;
const allowStacking = targetActor.system.metadata.quantifiable?.includes(item.type);
const batch = [];
// First add/update the item to the target actor
const existing = allowStacking ? targetActor.items.find(x => itemIsIdentical(x, item)) : null;
if (existing) {
batch.push({
action: 'update',
documentName: 'Item',
parent: targetActor,
updates: [{ '_id': existing.id, 'system.quantity': existing.system.quantity + quantity }]
});
} else {
const itemsToCreate = [];
if (allowStacking) {
itemsToCreate.push(foundry.utils.mergeObject(item.toObject(true), { system: { quantity } }));
} else {
const createData = new Array(Math.max(1, quantity))
.fill(0)
.map(() => foundry.utils.mergeObject(item.toObject(), { system: { quantity: 1 } }));
itemsToCreate.push(...createData);
}
batch.push({
action: 'create',
documentName: 'Item',
parent: targetActor,
data: itemsToCreate
});
}
// Remove the item from the original actor (by either deleting it, or updating its quantity)
if (quantity >= item.system.quantity) {
batch.push({
action: 'delete',
documentName: 'Item',
parent: originActor,
ids: [item.id]
});
} else {
batch.push({
action: 'update',
documentName: 'Item',
parent: originActor,
updates: [{ '_id': item.id, 'system.quantity': item.system.quantity - quantity }]
});
}
return foundry.documents.modifyBatch(batch);
}
/**
* On dragStart on the item.
* @param {DragEvent} event - The drag event

View file

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

View file

@ -31,7 +31,7 @@ export default class FeatureSheet extends DHBaseItemSheet {
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}
};
//Might be wrong location but testing out if here is okay.
//Might be wrong location but testing out if here is okay.
/**@override */
async _prepareContext(options) {
const context = await super._prepareContext(options);

View file

@ -46,67 +46,50 @@ export default class DhActorDirectory extends foundry.applications.sidebar.tabs.
_getEntryContextOptions() {
const options = super._getEntryContextOptions();
options.push(
{
label: 'DAGGERHEART.UI.Sidebar.actorDirectory.duplicateToNewTier',
icon: `<i class="fa-solid fa-arrow-trend-up" inert></i>`,
visible: li => {
const actor = game.actors.get(li.dataset.entryId);
return actor?.type === 'adversary' && actor.system.type !== 'social';
},
callback: async li => {
const actor = game.actors.get(li.dataset.entryId);
if (!actor) throw new Error('Unexpected missing actor');
const tiers = [1, 2, 3, 4].filter(t => t !== actor.system.tier);
const content = document.createElement('div');
const select = document.createElement('select');
select.name = 'tier';
select.append(
...tiers.map(t => {
const option = document.createElement('option');
option.value = t;
option.textContent = game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${t}`);
return option;
})
);
content.append(select);
const tier = await foundry.applications.api.Dialog.input({
classes: ['dh-style', 'dialog'],
window: { title: 'DAGGERHEART.UI.Sidebar.actorDirectory.pickTierTitle' },
content,
ok: {
label: 'DAGGERHEART.UI.Sidebar.actorDirectory.createAdversary',
callback: (event, button, dialog) => Number(button.form.elements.tier.value)
}
});
if (tier === actor.system.tier) {
ui.notifications.warn('This actor is already at this tier');
} else if (tier) {
const source = actor.system.adjustForTier(tier);
await Actor.create(source);
ui.notifications.info(`Tier ${tier} ${actor.name} created`);
}
}
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';
},
{
label: 'DAGGERHEART.UI.Sidebar.actorDirectory.activateParty',
icon: `<i class="fa-regular fa-square"></i>`,
visible: li => {
const actor = game.actors.get(li.dataset.entryId);
return actor && actor.type === 'party' && !actor.system.active;
},
callback: async li => {
const actor = game.actors.get(li.dataset.entryId);
if (!actor) throw new Error('Unexpected missing actor');
callback: async li => {
const actor = game.actors.get(li.dataset.entryId);
if (!actor) throw new Error('Unexpected missing actor');
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.ActiveParty, actor.id);
ui.actors.render();
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,6 +1,8 @@
import { abilities } from '../../config/actorConfig.mjs';
import { enrichedDualityRoll } from '../../enrichers/DualityRollEnricher.mjs';
import { enrichedFateRoll, getFateTypeData } from '../../enrichers/FateRollEnricher.mjs';
import { getCommandTarget, rollCommandToJSON } from '../../helpers/utils.mjs';
import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog {
constructor(options) {
@ -103,10 +105,23 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
_getEntryContextOptions() {
return [
...super._getEntryContextOptions(),
// {
// name: 'Reroll',
// icon: '<i class="fa-solid fa-dice"></i>',
// condition: li => {
// const message = game.messages.get(li.dataset.messageId);
// return (game.user.isGM || message.isAuthor) && message.rolls.length > 0;
// },
// callback: li => {
// const message = game.messages.get(li.dataset.messageId);
// new game.system.api.applications.dialogs.RerollDialog(message).render({ force: true });
// }
// },
{
label: 'DAGGERHEART.UI.ChatLog.rerollDamage',
name: game.i18n.localize('DAGGERHEART.UI.ChatLog.rerollDamage'),
icon: '<i class="fa-solid fa-dice"></i>',
visible: li => {
condition: li => {
const message = game.messages.get(li.dataset.messageId);
const hasRolledDamage = message.system.hasDamage
? Object.keys(message.system.damage).length > 0
@ -135,6 +150,18 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
html.querySelectorAll('.reroll-button').forEach(element =>
element.addEventListener('click', event => this.rerollEvent(event, message))
);
html.querySelectorAll('.group-roll-button').forEach(element =>
element.addEventListener('click', event => this.groupRollButton(event, message))
);
html.querySelectorAll('.group-roll-reroll').forEach(element =>
element.addEventListener('click', event => this.groupRollReroll(event, message))
);
html.querySelectorAll('.group-roll-success').forEach(element =>
element.addEventListener('click', event => this.groupRollSuccessEvent(event, message))
);
html.querySelectorAll('.group-roll-header-expand-section').forEach(element =>
element.addEventListener('click', this.groupRollExpandSection)
);
html.querySelectorAll('.risk-it-all-button').forEach(element =>
element.addEventListener('click', event => this.riskItAllClearStressAndHitPoints(event, data))
);
@ -278,6 +305,174 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
}
}
async groupRollButton(event, message) {
const path = event.currentTarget.dataset.path;
const isLeader = path === 'leader';
const { actor: actorData, trait } = foundry.utils.getProperty(message.system, path);
const actor = game.actors.get(actorData._id);
if (!actor) {
return ui.notifications.error(
game.i18n.format('DAGGERHEART.UI.Notifications.documentIsMissing', {
documentType: game.i18n.localize('TYPES.Actor.character')
})
);
}
if (!actor.testUserPermission(game.user, 'OWNER')) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.noActorOwnership'));
}
const traitLabel = game.i18n.localize(abilities[trait].label);
const config = {
event: event,
title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`,
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: traitLabel
}),
roll: {
trait: trait,
advantage: 0,
modifiers: [{ label: traitLabel, value: actor.system.traits[trait].value }]
},
hasRoll: true,
skips: {
createMessage: true,
resources: !isLeader,
updateCountdowns: !isLeader
}
};
const result = await actor.diceRoll({
...config,
headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`,
title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: traitLabel
})
});
if (!result) return;
const newMessageData = foundry.utils.deepClone(message.system);
foundry.utils.setProperty(newMessageData, `${path}.result`, result.roll);
const renderData = { system: new game.system.api.models.chatMessages.config.groupRoll(newMessageData) };
const updatedContent = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/groupRoll.hbs',
{ ...renderData, user: game.user }
);
const mess = game.messages.get(message._id);
await emitAsGM(
GMUpdateEvent.UpdateDocument,
mess.update.bind(mess),
{
...renderData,
content: updatedContent
},
mess.uuid
);
}
async groupRollReroll(event, message) {
const path = event.currentTarget.dataset.path;
const { actor: actorData, trait } = foundry.utils.getProperty(message.system, path);
const actor = game.actors.get(actorData._id);
if (!actor.testUserPermission(game.user, 'OWNER')) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.noActorOwnership'));
}
const traitLabel = game.i18n.localize(abilities[trait].label);
const config = {
event: event,
title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`,
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: traitLabel
}),
roll: {
trait: trait,
advantage: 0,
modifiers: [{ label: traitLabel, value: actor.system.traits[trait].value }]
},
hasRoll: true,
skips: {
createMessage: true,
updateCountdowns: true
}
};
const result = await actor.diceRoll({
...config,
headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`,
title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: traitLabel
})
});
const newMessageData = foundry.utils.deepClone(message.system);
foundry.utils.setProperty(newMessageData, `${path}.result`, { ...result.roll, rerolled: true });
const renderData = { system: new game.system.api.models.chatMessages.config.groupRoll(newMessageData) };
const updatedContent = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/groupRoll.hbs',
{ ...renderData, user: game.user }
);
const mess = game.messages.get(message._id);
await emitAsGM(
GMUpdateEvent.UpdateDocument,
mess.update.bind(mess),
{
...renderData,
content: updatedContent
},
mess.uuid
);
}
async groupRollSuccessEvent(event, message) {
if (!game.user.isGM) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmOnly'));
}
const { path, success } = event.currentTarget.dataset;
const { actor: actorData } = foundry.utils.getProperty(message.system, path);
const actor = game.actors.get(actorData._id);
if (!actor.testUserPermission(game.user, 'OWNER')) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.noActorOwnership'));
}
const newMessageData = foundry.utils.deepClone(message.system);
foundry.utils.setProperty(newMessageData, `${path}.manualSuccess`, Boolean(success));
const renderData = { system: new game.system.api.models.chatMessages.config.groupRoll(newMessageData) };
const updatedContent = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/groupRoll.hbs',
{ ...renderData, user: game.user }
);
const mess = game.messages.get(message._id);
await emitAsGM(
GMUpdateEvent.UpdateDocument,
mess.update.bind(mess),
{
...renderData,
content: updatedContent
},
mess.uuid
);
}
async groupRollExpandSection(event) {
event.target
.closest('.group-roll-header-expand-section')
.querySelectorAll('i')
.forEach(element => {
element.classList.toggle('fa-angle-up');
element.classList.toggle('fa-angle-down');
});
event.target.closest('.group-roll-section').querySelector('.group-roll-content').classList.toggle('closed');
}
async riskItAllClearStressAndHitPoints(event, data) {
const resourceValue = event.target.dataset.resourceValue;
const actor = game.actors.get(event.target.dataset.actorId);

View file

@ -84,15 +84,15 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
_getCombatContextOptions() {
return [
{
label: 'COMBAT.ClearMovementHistories',
name: 'COMBAT.ClearMovementHistories',
icon: '<i class="fa-solid fa-shoe-prints"></i>',
visible: () => game.user.isGM && this.viewed?.combatants.size > 0,
condition: () => game.user.isGM && this.viewed?.combatants.size > 0,
callback: () => this.viewed.clearMovementHistories()
},
{
label: 'COMBAT.Delete',
name: 'COMBAT.Delete',
icon: '<i class="fa-solid fa-trash"></i>',
visible: () => game.user.isGM && !!this.viewed,
condition: () => game.user.isGM && !!this.viewed,
callback: () => this.viewed.endCombat()
}
];

View file

@ -137,8 +137,6 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
}
static #getPlayerOwnership(user, setting, countdown) {
if (user.isGM) return CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER;
const playerOwnership = countdown.ownership[user.id];
return playerOwnership === undefined || playerOwnership === CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT
? setting.defaultOwnership

View file

@ -22,7 +22,7 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
tag: 'div',
window: {
frame: true,
title: 'DAGGERHEART.GENERAL.fear',
title: 'Fear',
positioned: true,
resizable: true,
minimizable: false

View file

@ -37,7 +37,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
tag: 'div',
window: {
frame: true,
title: 'DAGGERHEART.UI.ItemBrowser.windowTitle',
title: 'Compendium Browser',
icon: 'fa-solid fa-book-atlas',
positioned: true,
resizable: true
@ -207,23 +207,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
label: game.i18n.localize(col.label)
}));
const splitPath = folderId?.split('.') ?? [];
const { pathLabels } = splitPath.reduce(
(acc, curr) => {
acc.currentPath = !acc.currentPath ? curr : [acc.currentPath, curr].join('.');
if (curr === 'folder') return acc;
const label = foundry.utils.getProperty(this.config, acc.currentPath)?.label;
if (label) acc.pathLabels.push(game.i18n.localize(label));
return acc;
},
{ pathLabels: [], currentPath: '' }
);
this.selectedMenu = {
path: splitPath,
pathLabels: pathLabels,
path: folderId?.split('.') ?? [],
data: {
...folderData,
columns: columns
@ -583,9 +568,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
const { itemUuid } = event.target.closest('[data-item-uuid]').dataset,
item = await foundry.utils.fromUuid(itemUuid),
dragData = item.toDragData();
event.dataTransfer.setData('text/plain', JSON.stringify(dragData));
event.dataTransfer.setDragImage(event.target.querySelector('img'), 0, 0);
}
_canDragStart() {

View file

@ -6,11 +6,8 @@ export default class DHContextMenu extends foundry.applications.ux.ContextMenu {
static triggerContextMenu(event, altSelector) {
event.preventDefault();
event.stopPropagation();
const selector = altSelector ?? '[data-item-uuid]';
if (ui.context?.selector === selector) return;
const { clientX, clientY } = event;
const selector = altSelector ?? '[data-item-uuid]';
const target = event.target.closest(selector) ?? event.currentTarget.closest(selector);
target?.dispatchEvent(
new PointerEvent('contextmenu', {

View file

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

View file

@ -95,61 +95,4 @@ export default class DhRegionLayer extends foundry.canvas.layers.RegionLayer {
});
return inBounds.length === 1 ? inBounds[0] : null;
}
static getTemplateShape({ type, angle, range, direction } = {}) {
const { line, rectangle, inFront, cone, circle, emanation } = CONFIG.DH.GENERAL.templateTypes;
/* Length calculation */
const { grid, distance } = CONFIG.Scene.documentClass.schema.fields.grid.fields;
const sceneGridSize = canvas.scene?.grid.size ?? grid.size.initial;
const sceneGridDistance = canvas.scene?.grid.distance ?? distance.getInitialValue();
const dimensionConstant = sceneGridSize / sceneGridDistance;
const settings = canvas.scene?.rangeSettings;
const rangeNumber = Number(range);
const length = (!Number.isNaN(rangeNumber) ? rangeNumber : settings ? settings[range] : 0) * dimensionConstant;
/*----*/
const shapeData = {
...canvas.mousePosition,
type: type,
direction: direction ?? 0
};
switch (type) {
case rectangle.id:
shapeData.width = length;
shapeData.height = length;
break;
case line.id:
shapeData.length = length;
shapeData.width = 5 * dimensionConstant;
break;
case cone.id:
shapeData.angle = angle ?? CONFIG.MeasuredTemplate.defaults.angle;
shapeData.radius = length;
break;
case inFront.id:
shapeData.angle = '180';
shapeData.radius = length;
shapeData.type = cone.id;
break;
case circle.id:
shapeData.radius = length;
break;
case emanation.id:
shapeData.radius = length;
shapeData.base = {
type: 'token',
x: 0,
y: 0,
width: 1,
height: 1,
shape: game.canvas.grid.isHexagonal ? CONST.TOKEN_SHAPES.ELLIPSE_1 : CONST.TOKEN_SHAPES.RECTANGLE_1
};
break;
}
return shapeData;
}
}

View file

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

View file

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

View file

@ -70,40 +70,14 @@ export const range = {
}
};
export const groupAttackRange = {
melee: range.melee,
veryClose: range.veryClose,
close: range.close,
far: range.far,
veryFar: range.veryFar
};
/* circle|cone|rect|ray used to be CONST.MEASURED_TEMPLATE_TYPES. Hardcoded for now */
export const templateTypes = {
circle: {
id: 'circle',
label: 'Circle'
},
cone: {
id: 'cone',
label: 'Cone'
},
rectangle: {
id: 'rectangle',
label: 'Rectangle'
},
line: {
id: 'line',
label: 'Line'
},
emanation: {
id: 'emanation',
label: 'Emanation'
},
inFront: {
id: 'inFront',
label: 'In Front'
}
CIRCLE: 'circle',
CONE: 'cone',
RECTANGLE: 'rectangle',
LINE: 'line',
EMANATION: 'emanation',
INFRONT: 'inFront'
};
export const rangeInclusion = {
@ -510,7 +484,7 @@ export const defaultRestOptions = {
value: {
custom: {
enabled: true,
formula: '@system.armorScore.max'
formula: '@system.armorScore'
}
}
}
@ -734,14 +708,14 @@ const getDiceSoNiceSFX = sfxOptions => {
if (sfxOptions.critical && criticalAnimationData.class) {
return {
specialEffect: criticalAnimationData.class,
options: { ...criticalAnimationData.options }
options: {}
};
}
if (sfxOptions.higher && sfxOptions.data.higher) {
return {
specialEffect: sfxOptions.data.higher.class,
options: { ...sfxOptions.data.higher.options }
options: {}
};
}
@ -973,15 +947,15 @@ export const countdownAppMode = {
export const sceneRangeMeasurementSetting = {
disable: {
id: 'disable',
label: 'DAGGERHEART.CONFIG.SceneRangeMeasurementTypes.disable'
label: 'Disable Daggerheart Range Measurement'
},
default: {
id: 'default',
label: 'DAGGERHEART.CONFIG.SceneRangeMeasurementTypes.default'
label: 'Default'
},
custom: {
id: 'custom',
label: 'DAGGERHEART.CONFIG.SceneRangeMeasurementTypes.custom'
label: 'Custom'
}
};
@ -1110,18 +1084,3 @@ export const fallAndCollisionDamage = {
damageFormula: '1d20 + 5'
}
};
export const simpleDispositions = {
[-1]: {
id: -1,
label: 'TOKEN.DISPOSITION.HOSTILE'
},
[0]: {
id: 0,
label: 'TOKEN.DISPOSITION.NEUTRAL'
},
[1]: {
id: 1,
label: 'TOKEN.DISPOSITION.FRIENDLY'
}
};

View file

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

View file

@ -7,12 +7,7 @@ export const typeConfig = {
},
{
key: 'system.type',
label: 'DAGGERHEART.GENERAL.type',
format: type => {
if (!type) return '-';
return CONFIG.DH.ACTOR.allAdversaryTypes()[type].label;
}
label: 'DAGGERHEART.GENERAL.type'
}
],
filters: [
@ -74,18 +69,12 @@ export const typeConfig = {
columns: [
{
key: 'type',
label: 'DAGGERHEART.GENERAL.type',
format: type => (type ? `TYPES.Item.${type}` : '-')
label: 'DAGGERHEART.GENERAL.type'
},
{
key: 'system.secondary',
label: 'DAGGERHEART.UI.ItemBrowser.subtype',
format: isSecondary =>
isSecondary
? 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon.short'
: isSecondary === false
? 'DAGGERHEART.ITEMS.Weapon.primaryWeapon.short'
: '-'
format: isSecondary => (isSecondary ? 'secondary' : isSecondary === false ? 'primary' : '-')
},
{
key: 'system.tier',
@ -105,8 +94,8 @@ export const typeConfig = {
key: 'system.secondary',
label: 'DAGGERHEART.UI.ItemBrowser.subtype',
choices: [
{ value: false, label: 'DAGGERHEART.ITEMS.Weapon.primaryWeapon.full' },
{ value: true, label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon.full' }
{ value: false, label: 'DAGGERHEART.ITEMS.Weapon.primaryWeapon' },
{ value: true, label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon' }
]
},
{
@ -264,13 +253,11 @@ export const typeConfig = {
columns: [
{
key: 'system.type',
label: 'DAGGERHEART.GENERAL.type',
format: type => (type ? `DAGGERHEART.CONFIG.DomainCardTypes.${type}` : '-')
label: 'DAGGERHEART.GENERAL.type'
},
{
key: 'system.domain',
label: 'DAGGERHEART.GENERAL.Domain.single',
format: domain => (domain ? CONFIG.DH.DOMAIN.allDomains()[domain].label : '-')
label: 'DAGGERHEART.GENERAL.Domain.single'
},
{
key: 'system.level',
@ -331,14 +318,7 @@ export const typeConfig = {
},
{
key: 'system.domains',
label: 'DAGGERHEART.GENERAL.Domain.plural',
format: domains => {
const config = CONFIG.DH.DOMAIN.allDomains();
return domains
.map(x => (x ? game.i18n.localize(config[x].label) : null))
.filter(x => x)
.join(', ');
}
label: 'DAGGERHEART.GENERAL.Domain.plural'
}
],
filters: [
@ -382,19 +362,18 @@ export const typeConfig = {
columns: [
{
key: 'system.linkedClass',
label: 'TYPES.Item.class',
label: 'Class',
format: linkedClass => linkedClass?.name ?? 'DAGGERHEART.UI.ItemBrowser.missing'
},
{
key: 'system.spellcastingTrait',
label: 'DAGGERHEART.ITEMS.Subclass.spellcastingTrait',
format: trait => (trait ? `DAGGERHEART.CONFIG.Traits.${trait}.name` : '-')
label: 'DAGGERHEART.ITEMS.Subclass.spellcastingTrait'
}
],
filters: [
{
key: 'system.linkedClass.uuid',
label: 'TYPES.Item.class',
label: 'Class',
choices: items => {
const list = items
.filter(item => item.system.linkedClass)
@ -418,8 +397,7 @@ export const typeConfig = {
},
{
key: 'system.mainTrait',
label: 'DAGGERHEART.GENERAL.Trait.single',
format: trait => (trait ? `DAGGERHEART.CONFIG.Traits.${trait}.name` : '-')
label: 'DAGGERHEART.GENERAL.Trait.single'
}
],
filters: [

View file

@ -1,6 +1,5 @@
export const keybindings = {
spotlight: 'DHSpotlight',
partySheet: 'DHPartySheet'
spotlight: 'DHSpotlight'
};
export const menu = {
@ -41,21 +40,24 @@ export const gameSettings = {
LastMigrationVersion: 'LastMigrationVersion',
SpotlightRequestQueue: 'SpotlightRequestQueue',
CompendiumBrowserSettings: 'CompendiumBrowserSettings',
SpotlightTracker: 'SpotlightTracker',
ActiveParty: 'ActiveParty'
SpotlightTracker: 'SpotlightTracker'
};
export const actionAutomationChoices = {
never: {
id: 'never',
label: 'DAGGERHEART.CONFIG.ActionAutomationChoices.never'
label: 'Never'
},
showDialog: {
id: 'showDialog',
label: 'DAGGERHEART.CONFIG.ActionAutomationChoices.showDialog'
label: 'Show Dialog only'
},
// npcOnly: {
// id: "npcOnly",
// label: "Always for non-characters"
// },
always: {
id: 'always',
label: 'DAGGERHEART.CONFIG.ActionAutomationChoices.always'
label: 'Always'
}
};

View file

@ -4,7 +4,6 @@ export { default as DhRollTable } from './rollTable.mjs';
export { default as RegisteredTriggers } from './registeredTriggers.mjs';
export { default as CompendiumBrowserSettings } from './compendiumBrowserSettings.mjs';
export { default as TagTeamData } from './tagTeamData.mjs';
export { default as GroupRollData } from './groupRollData.mjs';
export { default as SpotlightTracker } from './spotlightTracker.mjs';
export * as countdowns from './countdowns.mjs';
@ -15,4 +14,3 @@ export * as chatMessages from './chat-message/_modules.mjs';
export * as fields from './fields/_module.mjs';
export * as items from './item/_module.mjs';
export * as scenes from './scene/_module.mjs';
export * as regionBehaviors from './regionBehavior/_module.mjs';

View file

@ -13,7 +13,7 @@ export default class DHAttackAction extends DHDamageAction {
if (!!this.item?.system?.attack) {
if (this.damage.includeBase) {
const baseDamage = this.getParentDamage();
this.damage.parts.hitPoints = new DHDamageData(baseDamage);
this.damage.parts.unshift(new DHDamageData(baseDamage));
}
if (this.roll.useDefault) {
this.roll.trait = this.item.system.attack.roll.trait;
@ -51,7 +51,7 @@ export default class DHAttackAction extends DHDamageAction {
async use(event, options) {
const result = await super.use(event, options);
if (result?.message?.system.action?.roll?.type === 'attack') {
if (result.message?.system.action.roll?.type === 'attack') {
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.characterAttack.id);
}

View file

@ -15,7 +15,7 @@ const fields = foundry.data.fields;
*/
export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel) {
static extraSchemas = ['areas', 'cost', 'uses', 'range'];
static extraSchemas = ['cost', 'uses', 'range'];
/** @inheritDoc */
static defineSchema() {
@ -110,11 +110,6 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return this._id;
}
/** Returns true if the current user is the owner of the containing item */
get isOwner() {
return this.item?.isOwner ?? true;
}
/**
* Return Item the action is attached too.
*/
@ -148,12 +143,6 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
: null;
}
/** Returns true if the action is usable */
get usable() {
const actor = this.actor;
return this.isOwner && actor?.type === 'character';
}
static getRollType(parent) {
return 'trait';
}
@ -291,26 +280,6 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
}
};
if (this.damage) {
config.isDirect = this.damage.direct;
const groupAttackTokens = this.damage.groupAttack
? game.system.api.fields.ActionFields.DamageField.getGroupAttackTokens(
this.actor.id,
this.damage.groupAttack
)
: null;
config.damageOptions = {
groupAttack: this.damage.groupAttack
? {
numAttackers: Math.max(groupAttackTokens.length, 1),
range: this.damage.groupAttack
}
: null
};
}
DHBaseAction.applyKeybindings(config);
return config;
}

View file

@ -93,12 +93,6 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
max: new fields.NumberField({ integer: true, label: 'DAGGERHEART.GENERAL.max' })
},
{ nullable: true, initial: null }
),
targetDispositions: new fields.SetField(
new fields.NumberField({
choices: CONFIG.DH.GENERAL.simpleDispositions
}),
{ label: 'DAGGERHEART.ACTIVEEFFECT.Config.targetDispositions' }
)
};
}
@ -137,14 +131,13 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
return armorChange.getArmorData();
}
static getDefaultObject(options = { transfer: true }) {
static getDefaultObject() {
return {
name: 'New Effect',
id: foundry.utils.randomID(),
disabled: false,
img: 'icons/magic/life/heart-cross-blue.webp',
description: '',
transfer: options.transfer,
statuses: [],
changes: [],
system: {

View file

@ -44,8 +44,7 @@ export default class ArmorChange extends foundry.abstract.DataModel {
label: 'Armor',
defaultPriority: 20,
handler: (actor, change, _options, _field, replacementData) => {
const baseParsedMax = itemAbleRollParse(change.value.max, actor, change.effect.parent);
const parsedMax = new Roll(baseParsedMax).evaluateSync().total;
const parsedMax = itemAbleRollParse(change.value.max, actor, change.effect.parent);
game.system.api.documents.DhActiveEffect.applyChange(
actor,
{
@ -82,7 +81,7 @@ export default class ArmorChange extends foundry.abstract.DataModel {
{
...change,
key: 'system.damageThresholds.major',
type: CONFIG.DH.GENERAL.activeEffectModes.add.id,
type: CONFIG.DH.GENERAL.activeEffectModes.override.id,
priority: 50,
value: major
},
@ -96,7 +95,7 @@ export default class ArmorChange extends foundry.abstract.DataModel {
{
...change,
key: 'system.damageThresholds.severe',
type: CONFIG.DH.GENERAL.activeEffectModes.add.id,
type: CONFIG.DH.GENERAL.activeEffectModes.override.id,
priority: 50,
value: severe
},
@ -111,8 +110,6 @@ export default class ArmorChange extends foundry.abstract.DataModel {
};
get isSuppressed() {
if (!this.parent.parent?.actor) return false;
switch (this.value.interaction) {
case CONFIG.DH.GENERAL.activeEffectArmorInteraction.active.id:
return !this.parent.parent?.actor.system.armor;

View file

@ -133,7 +133,7 @@ export default class DhpAdversary extends DhCreature {
}
isItemValid(source) {
return super.isItemValid(source) || source.type === 'feature';
return source.type === 'feature';
}
async _preUpdate(changes, options, user) {

View file

@ -107,8 +107,7 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
hasResistances: true,
hasAttribution: false,
hasLimitedView: true,
usesSize: false,
hasInventory: false
usesSize: false
};
}
@ -169,11 +168,6 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
/* -------------------------------------------- */
isItemValid(source) {
const inventoryTypes = ['weapon', 'armor', 'consumable', 'loot'];
return this.metadata.hasInventory && inventoryTypes.includes(source.type);
}
/**
* Obtain a data object used to evaluate any dice rolls associated with this Item Type
* @param {object} [options] - Options which modify the getRollData method.

View file

@ -3,7 +3,7 @@ import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import DhLevelData from '../levelData.mjs';
import { commonActorRules } from './base.mjs';
import DhCreature from './creature.mjs';
import { attributeField, stressDamageReductionRule, bonusField, GoldField } from '../fields/actorField.mjs';
import { attributeField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs';
import { ActionField } from '../fields/actionField.mjs';
import DHCharacterSettings from '../../applications/sheets-configs/character-settings.mjs';
import { getArmorSources } from '../../helpers/utils.mjs';
@ -18,9 +18,7 @@ export default class DhCharacter extends DhCreature {
label: 'TYPES.Actor.character',
type: 'character',
settingSheet: DHCharacterSettings,
isNPC: false,
hasInventory: true,
quantifiable: ['loot', 'consumable']
isNPC: false
});
}
@ -64,7 +62,12 @@ export default class DhCharacter extends DhCreature {
core: new fields.BooleanField({ initial: false })
})
),
gold: new GoldField(),
gold: new fields.SchemaField({
coins: new fields.NumberField({ initial: 0, integer: true }),
handfuls: new fields.NumberField({ initial: 1, integer: true }),
bags: new fields.NumberField({ initial: 0, integer: true }),
chests: new fields.NumberField({ initial: 0, integer: true })
}),
scars: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.scars' }),
biography: new fields.SchemaField({
background: new fields.HTMLField(),
@ -150,6 +153,7 @@ export default class DhCharacter extends DhCreature {
shortMoves: new fields.NumberField({
required: true,
integer: true,
min: 0,
initial: 0,
label: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.shortRestMoves.label',
hint: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.shortRestMoves.hint'
@ -157,6 +161,7 @@ export default class DhCharacter extends DhCreature {
longMoves: new fields.NumberField({
required: true,
integer: true,
min: 0,
initial: 0,
label: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.longRestMoves.label',
hint: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.longRestMoves.hint'
@ -166,6 +171,7 @@ export default class DhCharacter extends DhCreature {
shortMoves: new fields.NumberField({
required: true,
integer: true,
min: 0,
initial: 0,
label: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.shortRestMoves.label',
hint: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.shortRestMoves.hint'
@ -173,6 +179,7 @@ export default class DhCharacter extends DhCreature {
longMoves: new fields.NumberField({
required: true,
integer: true,
min: 0,
initial: 0,
label: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.longRestMoves.label',
hint: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.longRestMoves.hint'
@ -286,22 +293,6 @@ export default class DhCharacter extends DhCreature {
guaranteedCritical: new fields.BooleanField({
label: 'DAGGERHEART.ACTORS.Character.roll.guaranteedCritical.label',
hint: 'DAGGERHEART.ACTORS.Character.roll.guaranteedCritical.hint'
}),
defaultAdvantageDice: new fields.NumberField({
nullable: true,
required: true,
integer: true,
choices: CONFIG.DH.GENERAL.dieFaces,
initial: null,
label: 'DAGGERHEART.ACTORS.Character.defaultAdvantageDice'
}),
defaultDisadvantageDice: new fields.NumberField({
nullable: true,
required: true,
integer: true,
choices: CONFIG.DH.GENERAL.dieFaces,
initial: null,
label: 'DAGGERHEART.ACTORS.Character.defaultDisadvantageDice'
})
})
})
@ -447,11 +438,6 @@ export default class DhCharacter extends DhCreature {
return attack;
}
/* All items are valid on characters */
isItemValid() {
return true;
}
/** @inheritDoc */
isItemAvailable(item) {
if (!super.isItemAvailable(this)) return false;
@ -750,22 +736,13 @@ export default class DhCharacter extends DhCreature {
}
}
/* Armor and ArmorEffects can set a Base Damage Threshold. Characters only gain level*2 bonus to severe if this is not present */
const severeThresholdMulitplier =
this.armor ||
this.parent.appliedEffects.some(x =>
x.system.changes.some(x => x.type === 'armor' && x.value.damageThresholds)
)
? 1
: 2;
this.damageThresholds = {
major: this.armor
? this.armor.system.baseThresholds.major + this.levelData.level.current
: this.levelData.level.current,
severe: this.armor
? this.armor.system.baseThresholds.severe + this.levelData.level.current
: this.levelData.level.current * severeThresholdMulitplier
: this.levelData.level.current * 2
};
const globalHopeMax = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxHope;

View file

@ -61,24 +61,6 @@ export default class DhCompanion extends DhCreature {
initial: false,
label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.vulnerable'
})
}),
roll: new fields.SchemaField({
defaultAdvantageDice: new fields.NumberField({
nullable: true,
required: true,
integer: true,
choices: CONFIG.DH.GENERAL.dieFaces,
initial: null,
label: 'DAGGERHEART.ACTORS.Character.defaultAdvantageDice'
}),
defaultDisadvantageDice: new fields.NumberField({
nullable: true,
required: true,
integer: true,
choices: CONFIG.DH.GENERAL.dieFaces,
initial: null,
label: 'DAGGERHEART.ACTORS.Character.defaultDisadvantageDice'
})
})
}),
attack: new ActionField({
@ -136,6 +118,10 @@ export default class DhCompanion extends DhCreature {
return this.levelupChoicesLeft > 0;
}
isItemValid() {
return false;
}
prepareBaseData() {
super.prepareBaseData();
this.attack.roll.bonus = this.partner?.system?.spellcastModifier ?? 0;

View file

@ -56,7 +56,7 @@ export default class DhEnvironment extends BaseDataActor {
}
isItemValid(source) {
return super.isItemValid(source) || source.type === 'feature';
return source.type === 'feature';
}
_onUpdate(changes, options, userId) {

View file

@ -1,18 +1,8 @@
import BaseDataActor from './base.mjs';
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
import TagTeamData from '../tagTeamData.mjs';
import GroupRollData from '../groupRollData.mjs';
import { GoldField } from '../fields/actorField.mjs';
export default class DhParty extends BaseDataActor {
/** @inheritdoc */
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {
hasInventory: true,
quantifiable: ['weapon', 'armor', 'loot', 'consumable']
});
}
/**@inheritdoc */
static defineSchema() {
const fields = foundry.data.fields;
@ -20,16 +10,16 @@ export default class DhParty extends BaseDataActor {
...super.defineSchema(),
partyMembers: new ForeignDocumentUUIDArrayField({ type: 'Actor' }, { prune: true }),
notes: new fields.HTMLField(),
gold: new GoldField(),
tagTeam: new fields.EmbeddedDataField(TagTeamData),
groupRoll: new fields.EmbeddedDataField(GroupRollData)
gold: new fields.SchemaField({
coins: new fields.NumberField({ initial: 0, integer: true }),
handfuls: new fields.NumberField({ initial: 1, integer: true }),
bags: new fields.NumberField({ initial: 0, integer: true }),
chests: new fields.NumberField({ initial: 0, integer: true })
}),
tagTeam: new fields.EmbeddedDataField(TagTeamData)
};
}
get active() {
return game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.ActiveParty) === this.parent.id;
}
/* -------------------------------------------- */
/**@inheritdoc */
@ -37,6 +27,10 @@ export default class DhParty extends BaseDataActor {
/* -------------------------------------------- */
isItemValid(source) {
return ['weapon', 'armor', 'consumable', 'loot'].includes(source.type);
}
prepareBaseData() {
super.prepareBaseData();
@ -48,16 +42,6 @@ export default class DhParty extends BaseDataActor {
}
}
_onCreate(data, options, userId) {
super._onCreate(data, options, userId);
if (game.user.isActiveGM && !game.actors.party) {
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.ActiveParty, this.parent.id).then(_ => {
ui.actors.render();
});
}
}
_onDelete(options, userId) {
super._onDelete(options, userId);
@ -65,11 +49,5 @@ export default class DhParty extends BaseDataActor {
for (const member of this.partyMembers) {
member?.parties?.delete(this.parent);
}
// If this *was* the active party, delete it. We can't use game.actors.party as this actor was already deleted
const isWorldActor = !this.parent?.parent && !this.parent.compendium;
const activePartyId = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.ActiveParty);
if (isWorldActor && this.id === activePartyId)
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.ActiveParty, null);
}
}

View file

@ -1,5 +1,6 @@
import DHAbilityUse from './abilityUse.mjs';
import DHActorRoll from './actorRoll.mjs';
import DHGroupRoll from './groupRoll.mjs';
import DHSystemMessage from './systemMessage.mjs';
export const config = {
@ -8,5 +9,6 @@ export const config = {
damageRoll: DHActorRoll,
dualityRoll: DHActorRoll,
fateRoll: DHActorRoll,
groupRoll: DHGroupRoll,
systemMessage: DHSystemMessage
};

View file

@ -7,31 +7,26 @@ export default class DHAbilityUse extends foundry.abstract.TypeDataModel {
img: new fields.StringField({}),
name: new fields.StringField({}),
description: new fields.StringField({}),
source: new fields.SchemaField({
actor: new fields.StringField(),
item: new fields.StringField(),
action: new fields.StringField()
})
actions: new fields.ArrayField(
new fields.ObjectField({
name: new fields.StringField({}),
damage: new fields.SchemaField({
type: new fields.StringField({}),
value: new fields.StringField({})
}),
healing: new fields.SchemaField({
type: new fields.StringField({}),
value: new fields.StringField({})
}),
cost: new fields.SchemaField({
type: new fields.StringField({}),
value: new fields.NumberField({})
}),
target: new fields.SchemaField({
type: new fields.StringField({ nullable: true })
})
})
)
};
}
get actionActor() {
if (!this.source.actor) return null;
return fromUuidSync(this.source.actor);
}
get actionItem() {
const actionActor = this.actionActor;
if (!actionActor || !this.source.item) return null;
const item = actionActor.items.get(this.source.item);
return item ? item.system.actions?.find(a => a.id === this.source.action) : null;
}
get action() {
const { actionItem: itemAction } = this;
if (!this.source.action) return null;
if (itemAction) return itemAction;
return null;
}
}

View file

@ -48,7 +48,6 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
action: new fields.StringField()
}),
damage: new fields.ObjectField(),
damageOptions: new fields.ObjectField(),
costs: new fields.ArrayField(new fields.ObjectField()),
successConsumed: new fields.BooleanField({ initial: false })
};

View file

@ -0,0 +1,39 @@
import { abilities } from '../../config/actorConfig.mjs';
export default class DHGroupRoll extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
leader: new fields.EmbeddedDataField(GroupRollMemberField),
members: new fields.ArrayField(new fields.EmbeddedDataField(GroupRollMemberField))
};
}
get totalModifier() {
return this.members.reduce((acc, m) => {
if (m.manualSuccess === null) return acc;
return acc + (m.manualSuccess ? 1 : -1);
}, 0);
}
}
class GroupRollMemberField extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
actor: new fields.ObjectField(),
trait: new fields.StringField({ choices: abilities }),
difficulty: new fields.StringField(),
result: new fields.ObjectField({ nullable: true, initial: null }),
manualSuccess: new fields.BooleanField({ nullable: true, initial: null })
};
}
/* Can be expanded if we handle automation of success/failure */
get success() {
return manualSuccess;
}
}

View file

@ -5,6 +5,10 @@ export default class DhCountdowns extends foundry.abstract.DataModel {
const fields = foundry.data.fields;
return {
/* Outdated and unused. Needed for migration. Remove in next minor version. (1.3) */
narrative: new fields.EmbeddedDataField(DhCountdownData),
encounter: new fields.EmbeddedDataField(DhCountdownData),
/**/
countdowns: new fields.TypedObjectField(new fields.EmbeddedDataField(DhCountdown)),
defaultOwnership: new fields.NumberField({
required: true,
@ -15,6 +19,122 @@ export default class DhCountdowns extends foundry.abstract.DataModel {
}
}
/* Outdated and unused. Needed for migration. Remove in next minor version. (1.3) */
class DhCountdownData extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
countdowns: new fields.TypedObjectField(new fields.EmbeddedDataField(DhOldCountdown)),
ownership: new fields.SchemaField({
default: new fields.NumberField({
required: true,
choices: Object.values(CONST.DOCUMENT_OWNERSHIP_LEVELS),
initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE
}),
players: new fields.TypedObjectField(
new fields.SchemaField({
type: new fields.NumberField({
required: true,
choices: Object.values(CONST.DOCUMENT_OWNERSHIP_LEVELS),
initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT
})
})
)
}),
window: new fields.SchemaField({})
};
}
get playerOwnership() {
return Array.from(game.users).reduce((acc, user) => {
acc[user.id] = {
value: user.isGM
? CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER
: this.ownership.players[user.id] && this.ownership.players[user.id].type !== -1
? this.ownership.players[user.id].type
: this.ownership.default,
isGM: user.isGM
};
return acc;
}, {});
}
}
/* Outdated and unused. Needed for migration. Remove in next minor version. (1.3) */
class DhOldCountdown extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
name: new fields.StringField({
required: true,
label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.name.label'
}),
img: new fields.FilePathField({
categories: ['IMAGE'],
base64: false,
initial: 'icons/magic/time/hourglass-yellow-green.webp'
}),
ownership: new fields.SchemaField({
default: new fields.NumberField({
required: true,
choices: Object.values(CONST.DOCUMENT_OWNERSHIP_LEVELS),
initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE
}),
players: new fields.TypedObjectField(
new fields.SchemaField({
type: new fields.NumberField({
required: true,
choices: Object.values(CONST.DOCUMENT_OWNERSHIP_LEVELS),
initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT
})
})
)
}),
progress: new fields.SchemaField({
current: new fields.NumberField({
required: true,
integer: true,
initial: 1,
label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.progress.current.label'
}),
max: new fields.NumberField({
required: true,
integer: true,
initial: 1,
label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.progress.max.label'
}),
type: new fields.SchemaField({
value: new fields.StringField({
required: true,
choices: CONFIG.DH.GENERAL.countdownProgressionTypes,
initial: CONFIG.DH.GENERAL.countdownProgressionTypes.custom.id,
label: 'DAGGERHEART.GENERAL.type'
}),
label: new fields.StringField({
label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.progress.type.label.label'
})
})
})
};
}
get playerOwnership() {
return Array.from(game.users).reduce((acc, user) => {
acc[user.id] = {
value: user.isGM
? CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER
: this.ownership.players[user.id] && this.ownership.players[user.id].type !== -1
? this.ownership.players[user.id].type
: this.ownership.default,
isGM: user.isGM
};
return acc;
}, {});
}
}
export class DhCountdown extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;

View file

@ -1,4 +1,3 @@
export { default as AreasField } from './areasField.mjs';
export { default as CostField } from './costField.mjs';
export { default as CountdownField } from './countdownField.mjs';
export { default as UsesField } from './usesField.mjs';

View file

@ -1,40 +0,0 @@
const fields = foundry.data.fields;
export default class AreasField extends fields.ArrayField {
/**
* Action Workflow order
*/
static order = 150;
/** @inheritDoc */
constructor(options = {}, context = {}) {
const element = new fields.SchemaField({
name: new fields.StringField({
nullable: false,
initial: 'Area',
label: 'DAGGERHEART.GENERAL.name'
}),
type: new fields.StringField({
nullable: false,
choices: CONFIG.DH.ACTIONS.areaTypes,
initial: CONFIG.DH.ACTIONS.areaTypes.placed.id,
label: 'DAGGERHEART.GENERAL.type'
}),
shape: new fields.StringField({
nullable: false,
choices: CONFIG.DH.GENERAL.templateTypes,
initial: CONFIG.DH.GENERAL.templateTypes.circle.id,
label: 'DAGGERHEART.ACTIONS.Config.area.shape'
}),
/* Could be opened up to allow numbers to be input aswell. Probably best handled via an autocomplete in that case to allow the select options but also free text */
size: new fields.StringField({
nullable: false,
choices: CONFIG.DH.GENERAL.range,
initial: CONFIG.DH.GENERAL.range.veryClose.id,
label: 'DAGGERHEART.ACTIONS.Config.area.size'
}),
effects: new fields.ArrayField(new fields.DocumentIdField())
});
super(element, options, context);
}
}

View file

@ -57,10 +57,6 @@ export default class CountdownField extends fields.ArrayField {
data.countdowns[foundry.utils.randomID()] = {
...countdown,
ownership: game.users.reduce((acc, curr) => {
if (!curr.isGM) acc[curr.id] = countdown.defaultOwnership;
return acc;
}, {}),
progress: {
...countdown.progress,
current: countdownStart,

View file

@ -18,12 +18,7 @@ export default class DamageField extends fields.SchemaField {
initial: false,
label: 'DAGGERHEART.ACTIONS.Settings.includeBase.label'
}),
direct: new fields.BooleanField({ initial: false, label: 'DAGGERHEART.CONFIG.DamageType.direct.name' }),
groupAttack: new fields.StringField({
choices: CONFIG.DH.GENERAL.groupAttackRange,
blank: true,
label: 'DAGGERHEART.ACTIONS.Settings.groupAttack.label'
})
direct: new fields.BooleanField({ initial: false, label: 'DAGGERHEART.CONFIG.DamageType.direct.name' })
};
super(damageFields, options, context);
}
@ -229,22 +224,6 @@ export default class DamageField extends fields.SchemaField {
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.players)
);
}
static getGroupAttackTokens(actorId, range) {
if (!canvas.scene) return [];
const targets = Array.from(game.user.targets);
const rangeSettings = canvas.scene?.rangeSettings;
if (!rangeSettings) return [];
const maxDistance = rangeSettings[range];
return canvas.scene.tokens.filter(x => {
if (x.actor?.id !== actorId) return false;
if (targets.every(target => x.object.distanceTo(target) > maxDistance)) return false;
return true;
});
}
}
export class DHActionDiceData extends foundry.abstract.DataModel {
@ -320,7 +299,7 @@ export class DHDamageData extends DHResourceData {
required: true
}),
{
label: game.i18n.localize('DAGGERHEART.GENERAL.type')
label: 'Type'
}
)
};

View file

@ -1,4 +1,3 @@
import { itemAbleRollParse } from '../../../helpers/utils.mjs';
import FormulaField from '../formulaField.mjs';
const fields = foundry.data.fields;
@ -37,23 +36,20 @@ export default class DHSummonField extends fields.ArrayField {
const rolls = [];
const summonData = [];
for (const summon of this.summon) {
const roll = new Roll(itemAbleRollParse(summon.count, this.actor, this.item));
await roll.evaluate();
const count = roll.total;
if (!roll.isDeterministic && game.modules.get('dice-so-nice')?.active) rolls.push(roll);
let count = summon.count;
const roll = new Roll(summon.count);
if (!roll.isDeterministic) {
await roll.evaluate();
if (game.modules.get('dice-so-nice')?.active) rolls.push(roll);
count = roll.total;
}
const actor = await DHSummonField.getWorldActor(await foundry.utils.fromUuid(summon.actorUUID));
const actor = DHSummonField.getWorldActor(await foundry.utils.fromUuid(summon.actorUUID));
/* Extending summon data in memory so it's available in actionField.toChat. Think it's harmless, but ugly. Could maybe find a better way. */
summon.rolledCount = count;
summon.actor = actor.toObject();
const countNumber = Number.parseInt(count);
for (let i = 0; i < countNumber; i++) {
const remaining = countNumber - i;
summonData.push({
actor,
tokenPreviewName: `${actor.prototypeToken.name}${remaining > 1 ? ` (${remaining}x)` : ''}`
});
}
summonData.push({ actor, count: count });
}
if (rolls.length) await Promise.all(rolls.map(roll => game.dice3d.showForRoll(roll, game.user, true)));
@ -62,22 +58,32 @@ export default class DHSummonField extends fields.ArrayField {
DHSummonField.handleSummon(summonData, this.actor);
}
/* Check for any available instances of the actor present in the world if we're missing artwork in the compendium. If none exists, create one. */
static async getWorldActor(baseActor) {
/* Check for any available instances of the actor present in the world if we're missing artwork in the compendium */
static getWorldActor(baseActor) {
const dataType = game.system.api.data.actors[`Dh${baseActor.type.capitalize()}`];
if (baseActor.inCompendium && dataType && baseActor.img === dataType.DEFAULT_ICON) {
const worldActorCopy = game.actors.find(x => x.name === baseActor.name);
if (worldActorCopy) return worldActorCopy;
return await game.system.api.documents.DhpActor.create(baseActor.toObject());
return worldActorCopy ?? baseActor;
}
return baseActor;
}
static async handleSummon(summonData, actionActor) {
await CONFIG.ux.TokenManager.createTokensWithPreview(summonData, { elevation: actionActor.token?.elevation });
static async handleSummon(summonData, actionActor, summonIndex = 0) {
const summon = summonData[summonIndex];
const result = await CONFIG.ux.TokenManager.createPreviewAsync(summon.actor, {
name: `${summon.actor.prototypeToken.name}${summon.count > 1 ? ` (${summon.count}x)` : ''}`
});
return actionActor.sheet?.maximize();
if (!result) return actionActor.sheet?.maximize();
summon.actor = result.actor;
summon.count--;
if (summon.count <= 0) {
summonIndex++;
if (summonIndex === summonData.length) return actionActor.sheet?.maximize();
}
DHSummonField.handleSummon(summonData, actionActor, summonIndex);
}
}

View file

@ -281,14 +281,8 @@ export function ActionMixin(Base) {
name: this.name,
img: this.baseAction ? this.parent.parent.img : this.img,
tags: this.tags ? this.tags : ['Spell', 'Arcana', 'Lv 10'],
areas: this.areas,
summon: this.summon
},
source: {
actor: this.actor.uuid,
item: this.item.id,
action: this.id
},
itemOrigin: this.item,
description: this.description || (this.item instanceof Item ? this.item.system.description : '')
};

View file

@ -89,13 +89,13 @@ class ResourcesField extends fields.TypedObjectField {
*/
_getField(path) {
if (path.length === 0) return this;
const name = path.pop();
if (name === this.element.name) return this.element_getField(path);
const first = path.shift();
if (first === this.element.name) return this.element_getField(path);
const resources = CONFIG.DH.RESOURCE[this.actorType].all;
if (name in resources) {
if (first in resources) {
const field = this.element._getField(path);
field.label = resources[name].label;
field.label = resources[first].label;
return field;
}
@ -103,15 +103,4 @@ class ResourcesField extends fields.TypedObjectField {
}
}
class GoldField extends fields.SchemaField {
constructor() {
super({
coins: new fields.NumberField({ initial: 0, integer: true }),
handfuls: new fields.NumberField({ initial: 1, integer: true }),
bags: new fields.NumberField({ initial: 0, integer: true }),
chests: new fields.NumberField({ initial: 0, integer: true })
});
}
}
export { attributeField, ResourcesField, GoldField, stressDamageReductionRule, bonusField };
export { attributeField, ResourcesField, stressDamageReductionRule, bonusField };

View file

@ -1,40 +0,0 @@
export default class GroupRollData extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
leader: new fields.EmbeddedDataField(CharacterData, { nullable: true, initial: null }),
aidingCharacters: new fields.TypedObjectField(new fields.EmbeddedDataField(CharacterData))
};
}
get participants() {
return {
...(this.leader ? { [this.leader.id]: this.leader } : {}),
...this.aidingCharacters
};
}
}
export class CharacterData extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
id: new fields.StringField({ required: true }),
name: new fields.StringField({ required: true }),
img: new fields.StringField({ required: true }),
rollChoice: new fields.StringField({
choices: CONFIG.DH.ACTOR.abilities,
initial: CONFIG.DH.ACTOR.abilities.agility.id
}),
rollData: new fields.JSONField({ nullable: true, initial: null }),
selected: new fields.BooleanField({ initial: false }),
successfull: new fields.BooleanField({ nullable: true, initial: null })
};
}
get roll() {
return this.rollData ? CONFIG.Dice.daggerheart.DualityRoll.fromData(this.rollData) : null;
}
}

View file

@ -4,6 +4,7 @@
* @property {string} label - A localizable label used on application.
* @property {string} type - The system type that this data model represents.
* @property {boolean} hasDescription - Indicates whether items of this type have description field
* @property {boolean} isQuantifiable - Indicates whether items of this type have quantity field
* @property {boolean} isInventoryItem- Indicates whether items of this type is a Inventory Item
*/
@ -23,6 +24,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
type: 'base',
hasDescription: false,
hasResource: false,
isQuantifiable: false,
isInventoryItem: false,
hasActions: false,
hasAttribution: true
@ -81,7 +83,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
);
}
if (this.metadata.isInventoryItem)
if (this.metadata.isQuantifiable)
schema.quantity = new fields.NumberField({ integer: true, initial: 1, min: 0, required: true });
if (this.metadata.hasActions) schema.actions = new ActionsField();
@ -108,10 +110,6 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
}
get actionsList() {
// No actions on non-characters
if (this.metadata.isInventoryItem && this.actor && this.actor.type !== 'character') {
return [];
}
return this.actions;
}

View file

@ -7,6 +7,7 @@ export default class DHConsumable extends BaseDataItem {
label: 'TYPES.Item.consumable',
type: 'consumable',
hasDescription: true,
isQuantifiable: true,
isInventoryItem: true,
hasActions: true
});

View file

@ -7,6 +7,7 @@ export default class DHLoot extends BaseDataItem {
label: 'TYPES.Item.loot',
type: 'loot',
hasDescription: true,
isQuantifiable: true,
isInventoryItem: true,
hasActions: true
});

View file

@ -99,9 +99,7 @@ export default class DHWeapon extends AttachableItem {
/* -------------------------------------------- */
get actionsList() {
// No actions on non-characters
if (this.actor && this.actor.type !== 'character') return [];
return [this.attack, ...super.actionsList];
return [this.attack, ...this.actions];
}
get customActions() {

View file

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

View file

@ -1,40 +0,0 @@
export default class DhApplyActiveEffect extends CONFIG.RegionBehavior.dataModels.applyActiveEffect {
static async #getApplicableEffects(token) {
const effects = await Promise.all(this.effects.map(foundry.utils.fromUuid));
return effects.filter(
effect => !effect.system.targetDispositions.size || effect.system.targetDispositions.has(token.disposition)
);
}
static async #onTokenEnter(event) {
if (!event.user.isSelf) return;
const { token, movement } = event.data;
const actor = token.actor;
if (!actor) return;
const resumeMovement = movement ? token.pauseMovement() : undefined;
const effects = await DhApplyActiveEffect.#getApplicableEffects.bind(this)(event.data.token);
const toCreate = [];
for (const effect of effects) {
const data = effect.toObject();
delete data._id;
if (effect.compendium) {
data._stats.duplicateSource = null;
data._stats.compendiumSource = effect.uuid;
} else {
data._stats.duplicateSource = effect.uuid;
data._stats.compendiumSource = null;
}
data._stats.exportSource = null;
data.origin = this.parent.uuid;
toCreate.push(data);
}
if (toCreate.length) await actor.createEmbeddedDocuments('ActiveEffect', toCreate);
await resumeMovement?.();
}
/** @override */
static events = {
...CONFIG.RegionBehavior.dataModels.applyActiveEffect.events,
[CONST.REGION_EVENTS.TOKEN_ENTER]: this.#onTokenEnter
};
}

View file

@ -8,9 +8,6 @@ export default class DhAppearance extends foundry.abstract.DataModel {
initial: null,
blank: true,
choices: CONFIG.DH.GENERAL.diceSoNiceSFXClasses
}),
options: new foundry.data.fields.SchemaField({
muteSound: new foundry.data.fields.BooleanField()
})
});

View file

@ -33,7 +33,7 @@ export class MemberData extends foundry.abstract.DataModel {
required: true,
choices: CONFIG.DH.GENERAL.tagTeamRollTypes,
initial: CONFIG.DH.GENERAL.tagTeamRollTypes.trait.id,
label: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.rollType')
label: 'Roll Type'
}),
rollChoice: new fields.StringField({ nullable: true, initial: null }),
rollData: new fields.JSONField({ nullable: true, initial: null }),

View file

@ -144,7 +144,6 @@ export default class DamageRoll extends DHRoll {
constructFormula(config) {
this.options.isCritical = config.isCritical;
for (const [index, part] of this.options.roll.entries()) {
const isHitpointPart = part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id;
part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data));
part.roll.terms = Roll.parse(part.roll.formula, config.data);
if (part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
@ -170,16 +169,7 @@ export default class DamageRoll extends DHRoll {
);
}
if (config.damageOptions.groupAttack?.numAttackers > 1 && isHitpointPart) {
const damageTypes = [foundry.dice.terms.Die, foundry.dice.terms.NumericTerm];
for (const term of part.roll.terms) {
if (damageTypes.some(type => term instanceof type)) {
term.number *= config.damageOptions.groupAttack.numAttackers;
}
}
}
if (config.isCritical && isHitpointPart) {
if (config.isCritical && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
const total = part.roll.dice.reduce((acc, term) => acc + term._faces * term._number, 0);
if (total > 0) {
part.roll.terms.push(...this.formatModifier(total));

View file

@ -145,7 +145,6 @@ export default class DHRoll extends Roll {
roll: this,
parent: chatData.parent,
targetMode: chatData.targetMode,
areas: chatData.action?.areas,
metagamingSettings
});
}
@ -247,7 +246,7 @@ export default class DHRoll extends Roll {
return (this._formula = this.constructor.getFormula(this.terms));
}
/**
/**
* Calculate total modifiers of any rolls, including non-dh rolls.
* This exists because damage rolls still may receive base roll classes
*/
@ -257,7 +256,7 @@ export default class DHRoll extends Roll {
if (!roll.terms[i].isDeterministic) continue;
const termTotal = roll.terms[i].total;
if (typeof termTotal === 'number') {
const multiplier = roll.terms[i - 1]?.operator === ' - ' ? -1 : 1;
const multiplier = roll.terms[i - 1]?.operator === " - " ? -1 : 1;
modifierTotal += multiplier * termTotal;
}
}
@ -273,7 +272,7 @@ export default class DHRoll extends Roll {
const changeKeys = this.getActionChangeKeys();
return (
this.options.effects?.reduce((acc, effect) => {
if (effect.system.changes.some(x => changeKeys.some(key => x.key?.includes(key)))) {
if (effect.system.changes.some(x => changeKeys.some(key => x.key.includes(key)))) {
acc[effect.id] = {
id: effect.id,
name: effect.name,

View file

@ -1,13 +1,9 @@
import DualityDie from './dualityDie.mjs';
import HopeDie from './hopeDie.mjs';
import FearDie from './fearDie.mjs';
import AdvantageDie from './advantageDie.mjs';
import DisadvantageDie from './disadvantageDie.mjs';
export const diceTypes = {
DualityDie,
HopeDie,
FearDie,
AdvantageDie,
DisadvantageDie
};

View file

@ -43,10 +43,9 @@ export default class DualityDie extends foundry.dice.terms.Die {
options: { appearance: {} }
};
const diceAppearance = await this.getDiceSoNiceAppearance(options.liveRoll.roll);
diceSoNiceRoll.dice[0].options.appearance = diceAppearance.appearance;
diceSoNiceRoll.dice[0].options.modelFile = diceAppearance.modelFile;
diceSoNiceRoll.dice[0].results = diceSoNiceRoll.dice[0].results.filter(x => x.active);
const preset = await getDiceSoNicePreset(diceSoNice[key], faces);
diceSoNiceRoll.dice[0].options.appearance = preset.appearance;
diceSoNiceRoll.dice[0].options.modelFile = preset.modelFile;
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
} else {
@ -60,11 +59,4 @@ export default class DualityDie extends foundry.dice.terms.Die {
this.#updateResources(oldDuality, newDuality, options.liveRoll.actor);
}
}
/**
* Overridden by extending classes HopeDie and FearDie
*/
async getDiceSoNiceAppearance() {
return {};
}
}

View file

@ -1,9 +0,0 @@
import { getDiceSoNicePresets } from '../../config/generalConfig.mjs';
import DualityDie from './dualityDie.mjs';
export default class FearDie extends DualityDie {
async getDiceSoNiceAppearance(roll) {
const { fear } = await getDiceSoNicePresets(roll, this.denomination, this.denomination);
return fear;
}
}

View file

@ -1,9 +0,0 @@
import { getDiceSoNicePresets } from '../../config/generalConfig.mjs';
import DualityDie from './dualityDie.mjs';
export default class HopeDie extends DualityDie {
async getDiceSoNiceAppearance(roll) {
const { hope } = await getDiceSoNicePresets(roll, this.denomination, this.denomination);
return hope;
}
}

View file

@ -3,6 +3,7 @@ import D20Roll from './d20Roll.mjs';
import { parseRallyDice, setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
export default class DualityRoll extends D20Roll {
_advantageFaces = 6;
_advantageNumber = 1;
_rallyIndex;
@ -10,11 +11,6 @@ export default class DualityRoll extends D20Roll {
super(formula, data, options);
this.rallyChoices = this.setRallyChoices();
this.guaranteedCritical = options.guaranteedCritical;
const advantageFaces = data.rules?.roll?.defaultAdvantageDice
? Number.parseInt(data.rules.roll.defaultAdvantageDice)
: 6;
this.advantageFaces = Number.isNaN(advantageFaces) ? 6 : advantageFaces;
}
static messageType = 'dualityRoll';
@ -28,7 +24,7 @@ export default class DualityRoll extends D20Roll {
}
get dHope() {
if (!(this.dice[0] instanceof game.system.api.dice.diceTypes.HopeDie)) this.createBaseDice();
if (!(this.dice[0] instanceof game.system.api.dice.diceTypes.DualityDie)) this.createBaseDice();
return this.dice[0];
}
@ -38,7 +34,7 @@ export default class DualityRoll extends D20Roll {
}
get dFear() {
if (!(this.dice[1] instanceof game.system.api.dice.diceTypes.FearDie)) this.createBaseDice();
if (!(this.dice[1] instanceof game.system.api.dice.diceTypes.DualityDie)) this.createBaseDice();
return this.dice[1];
}
@ -55,6 +51,14 @@ export default class DualityRoll extends D20Roll {
return this.dice[2] instanceof game.system.api.dice.diceTypes.DisadvantageDie ? this.dice[2] : null;
}
get advantageFaces() {
return this._advantageFaces;
}
set advantageFaces(faces) {
this._advantageFaces = this.getFaces(faces);
}
get advantageNumber() {
return this._advantageNumber;
}
@ -64,8 +68,8 @@ export default class DualityRoll extends D20Roll {
}
get extraDice() {
const { HopeDie, FearDie, AdvantageDie, DisadvantageDie } = game.system.api.dice.diceTypes;
return this.dice.filter(x => ![HopeDie, FearDie, AdvantageDie, DisadvantageDie].some(die => x instanceof die));
const { DualityDie, AdvantageDie, DisadvantageDie } = game.system.api.dice.diceTypes;
return this.dice.filter(x => ![DualityDie, AdvantageDie, DisadvantageDie].some(die => x instanceof die));
}
setRallyChoices() {
@ -121,8 +125,8 @@ export default class DualityRoll extends D20Roll {
/** @inheritDoc */
static fromData(data) {
data.terms[0].class = 'HopeDie';
data.terms[2].class = 'FearDie';
data.terms[0].class = 'DualityDie';
data.terms[2].class = 'DualityDie';
if (data.options.roll.advantage?.type && data.terms[4]?.faces) {
data.terms[4].class = data.options.roll.advantage.type === 1 ? 'AdvantageDie' : 'DisadvantageDie';
}
@ -131,18 +135,18 @@ export default class DualityRoll extends D20Roll {
createBaseDice() {
if (
this.dice[0] instanceof game.system.api.dice.diceTypes.HopeDie &&
this.dice[1] instanceof game.system.api.dice.diceTypes.FearDie
this.dice[0] instanceof game.system.api.dice.diceTypes.DualityDie &&
this.dice[1] instanceof game.system.api.dice.diceTypes.DualityDie
) {
this.terms = [this.terms[0], this.terms[1], this.terms[2]];
return;
}
this.terms[0] = new game.system.api.dice.diceTypes.HopeDie({
this.terms[0] = new game.system.api.dice.diceTypes.DualityDie({
faces: this.data.rules.dualityRoll?.defaultHopeDice ?? 12
});
this.terms[1] = new foundry.dice.terms.OperatorTerm({ operator: '+' });
this.terms[2] = new game.system.api.dice.diceTypes.FearDie({
this.terms[2] = new game.system.api.dice.diceTypes.DualityDie({
faces: this.data.rules.dualityRoll?.defaultFearDice ?? 12
});
}

View file

@ -200,6 +200,7 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
static effectSafeEval(expression) {
let result;
try {
// eslint-disable-next-line no-new-func
const evl = new Function('sandbox', `with (sandbox) { return ${expression}}`);
result = evl(Roll.MATH_PROXY);
} catch (err) {

View file

@ -113,13 +113,11 @@ export default class DhpActor extends Actor {
_onUpdate(changes, options, userId) {
super._onUpdate(changes, options, userId);
for (const party of this.parties) {
party.render({ parts: ['partyMembers'] });
party.render();
}
}
async _preDelete(options, user) {
if ((await super._preDelete(options, user)) === false) return false;
async _preDelete() {
if (this.prototypeToken.actorLink) {
game.system.registeredTriggers.unregisterItemTriggers(this.items);
} else {
@ -132,7 +130,7 @@ export default class DhpActor extends Actor {
_onDelete(options, userId) {
super._onDelete(options, userId);
for (const party of this.parties) {
party.render({ parts: ['partyMembers'] });
party.render();
}
}
@ -602,7 +600,6 @@ export default class DhpActor extends Actor {
rollData.system = this.system.getRollData();
rollData.prof = this.system.proficiency ?? 1;
rollData.cast = this.system.spellcastModifier ?? 1;
return rollData;
}

View file

@ -137,10 +137,6 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
element.addEventListener('click', this.onApplyEffect.bind(this))
);
for (const element of html.querySelectorAll('.action-areas')) {
element.addEventListener('click', this.onCreateAreas.bind(this));
}
html.querySelectorAll('.roll-target').forEach(element => {
element.addEventListener('mouseenter', this.hoverTarget);
element.addEventListener('mouseleave', this.unhoverTarget);
@ -182,8 +178,8 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
const pendingingSaves = targets.filter(t => t.saved.success === null);
if (pendingingSaves.length) {
const confirm = await foundry.applications.api.DialogV2.confirm({
window: { title: game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.title') },
content: `<p>${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.unfinishedRolls')}</p><p>${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.confirmation')}</p><p><i>${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.warning')}</i></p>`
window: { title: 'Pending Reaction Rolls found' },
content: `<p>Some Tokens still need to roll their Reaction Roll.</p><p>Are you sure you want to continue ?</p><p><i>Undone reaction rolls will be considered as failed</i></p>`
});
if (!confirm) return;
}
@ -253,54 +249,6 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
this.system.action?.workflow.get('effects')?.execute(config, targets, true);
}
async onCreateAreas(event) {
const createArea = async selectedArea => {
const effects = selectedArea.effects.map(effect => this.system.action.item.effects.get(effect).uuid);
const { shape: type, size: range } = selectedArea;
const shapeData = CONFIG.Canvas.layers.regions.layerClass.getTemplateShape({ type, range });
await canvas.regions.placeRegion(
{
name: selectedArea.name,
shapes: [shapeData],
restriction: { enabled: false, type: 'move', priority: 0 },
behaviors: [
{
name: game.i18n.localize('TYPES.RegionBehavior.applyActiveEffect'),
type: 'applyActiveEffect',
system: {
effects: effects
}
}
],
displayMeasurements: true,
locked: false,
ownership: { default: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE },
visibility: CONST.REGION_VISIBILITY.ALWAYS
},
{ create: true }
);
};
if (this.system.action.areas.length === 1) createArea(this.system.action.areas[0]);
else if (this.system.action.areas.length > 1) {
new foundry.applications.ux.ContextMenu.implementation(
event.target,
'.action-areas',
this.system.action.areas.map(area => ({
label: area.name,
onClick: () => createArea(area)
})),
{
jQuery: false,
fixed: true
}
);
CONFIG.ux.ContextMenu.triggerContextMenu(event, '.action-areas');
}
}
filterPermTargets(targets) {
return targets.filter(t => fromUuidSync(t.actorId)?.canUserModify(game.user, 'update'));
}

View file

@ -1,11 +1,4 @@
export default class DhActorCollection extends foundry.documents.collections.Actors {
/** @returns the active party */
get party() {
const id = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.ActiveParty);
const actor = game.actors.get(id);
return actor?.type === 'party' ? actor : null;
}
/** Ensure companions are initialized after all other subtypes. */
_initialize() {
super._initialize();

View file

@ -31,13 +31,8 @@ export default class DHItem extends foundry.documents.Item {
static async createDocuments(sources, operation) {
// Ensure that items being created are valid to the actor its being added to
const actor = operation.parent;
const filtered = actor ? sources.filter(s => actor.system.isItemValid(s)) : sources;
if (actor && filtered.length === 0 && sources.length > 0) {
const itemType = _loc(`TYPES.Item.${sources[0].type}`);
const actorType = _loc(`TYPES.Actor.${actor.type}`);
ui.notifications.error('DAGGERHEART.ACTORS.Base.CannotAddType', { format: { itemType, actorType } });
}
return super.createDocuments(filtered, operation);
sources = actor?.system?.isItemValid ? sources.filter(s => actor.system.isItemValid(s)) : sources;
return super.createDocuments(sources, operation);
}
/* -------------------------------------------- */
@ -76,13 +71,6 @@ export default class DHItem extends foundry.documents.Item {
return this.system.metadata.isInventoryItem ?? false;
}
/** Returns true if the item can be used */
get usable() {
const actor = this.actor;
const actionsList = this.system.actionsList;
return this.isOwner && actor?.type === 'character' && (actionsList?.size || actionsList?.length);
}
/** @inheritdoc */
static async createDialog(data = {}, createOptions = {}, options = {}) {
const { folders, types, template, context = {}, ...dialogOptions } = options;

View file

@ -1,16 +1,6 @@
import DHToken from './token.mjs';
export default class DhScene extends Scene {
get rangeSettings() {
const { custom } = CONFIG.DH.GENERAL.sceneRangeMeasurementSetting;
const sceneMeasurements = this.flags.daggerheart?.rangeMeasurement;
const globalMeasurements = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.variantRules
).rangeMeasurement;
return sceneMeasurements?.setting === custom.id ? sceneMeasurements : globalMeasurements;
}
/** A map of `TokenDocument` IDs embedded in this scene long with new dimensions from actor size-category changes */
#sizeSyncBatch = new Map();

View file

@ -1,68 +1,104 @@
/**
* A singleton class that handles creating tokens.
* A singleton class that handles preview tokens.
*/
export default class DhTokenManager {
#activePreview;
#actor;
#resolve;
/**
* Create a token previer
* @param {Actor} actor
* @param {object} tokenData
* Create a template preview, deactivating any existing ones.
* @param {object} data
*/
async createPreview(actor, tokenData) {
const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes;
if (actor?.system.metadata.usesSize) {
const tokenSize = tokenSizes[actor.system.size];
if (tokenSize && actor.system.size !== CONFIG.DH.ACTOR.tokenSize.custom.id) {
tokenData.width = tokenSize;
tokenData.height = tokenSize;
}
}
return await canvas.tokens.placeTokens(
[
{
...actor.prototypeToken.toObject(),
actorId: actor.id,
displayName: 50,
...tokenData
}
],
{ create: false }
this.#actor = actor;
const token = await canvas.tokens._createPreview(
{
...actor.prototypeToken,
displayName: 50,
...tokenData
},
{ renderSheet: false, actor }
);
this.#activePreview = {
document: token.document,
object: token,
origin: { x: token.document.x, y: token.document.y }
};
this.#activePreview.events = {
contextmenu: this.#cancelTemplate.bind(this),
mousedown: this.#confirmTemplate.bind(this),
mousemove: this.#onDragMouseMove.bind(this)
};
canvas.stage.on('mousemove', this.#activePreview.events.mousemove);
canvas.stage.on('mousedown', this.#activePreview.events.mousedown);
canvas.app.view.addEventListener('contextmenu', this.#activePreview.events.contextmenu);
}
/* Currently intended for using as a preview of where to create a token. (note the flag) */
async createPreviewAsync(actor, tokenData = {}) {
return new Promise(resolve => {
this.#resolve = resolve;
this.createPreview(actor, { ...tokenData, flags: { daggerheart: { createPlacement: true } } });
});
}
/**
* Creates new tokens on the canvas by placing previews.
* @param {object} tokenData
* @param {object} options
* Handles the movement of the token preview on mousedrag.
* @param {mousemove Event} event
*/
async createTokensWithPreview(tokensData, { elevation } = {}) {
const scene = game.scenes.get(game.user.viewedScene);
if (!scene) return;
#onDragMouseMove(event) {
event.stopPropagation();
const { moveTime, object } = this.#activePreview;
const update = {};
const level = scene.levels.get(game.user.viewedLevel);
if (!level) return;
const now = Date.now();
if (now - (moveTime || 0) <= 16) return;
this.#activePreview.moveTime = now;
const createElevation = elevation ?? level.elevation.bottom;
for (const tokenData of tokensData) {
const previewTokens = await this.createPreview(tokenData.actor, {
name: tokenData.tokenPreviewName,
level: game.user.viewedLevel,
elevation: createElevation,
flags: { daggerheart: { createPlacement: true } }
});
if (!previewTokens?.length) return null;
let cursor = event.getLocalPosition(canvas.templates);
await canvas.scene.createEmbeddedDocuments(
'Token',
previewTokens.map(x => ({
...x.toObject(),
name: tokenData.actor.prototypeToken.name,
displayName: tokenData.actor.prototypeToken.displayName,
flags: tokenData.actor.prototypeToken.flags
})),
{ controlObject: true, parent: canvas.scene }
);
}
Object.assign(update, canvas.grid.getTopLeftPoint(cursor));
object.document.updateSource(update);
object.renderFlags.set({ refresh: true });
}
/**
* Cancels the preview token on right-click.
* @param {contextmenu Event} event
*/
#cancelTemplate(_event, resolved) {
const { mousemove, mousedown, contextmenu } = this.#activePreview.events;
this.#activePreview.object.destroy();
canvas.stage.off('mousemove', mousemove);
canvas.stage.off('mousedown', mousedown);
canvas.app.view.removeEventListener('contextmenu', contextmenu);
if (this.#resolve && !resolved) this.#resolve(false);
}
/**
* Creates a real Actor and token at the preview location and cancels the preview.
* @param {click Event} event
*/
async #confirmTemplate(event) {
event.stopPropagation();
this.#cancelTemplate(event, true);
const actor = this.#actor.inCompendium
? await game.system.api.documents.DhpActor.create(this.#actor.toObject())
: this.#actor;
const tokenData = await actor.getTokenDocument();
const result = await canvas.scene.createEmbeddedDocuments('Token', [
{ ...tokenData.toObject(), x: this.#activePreview.document.x, y: this.#activePreview.document.y }
]);
this.#activePreview = undefined;
if (this.#resolve && result.length) this.#resolve(result[0]);
}
}

View file

@ -12,7 +12,7 @@ export default function DhTemplateEnricher(match, _options) {
)?.id
: params.range;
if (!CONFIG.DH.GENERAL.templateTypes[type] || !range) return match[0];
if (!Object.values(CONFIG.DH.GENERAL.templateTypes).find(x => x === type) || !range) return match[0];
const label = game.i18n.localize(`DAGGERHEART.CONFIG.TemplateTypes.${type}`);
const rangeDisplay = Number.isNaN(Number(range))
@ -49,6 +49,8 @@ export default function DhTemplateEnricher(match, _options) {
}
export const renderMeasuredTemplate = async event => {
const { LINE, RECTANGLE, INFRONT, CONE } = CONFIG.DH.GENERAL.templateTypes;
const button = event.currentTarget,
type = button.dataset.type,
range = button.dataset.range,
@ -57,16 +59,49 @@ export const renderMeasuredTemplate = async event => {
if (!type || !range || !game.canvas.scene) return;
const shapeData = CONFIG.Canvas.layers.regions.layerClass.getTemplateShape({
type,
angle,
range,
direction
});
const usedType = type === 'inFront' ? 'cone' : type;
const usedAngle =
type === CONE ? (angle ?? CONFIG.MeasuredTemplate.defaults.angle) : type === INFRONT ? '180' : undefined;
let baseDistance = getTemplateDistance(range);
const { grid, distance } = CONFIG.Scene.documentClass.schema.fields.grid.fields;
const sceneGridSize = canvas.scene?.grid.size ?? grid.size.initial;
const sceneGridDistance = canvas.scene?.grid.distance ?? distance.getInitialValue();
const dimensionConstant = sceneGridSize / sceneGridDistance;
baseDistance *= dimensionConstant;
const length = baseDistance;
const radius = length;
const shapeWidth = type === LINE ? 5 * dimensionConstant : type === RECTANGLE ? length : undefined;
const { width, height } = game.canvas.scene.dimensions;
const shapeData = {
x: width / 2,
y: height / 2,
base: {
type: 'token',
x: 0,
y: 0,
width: 1,
height: 1,
shape: game.canvas.grid.isHexagonal ? CONST.TOKEN_SHAPES.ELLIPSE_1 : CONST.TOKEN_SHAPES.RECTANGLE_1
},
t: usedType,
length: length,
width: shapeWidth,
height: length,
angle: usedAngle,
radius: radius,
direction: direction,
type: usedType
};
await canvas.regions.placeRegion(
{
name: type.capitalize(),
name: usedType.capitalize(),
shapes: [shapeData],
restriction: { enabled: false, type: 'move', priority: 0 },
behaviors: [],
@ -78,3 +113,18 @@ export const renderMeasuredTemplate = async event => {
{ create: true }
);
};
const getTemplateDistance = range => {
const rangeNumber = Number(range);
if (!Number.isNaN(rangeNumber)) return rangeNumber;
const { custom } = CONFIG.DH.GENERAL.sceneRangeMeasurementSetting;
const sceneMeasurements = canvas.scene?.flags.daggerheart?.rangeMeasurement;
const globalMeasurements = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.variantRules
).rangeMeasurement;
const settings = sceneMeasurements?.setting === custom.id ? sceneMeasurements : globalMeasurements;
return settings[range];
};

View file

@ -189,13 +189,7 @@ export const getDeleteKeys = (property, innerProperty, innerPropertyDefaultValue
// Fix on Foundry native formula replacement for DH
const nativeReplaceFormulaData = Roll.replaceFormulaData;
Roll.replaceFormulaData = function (formula, baseData = {}, { missing, warn = false } = {}) {
/* Inserting global data */
const data = {
...baseData,
partySize: game.actors?.party?.system.partyMembers.length ?? 0
};
Roll.replaceFormulaData = function (formula, data = {}, { missing, warn = false } = {}) {
const terms = Object.keys(CONFIG.DH.GENERAL.multiplierTypes).map(type => {
return { term: type, default: 1 };
});

View file

@ -10,7 +10,6 @@ export const preloadHandlebarsTemplates = async function () {
'templates/generic/tab-navigation.hbs',
'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs',
'systems/daggerheart/templates/sheets/global/partials/action-item.hbs',
'systems/daggerheart/templates/sheets/global/partials/gold.hbs',
'systems/daggerheart/templates/sheets/global/partials/domain-card-item.hbs',
'systems/daggerheart/templates/sheets/global/partials/item-resource.hbs',
'systems/daggerheart/templates/sheets/global/partials/resource-section/resource-section.hbs',
@ -29,7 +28,6 @@ export const preloadHandlebarsTemplates = async function () {
'systems/daggerheart/templates/actionTypes/uses.hbs',
'systems/daggerheart/templates/actionTypes/roll.hbs',
'systems/daggerheart/templates/actionTypes/save.hbs',
'systems/daggerheart/templates/actionTypes/areas.hbs',
'systems/daggerheart/templates/actionTypes/cost.hbs',
'systems/daggerheart/templates/actionTypes/range-target.hbs',
'systems/daggerheart/templates/actionTypes/effect.hbs',

View file

@ -1,4 +1,3 @@
import { defaultRestOptions } from '../config/generalConfig.mjs';
import { RefreshType, socketEvent } from './socket.mjs';
export async function runMigrations() {
@ -342,18 +341,6 @@ export async function runMigrations() {
lastMigrationVersion = '2.0.0';
}
if (foundry.utils.isNewerVersion('2.1.0', lastMigrationVersion)) {
const downtimeMoves = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew);
if (downtimeMoves.restMoves.longRest.moves.repairArmor) {
await downtimeMoves.updateSource({
'restMoves.longRest.moves.repairArmor': defaultRestOptions.longRest().repairArmor
});
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, downtimeMoves.toObject());
}
lastMigrationVersion = '2.1.0';
}
//#endregion
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion, lastMigrationVersion);

View file

@ -52,27 +52,6 @@ export const registerKeyBindings = () => {
reservedModifiers: [],
precedence: CONST.KEYBINDING_PRECEDENCE.NORMAL
});
game.keybindings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.keybindings.partySheet, {
name: _loc('DAGGERHEART.SETTINGS.Keybindings.partySheet.name'),
hint: _loc('DAGGERHEART.SETTINGS.Keybindings.partySheet.hint'),
editable: [{ key: 'KeyP' }],
onDown: () => {
const controlled = canvas.ready ? canvas.tokens.controlled : [];
const selectedParty = controlled.find(c => c.actor?.type === 'party')?.actor;
const party = selectedParty ?? game.actors.party;
if (!party) return;
const sheet = party.sheet;
if (!sheet.rendered) {
sheet.render(true);
} else if (sheet.minimized) {
sheet.maximize();
} else {
sheet.close();
}
}
});
};
const registerMenuSettings = () => {
@ -210,11 +189,4 @@ const registerNonConfigSettings = () => {
config: false,
type: SpotlightTracker
});
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.ActiveParty, {
scope: 'world',
config: false,
type: String,
default: null
});
};

View file

@ -18,8 +18,6 @@ export function handleSocketEvent({ action = null, data = {} } = {}) {
case socketEvent.TagTeamStart:
Hooks.callAll(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, data);
break;
case socketEvent.GroupRollStart:
Hooks.callAll(CONFIG.DH.HOOKS.hooksConfig.groupRollStart, data);
}
}
@ -28,8 +26,7 @@ export const socketEvent = {
Refresh: 'DhRefresh',
DhpFearUpdate: 'DhFearUpdate',
DowntimeTrigger: 'DowntimeTrigger',
TagTeamStart: 'DhTagTeamStart',
GroupRollStart: 'DhGroupRollStart'
TagTeamStart: 'DhTagTeamStart'
};
export const GMUpdateEvent = {
@ -44,7 +41,6 @@ export const GMUpdateEvent = {
export const RefreshType = {
Countdown: 'DhCoundownRefresh',
TagTeamRoll: 'DhTagTeamRollRefresh',
GroupRoll: 'DhGroupRollRefresh',
EffectsDisplay: 'DhEffectsDisplayRefresh',
Scene: 'DhSceneRefresh',
CompendiumBrowser: 'DhCompendiumBrowserRefresh'

872
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -17,18 +17,13 @@
"pullYMLtoLDB": "node ./tools/pullYMLtoLDB.mjs",
"pullYMLtoLDBBuild": "node ./tools/pullYMLtoLDB.mjs --build",
"createSymlink": "node ./tools/create-symlink.mjs",
"setup:dev": "node ./tools/dev-setup.mjs",
"lint": "eslint",
"lint:fix": "eslint --fix"
"setup:dev": "node ./tools/dev-setup.mjs"
},
"devDependencies": {
"@foundryvtt/foundryvtt-cli": "^1.0.2",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"concurrently": "^8.2.2",
"eslint": "^10.2.1",
"eslint-plugin-prettier": "^5.5.5",
"globals": "^17.5.0",
"husky": "^9.1.5",
"lint-staged": "^15.2.10",
"postcss": "^8.4.32",

View file

@ -169,9 +169,12 @@
"src": "systems/daggerheart/assets/icons/documents/actors/dragon-head.svg",
"anchorX": 0.5,
"anchorY": 0.5,
"offsetX": 0,
"offsetY": 0,
"fit": "contain",
"scaleX": 1,
"scaleY": 1,
"rotation": 0,
"tint": "#ffffff",
"alphaThreshold": 0.75
},
@ -222,7 +225,7 @@
"saturation": 0,
"contrast": 0
},
"detectionModes": {},
"detectionModes": [],
"occludable": {
"radius": 0
},
@ -248,8 +251,7 @@
"flags": {},
"randomImg": false,
"appendNumber": false,
"prependAdjective": false,
"depth": 1
"prependAdjective": false
},
"items": [
{
@ -317,7 +319,7 @@
"_id": "ctXYwil2D1zfsekT",
"img": "icons/magic/earth/barrier-stone-explosion-red.webp",
"system": {
"description": "<p><strong>Mark a Stress</strong> to have the @Lookup[@name] burst out of the ground. All creatures within Very Close range must succeed on an Agility Reaction Roll or be knocked over, making them Vulnerable until they next act.</p>",
"description": "<p><strong>Mark a Stress</strong> to have the @Lookup[@name] burst out of the ground. All creatures within Very Close range must succeed on an Agility Reaction Roll or be knocked over, making them Vulnerable until they next act.</p><p>@Template[type:emanation|range:vc]</p>",
"resource": null,
"actions": {
"4ppSeiTdbqnMzWAs": {
@ -342,8 +344,7 @@
},
"damage": {
"parts": {},
"includeBase": false,
"groupAttack": ""
"includeBase": false
},
"target": {
"type": "any",
@ -377,16 +378,7 @@
},
"name": "Roll Save",
"img": "icons/magic/earth/barrier-stone-explosion-red.webp",
"range": "veryClose",
"areas": [
{
"name": "Earth Eruption",
"type": "placed",
"shape": "emanation",
"size": "veryClose",
"effects": []
}
]
"range": "veryClose"
}
},
"originItemType": null,
@ -589,7 +581,7 @@
"_id": "aNIVT5LKhwLyjKpI",
"img": "icons/magic/acid/dissolve-drip-droplet-smoke.webp",
"system": {
"description": "<p>When the @Lookup[@name] takes Severe damage, all creatures within Close range are bathed in their acidic blood, taking <strong>1d10</strong> physical damage. This splash covers the ground within Very Close range with blood, and all creatures other than the @Lookup[@name] who move through it take <strong>1d6</strong> physical damage.</p>",
"description": "<p>When the @Lookup[@name] takes Severe damage, all creatures within Close range are bathed in their acidic blood, taking <strong>1d10</strong> physical damage. This splash covers the ground within Very Close range with blood, and all creatures other than the @Lookup[@name] who move through it take <strong>1d6</strong> physical damage.</p><p>@Template[type:emanation|range:c]</p>",
"resource": null,
"actions": {
"XbtTzOBvlTaxOKTy": {
@ -635,8 +627,7 @@
}
}
},
"includeBase": false,
"groupAttack": ""
"includeBase": false
},
"target": {
"type": "any",
@ -645,23 +636,7 @@
"effects": [],
"name": "Splash",
"img": "icons/magic/acid/dissolve-drip-droplet-smoke.webp",
"range": "close",
"areas": [
{
"name": "Acid Bath: Damage Area",
"type": "placed",
"shape": "emanation",
"size": "close",
"effects": []
},
{
"name": "Acid Ground",
"type": "placed",
"shape": "emanation",
"size": "veryClose",
"effects": []
}
]
"range": "close"
},
"xpcp1ECTWF20kxve": {
"type": "damage",
@ -715,8 +690,7 @@
"effects": [],
"name": "Acid Ground",
"img": "icons/magic/acid/dissolve-pool-bubbles.webp",
"range": "",
"areas": []
"range": ""
}
},
"originItemType": null,

View file

@ -132,9 +132,12 @@
"src": "systems/daggerheart/assets/icons/documents/actors/dragon-head.svg",
"anchorX": 0.5,
"anchorY": 0.5,
"offsetX": 0,
"offsetY": 0,
"fit": "contain",
"scaleX": 1,
"scaleY": 1,
"rotation": 0,
"tint": "#ffffff",
"alphaThreshold": 0.75
},
@ -185,7 +188,7 @@
"saturation": 0,
"contrast": 0
},
"detectionModes": {},
"detectionModes": [],
"occludable": {
"radius": 0
},
@ -211,8 +214,7 @@
"flags": {},
"randomImg": false,
"appendNumber": false,
"prependAdjective": false,
"depth": 1
"prependAdjective": false
},
"items": [
{
@ -374,7 +376,7 @@
"name": "Whirlwind",
"type": "feature",
"system": {
"description": "<p><strong>Spend a Fear</strong> to whirl, making an attack against all targets within Very Close range. Targets the @Lookup[@name] succeeds against take <strong>3d8</strong> direct physical damage.</p>",
"description": "<p><strong>Spend a Fear</strong> to whirl, making an attack against all targets within Very Close range. Targets the @Lookup[@name] succeeds against take <strong>3d8</strong> direct physical damage.</p><p>@Template[type:emanation|range:vc]</p>",
"resource": null,
"actions": {
"RV1wKufKrMPN6MOo": {
@ -427,8 +429,7 @@
}
},
"includeBase": false,
"direct": true,
"groupAttack": ""
"direct": true
},
"target": {
"type": "any",
@ -457,16 +458,7 @@
},
"name": "Attack",
"img": "icons/skills/melee/strike-slashes-orange.webp",
"range": "veryClose",
"areas": [
{
"name": "Whirlwind",
"type": "placed",
"shape": "emanation",
"size": "veryClose",
"effects": []
}
]
"range": "veryClose"
}
},
"originItemType": null,
@ -492,7 +484,7 @@
"name": "Mind Dance",
"type": "feature",
"system": {
"description": "<p><strong>Mark a Stress</strong> to create a magically dazzling display that grapples the minds of nearby foes. All targets within Close range must make an Instinct Reaction Roll. For each target who failed, you gain a Fear and the @Lookup[@name] learns one of the targets fears.</p>",
"description": "<p><strong>Mark a Stress</strong> to create a magically dazzling display that grapples the minds of nearby foes. All targets within Close range must make an Instinct Reaction Roll. For each target who failed, you gain a Fear and the @Lookup[@name] learns one of the targets fears.</p><p>@Template[type:emanation|range:c]</p>",
"resource": null,
"actions": {
"GNwsDlCabx3fiG4g": {
@ -517,8 +509,7 @@
},
"damage": {
"parts": {},
"includeBase": false,
"groupAttack": ""
"includeBase": false
},
"target": {
"type": "any",
@ -547,16 +538,7 @@
},
"name": "Roll Save",
"img": "icons/magic/light/explosion-glow-spiral-yellow.webp",
"range": "close",
"areas": [
{
"name": "Mind Dance",
"type": "placed",
"shape": "emanation",
"size": "close",
"effects": []
}
]
"range": "close"
}
},
"originItemType": null,
@ -582,14 +564,14 @@
"name": "Hallucinatory Breath",
"type": "feature",
"system": {
"description": "<p><em>Countdown (Loop 1d6)</em>. When the @Lookup[@name] takes damage for the first time, activate the countdown. When it triggers, the @Lookup[@name] breathes hallucinatory gas on all targets in front of them up to Far range. Targets must make an Instinct Reaction Roll or be tormented by fearful hallucinations. Targets whose fears are known to the @Lookup[@name] have disadvantage on this roll. Targets who fail lose 2 Hope and take <strong>3d8+3</strong> direct magic damage.</p>",
"description": "<p><em>Countdown (Loop 1d6)</em>. When the @Lookup[@name] takes damage for the first time, activate the countdown. When it triggers, the @Lookup[@name] breathes hallucinatory gas on all targets in front of them up to Far range. Targets must make an Instinct Reaction Roll or be tormented by fearful hallucinations. Targets whose fears are known to the @Lookup[@name] have disadvantage on this roll. Targets who fail lose 2 Hope and take <strong>3d8+3</strong> direct magic damage.</p><p>@Template[type:inFront|range:f]</p>",
"resource": null,
"actions": {
"YOyKyKGTUEWkMmJe": {
"type": "attack",
"_id": "YOyKyKGTUEWkMmJe",
"systemPath": "actions",
"description": "",
"description": "<p>The @Lookup[@name] breathes hallucinatory gas on all targets in front of them up to Far range. Targets must make an Instinct Reaction Roll or be tormented by fearful hallucinations. Targets whose fears are known to the @Lookup[@name] have disadvantage on this roll. Targets who fail lose 2 Hope and take <strong>3d8+3</strong> direct magic damage.</p><p>@Template[type:inFront|range:f]</p>",
"chatDisplay": true,
"actionType": "action",
"cost": [],
@ -652,8 +634,7 @@
"type": []
}
},
"includeBase": false,
"groupAttack": ""
"includeBase": false
},
"target": {
"type": "any",
@ -682,16 +663,7 @@
},
"name": "Roll Save",
"img": "icons/magic/air/fog-gas-smoke-purple.webp",
"range": "far",
"areas": [
{
"name": "Hallucinatory Breath",
"type": "placed",
"shape": "inFront",
"size": "far",
"effects": []
}
]
"range": "far"
},
"lBhmLc33pcXzJHT3": {
"type": "countdown",

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