mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-01-15 13:11:08 +01:00
Merge branch 'development' of https://github.com/Foundryborne/daggerheart into feature/party-sheet
This commit is contained in:
commit
1a0c6f46bc
648 changed files with 7471 additions and 3950 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
name: Feature report
|
name: Feature request
|
||||||
about: Create a feature report for suggestions on improving the system
|
about: Create a feature request for suggestions on improving the system
|
||||||
title: "[Feature] <Insert Title here> "
|
title: "[Feature] <Insert Title here> "
|
||||||
labels: enhancement, discussion, maybe
|
labels: enhancement, discussion, maybe
|
||||||
type: feature
|
type: feature
|
||||||
|
|
@ -17,6 +17,10 @@ We welcome contributions of all kinds:
|
||||||
|
|
||||||
Please be respectful and collaborative — we’re all here to build something great together.
|
Please be respectful and collaborative — we’re all here to build something great together.
|
||||||
|
|
||||||
|
### Community Translations
|
||||||
|
|
||||||
|
Please note that we are not accepting community translations in the main project. Instead, community translations should be published as a module.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🧭 General Guidelines
|
## 🧭 General Guidelines
|
||||||
|
|
@ -40,12 +44,14 @@ We encourage contributors to leave comments or open Discussions when proposing s
|
||||||
## 🧾 Issue & PR Best Practices
|
## 🧾 Issue & PR Best Practices
|
||||||
|
|
||||||
**For Issues:**
|
**For Issues:**
|
||||||
|
|
||||||
- Use clear, descriptive titles
|
- Use clear, descriptive titles
|
||||||
- Provide a concise explanation of the problem or idea
|
- Provide a concise explanation of the problem or idea
|
||||||
- Include reproduction steps or example scenarios if it's a bug
|
- Include reproduction steps or example scenarios if it's a bug
|
||||||
- Add screenshots or logs if helpful
|
- Add screenshots or logs if helpful
|
||||||
|
|
||||||
**For Pull Requests:**
|
**For Pull Requests:**
|
||||||
|
|
||||||
- Use a clear title summarizing the change
|
- Use a clear title summarizing the change
|
||||||
- Provide a brief description of what your code does and why
|
- Provide a brief description of what your code does and why
|
||||||
- Link to any related Issues
|
- Link to any related Issues
|
||||||
|
|
@ -67,6 +73,6 @@ Discussions are currently happening on GitHub — in Issues, PRs, and [GitHub Di
|
||||||
|
|
||||||
## 🤗 Thank You!
|
## 🤗 Thank You!
|
||||||
|
|
||||||
Whether you're fixing a typo or designing entire mechanics — every contribution matters. Thank you for helping bring *Daggerheart* to life in FoundryVTT through **Foundryborne**!
|
Whether you're fixing a typo or designing entire mechanics — every contribution matters. Thank you for helping bring _Daggerheart_ to life in FoundryVTT through **Foundryborne**!
|
||||||
|
|
||||||
🐸🛠️
|
🐸🛠️
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { SYSTEM } from './module/config/system.mjs';
|
import { SYSTEM } from './module/config/system.mjs';
|
||||||
import * as applications from './module/applications/_module.mjs';
|
import * as applications from './module/applications/_module.mjs';
|
||||||
|
import * as data from './module/data/_module.mjs';
|
||||||
import * as models from './module/data/_module.mjs';
|
import * as models from './module/data/_module.mjs';
|
||||||
import * as documents from './module/documents/_module.mjs';
|
import * as documents from './module/documents/_module.mjs';
|
||||||
import * as dice from './module/dice/_module.mjs';
|
import * as dice from './module/dice/_module.mjs';
|
||||||
|
|
@ -13,6 +14,7 @@ import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs'
|
||||||
import { registerCountdownHooks } from './module/data/countdowns.mjs';
|
import { registerCountdownHooks } from './module/data/countdowns.mjs';
|
||||||
import {
|
import {
|
||||||
handlebarsRegistration,
|
handlebarsRegistration,
|
||||||
|
runMigrations,
|
||||||
settingsRegistration,
|
settingsRegistration,
|
||||||
socketRegistration
|
socketRegistration
|
||||||
} from './module/systemRegistration/_module.mjs';
|
} from './module/systemRegistration/_module.mjs';
|
||||||
|
|
@ -25,6 +27,7 @@ Hooks.once('init', () => {
|
||||||
CONFIG.DH = SYSTEM;
|
CONFIG.DH = SYSTEM;
|
||||||
game.system.api = {
|
game.system.api = {
|
||||||
applications,
|
applications,
|
||||||
|
data,
|
||||||
models,
|
models,
|
||||||
documents,
|
documents,
|
||||||
dice,
|
dice,
|
||||||
|
|
@ -136,6 +139,8 @@ Hooks.once('init', () => {
|
||||||
CONFIG.ui.combat = applications.ui.DhCombatTracker;
|
CONFIG.ui.combat = applications.ui.DhCombatTracker;
|
||||||
CONFIG.ui.chat = applications.ui.DhChatLog;
|
CONFIG.ui.chat = applications.ui.DhChatLog;
|
||||||
CONFIG.ui.hotbar = applications.ui.DhHotbar;
|
CONFIG.ui.hotbar = applications.ui.DhHotbar;
|
||||||
|
CONFIG.ui.sidebar = applications.sidebar.DhSidebar;
|
||||||
|
CONFIG.ui.daggerheartMenu = applications.sidebar.DaggerheartMenu;
|
||||||
CONFIG.Token.rulerClass = placeables.DhTokenRuler;
|
CONFIG.Token.rulerClass = placeables.DhTokenRuler;
|
||||||
|
|
||||||
CONFIG.ui.resources = applications.ui.DhFearTracker;
|
CONFIG.ui.resources = applications.ui.DhFearTracker;
|
||||||
|
|
@ -149,6 +154,11 @@ Hooks.once('init', () => {
|
||||||
// Make Compendium Dialog resizable
|
// Make Compendium Dialog resizable
|
||||||
foundry.applications.sidebar.apps.Compendium.DEFAULT_OPTIONS.window.resizable = true;
|
foundry.applications.sidebar.apps.Compendium.DEFAULT_OPTIONS.window.resizable = true;
|
||||||
|
|
||||||
|
DocumentSheetConfig.registerSheet(foundry.documents.Scene, SYSTEM.id, applications.scene.DhSceneConfigSettings, {
|
||||||
|
makeDefault: true,
|
||||||
|
label: 'Daggerheart'
|
||||||
|
});
|
||||||
|
|
||||||
settingsRegistration.registerDHSettings();
|
settingsRegistration.registerDHSettings();
|
||||||
RegisterHandlebarsHelpers.registerHelpers();
|
RegisterHandlebarsHelpers.registerHelpers();
|
||||||
|
|
||||||
|
|
@ -160,6 +170,9 @@ Hooks.on('ready', async () => {
|
||||||
if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).displayFear !== 'hide')
|
if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).displayFear !== 'hide')
|
||||||
ui.resources.render({ force: true });
|
ui.resources.render({ force: true });
|
||||||
|
|
||||||
|
if (!(ui.compendiumBrowser instanceof applications.ui.ItemBrowser))
|
||||||
|
ui.compendiumBrowser = new applications.ui.ItemBrowser();
|
||||||
|
|
||||||
registerCountdownHooks();
|
registerCountdownHooks();
|
||||||
socketRegistration.registerSocketHooks();
|
socketRegistration.registerSocketHooks();
|
||||||
registerRollDiceHooks();
|
registerRollDiceHooks();
|
||||||
|
|
@ -172,6 +185,8 @@ Hooks.on('ready', async () => {
|
||||||
game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.welcomeMessage, true);
|
game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.welcomeMessage, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
runMigrations();
|
||||||
});
|
});
|
||||||
|
|
||||||
Hooks.once('dicesoniceready', () => {});
|
Hooks.once('dicesoniceready', () => {});
|
||||||
|
|
@ -301,3 +316,6 @@ Hooks.on('moveToken', async (movedToken, data) => {
|
||||||
await effect.value.update({ disabled: effect.disabled });
|
await effect.value.update({ disabled: effect.disabled });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Hooks.on('renderCompendiumDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html));
|
||||||
|
Hooks.on('renderDocumentDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html));
|
||||||
|
|
|
||||||
212
lang/en.json
212
lang/en.json
|
|
@ -27,6 +27,14 @@
|
||||||
"CONTROLS": {
|
"CONTROLS": {
|
||||||
"inFront": "In Front"
|
"inFront": "In Front"
|
||||||
},
|
},
|
||||||
|
"SCENE": {
|
||||||
|
"TABS": {
|
||||||
|
"SHEET": {
|
||||||
|
"dh": "Daggerheart"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"DAGGERHEART": {
|
"DAGGERHEART": {
|
||||||
"ACTIONS": {
|
"ACTIONS": {
|
||||||
"TYPES": {
|
"TYPES": {
|
||||||
|
|
@ -65,6 +73,14 @@
|
||||||
"exactHint": "The Character's Tier is used if empty",
|
"exactHint": "The Character's Tier is used if empty",
|
||||||
"label": "Beastform"
|
"label": "Beastform"
|
||||||
},
|
},
|
||||||
|
"damage": {
|
||||||
|
"multiplier": "Multiplier",
|
||||||
|
"flatMultiplier": "Flat Multiplier"
|
||||||
|
},
|
||||||
|
"general": {
|
||||||
|
"customFormula": "Custom Formula",
|
||||||
|
"formula": "Formula"
|
||||||
|
},
|
||||||
"displayInChat": "Display in chat"
|
"displayInChat": "Display in chat"
|
||||||
},
|
},
|
||||||
"RollField": {
|
"RollField": {
|
||||||
|
|
@ -81,6 +97,7 @@
|
||||||
"attackName": "Attack Name",
|
"attackName": "Attack Name",
|
||||||
"includeBase": { "label": "Include Item Damage" },
|
"includeBase": { "label": "Include Item Damage" },
|
||||||
"multiplier": "Multiplier",
|
"multiplier": "Multiplier",
|
||||||
|
"saveHint": "Set a default Trait to enable Reaction Roll. It can be changed later in Reaction Roll Dialog.",
|
||||||
"resultBased": {
|
"resultBased": {
|
||||||
"label": "Formula based on Hope/Fear result."
|
"label": "Formula based on Hope/Fear result."
|
||||||
},
|
},
|
||||||
|
|
@ -154,7 +171,9 @@
|
||||||
"hint": "Add single words or short text as reminders and hints of what a character has advantage on."
|
"hint": "Add single words or short text as reminders and hints of what a character has advantage on."
|
||||||
},
|
},
|
||||||
"age": "Age",
|
"age": "Age",
|
||||||
|
"backgroundQuestions": "Backgrounds",
|
||||||
"companionFeatures": "Companion Features",
|
"companionFeatures": "Companion Features",
|
||||||
|
"connections": "Connections",
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"consume": "Consume Item",
|
"consume": "Consume Item",
|
||||||
"equip": "Equip",
|
"equip": "Equip",
|
||||||
|
|
@ -195,7 +214,9 @@
|
||||||
"confirmTitle": "Companion Levelup",
|
"confirmTitle": "Companion Levelup",
|
||||||
"confirmText": "Would you like to level up your companion {name} by {levelChange} levels at this time? (You can do it manually later)"
|
"confirmText": "Would you like to level up your companion {name} by {levelChange} levels at this time? (You can do it manually later)"
|
||||||
},
|
},
|
||||||
"viewLevelups": "View Levelups"
|
"viewLevelups": "View Levelups",
|
||||||
|
"InvalidOldCharacterImportTitle": "Old Character Import",
|
||||||
|
"InvalidOldCharacterImportText": "Character data exported prior to system version 1.1 will not generate a complete character. Do you wish to continue?"
|
||||||
},
|
},
|
||||||
"Companion": {
|
"Companion": {
|
||||||
"FIELDS": {
|
"FIELDS": {
|
||||||
|
|
@ -249,7 +270,8 @@
|
||||||
"experience": "Experience",
|
"experience": "Experience",
|
||||||
"traits": "Traits",
|
"traits": "Traits",
|
||||||
"domainCards": "Domain Cards",
|
"domainCards": "Domain Cards",
|
||||||
"equipment": "Equipment"
|
"equipment": "Equipment",
|
||||||
|
"story": "Story"
|
||||||
},
|
},
|
||||||
"ancestryNamePlaceholder": "Your ancestry's name",
|
"ancestryNamePlaceholder": "Your ancestry's name",
|
||||||
"buttonTitle": "Character Setup",
|
"buttonTitle": "Character Setup",
|
||||||
|
|
@ -270,6 +292,7 @@
|
||||||
"selectSubclass": "Select Subclass",
|
"selectSubclass": "Select Subclass",
|
||||||
"startingItems": "Starting Items",
|
"startingItems": "Starting Items",
|
||||||
"story": "Story",
|
"story": "Story",
|
||||||
|
"storyExplanation": "Select which background and connection prompts you want to copy into your character's background.",
|
||||||
"suggestedArmor": "Suggested Armor",
|
"suggestedArmor": "Suggested Armor",
|
||||||
"suggestedPrimaryWeapon": "Suggested Primary Weapon",
|
"suggestedPrimaryWeapon": "Suggested Primary Weapon",
|
||||||
"suggestedSecondaryWeapon": "Suggested Secondary Weapon",
|
"suggestedSecondaryWeapon": "Suggested Secondary Weapon",
|
||||||
|
|
@ -293,7 +316,8 @@
|
||||||
"toLoadout": "Send to Loadout",
|
"toLoadout": "Send to Loadout",
|
||||||
"toVault": "Send to Vault",
|
"toVault": "Send to Vault",
|
||||||
"unequip": "Unequip",
|
"unequip": "Unequip",
|
||||||
"useItem": "Use Item"
|
"useItem": "Use Item",
|
||||||
|
"cancelBeastform": "Cancel Beastform"
|
||||||
},
|
},
|
||||||
"Countdown": {
|
"Countdown": {
|
||||||
"addCountdown": "Add Countdown",
|
"addCountdown": "Add Countdown",
|
||||||
|
|
@ -478,18 +502,21 @@
|
||||||
},
|
},
|
||||||
"takeLevelUp": "Finish Level Up",
|
"takeLevelUp": "Finish Level Up",
|
||||||
"tier2": {
|
"tier2": {
|
||||||
|
"name": "Tier 2",
|
||||||
"label": "Levels 2-4",
|
"label": "Levels 2-4",
|
||||||
"infoLabel": "At Level 2, gain an additional Experience at +2 and gain a +1 bonus to your Proficiency.",
|
"infoLabel": "At Level 2, gain an additional Experience at +2 and gain a +1 bonus to your Proficiency.",
|
||||||
"pretext": "Choose two options from the list below",
|
"pretext": "Choose two options from the list below",
|
||||||
"posttext": "Take an additional domain card of your level or lower from a domain you have access to."
|
"posttext": "Take an additional domain card of your level or lower from a domain you have access to."
|
||||||
},
|
},
|
||||||
"tier3": {
|
"tier3": {
|
||||||
|
"name": "Tier 3",
|
||||||
"label": "Levels 5-7",
|
"label": "Levels 5-7",
|
||||||
"infoLabel": "At Level 5, take an additional Experience and clear all marks on Character Traits.",
|
"infoLabel": "At Level 5, take an additional Experience and clear all marks on Character Traits.",
|
||||||
"pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.",
|
"pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.",
|
||||||
"posttext": "Take an additional domain card of your level or lower from a domain you have access to."
|
"posttext": "Take an additional domain card of your level or lower from a domain you have access to."
|
||||||
},
|
},
|
||||||
"tier4": {
|
"tier4": {
|
||||||
|
"name": "Tier 4",
|
||||||
"label": "Levels 8-10",
|
"label": "Levels 8-10",
|
||||||
"infoLabel": "At Level 8, take an additional Experience and clear all marks on Character Traits.",
|
"infoLabel": "At Level 8, take an additional Experience and clear all marks on Character Traits.",
|
||||||
"pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.",
|
"pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.",
|
||||||
|
|
@ -1003,6 +1030,12 @@
|
||||||
"selectType": "Select Action Type",
|
"selectType": "Select Action Type",
|
||||||
"selectAction": "Action Selection"
|
"selectAction": "Action Selection"
|
||||||
},
|
},
|
||||||
|
"TargetTypes": {
|
||||||
|
"any": "Any",
|
||||||
|
"friendly": "Friendly",
|
||||||
|
"hostile": "Hostile",
|
||||||
|
"self": "Self"
|
||||||
|
},
|
||||||
"TemplateTypes": {
|
"TemplateTypes": {
|
||||||
"circle": "Circle",
|
"circle": "Circle",
|
||||||
"cone": "Cone",
|
"cone": "Cone",
|
||||||
|
|
@ -1892,9 +1925,15 @@
|
||||||
"tier4": "tier 4",
|
"tier4": "tier 4",
|
||||||
"domains": "Domains",
|
"domains": "Domains",
|
||||||
"downtime": "Downtime",
|
"downtime": "Downtime",
|
||||||
|
"roll": "Roll",
|
||||||
"rules": "Rules",
|
"rules": "Rules",
|
||||||
"partyMembers": "Party Members",
|
"partyMembers": "Party Members",
|
||||||
"projects": "Projects"
|
"projects": "Projects",
|
||||||
|
"types": "Types",
|
||||||
|
"itemFeatures": "Item Features",
|
||||||
|
"questions": "Questions",
|
||||||
|
"configuration": "Configuration",
|
||||||
|
"base": "Base"
|
||||||
},
|
},
|
||||||
"Tiers": {
|
"Tiers": {
|
||||||
"singular": "Tier",
|
"singular": "Tier",
|
||||||
|
|
@ -1911,6 +1950,8 @@
|
||||||
"amount": "Amount",
|
"amount": "Amount",
|
||||||
"any": "Any",
|
"any": "Any",
|
||||||
"armor": "Armor",
|
"armor": "Armor",
|
||||||
|
"armorFeatures": "Armor Features",
|
||||||
|
"armors": "Armors",
|
||||||
"armorScore": "Armor Score",
|
"armorScore": "Armor Score",
|
||||||
"activeEffects": "Active Effects",
|
"activeEffects": "Active Effects",
|
||||||
"armorSlots": "Armor Slots",
|
"armorSlots": "Armor Slots",
|
||||||
|
|
@ -1922,6 +1963,7 @@
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"criticalSuccess": "Critical Success",
|
"criticalSuccess": "Critical Success",
|
||||||
"criticalShort": "Critical",
|
"criticalShort": "Critical",
|
||||||
|
"custom": "Custom",
|
||||||
"d20Roll": "D20 Roll",
|
"d20Roll": "D20 Roll",
|
||||||
"damage": "Damage",
|
"damage": "Damage",
|
||||||
"damageRoll": "Damage Roll",
|
"damageRoll": "Damage Roll",
|
||||||
|
|
@ -1944,6 +1986,7 @@
|
||||||
"fear": "Fear",
|
"fear": "Fear",
|
||||||
"features": "Features",
|
"features": "Features",
|
||||||
"formula": "Formula",
|
"formula": "Formula",
|
||||||
|
"gm": "GM",
|
||||||
"healing": "Healing",
|
"healing": "Healing",
|
||||||
"healingRoll": "Healing Roll",
|
"healingRoll": "Healing Roll",
|
||||||
"hit": {
|
"hit": {
|
||||||
|
|
@ -1962,6 +2005,8 @@
|
||||||
"inactiveEffects": "Inactive Effects",
|
"inactiveEffects": "Inactive Effects",
|
||||||
"inventory": "Inventory",
|
"inventory": "Inventory",
|
||||||
"itemResource": "Item Resource",
|
"itemResource": "Item Resource",
|
||||||
|
"itemQuantity": "Item Quantity",
|
||||||
|
"items": "Items",
|
||||||
"label": "Label",
|
"label": "Label",
|
||||||
"level": "Level",
|
"level": "Level",
|
||||||
"levelShort": "Lv",
|
"levelShort": "Lv",
|
||||||
|
|
@ -1973,11 +2018,16 @@
|
||||||
"plural": "Miss"
|
"plural": "Miss"
|
||||||
},
|
},
|
||||||
"maxWithThing": "Max {thing}",
|
"maxWithThing": "Max {thing}",
|
||||||
|
"missingDragDropThing": "Drop {thing} here",
|
||||||
"multiclass": "Multiclass",
|
"multiclass": "Multiclass",
|
||||||
"newCategory": "New Category",
|
"newCategory": "New Category",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"noTarget": "No current target",
|
"noTarget": "No current target",
|
||||||
"partner": "Partner",
|
"partner": "Partner",
|
||||||
|
"player": {
|
||||||
|
"single": "Player",
|
||||||
|
"plurial": "Players"
|
||||||
|
},
|
||||||
"proficiency": "Proficiency",
|
"proficiency": "Proficiency",
|
||||||
"quantity": "Quantity",
|
"quantity": "Quantity",
|
||||||
"range": "Range",
|
"range": "Range",
|
||||||
|
|
@ -1993,7 +2043,10 @@
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"scalable": "Scalable",
|
"scalable": "Scalable",
|
||||||
"situationalBonus": "Situational Bonus",
|
"situationalBonus": "Situational Bonus",
|
||||||
|
"spent": "Spent",
|
||||||
|
"step": "Step",
|
||||||
"stress": "Stress",
|
"stress": "Stress",
|
||||||
|
"subclasses": "Subclasses",
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
"take": "Take",
|
"take": "Take",
|
||||||
"Target": {
|
"Target": {
|
||||||
|
|
@ -2002,6 +2055,7 @@
|
||||||
},
|
},
|
||||||
"title": "Title",
|
"title": "Title",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
|
"traitModifier": "Trait Modifier",
|
||||||
"true": "True",
|
"true": "True",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"unarmed": "Unarmed",
|
"unarmed": "Unarmed",
|
||||||
|
|
@ -2011,6 +2065,8 @@
|
||||||
"used": "Used",
|
"used": "Used",
|
||||||
"uses": "Uses",
|
"uses": "Uses",
|
||||||
"value": "Value",
|
"value": "Value",
|
||||||
|
"weaponFeatures": "Weapon Features",
|
||||||
|
"weapons": "Weapons",
|
||||||
"withThing": "With {thing}"
|
"withThing": "With {thing}"
|
||||||
},
|
},
|
||||||
"ITEMS": {
|
"ITEMS": {
|
||||||
|
|
@ -2094,7 +2150,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Consumable": {
|
"Consumable": {
|
||||||
"consumeOnUse": "Consume On Use"
|
"consumeOnUse": "Consume On Use",
|
||||||
|
"destroyOnEmpty": "Destroy On Empty"
|
||||||
},
|
},
|
||||||
"DomainCard": {
|
"DomainCard": {
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
|
|
@ -2115,20 +2172,43 @@
|
||||||
"SETTINGS": {
|
"SETTINGS": {
|
||||||
"Appearance": {
|
"Appearance": {
|
||||||
"FIELDS": {
|
"FIELDS": {
|
||||||
"displayFear": { "label": "Fear Display" },
|
"displayFear": {
|
||||||
"dualityColorScheme": { "label": "Chat Style" },
|
"label": "Display Fear"
|
||||||
"hideAttribution": { "label": "Hide Attribution" },
|
},
|
||||||
|
"showGenericStatusEffects": {
|
||||||
|
"label": "Show Foundry Status Effects"
|
||||||
|
},
|
||||||
|
"hideAttribution": {
|
||||||
|
"label": "Hide Attribution"
|
||||||
|
},
|
||||||
"expandedTitle": "Auto-expand Descriptions",
|
"expandedTitle": "Auto-expand Descriptions",
|
||||||
"extendCharacterDescriptions": { "label": "Characters" },
|
"extendCharacterDescriptions": {
|
||||||
"extendAdversaryDescriptions": { "label": "Adversaries" },
|
"label": "Characters"
|
||||||
"extendEnvironmentDescriptions": { "label": "Environments" },
|
},
|
||||||
"extendItemDescriptions": { "label": "Items" },
|
"extendAdversaryDescriptions": {
|
||||||
"expandRollMessage": "Auto-expand Message Sections",
|
"label": "Adversaries"
|
||||||
"expandRollMessageDesc": { "label": "Description" },
|
},
|
||||||
"expandRollMessageRoll": { "label": "Formula" },
|
"extendEnvironmentDescriptions": {
|
||||||
"expandRollMessageDamage": { "label": "Damage/Healing" },
|
"label": "Environments"
|
||||||
"expandRollMessageTarget": { "label": "Target" },
|
},
|
||||||
"showGenericStatusEffects": { "label": "Show Foundry Status Effects" }
|
"extendItemDescriptions": {
|
||||||
|
"label": "Items"
|
||||||
|
},
|
||||||
|
"expandRollMessage": {
|
||||||
|
"title": "Auto-expand Message Sections",
|
||||||
|
"desc": {
|
||||||
|
"label": "Description"
|
||||||
|
},
|
||||||
|
"roll": {
|
||||||
|
"label": "Formula"
|
||||||
|
},
|
||||||
|
"damage": {
|
||||||
|
"label": "Damage/Healing"
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"label": "Target"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"fearDisplay": {
|
"fearDisplay": {
|
||||||
"token": "Tokens",
|
"token": "Tokens",
|
||||||
|
|
@ -2183,15 +2263,42 @@
|
||||||
"playerCanEditSheet": {
|
"playerCanEditSheet": {
|
||||||
"label": "Players Can Manually Edit Character Settings",
|
"label": "Players Can Manually Edit Character Settings",
|
||||||
"hint": "Players are allowed to access the manual Character Settings and change their statistics beyond the rules."
|
"hint": "Players are allowed to access the manual Character Settings and change their statistics beyond the rules."
|
||||||
|
},
|
||||||
|
"roll": {
|
||||||
|
"roll": {
|
||||||
|
"label": "Roll",
|
||||||
|
"hint": "Auto behavior for rolls like Attack, Spellcast, etc."
|
||||||
|
},
|
||||||
|
"damage": {
|
||||||
|
"label": "Damage/Healing Roll",
|
||||||
|
"hint": "Auto behavior for Damage & Healing rolls after the Attack/Spellcast."
|
||||||
|
},
|
||||||
|
"save": {
|
||||||
|
"label": "Reaction Roll",
|
||||||
|
"hint": "Auto behavior if a Reaction Roll is needed. Targets must be selected before the action is made"
|
||||||
|
},
|
||||||
|
"damageApply": {
|
||||||
|
"label": "Apply Damage/Healing",
|
||||||
|
"hint": "Automatically apply damages & healings. Targets must be selected before the action is made and Reaction Roll Automation must be different than Never. Bypass users permissions."
|
||||||
|
},
|
||||||
|
"effect": {
|
||||||
|
"label": "Apply Effects",
|
||||||
|
"hint": "Automatically apply effects. Targets must be selected before the action is made and Reaction Roll Automation must be different than Never. Bypass users permissions."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defeated": {
|
"defeated": {
|
||||||
"title": "Defeated Handling"
|
"title": "Defeated Handling"
|
||||||
|
},
|
||||||
|
"roll": {
|
||||||
|
"title": "Actions"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Homebrew": {
|
"Homebrew": {
|
||||||
"newDowntimeMove": "Downtime Move",
|
"newDowntimeMove": "Downtime Move",
|
||||||
|
"newFeature": "New ItemFeature",
|
||||||
"downtimeMoves": "Downtime Moves",
|
"downtimeMoves": "Downtime Moves",
|
||||||
|
"itemFeatures": "Item Features",
|
||||||
"nrChoices": "# Moves Per Rest",
|
"nrChoices": "# Moves Per Rest",
|
||||||
"resetMovesTitle": "Reset {type} Downtime Moves",
|
"resetMovesTitle": "Reset {type} Downtime Moves",
|
||||||
"resetMovesText": "Are you sure you want to reset?",
|
"resetMovesText": "Are you sure you want to reset?",
|
||||||
|
|
@ -2224,6 +2331,10 @@
|
||||||
"deleteDomain": "Delete Domain",
|
"deleteDomain": "Delete Domain",
|
||||||
"deleteDomainText": "Are you sure you want to delete the {name} domain? It will be immediately removed from all Actors in this world where it's currently used. Compendiums are not cleared.",
|
"deleteDomainText": "Are you sure you want to delete the {name} domain? It will be immediately removed from all Actors in this world where it's currently used. Compendiums are not cleared.",
|
||||||
"duplicateDomain": "There is already a domain with this identification."
|
"duplicateDomain": "There is already a domain with this identification."
|
||||||
|
},
|
||||||
|
"adversaryType": {
|
||||||
|
"title": "Custom Adversary Types",
|
||||||
|
"newType": "Adversary Type"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Menu": {
|
"Menu": {
|
||||||
|
|
@ -2244,10 +2355,8 @@
|
||||||
"hint": "System ruler setup for displaying ranges in Daggerheart"
|
"hint": "System ruler setup for displaying ranges in Daggerheart"
|
||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Appearance Settings",
|
|
||||||
"label": "Appearance Settings",
|
"label": "Appearance Settings",
|
||||||
"hint": "Modify the look of various parts of the system",
|
"hint": "Modify the look of various parts of the system",
|
||||||
"name": "Appearance Settings",
|
|
||||||
"duality": "Duality Rolls",
|
"duality": "Duality Rolls",
|
||||||
"diceSoNice": {
|
"diceSoNice": {
|
||||||
"title": "Dice So Nice",
|
"title": "Dice So Nice",
|
||||||
|
|
@ -2287,6 +2396,9 @@
|
||||||
"ResetSettings": {
|
"ResetSettings": {
|
||||||
"resetConfirmationTitle": "Reset Settings",
|
"resetConfirmationTitle": "Reset Settings",
|
||||||
"resetConfirmationText": "Are you sure you want to reset the {settings}?"
|
"resetConfirmationText": "Are you sure you want to reset the {settings}?"
|
||||||
|
},
|
||||||
|
"Scene": {
|
||||||
|
"rangeMeasurementOverride": "Override Global Range Measurement Settings"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"UI": {
|
"UI": {
|
||||||
|
|
@ -2330,6 +2442,10 @@
|
||||||
"heal": "Heal",
|
"heal": "Heal",
|
||||||
"applyHealing": "Apply Healing"
|
"applyHealing": "Apply Healing"
|
||||||
},
|
},
|
||||||
|
"refreshMessage": {
|
||||||
|
"title": "Feature Refresh",
|
||||||
|
"header": "Refreshed"
|
||||||
|
},
|
||||||
"reroll": {
|
"reroll": {
|
||||||
"confirmTitle": "Reroll Dice",
|
"confirmTitle": "Reroll Dice",
|
||||||
"confirmText": "Are you sure you want to reroll?"
|
"confirmText": "Are you sure you want to reroll?"
|
||||||
|
|
@ -2338,11 +2454,53 @@
|
||||||
"playerMessage": "{user} rerolled their {name}"
|
"playerMessage": "{user} rerolled their {name}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ItemBrowser": {
|
||||||
|
"title": "Daggerheart Compendium Browser",
|
||||||
|
"hint": "Select a Folder in sidebar to start browsing through the compendium",
|
||||||
|
"searchPlaceholder": "Search...",
|
||||||
|
"columnName": "Name",
|
||||||
|
"tooltipFilters": "Filters",
|
||||||
|
"tooltipErase": "Erase",
|
||||||
|
"difficultyMin": "Difficulty (Min)",
|
||||||
|
"difficultyMax": "Difficulty (Max)",
|
||||||
|
"hitPointsMin": "Hit Points (Min)",
|
||||||
|
"hitPointsMax": "Hit Points (Max)",
|
||||||
|
"stressMin": "Stress (Min)",
|
||||||
|
"stressMax": "Stress (Max)",
|
||||||
|
"armorScoreMin": "Armor Score (Min)",
|
||||||
|
"armorScoreMax": "Armor Score (Max)",
|
||||||
|
"levelMin": "Level (Min)",
|
||||||
|
"levelMax": "Level (Max)",
|
||||||
|
"recallCostMin": "Recall Cost (Min)",
|
||||||
|
"recallCostMax": "Recall Cost (Max)",
|
||||||
|
"evasionMin": "Evasion (Min)",
|
||||||
|
"evasionMax": "Evasion (Max)",
|
||||||
|
"subtype": "Subtype",
|
||||||
|
"folders": {
|
||||||
|
"characters": "Characters",
|
||||||
|
"adversaries": "Adversaries",
|
||||||
|
"ancestries": "Ancestries",
|
||||||
|
"equipment": "Equipment",
|
||||||
|
"classes": "Classes",
|
||||||
|
"subclasses": "Subclasses",
|
||||||
|
"domainCards": "Domain Cards",
|
||||||
|
"communities": "Communities",
|
||||||
|
"environments": "Environments",
|
||||||
|
"beastforms": "Beastforms",
|
||||||
|
"features": "Features",
|
||||||
|
"items": "Items",
|
||||||
|
"weapons": "Weapons",
|
||||||
|
"armors": "Armors",
|
||||||
|
"consumables": "Consumables",
|
||||||
|
"loots": "Loots"
|
||||||
|
}
|
||||||
|
},
|
||||||
"Notifications": {
|
"Notifications": {
|
||||||
"adversaryMissing": "The linked adversary doesn't exist in the world.",
|
"adversaryMissing": "The linked adversary doesn't exist in the world.",
|
||||||
"beastformInapplicable": "A beastform can only be applied to a Character.",
|
"beastformInapplicable": "A beastform can only be applied to a Character.",
|
||||||
"beastformAlreadyApplied": "The character already has a beastform applied!",
|
"beastformAlreadyApplied": "The character already has a beastform applied!",
|
||||||
"noTargetsSelected": "No targets are selected.",
|
"noTargetsSelected": "No targets are selected.",
|
||||||
|
"noTargetsSelectedOrPerm": "No targets are selected or with the update permission.",
|
||||||
"attackTargetDoesNotExist": "The target token no longer exists",
|
"attackTargetDoesNotExist": "The target token no longer exists",
|
||||||
"insufficentAdvancements": "You don't have enough advancements left.",
|
"insufficentAdvancements": "You don't have enough advancements left.",
|
||||||
"noAssignedPlayerCharacter": "You have no assigned character.",
|
"noAssignedPlayerCharacter": "You have no assigned character.",
|
||||||
|
|
@ -2399,10 +2557,20 @@
|
||||||
"beastformEquipWeapon": "You cannot use weapons while in a Beastform.",
|
"beastformEquipWeapon": "You cannot use weapons while in a Beastform.",
|
||||||
"loadoutMaxReached": "You've reached maximum loadout. Move atleast one domain card to the vault, or increase the limit in homebrew settings if desired.",
|
"loadoutMaxReached": "You've reached maximum loadout. Move atleast one domain card to the vault, or increase the limit in homebrew settings if desired.",
|
||||||
"domainMaxReached": "You've reached the maximum domains for the class. Increase the limit in homebrew settings if desired.",
|
"domainMaxReached": "You've reached the maximum domains for the class. Increase the limit in homebrew settings if desired.",
|
||||||
"insufficientResources": "You have insufficient resources",
|
"insufficientResources": "You don't have enough resources to use that action.",
|
||||||
|
"actionNoUsesRemaining": "That action doesn't have remaining uses.",
|
||||||
"multiclassAlreadyPresent": "You already have a class and multiclass",
|
"multiclassAlreadyPresent": "You already have a class and multiclass",
|
||||||
"subclassesAlreadyPresent": "You already have a class and multiclass subclass",
|
"subclassesAlreadyPresent": "You already have a class and multiclass subclass",
|
||||||
"noDiceSystem": "Your selected dice {system} does not have a {faces} dice"
|
"noDiceSystem": "Your selected dice {system} does not have a {faces} dice",
|
||||||
|
"gmMenuRefresh": "You refreshed all actions and resources {types}",
|
||||||
|
"subclassAlreadyLinked": "{name} is already a subclass in the class {class}. Remove it from there if you want it to be a subclass to this class."
|
||||||
|
},
|
||||||
|
"Sidebar": {
|
||||||
|
"daggerheartMenu": {
|
||||||
|
"title": "Daggerheart Menu",
|
||||||
|
"startSession": "Start Session",
|
||||||
|
"startScene": "Start Scene"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"Tooltip": {
|
"Tooltip": {
|
||||||
"disableEffect": "Disable Effect",
|
"disableEffect": "Disable Effect",
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@ export * as characterCreation from './characterCreation/_module.mjs';
|
||||||
export * as dialogs from './dialogs/_module.mjs';
|
export * as dialogs from './dialogs/_module.mjs';
|
||||||
export * as hud from './hud/_module.mjs';
|
export * as hud from './hud/_module.mjs';
|
||||||
export * as levelup from './levelup/_module.mjs';
|
export * as levelup from './levelup/_module.mjs';
|
||||||
|
export * as scene from './scene/_module.mjs';
|
||||||
export * as settings from './settings/_module.mjs';
|
export * as settings from './settings/_module.mjs';
|
||||||
export * as sheets from './sheets/_module.mjs';
|
export * as sheets from './sheets/_module.mjs';
|
||||||
export * as sheetConfigs from './sheets-configs/_module.mjs';
|
export * as sheetConfigs from './sheets-configs/_module.mjs';
|
||||||
|
export * as sidebar from './sidebar/_module.mjs';
|
||||||
export * as ui from './ui/_module.mjs';
|
export * as ui from './ui/_module.mjs';
|
||||||
export * as ux from './ux/_module.mjs';
|
export * as ux from './ux/_module.mjs';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { abilities } from '../../config/actorConfig.mjs';
|
import { abilities } from '../../config/actorConfig.mjs';
|
||||||
import { burden } from '../../config/generalConfig.mjs';
|
import { burden } from '../../config/generalConfig.mjs';
|
||||||
import { ItemBrowser } from '../ui/itemBrowser.mjs';
|
|
||||||
import { createEmbeddedItemsWithEffects, createEmbeddedItemWithEffects } from '../../helpers/utils.mjs';
|
import { createEmbeddedItemsWithEffects, createEmbeddedItemWithEffects } from '../../helpers/utils.mjs';
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
@ -46,8 +45,6 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
|
||||||
};
|
};
|
||||||
|
|
||||||
this._dragDrop = this._createDragDropHandlers();
|
this._dragDrop = this._createDragDropHandlers();
|
||||||
|
|
||||||
this.itemBrowser = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get title() {
|
get title() {
|
||||||
|
|
@ -425,26 +422,30 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
|
||||||
equipment = ['armor', 'weapon'];
|
equipment = ['armor', 'weapon'];
|
||||||
|
|
||||||
const presets = {
|
const presets = {
|
||||||
compendium: 'daggerheart',
|
folder: equipment.includes(type) ? `equipments.folders.${type}s` : type,
|
||||||
folder: equipment.includes(type) ? 'equipments' : type,
|
|
||||||
render: {
|
render: {
|
||||||
noFolder: true
|
noFolder: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (type == 'domains')
|
if (type === 'domains')
|
||||||
presets.filter = {
|
presets.filter = {
|
||||||
'level.max': { key: 'level.max', value: 1 },
|
'level.max': { key: 'level.max', value: 1 },
|
||||||
'system.domain': { key: 'system.domain', value: this.setup.class?.system.domains ?? null }
|
'system.domain': { key: 'system.domain', value: this.setup.class?.system.domains ?? null }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (type === 'subclasses')
|
||||||
|
presets.filter = {
|
||||||
|
'system.linkedClass.uuid': { key: 'system.linkedClass.uuid', value: this.setup.class?.uuid }
|
||||||
|
};
|
||||||
|
|
||||||
if (equipment.includes(type))
|
if (equipment.includes(type))
|
||||||
presets.filter = {
|
presets.filter = {
|
||||||
'system.tier': { key: 'system.tier', value: 1 },
|
'system.tier': { key: 'system.tier', value: 1 },
|
||||||
'type': { key: 'type', value: type }
|
'type': { key: 'type', value: type }
|
||||||
};
|
};
|
||||||
|
|
||||||
return (this.itemBrowser = await new ItemBrowser({ presets }).render({ force: true }));
|
ui.compendiumBrowser.open(presets);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async viewItem(_, target) {
|
static async viewItem(_, target) {
|
||||||
|
|
@ -562,7 +563,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
|
||||||
{ overwrite: true }
|
{ overwrite: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.itemBrowser) this.itemBrowser.close();
|
if (ui.compendiumBrowser) ui.compendiumBrowser.close();
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { abilities } from '../../config/actorConfig.mjs';
|
||||||
|
|
||||||
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
||||||
|
|
||||||
export default class D20RollDialog extends HandlebarsApplicationMixin(ApplicationV2) {
|
export default class D20RollDialog extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
|
|
@ -7,7 +9,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
this.roll = roll;
|
this.roll = roll;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.config.experiences = [];
|
this.config.experiences = [];
|
||||||
this.reactionOverride = config.roll?.type === 'reaction';
|
this.reactionOverride = config.actionType === 'reaction';
|
||||||
|
|
||||||
if (config.source?.action) {
|
if (config.source?.action) {
|
||||||
this.item = config.data.parent.items.get(config.source.item) ?? config.data.parent;
|
this.item = config.data.parent.items.get(config.source.item) ?? config.data.parent;
|
||||||
|
|
@ -20,7 +22,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
|
|
||||||
static DEFAULT_OPTIONS = {
|
static DEFAULT_OPTIONS = {
|
||||||
tag: 'form',
|
tag: 'form',
|
||||||
id: 'roll-selection',
|
// id: 'roll-selection',
|
||||||
classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'roll-selection'],
|
classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'roll-selection'],
|
||||||
position: {
|
position: {
|
||||||
width: 'auto'
|
width: 'auto'
|
||||||
|
|
@ -42,7 +44,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
};
|
};
|
||||||
|
|
||||||
get title() {
|
get title() {
|
||||||
return this.config.title;
|
return `${this.config.title}${this.actor ? `: ${this.actor.name}` : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get actor() {
|
get actor() {
|
||||||
|
|
@ -81,7 +83,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
);
|
);
|
||||||
context.costs = updatedCosts.map(x => ({
|
context.costs = updatedCosts.map(x => ({
|
||||||
...x,
|
...x,
|
||||||
label: x.keyIsID
|
label: x.itemId
|
||||||
? this.action.parent.parent.name
|
? this.action.parent.parent.name
|
||||||
: game.i18n.localize(CONFIG.DH.GENERAL.abilityCosts[x.key].label)
|
: game.i18n.localize(CONFIG.DH.GENERAL.abilityCosts[x.key].label)
|
||||||
}));
|
}));
|
||||||
|
|
@ -113,15 +115,24 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
context.isLite = this.config.roll?.lite;
|
context.isLite = this.config.roll?.lite;
|
||||||
context.extraFormula = this.config.extraFormula;
|
context.extraFormula = this.config.extraFormula;
|
||||||
context.formula = this.roll.constructFormula(this.config);
|
context.formula = this.roll.constructFormula(this.config);
|
||||||
|
if (this.actor.system.traits) context.abilities = this.getTraitModifiers();
|
||||||
|
|
||||||
context.showReaction = !context.rollConfig.type && context.rollType === 'DualityRoll';
|
context.showReaction = !this.config.roll?.type && context.rollType === 'DualityRoll';
|
||||||
context.reactionOverride = this.reactionOverride;
|
context.reactionOverride = this.reactionOverride;
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTraitModifiers() {
|
||||||
|
return Object.values(abilities).map(a => ({
|
||||||
|
id: a.id,
|
||||||
|
label: `${game.i18n.localize(a.label)} (${this.actor.system.traits[a.id]?.value.signedString() ?? 0})`
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
static updateRollConfiguration(event, _, formData) {
|
static updateRollConfiguration(event, _, formData) {
|
||||||
const { ...rest } = foundry.utils.expandObject(formData.object);
|
const { ...rest } = foundry.utils.expandObject(formData.object);
|
||||||
|
|
||||||
this.config.selectedRollMode = rest.selectedRollMode;
|
this.config.selectedRollMode = rest.selectedRollMode;
|
||||||
|
|
||||||
if (this.config.costs) {
|
if (this.config.costs) {
|
||||||
|
|
@ -133,6 +144,12 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
this.roll[key] = value;
|
this.roll[key] = value;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (rest.hasOwnProperty('trait')) {
|
||||||
|
this.config.roll.trait = rest.trait;
|
||||||
|
this.config.title = game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
|
||||||
|
ability: game.i18n.localize(abilities[this.config.roll.trait]?.label)
|
||||||
|
});
|
||||||
|
}
|
||||||
this.config.extraFormula = rest.extraFormula;
|
this.config.extraFormula = rest.extraFormula;
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
@ -151,31 +168,29 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
this.config.experiences.indexOf(button.dataset.key) > -1
|
this.config.experiences.indexOf(button.dataset.key) > -1
|
||||||
? this.config.experiences.filter(x => x !== button.dataset.key)
|
? this.config.experiences.filter(x => x !== button.dataset.key)
|
||||||
: [...this.config.experiences, button.dataset.key];
|
: [...this.config.experiences, button.dataset.key];
|
||||||
if (this.config?.data?.parent?.type === 'character' || this.config?.data?.parent?.type === 'companion') {
|
this.config.costs =
|
||||||
this.config.costs =
|
this.config.costs.indexOf(this.config.costs.find(c => c.extKey === button.dataset.key)) > -1
|
||||||
this.config.costs.indexOf(this.config.costs.find(c => c.extKey === button.dataset.key)) > -1
|
? this.config.costs.filter(x => x.extKey !== button.dataset.key)
|
||||||
? this.config.costs.filter(x => x.extKey !== button.dataset.key)
|
: [
|
||||||
: [
|
...this.config.costs,
|
||||||
...this.config.costs,
|
{
|
||||||
{
|
extKey: button.dataset.key,
|
||||||
extKey: button.dataset.key,
|
key: this.config?.data?.parent?.isNPC ? 'fear' : 'hope',
|
||||||
key: 'hope',
|
value: 1,
|
||||||
value: 1,
|
name: this.config.data?.experiences?.[button.dataset.key]?.name
|
||||||
name: this.config.data?.experiences?.[button.dataset.key]?.name
|
}
|
||||||
}
|
];
|
||||||
];
|
|
||||||
}
|
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
static toggleReaction() {
|
static toggleReaction() {
|
||||||
if (this.config.roll) {
|
if (this.config.roll) {
|
||||||
this.reactionOverride = !this.reactionOverride;
|
this.reactionOverride = !this.reactionOverride;
|
||||||
this.config.roll.type = this.reactionOverride
|
this.config.actionType = this.reactionOverride
|
||||||
? CONFIG.DH.ITEM.actionTypes.reaction.id
|
? CONFIG.DH.ITEM.actionTypes.reaction.id
|
||||||
: this.config.roll.type === CONFIG.DH.ITEM.actionTypes.reaction.id
|
: this.config.actionType === CONFIG.DH.ITEM.actionTypes.reaction.id
|
||||||
? null
|
? null
|
||||||
: this.config.roll.type;
|
: this.config.actionType;
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
|
export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
|
||||||
static DEFAULT_OPTIONS = {
|
static DEFAULT_OPTIONS = {
|
||||||
classes: ['daggerheart']
|
classes: ['daggerheart'],
|
||||||
|
actions: {
|
||||||
|
combat: DHTokenHUD.#onToggleCombat
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @override */
|
/** @override */
|
||||||
|
|
@ -11,8 +14,14 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static #nonCombatTypes = ['environment', 'companion'];
|
||||||
|
|
||||||
async _prepareContext(options) {
|
async _prepareContext(options) {
|
||||||
const context = await super._prepareContext(options);
|
const context = await super._prepareContext(options);
|
||||||
|
|
||||||
|
context.canToggleCombat = DHTokenHUD.#nonCombatTypes.includes(this.actor.type)
|
||||||
|
? false
|
||||||
|
: context.canToggleCombat;
|
||||||
context.systemStatusEffects = Object.keys(context.statusEffects).reduce((acc, key) => {
|
context.systemStatusEffects = Object.keys(context.statusEffects).reduce((acc, key) => {
|
||||||
const effect = context.statusEffects[key];
|
const effect = context.statusEffects[key];
|
||||||
if (effect.systemEffect) acc[key] = effect;
|
if (effect.systemEffect) acc[key] = effect;
|
||||||
|
|
@ -36,6 +45,20 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async #onToggleCombat() {
|
||||||
|
const tokens = canvas.tokens.controlled
|
||||||
|
.filter(t => !t.actor || !DHTokenHUD.#nonCombatTypes.includes(t.actor.type))
|
||||||
|
.map(t => t.document);
|
||||||
|
if (!this.object.controlled) tokens.push(this.document);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.document.inCombat) await TokenDocument.implementation.deleteCombatants(tokens);
|
||||||
|
else await TokenDocument.implementation.createCombatants(tokens);
|
||||||
|
} catch (err) {
|
||||||
|
ui.notifications.warn(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_getStatusEffectChoices() {
|
_getStatusEffectChoices() {
|
||||||
// Include all HUD-enabled status effects
|
// Include all HUD-enabled status effects
|
||||||
const choices = {};
|
const choices = {};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { abilities, subclassFeatureLabels } from '../../config/actorConfig.mjs';
|
import { abilities, subclassFeatureLabels } from '../../config/actorConfig.mjs';
|
||||||
import { getDeleteKeys, tagifyElement } from '../../helpers/utils.mjs';
|
import { getDeleteKeys, tagifyElement } from '../../helpers/utils.mjs';
|
||||||
import { ItemBrowser } from '../ui/itemBrowser.mjs';
|
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
||||||
|
|
@ -12,8 +11,6 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
|
||||||
|
|
||||||
this._dragDrop = this._createDragDropHandlers();
|
this._dragDrop = this._createDragDropHandlers();
|
||||||
this.tabGroups.primary = 'advancements';
|
this.tabGroups.primary = 'advancements';
|
||||||
|
|
||||||
this.itemBrowser = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get title() {
|
get title() {
|
||||||
|
|
@ -540,7 +537,6 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
|
||||||
const type = target.dataset.compendium ?? target.dataset.type;
|
const type = target.dataset.compendium ?? target.dataset.type;
|
||||||
|
|
||||||
const presets = {
|
const presets = {
|
||||||
compendium: 'daggerheart',
|
|
||||||
folder: type,
|
folder: type,
|
||||||
render: {
|
render: {
|
||||||
noFolder: true
|
noFolder: true
|
||||||
|
|
@ -559,7 +555,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return (this.itemBrowser = await new ItemBrowser({ presets }).render({ force: true }));
|
ui.compendiumBrowser.open(presets);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async selectPreview(_, button) {
|
static async selectPreview(_, button) {
|
||||||
|
|
@ -662,7 +658,8 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
await this.actor.levelUp(levelupData);
|
await this.actor.levelUp(levelupData);
|
||||||
if (this.itemBrowser) this.itemBrowser.close();
|
|
||||||
|
if (ui.compendiumBrowser) ui.compendiumBrowser.close();
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
module/applications/scene/_module.mjs
Normal file
1
module/applications/scene/_module.mjs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as DhSceneConfigSettings } from './sceneConfigSettings.mjs';
|
||||||
24
module/applications/scene/sceneConfigSettings.mjs
Normal file
24
module/applications/scene/sceneConfigSettings.mjs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
export default class DhSceneConfigSettings extends foundry.applications.sheets.SceneConfig {
|
||||||
|
constructor(options, ...args) {
|
||||||
|
super(options, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
static buildParts() {
|
||||||
|
const { footer, ...parts } = super.PARTS;
|
||||||
|
const tmpParts = {
|
||||||
|
...parts,
|
||||||
|
dh: { template: 'systems/daggerheart/templates/scene/dh-config.hbs' },
|
||||||
|
footer
|
||||||
|
};
|
||||||
|
return tmpParts;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PARTS = DhSceneConfigSettings.buildParts();
|
||||||
|
|
||||||
|
static buildTabs() {
|
||||||
|
super.TABS.sheet.tabs.push({ id: 'dh', icon: 'fa-solid' });
|
||||||
|
return super.TABS;
|
||||||
|
}
|
||||||
|
|
||||||
|
static TABS = DhSceneConfigSettings.buildTabs();
|
||||||
|
}
|
||||||
|
|
@ -3,43 +3,48 @@ import { getDiceSoNicePreset } from '../../config/generalConfig.mjs';
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @import {ApplicationClickAction} from "@client/applications/_types.mjs"
|
||||||
|
*/
|
||||||
|
|
||||||
export default class DHAppearanceSettings extends HandlebarsApplicationMixin(ApplicationV2) {
|
export default class DHAppearanceSettings extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
constructor() {
|
/**@inheritdoc */
|
||||||
super({});
|
|
||||||
|
|
||||||
this.settings = new DhAppearance(
|
|
||||||
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance).toObject()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get title() {
|
|
||||||
return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.title');
|
|
||||||
}
|
|
||||||
|
|
||||||
static DEFAULT_OPTIONS = {
|
static DEFAULT_OPTIONS = {
|
||||||
tag: 'form',
|
tag: 'form',
|
||||||
id: 'daggerheart-appearance-settings',
|
id: 'daggerheart-appearance-settings',
|
||||||
classes: ['daggerheart', 'dialog', 'dh-style', 'setting'],
|
classes: ['daggerheart', 'dialog', 'dh-style', 'setting'],
|
||||||
position: { width: '600', height: 'auto' },
|
position: { width: '600', height: 'auto' },
|
||||||
window: {
|
window: {
|
||||||
|
title: 'DAGGERHEART.SETTINGS.Menu.title',
|
||||||
icon: 'fa-solid fa-gears'
|
icon: 'fa-solid fa-gears'
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
reset: this.reset,
|
reset: DHAppearanceSettings.#onReset,
|
||||||
save: this.save,
|
preview: DHAppearanceSettings.#onPreview
|
||||||
preview: this.preview
|
|
||||||
},
|
},
|
||||||
form: { handler: this.updateData, submitOnChange: true }
|
form: {
|
||||||
|
closeOnSubmit: true,
|
||||||
|
handler: DHAppearanceSettings.#onSubmit
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
static PARTS = {
|
static PARTS = {
|
||||||
main: {
|
header: { template: 'systems/daggerheart/templates/settings/appearance-settings/header.hbs' },
|
||||||
template: 'systems/daggerheart/templates/settings/appearance-settings.hbs'
|
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
|
||||||
}
|
main: { template: 'systems/daggerheart/templates/settings/appearance-settings/main.hbs' },
|
||||||
|
diceSoNice: { template: 'systems/daggerheart/templates/settings/appearance-settings/diceSoNice.hbs' },
|
||||||
|
footer: { template: 'templates/generic/form-footer.hbs' }
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
static TABS = {
|
static TABS = {
|
||||||
|
general: {
|
||||||
|
tabs: [
|
||||||
|
{ id: 'main', label: 'DAGGERHEART.GENERAL.Tabs.general' },
|
||||||
|
{ id: 'diceSoNice', label: 'DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.title' }
|
||||||
|
],
|
||||||
|
initial: 'main'
|
||||||
|
},
|
||||||
diceSoNice: {
|
diceSoNice: {
|
||||||
tabs: [
|
tabs: [
|
||||||
{ id: 'hope', label: 'DAGGERHEART.GENERAL.hope' },
|
{ id: 'hope', label: 'DAGGERHEART.GENERAL.hope' },
|
||||||
|
|
@ -51,79 +56,149 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
changeTab(tab, group, options) {
|
/**@type {DhAppearance}*/
|
||||||
super.changeTab(tab, group, options);
|
setting;
|
||||||
|
|
||||||
this.render();
|
static #localized = false;
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
|
async _preFirstRender(_context, _options) {
|
||||||
|
await super._preFirstRender(_context, _options);
|
||||||
|
if (!DHAppearanceSettings.#localized) {
|
||||||
|
foundry.helpers.Localization.localizeDataModel(this.setting.constructor);
|
||||||
|
DHAppearanceSettings.#localized = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _prepareContext(_options) {
|
/** @inheritdoc */
|
||||||
const context = await super._prepareContext(_options);
|
_configureRenderParts(options) {
|
||||||
context.settingFields = this.settings;
|
const parts = super._configureRenderParts(options);
|
||||||
|
if (!game.modules.get('dice-so-nice')?.active) {
|
||||||
context.showDiceSoNice = game.modules.get('dice-so-nice')?.active;
|
delete parts.diceSoNice;
|
||||||
if (game.dice3d) {
|
delete parts.tabs;
|
||||||
context.diceSoNiceTextures = game.dice3d.exports.TEXTURELIST;
|
|
||||||
context.diceSoNiceColorsets = game.dice3d.exports.COLORSETS;
|
|
||||||
context.diceSoNiceMaterials = Object.keys(game.dice3d.DiceFactory.material_options).map(key => ({
|
|
||||||
key: key,
|
|
||||||
name: `DICESONICE.Material${key.capitalize()}`
|
|
||||||
}));
|
|
||||||
context.diceSoNiceSystems = [];
|
|
||||||
for (const [key, system] of game.dice3d.DiceFactory.systems.entries()) {
|
|
||||||
context.diceSoNiceSystems.push({ key, name: system.name });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
context.diceTab = {
|
/**@inheritdoc */
|
||||||
key: this.tabGroups.diceSoNice,
|
async _prepareContext(options) {
|
||||||
source: this.settings._source.diceSoNice[this.tabGroups.diceSoNice],
|
const context = await super._prepareContext(options);
|
||||||
fields: this.settings.schema.fields.diceSoNice.fields[this.tabGroups.diceSoNice].fields
|
if (options.isFirstRender)
|
||||||
};
|
this.setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
|
||||||
|
|
||||||
|
context.setting = this.setting;
|
||||||
|
context.fields = this.setting.schema.fields;
|
||||||
|
|
||||||
|
context.tabs = this._prepareTabs('general');
|
||||||
|
context.dsnTabs = this._prepareTabs('diceSoNice');
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async updateData(event, element, formData) {
|
/**@inheritdoc */
|
||||||
const updatedSettings = foundry.utils.expandObject(formData.object);
|
async _preparePartContext(partId, context, options) {
|
||||||
|
const partContext = await super._preparePartContext(partId, context, options);
|
||||||
await this.settings.updateSource(updatedSettings);
|
if (partId in context.tabs) partContext.tab = partContext.tabs[partId];
|
||||||
this.render();
|
switch (partId) {
|
||||||
|
case 'diceSoNice':
|
||||||
|
await this.prepareDiceSoNiceContext(partContext);
|
||||||
|
break;
|
||||||
|
case 'footer':
|
||||||
|
partContext.buttons = [
|
||||||
|
{ type: 'button', action: 'reset', icon: 'fa-solid fa-arrow-rotate-left', label: 'Reset' },
|
||||||
|
{ type: 'submit', icon: 'fa-solid fa-floppy-disk', label: 'Save Changes' }
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return partContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async preview() {
|
/**
|
||||||
const source = this.settings._source.diceSoNice[this.tabGroups.diceSoNice];
|
* Prepare render context for the DSN part.
|
||||||
let faces = 'd12';
|
* @param {ApplicationRenderContext} context
|
||||||
switch (this.tabGroups.diceSoNice) {
|
* @returns {Promise<void>}
|
||||||
case 'advantage':
|
* @protected
|
||||||
case 'disadvantage':
|
*/
|
||||||
faces = 'd6';
|
async prepareDiceSoNiceContext(context) {
|
||||||
}
|
context.diceSoNiceTextures = Object.entries(game.dice3d.exports.TEXTURELIST).reduce(
|
||||||
const preset = await getDiceSoNicePreset(source, faces);
|
(acc, [k, v]) => ({
|
||||||
const diceSoNiceRoll = await new Roll(`1${faces}`).evaluate();
|
...acc,
|
||||||
|
[k]: v.name
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
context.diceSoNiceColorsets = Object.values(game.dice3d.exports.COLORSETS).reduce(
|
||||||
|
(acc, v) => ({
|
||||||
|
...acc,
|
||||||
|
[v.id]: v.description
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
context.diceSoNiceMaterials = Object.keys(game.dice3d.DiceFactory.material_options).reduce(
|
||||||
|
(acc, key) => ({
|
||||||
|
...acc,
|
||||||
|
[key]: `DICESONICE.Material${key.capitalize()}`
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
context.diceSoNiceSystems = Object.fromEntries(
|
||||||
|
[...game.dice3d.DiceFactory.systems].map(([k, v]) => [k, v.name])
|
||||||
|
);
|
||||||
|
|
||||||
|
foundry.utils.mergeObject(
|
||||||
|
context.dsnTabs,
|
||||||
|
['hope', 'fear', 'advantage', 'disadvantage'].reduce(
|
||||||
|
(acc, key) => ({
|
||||||
|
...acc,
|
||||||
|
[key]: {
|
||||||
|
values: this.setting.diceSoNice[key],
|
||||||
|
fields: this.setting.schema.getField(`diceSoNice.${key}`).fields
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit the configuration form.
|
||||||
|
* @this {DHAppearanceSettings}
|
||||||
|
* @param {SubmitEvent} event
|
||||||
|
* @param {HTMLFormElement} form
|
||||||
|
* @param {foundry.applications.ux.FormDataExtended} formData
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async #onSubmit(event, form, formData) {
|
||||||
|
const data = this.setting.schema.clean(foundry.utils.expandObject(formData.object));
|
||||||
|
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit the configuration form.
|
||||||
|
* @this {DHAppearanceSettings}
|
||||||
|
* @type {ApplicationClickAction}
|
||||||
|
*/
|
||||||
|
static async #onPreview(_, target) {
|
||||||
|
const formData = new foundry.applications.ux.FormDataExtended(target.closest('form'));
|
||||||
|
const { diceSoNice } = foundry.utils.expandObject(formData.object);
|
||||||
|
const { key } = target.dataset;
|
||||||
|
const faces = ['advantage', 'disadvantage'].includes(key) ? 'd6' : 'd12';
|
||||||
|
const preset = await getDiceSoNicePreset(diceSoNice[key], faces);
|
||||||
|
const diceSoNiceRoll = await new foundry.dice.Roll(`1${faces}`).evaluate();
|
||||||
diceSoNiceRoll.dice[0].options.appearance = preset.appearance;
|
diceSoNiceRoll.dice[0].options.appearance = preset.appearance;
|
||||||
diceSoNiceRoll.dice[0].options.modelFile = preset.modelFile;
|
diceSoNiceRoll.dice[0].options.modelFile = preset.modelFile;
|
||||||
|
|
||||||
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, false);
|
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async reset() {
|
/**
|
||||||
this.settings = new DhAppearance();
|
* Reset the form back to default values.
|
||||||
this.render();
|
* @this {DHAppearanceSettings}
|
||||||
}
|
* @type {ApplicationClickAction}
|
||||||
|
*/
|
||||||
static async save() {
|
static async #onReset() {
|
||||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance, this.settings.toObject());
|
this.setting = new this.setting.constructor();
|
||||||
|
this.render({ force: false });
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
_getTabs(tabs) {
|
|
||||||
for (const v of Object.values(tabs)) {
|
|
||||||
v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active;
|
|
||||||
v.cssClass = v.active ? 'active' : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return tabs;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,13 +35,14 @@ export default class DhAutomationSettings extends HandlebarsApplicationMixin(App
|
||||||
header: { template: 'systems/daggerheart/templates/settings/automation-settings/header.hbs' },
|
header: { template: 'systems/daggerheart/templates/settings/automation-settings/header.hbs' },
|
||||||
general: { template: 'systems/daggerheart/templates/settings/automation-settings/general.hbs' },
|
general: { template: 'systems/daggerheart/templates/settings/automation-settings/general.hbs' },
|
||||||
rules: { template: 'systems/daggerheart/templates/settings/automation-settings/rules.hbs' },
|
rules: { template: 'systems/daggerheart/templates/settings/automation-settings/rules.hbs' },
|
||||||
|
roll: { template: 'systems/daggerheart/templates/settings/automation-settings/roll.hbs' },
|
||||||
footer: { template: 'systems/daggerheart/templates/settings/automation-settings/footer.hbs' }
|
footer: { template: 'systems/daggerheart/templates/settings/automation-settings/footer.hbs' }
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
static TABS = {
|
static TABS = {
|
||||||
main: {
|
main: {
|
||||||
tabs: [{ id: 'general' }, { id: 'rules' }],
|
tabs: [{ id: 'general' }, { id: 'rules' }, { id: 'roll' }],
|
||||||
initial: 'general',
|
initial: 'general',
|
||||||
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
|
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { DhHomebrew } from '../../data/settings/_module.mjs';
|
import { DhHomebrew } from '../../data/settings/_module.mjs';
|
||||||
import { slugify } from '../../helpers/utils.mjs';
|
import { slugify } from '../../helpers/utils.mjs';
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
||||||
export default class DhHomebrewSettings extends HandlebarsApplicationMixin(ApplicationV2) {
|
export default class DhHomebrewSettings extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
|
|
@ -10,11 +11,14 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).toObject()
|
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).toObject()
|
||||||
);
|
);
|
||||||
|
|
||||||
this.selected = {
|
this.selected = this.#getDefaultAdversaryType();
|
||||||
domain: null
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#getDefaultAdversaryType = () => ({
|
||||||
|
domain: null,
|
||||||
|
adversaryType: null
|
||||||
|
});
|
||||||
|
|
||||||
get title() {
|
get title() {
|
||||||
return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.title');
|
return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.title');
|
||||||
}
|
}
|
||||||
|
|
@ -35,6 +39,9 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
addDomain: this.addDomain,
|
addDomain: this.addDomain,
|
||||||
toggleSelectedDomain: this.toggleSelectedDomain,
|
toggleSelectedDomain: this.toggleSelectedDomain,
|
||||||
deleteDomain: this.deleteDomain,
|
deleteDomain: this.deleteDomain,
|
||||||
|
addAdversaryType: this.addAdversaryType,
|
||||||
|
deleteAdversaryType: this.deleteAdversaryType,
|
||||||
|
selectAdversaryType: this.selectAdversaryType,
|
||||||
save: this.save,
|
save: this.save,
|
||||||
reset: this.reset
|
reset: this.reset
|
||||||
},
|
},
|
||||||
|
|
@ -45,6 +52,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
|
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
|
||||||
settings: { template: 'systems/daggerheart/templates/settings/homebrew-settings/settings.hbs' },
|
settings: { template: 'systems/daggerheart/templates/settings/homebrew-settings/settings.hbs' },
|
||||||
domains: { template: 'systems/daggerheart/templates/settings/homebrew-settings/domains.hbs' },
|
domains: { template: 'systems/daggerheart/templates/settings/homebrew-settings/domains.hbs' },
|
||||||
|
types: { template: 'systems/daggerheart/templates/settings/homebrew-settings/types.hbs' },
|
||||||
|
itemTypes: { template: 'systems/daggerheart/templates/settings/homebrew-settings/itemFeatures.hbs' },
|
||||||
downtime: { template: 'systems/daggerheart/templates/settings/homebrew-settings/downtime.hbs' },
|
downtime: { template: 'systems/daggerheart/templates/settings/homebrew-settings/downtime.hbs' },
|
||||||
footer: { template: 'systems/daggerheart/templates/settings/homebrew-settings/footer.hbs' }
|
footer: { template: 'systems/daggerheart/templates/settings/homebrew-settings/footer.hbs' }
|
||||||
};
|
};
|
||||||
|
|
@ -52,12 +61,19 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
static TABS = {
|
static TABS = {
|
||||||
main: {
|
main: {
|
||||||
tabs: [{ id: 'settings' }, { id: 'domains' }, { id: 'downtime' }],
|
tabs: [{ id: 'settings' }, { id: 'domains' }, { id: 'types' }, { id: 'itemFeatures' }, { id: 'downtime' }],
|
||||||
initial: 'settings',
|
initial: 'settings',
|
||||||
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
|
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
changeTab(tab, group, options) {
|
||||||
|
super.changeTab(tab, group, options);
|
||||||
|
this.selected = this.#getDefaultAdversaryType();
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
async _prepareContext(_options) {
|
async _prepareContext(_options) {
|
||||||
const context = await super._prepareContext(_options);
|
const context = await super._prepareContext(_options);
|
||||||
context.settingFields = this.settings;
|
context.settingFields = this.settings;
|
||||||
|
|
@ -79,6 +95,11 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
context.configDomains = CONFIG.DH.DOMAIN.domains;
|
context.configDomains = CONFIG.DH.DOMAIN.domains;
|
||||||
context.homebrewDomains = this.settings.domains;
|
context.homebrewDomains = this.settings.domains;
|
||||||
break;
|
break;
|
||||||
|
case 'types':
|
||||||
|
context.selectedAdversaryType = this.selected.adversaryType
|
||||||
|
? { id: this.selected.adversaryType, ...this.settings.adversaryTypes[this.selected.adversaryType] }
|
||||||
|
: null;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
|
|
@ -95,33 +116,53 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
}
|
}
|
||||||
|
|
||||||
static async addItem(_, target) {
|
static async addItem(_, target) {
|
||||||
await this.settings.updateSource({
|
const { type } = target.dataset;
|
||||||
[`restMoves.${target.dataset.type}.moves.${foundry.utils.randomID()}`]: {
|
if (['shortRest', 'longRest'].includes(type)) {
|
||||||
name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newDowntimeMove'),
|
await this.settings.updateSource({
|
||||||
img: 'icons/magic/life/cross-worn-green.webp',
|
[`restMoves.${type}.moves.${foundry.utils.randomID()}`]: {
|
||||||
description: '',
|
name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newDowntimeMove'),
|
||||||
actions: []
|
img: 'icons/magic/life/cross-worn-green.webp',
|
||||||
}
|
description: '',
|
||||||
});
|
actions: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (['armorFeatures', 'weaponFeatures'].includes(type)) {
|
||||||
|
await this.settings.updateSource({
|
||||||
|
[`itemFeatures.${type}.${foundry.utils.randomID()}`]: {
|
||||||
|
name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newFeature'),
|
||||||
|
img: 'icons/magic/life/cross-worn-green.webp',
|
||||||
|
description: '',
|
||||||
|
actions: [],
|
||||||
|
effects: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
static async editItem(_, target) {
|
static async editItem(_, target) {
|
||||||
const move = this.settings.restMoves[target.dataset.type].moves[target.dataset.id];
|
const { type, id } = target.dataset;
|
||||||
const path = `restMoves.${target.dataset.type}.moves.${target.dataset.id}`;
|
const isDowntime = ['shortRest', 'longRest'].includes(type);
|
||||||
const editedMove = await game.system.api.applications.sheetConfigs.DowntimeConfig.configure(
|
const path = isDowntime ? `restMoves.${type}.moves.${id}` : `itemFeatures.${type}.${id}`;
|
||||||
move,
|
const featureBase = isDowntime ? this.settings.restMoves[type].moves[id] : this.settings.itemFeatures[type][id];
|
||||||
path,
|
|
||||||
this.settings
|
|
||||||
);
|
|
||||||
if (!editedMove) return;
|
|
||||||
|
|
||||||
await this.updateAction.bind(this)(editedMove, target.dataset.type, target.dataset.id);
|
const editedBase = await game.system.api.applications.sheetConfigs.SettingFeatureConfig.configure(
|
||||||
|
featureBase,
|
||||||
|
path,
|
||||||
|
this.settings,
|
||||||
|
{ hasIcon: isDowntime, hasEffects: !isDowntime }
|
||||||
|
);
|
||||||
|
if (!editedBase) return;
|
||||||
|
|
||||||
|
await this.updateAction.bind(this)(editedBase, target.dataset.type, target.dataset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAction(data, type, id) {
|
async updateAction(data, type, id) {
|
||||||
|
const isDowntime = ['shortRest', 'longRest'].includes(type);
|
||||||
|
const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`;
|
||||||
await this.settings.updateSource({
|
await this.settings.updateSource({
|
||||||
[`restMoves.${type}.moves.${id}`]: {
|
[`${path}.${id}`]: {
|
||||||
actions: data.actions,
|
actions: data.actions,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
icon: data.icon,
|
icon: data.icon,
|
||||||
|
|
@ -129,12 +170,16 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
description: data.description
|
description: data.description
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
static async removeItem(_, target) {
|
static async removeItem(_, target) {
|
||||||
|
const { type, id } = target.dataset;
|
||||||
|
const isDowntime = ['shortRest', 'longRest'].includes(type);
|
||||||
|
const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`;
|
||||||
await this.settings.updateSource({
|
await this.settings.updateSource({
|
||||||
[`restMoves.${target.dataset.type}.moves.-=${target.dataset.id}`]: null
|
[`${path}.-=${id}`]: null
|
||||||
});
|
});
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
@ -301,6 +346,32 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async addAdversaryType(_, target) {
|
||||||
|
const newId = foundry.utils.randomID();
|
||||||
|
await this.settings.updateSource({
|
||||||
|
[`adversaryTypes.${newId}`]: {
|
||||||
|
id: newId,
|
||||||
|
label: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.adversaryType.newType')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.selected.adversaryType = newId;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async deleteAdversaryType(_, target) {
|
||||||
|
const { key } = target.dataset;
|
||||||
|
await this.settings.updateSource({ [`adversaryTypes.-=${key}`]: null });
|
||||||
|
|
||||||
|
this.selected.adversaryType = this.selected.adversaryType === key ? null : this.selected.adversaryType;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async selectAdversaryType(_, target) {
|
||||||
|
this.selected.adversaryType = this.selected.adversaryType === target.dataset.type ? null : target.dataset.type;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
static async save() {
|
static async save() {
|
||||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
|
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
|
||||||
this.close();
|
this.close();
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ export { default as ActionConfig } from './action-config.mjs';
|
||||||
export { default as CharacterSettings } from './character-settings.mjs';
|
export { default as CharacterSettings } from './character-settings.mjs';
|
||||||
export { default as AdversarySettings } from './adversary-settings.mjs';
|
export { default as AdversarySettings } from './adversary-settings.mjs';
|
||||||
export { default as CompanionSettings } from './companion-settings.mjs';
|
export { default as CompanionSettings } from './companion-settings.mjs';
|
||||||
export { default as DowntimeConfig } from './downtimeConfig.mjs';
|
export { default as SettingActiveEffectConfig } from './setting-active-effect-config.mjs';
|
||||||
|
export { default as SettingFeatureConfig } from './setting-feature-config.mjs';
|
||||||
export { default as EnvironmentSettings } from './environment-settings.mjs';
|
export { default as EnvironmentSettings } from './environment-settings.mjs';
|
||||||
export { default as ActiveEffectConfig } from './activeEffectConfig.mjs';
|
export { default as ActiveEffectConfig } from './activeEffectConfig.mjs';
|
||||||
export { default as DhTokenConfig } from './token-config.mjs';
|
export { default as DhTokenConfig } from './token-config.mjs';
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
|
||||||
group: 'primary',
|
group: 'primary',
|
||||||
id: 'base',
|
id: 'base',
|
||||||
icon: null,
|
icon: null,
|
||||||
label: 'Base'
|
label: 'DAGGERHEART.GENERAL.Tabs.base'
|
||||||
},
|
},
|
||||||
config: {
|
config: {
|
||||||
active: false,
|
active: false,
|
||||||
|
|
@ -74,7 +74,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
|
||||||
group: 'primary',
|
group: 'primary',
|
||||||
id: 'config',
|
id: 'config',
|
||||||
icon: null,
|
icon: null,
|
||||||
label: 'Configuration'
|
label: 'DAGGERHEART.GENERAL.Tabs.configuration'
|
||||||
},
|
},
|
||||||
effect: {
|
effect: {
|
||||||
active: false,
|
active: false,
|
||||||
|
|
@ -82,7 +82,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
|
||||||
group: 'primary',
|
group: 'primary',
|
||||||
id: 'effect',
|
id: 'effect',
|
||||||
icon: null,
|
icon: null,
|
||||||
label: 'Effect'
|
label: 'DAGGERHEART.GENERAL.Tabs.effects'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -132,12 +132,19 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
|
||||||
const options = foundry.utils.deepClone(CONFIG.DH.GENERAL.abilityCosts);
|
const options = foundry.utils.deepClone(CONFIG.DH.GENERAL.abilityCosts);
|
||||||
const resource = this.action.parent.resource;
|
const resource = this.action.parent.resource;
|
||||||
if (resource) {
|
if (resource) {
|
||||||
options[this.action.parent.parent.id] = {
|
options.resource = {
|
||||||
label: 'DAGGERHEART.GENERAL.itemResource',
|
label: 'DAGGERHEART.GENERAL.itemResource',
|
||||||
group: 'Global'
|
group: 'Global'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.action.parent.metadata?.isQuantifiable) {
|
||||||
|
options.quantity = {
|
||||||
|
label: 'DAGGERHEART.GENERAL.itemQuantity',
|
||||||
|
group: 'Global'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,13 +171,14 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
|
||||||
|
|
||||||
_prepareSubmitData(_event, formData) {
|
_prepareSubmitData(_event, formData) {
|
||||||
const submitData = foundry.utils.expandObject(formData.object);
|
const submitData = foundry.utils.expandObject(formData.object);
|
||||||
|
|
||||||
|
const itemAbilityCostKeys = Object.keys(CONFIG.DH.GENERAL.itemAbilityCosts);
|
||||||
for (const keyPath of this.constructor.CLEAN_ARRAYS) {
|
for (const keyPath of this.constructor.CLEAN_ARRAYS) {
|
||||||
const data = foundry.utils.getProperty(submitData, keyPath);
|
const data = foundry.utils.getProperty(submitData, keyPath);
|
||||||
const dataValues = data ? Object.values(data) : [];
|
const dataValues = data ? Object.values(data) : [];
|
||||||
if (keyPath === 'cost') {
|
if (keyPath === 'cost') {
|
||||||
for (var value of dataValues) {
|
for (var value of dataValues) {
|
||||||
const item = this.action.parent.parent.id === value.key;
|
value.itemId = itemAbilityCostKeys.includes(value.key) ? this.action.parent.parent.id : null;
|
||||||
value.keyIsID = Boolean(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,13 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _prepareContext(options) {
|
||||||
|
const context = await super._prepareContext(options);
|
||||||
|
context.systemFields = context.document.system.schema.fields;
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
async _preparePartContext(partId, context) {
|
async _preparePartContext(partId, context) {
|
||||||
const partContext = await super._preparePartContext(partId, context);
|
const partContext = await super._preparePartContext(partId, context);
|
||||||
switch (partId) {
|
switch (partId) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,227 @@
|
||||||
|
import autocomplete from 'autocompleter';
|
||||||
|
|
||||||
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
||||||
|
export default class SettingActiveEffectConfig extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
|
constructor(effect) {
|
||||||
|
super({});
|
||||||
|
|
||||||
|
this.effect = foundry.utils.deepClone(effect);
|
||||||
|
const ignoredActorKeys = ['config', 'DhEnvironment'];
|
||||||
|
this.changeChoices = Object.keys(game.system.api.models.actors).reduce((acc, key) => {
|
||||||
|
if (!ignoredActorKeys.includes(key)) {
|
||||||
|
const model = game.system.api.models.actors[key];
|
||||||
|
const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model);
|
||||||
|
const group = game.i18n.localize(model.metadata.label);
|
||||||
|
const choices = CONFIG.Token.documentClass
|
||||||
|
.getTrackedAttributeChoices(attributes, model)
|
||||||
|
.map(x => ({ ...x, group: group }));
|
||||||
|
acc.push(...choices);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
classes: ['daggerheart', 'sheet', 'dh-style', 'active-effect-config'],
|
||||||
|
tag: 'form',
|
||||||
|
position: {
|
||||||
|
width: 560
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
submitOnChange: false,
|
||||||
|
closeOnSubmit: false,
|
||||||
|
handler: SettingActiveEffectConfig.#onSubmit
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
editImage: SettingActiveEffectConfig.#editImage,
|
||||||
|
addChange: SettingActiveEffectConfig.#addChange,
|
||||||
|
deleteChange: SettingActiveEffectConfig.#deleteChange
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static PARTS = {
|
||||||
|
header: { template: 'systems/daggerheart/templates/sheets/activeEffect/header.hbs' },
|
||||||
|
tabs: { template: 'templates/generic/tab-navigation.hbs' },
|
||||||
|
details: { template: 'systems/daggerheart/templates/sheets/activeEffect/details.hbs', scrollable: [''] },
|
||||||
|
settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' },
|
||||||
|
changes: {
|
||||||
|
template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs',
|
||||||
|
scrollable: ['ol[data-changes]']
|
||||||
|
},
|
||||||
|
footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' }
|
||||||
|
};
|
||||||
|
|
||||||
|
static TABS = {
|
||||||
|
sheet: {
|
||||||
|
tabs: [
|
||||||
|
{ id: 'details', icon: 'fa-solid fa-book' },
|
||||||
|
{ id: 'settings', icon: 'fa-solid fa-bars', label: 'DAGGERHEART.GENERAL.Tabs.settings' },
|
||||||
|
{ id: 'changes', icon: 'fa-solid fa-gears' }
|
||||||
|
],
|
||||||
|
initial: 'details',
|
||||||
|
labelPrefix: 'EFFECT.TABS'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**@inheritdoc */
|
||||||
|
async _onFirstRender(context, options) {
|
||||||
|
await super._onFirstRender(context, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _prepareContext(_options) {
|
||||||
|
const context = await super._prepareContext(_options);
|
||||||
|
context.source = this.effect;
|
||||||
|
context.fields = game.system.api.documents.DhActiveEffect.schema.fields;
|
||||||
|
context.systemFields = game.system.api.data.activeEffects.BaseEffect._schema.fields;
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
_attachPartListeners(partId, htmlElement, options) {
|
||||||
|
super._attachPartListeners(partId, htmlElement, options);
|
||||||
|
const changeChoices = this.changeChoices;
|
||||||
|
|
||||||
|
htmlElement.querySelectorAll('.effect-change-input').forEach(element => {
|
||||||
|
autocomplete({
|
||||||
|
input: element,
|
||||||
|
fetch: function (text, update) {
|
||||||
|
if (!text) {
|
||||||
|
update(changeChoices);
|
||||||
|
} else {
|
||||||
|
text = text.toLowerCase();
|
||||||
|
var suggestions = changeChoices.filter(n => n.label.toLowerCase().includes(text));
|
||||||
|
update(suggestions);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render: function (item, search) {
|
||||||
|
const label = game.i18n.localize(item.label);
|
||||||
|
const matchIndex = label.toLowerCase().indexOf(search);
|
||||||
|
|
||||||
|
const beforeText = label.slice(0, matchIndex);
|
||||||
|
const matchText = label.slice(matchIndex, matchIndex + search.length);
|
||||||
|
const after = label.slice(matchIndex + search.length, label.length);
|
||||||
|
|
||||||
|
const element = document.createElement('li');
|
||||||
|
element.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
|
||||||
|
if (item.hint) {
|
||||||
|
element.dataset.tooltip = game.i18n.localize(item.hint);
|
||||||
|
}
|
||||||
|
|
||||||
|
return element;
|
||||||
|
},
|
||||||
|
renderGroup: function (label) {
|
||||||
|
const itemElement = document.createElement('div');
|
||||||
|
itemElement.textContent = game.i18n.localize(label);
|
||||||
|
return itemElement;
|
||||||
|
},
|
||||||
|
onSelect: function (item) {
|
||||||
|
element.value = `system.${item.value}`;
|
||||||
|
},
|
||||||
|
click: e => e.fetch(),
|
||||||
|
customize: function (_input, _inputRect, container) {
|
||||||
|
container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ;
|
||||||
|
},
|
||||||
|
minLength: 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _preparePartContext(partId, context) {
|
||||||
|
if (partId in context.tabs) context.tab = context.tabs[partId];
|
||||||
|
switch (partId) {
|
||||||
|
case 'details':
|
||||||
|
context.isActorEffect = false;
|
||||||
|
context.isItemEffect = true;
|
||||||
|
const useGeneric = game.settings.get(
|
||||||
|
CONFIG.DH.id,
|
||||||
|
CONFIG.DH.SETTINGS.gameSettings.appearance
|
||||||
|
).showGenericStatusEffects;
|
||||||
|
if (!useGeneric) {
|
||||||
|
context.statuses = Object.values(CONFIG.DH.GENERAL.conditions).map(status => ({
|
||||||
|
value: status.id,
|
||||||
|
label: game.i18n.localize(status.name)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'changes':
|
||||||
|
context.modes = Object.entries(CONST.ACTIVE_EFFECT_MODES).reduce((modes, [key, value]) => {
|
||||||
|
modes[value] = game.i18n.localize(`EFFECT.MODE_${key}`);
|
||||||
|
return modes;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
context.priorities = ActiveEffectConfig.DEFAULT_PRIORITIES;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onSubmit(event, form, formData) {
|
||||||
|
this.data = foundry.utils.expandObject(formData.object);
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit a Document image.
|
||||||
|
* @this {DocumentSheetV2}
|
||||||
|
* @type {ApplicationClickAction}
|
||||||
|
*/
|
||||||
|
static async #editImage(_event, target) {
|
||||||
|
if (target.nodeName !== 'IMG') {
|
||||||
|
throw new Error('The editImage action is available only for IMG elements.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const attr = target.dataset.edit;
|
||||||
|
const current = foundry.utils.getProperty(this.effect, attr);
|
||||||
|
const fp = new FilePicker.implementation({
|
||||||
|
current,
|
||||||
|
type: 'image',
|
||||||
|
callback: path => (target.src = path),
|
||||||
|
position: {
|
||||||
|
top: this.position.top + 40,
|
||||||
|
left: this.position.left + 10
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await fp.browse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new change to the effect's changes array.
|
||||||
|
* @this {ActiveEffectConfig}
|
||||||
|
* @type {ApplicationClickAction}
|
||||||
|
*/
|
||||||
|
static async #addChange() {
|
||||||
|
const submitData = foundry.utils.expandObject(new FormDataExtended(this.form).object);
|
||||||
|
const changes = Object.values(submitData.changes ?? {});
|
||||||
|
changes.push({});
|
||||||
|
|
||||||
|
this.effect.changes = changes;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a change from the effect's changes array.
|
||||||
|
* @this {ActiveEffectConfig}
|
||||||
|
* @type {ApplicationClickAction}
|
||||||
|
*/
|
||||||
|
static async #deleteChange(event) {
|
||||||
|
const submitData = foundry.utils.expandObject(new FormDataExtended(this.form).object);
|
||||||
|
const changes = Object.values(submitData.changes);
|
||||||
|
const row = event.target.closest('li');
|
||||||
|
const index = Number(row.dataset.index) || 0;
|
||||||
|
changes.splice(index, 1);
|
||||||
|
|
||||||
|
this.effect.changes = changes;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async configure(effect, options = {}) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const app = new this(effect, options);
|
||||||
|
app.addEventListener('close', () => resolve(app.data), { once: true });
|
||||||
|
app.render({ force: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,8 +3,8 @@ import DHActionConfig from './action-config.mjs';
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
||||||
export default class DowntimeConfig extends HandlebarsApplicationMixin(ApplicationV2) {
|
export default class SettingFeatureConfig extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
constructor(move, movePath, settings, options) {
|
constructor(move, movePath, settings, optionalParts, options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.move = move;
|
this.move = move;
|
||||||
|
|
@ -12,6 +12,10 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati
|
||||||
this.movePath = movePath;
|
this.movePath = movePath;
|
||||||
this.actionsPath = `${movePath}.actions`;
|
this.actionsPath = `${movePath}.actions`;
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
|
|
||||||
|
const { hasIcon, hasEffects } = optionalParts;
|
||||||
|
this.hasIcon = hasIcon;
|
||||||
|
this.hasEffects = hasEffects;
|
||||||
}
|
}
|
||||||
|
|
||||||
get title() {
|
get title() {
|
||||||
|
|
@ -30,6 +34,7 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati
|
||||||
addItem: this.addItem,
|
addItem: this.addItem,
|
||||||
editItem: this.editItem,
|
editItem: this.editItem,
|
||||||
removeItem: this.removeItem,
|
removeItem: this.removeItem,
|
||||||
|
addEffect: this.addEffect,
|
||||||
resetMoves: this.resetMoves,
|
resetMoves: this.resetMoves,
|
||||||
saveForm: this.saveForm
|
saveForm: this.saveForm
|
||||||
},
|
},
|
||||||
|
|
@ -41,13 +46,14 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati
|
||||||
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
|
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
|
||||||
main: { template: 'systems/daggerheart/templates/settings/downtime-config/main.hbs' },
|
main: { template: 'systems/daggerheart/templates/settings/downtime-config/main.hbs' },
|
||||||
actions: { template: 'systems/daggerheart/templates/settings/downtime-config/actions.hbs' },
|
actions: { template: 'systems/daggerheart/templates/settings/downtime-config/actions.hbs' },
|
||||||
|
effects: { template: 'systems/daggerheart/templates/settings/downtime-config/effects.hbs' },
|
||||||
footer: { template: 'systems/daggerheart/templates/settings/downtime-config/footer.hbs' }
|
footer: { template: 'systems/daggerheart/templates/settings/downtime-config/footer.hbs' }
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
static TABS = {
|
static TABS = {
|
||||||
primary: {
|
primary: {
|
||||||
tabs: [{ id: 'main' }, { id: 'actions' }],
|
tabs: [{ id: 'main' }, { id: 'actions' }, { id: 'effects' }],
|
||||||
initial: 'main',
|
initial: 'main',
|
||||||
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
|
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
|
||||||
}
|
}
|
||||||
|
|
@ -55,6 +61,9 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati
|
||||||
|
|
||||||
async _prepareContext(_options) {
|
async _prepareContext(_options) {
|
||||||
const context = await super._prepareContext(_options);
|
const context = await super._prepareContext(_options);
|
||||||
|
context.tabs = this._filterTabs(context.tabs);
|
||||||
|
context.hasIcon = this.hasIcon;
|
||||||
|
context.hasEffects = this.hasEffects;
|
||||||
context.move = this.move;
|
context.move = this.move;
|
||||||
context.move.enrichedDescription = await foundry.applications.ux.TextEditor.enrichHTML(
|
context.move.enrichedDescription = await foundry.applications.ux.TextEditor.enrichHTML(
|
||||||
context.move.description
|
context.move.description
|
||||||
|
|
@ -130,13 +139,30 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati
|
||||||
}
|
}
|
||||||
|
|
||||||
static async editItem(_, target) {
|
static async editItem(_, target) {
|
||||||
const actionId = target.dataset.id;
|
const { type, id } = target.dataset;
|
||||||
const action = this.move.actions.get(actionId);
|
if (type === 'effect') {
|
||||||
await new DHActionConfig(action, async updatedMove => {
|
const effectIndex = this.move.effects.findIndex(x => x.id === id);
|
||||||
await this.settings.updateSource({ [`${this.actionsPath}.${actionId}`]: updatedMove });
|
const effect = this.move.effects[effectIndex];
|
||||||
|
const updatedEffect =
|
||||||
|
await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect);
|
||||||
|
if (!updatedEffect) return;
|
||||||
|
|
||||||
|
await this.settings.updateSource({
|
||||||
|
[`${this.movePath}.effects`]: this.move.effects.reduce((acc, effect, index) => {
|
||||||
|
acc.push(index === effectIndex ? { ...updatedEffect, id: effect.id } : effect);
|
||||||
|
return acc;
|
||||||
|
}, [])
|
||||||
|
});
|
||||||
this.move = foundry.utils.getProperty(this.settings, this.movePath);
|
this.move = foundry.utils.getProperty(this.settings, this.movePath);
|
||||||
this.render();
|
this.render();
|
||||||
}).render(true);
|
} else {
|
||||||
|
const action = this.move.actions.get(id);
|
||||||
|
await new DHActionConfig(action, async updatedMove => {
|
||||||
|
await this.settings.updateSource({ [`${this.actionsPath}.${id}`]: updatedMove });
|
||||||
|
this.move = foundry.utils.getProperty(this.settings, this.movePath);
|
||||||
|
this.render();
|
||||||
|
}).render(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async removeItem(_, target) {
|
static async removeItem(_, target) {
|
||||||
|
|
@ -145,16 +171,38 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async addEffect(_, target) {
|
||||||
|
const currentEffects = foundry.utils.getProperty(this.settings, `${this.movePath}.effects`);
|
||||||
|
await this.settings.updateSource({
|
||||||
|
[`${this.movePath}.effects`]: [
|
||||||
|
...currentEffects,
|
||||||
|
game.system.api.data.activeEffects.BaseEffect.getDefaultObject()
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
this.move = foundry.utils.getProperty(this.settings, this.movePath);
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
static resetMoves() {}
|
static resetMoves() {}
|
||||||
|
|
||||||
|
_filterTabs(tabs) {
|
||||||
|
return this.hasEffects
|
||||||
|
? tabs
|
||||||
|
: Object.keys(tabs).reduce((acc, key) => {
|
||||||
|
if (key !== 'effects') acc[key] = tabs[key];
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
/** @override */
|
/** @override */
|
||||||
_onClose(options = {}) {
|
_onClose(options = {}) {
|
||||||
if (!options.submitted) this.move = null;
|
if (!options.submitted) this.move = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async configure(move, movePath, settings, options = {}) {
|
static async configure(move, movePath, settings, optionalParts, options = {}) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const app = new this(move, movePath, settings, options);
|
const app = new this(move, movePath, settings, optionalParts, options);
|
||||||
app.addEventListener('close', () => resolve(app.move), { once: true });
|
app.addEventListener('close', () => resolve(app.move), { once: true });
|
||||||
app.render({ force: true });
|
app.render({ force: true });
|
||||||
});
|
});
|
||||||
|
|
@ -25,11 +25,22 @@ export default class AdversarySheet extends DHBaseActorSheet {
|
||||||
};
|
};
|
||||||
|
|
||||||
static PARTS = {
|
static PARTS = {
|
||||||
sidebar: { template: 'systems/daggerheart/templates/sheets/actors/adversary/sidebar.hbs' },
|
sidebar: {
|
||||||
|
template: 'systems/daggerheart/templates/sheets/actors/adversary/sidebar.hbs',
|
||||||
|
scrollable: ['.shortcut-items-section']
|
||||||
|
},
|
||||||
header: { template: 'systems/daggerheart/templates/sheets/actors/adversary/header.hbs' },
|
header: { template: 'systems/daggerheart/templates/sheets/actors/adversary/header.hbs' },
|
||||||
features: { template: 'systems/daggerheart/templates/sheets/actors/adversary/features.hbs' },
|
features: {
|
||||||
notes: { template: 'systems/daggerheart/templates/sheets/actors/adversary/notes.hbs' },
|
template: 'systems/daggerheart/templates/sheets/actors/adversary/features.hbs',
|
||||||
effects: { template: 'systems/daggerheart/templates/sheets/actors/adversary/effects.hbs' }
|
scrollable: ['.feature-section']
|
||||||
|
},
|
||||||
|
notes: {
|
||||||
|
template: 'systems/daggerheart/templates/sheets/actors/adversary/notes.hbs'
|
||||||
|
},
|
||||||
|
effects: {
|
||||||
|
template: 'systems/daggerheart/templates/sheets/actors/adversary/effects.hbs',
|
||||||
|
scrollable: ['.effects-sections']
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
|
|
@ -45,6 +56,7 @@ export default class AdversarySheet extends DHBaseActorSheet {
|
||||||
async _prepareContext(options) {
|
async _prepareContext(options) {
|
||||||
const context = await super._prepareContext(options);
|
const context = await super._prepareContext(options);
|
||||||
context.systemFields.attack.fields = this.document.system.attack.schema.fields;
|
context.systemFields.attack.fields = this.document.system.attack.schema.fields;
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,6 +66,9 @@ export default class AdversarySheet extends DHBaseActorSheet {
|
||||||
switch (partId) {
|
switch (partId) {
|
||||||
case 'header':
|
case 'header':
|
||||||
await this._prepareHeaderContext(context, options);
|
await this._prepareHeaderContext(context, options);
|
||||||
|
|
||||||
|
const adversaryTypes = CONFIG.DH.ACTOR.allAdversaryTypes();
|
||||||
|
context.adversaryType = game.i18n.localize(adversaryTypes[this.document.system.type].label);
|
||||||
break;
|
break;
|
||||||
case 'notes':
|
case 'notes':
|
||||||
await this._prepareNotesContext(context, options);
|
await this._prepareNotesContext(context, options);
|
||||||
|
|
@ -131,9 +146,9 @@ export default class AdversarySheet extends DHBaseActorSheet {
|
||||||
title: `Reaction Roll: ${this.actor.name}`,
|
title: `Reaction Roll: ${this.actor.name}`,
|
||||||
headerTitle: 'Adversary Reaction Roll',
|
headerTitle: 'Adversary Reaction Roll',
|
||||||
roll: {
|
roll: {
|
||||||
type: 'reaction'
|
type: 'trait'
|
||||||
},
|
},
|
||||||
type: 'trait',
|
actionType: 'reaction',
|
||||||
hasRoll: true,
|
hasRoll: true,
|
||||||
data: this.actor.getRollData()
|
data: this.actor.getRollData()
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
|
||||||
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
|
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
|
||||||
import FilterMenu from '../../ux/filter-menu.mjs';
|
import FilterMenu from '../../ux/filter-menu.mjs';
|
||||||
import { getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs';
|
import { getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs';
|
||||||
import { ItemBrowser } from '../../ui/itemBrowser.mjs';
|
|
||||||
|
|
||||||
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
|
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
|
||||||
|
|
||||||
|
|
@ -15,6 +14,8 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
static DEFAULT_OPTIONS = {
|
static DEFAULT_OPTIONS = {
|
||||||
classes: ['character'],
|
classes: ['character'],
|
||||||
position: { width: 850, height: 800 },
|
position: { width: 850, height: 800 },
|
||||||
|
/* Foundry adds disabled to all buttons and inputs if editPermission is missing. This is not desired. */
|
||||||
|
editPermission: CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER,
|
||||||
actions: {
|
actions: {
|
||||||
toggleVault: CharacterSheet.#toggleVault,
|
toggleVault: CharacterSheet.#toggleVault,
|
||||||
rollAttribute: CharacterSheet.#rollAttribute,
|
rollAttribute: CharacterSheet.#rollAttribute,
|
||||||
|
|
@ -27,8 +28,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
toggleEquipItem: CharacterSheet.#toggleEquipItem,
|
toggleEquipItem: CharacterSheet.#toggleEquipItem,
|
||||||
toggleResourceDice: CharacterSheet.#toggleResourceDice,
|
toggleResourceDice: CharacterSheet.#toggleResourceDice,
|
||||||
handleResourceDice: CharacterSheet.#handleResourceDice,
|
handleResourceDice: CharacterSheet.#handleResourceDice,
|
||||||
useDowntime: this.useDowntime,
|
useDowntime: this.useDowntime
|
||||||
tempBrowser: CharacterSheet.#tempBrowser
|
|
||||||
},
|
},
|
||||||
window: {
|
window: {
|
||||||
resizable: true,
|
resizable: true,
|
||||||
|
|
@ -78,6 +78,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
static PARTS = {
|
static PARTS = {
|
||||||
sidebar: {
|
sidebar: {
|
||||||
id: 'sidebar',
|
id: 'sidebar',
|
||||||
|
scrollable: ['.shortcut-items-section'],
|
||||||
template: 'systems/daggerheart/templates/sheets/actors/character/sidebar.hbs'
|
template: 'systems/daggerheart/templates/sheets/actors/character/sidebar.hbs'
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
|
|
@ -86,22 +87,27 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
},
|
},
|
||||||
features: {
|
features: {
|
||||||
id: 'features',
|
id: 'features',
|
||||||
|
scrollable: ['.features-sections'],
|
||||||
template: 'systems/daggerheart/templates/sheets/actors/character/features.hbs'
|
template: 'systems/daggerheart/templates/sheets/actors/character/features.hbs'
|
||||||
},
|
},
|
||||||
loadout: {
|
loadout: {
|
||||||
id: 'loadout',
|
id: 'loadout',
|
||||||
|
scrollable: ['.items-section'],
|
||||||
template: 'systems/daggerheart/templates/sheets/actors/character/loadout.hbs'
|
template: 'systems/daggerheart/templates/sheets/actors/character/loadout.hbs'
|
||||||
},
|
},
|
||||||
inventory: {
|
inventory: {
|
||||||
id: 'inventory',
|
id: 'inventory',
|
||||||
|
scrollable: ['.items-section'],
|
||||||
template: 'systems/daggerheart/templates/sheets/actors/character/inventory.hbs'
|
template: 'systems/daggerheart/templates/sheets/actors/character/inventory.hbs'
|
||||||
},
|
},
|
||||||
biography: {
|
biography: {
|
||||||
id: 'biography',
|
id: 'biography',
|
||||||
|
scrollable: ['.items-section'],
|
||||||
template: 'systems/daggerheart/templates/sheets/actors/character/biography.hbs'
|
template: 'systems/daggerheart/templates/sheets/actors/character/biography.hbs'
|
||||||
},
|
},
|
||||||
effects: {
|
effects: {
|
||||||
id: 'effects',
|
id: 'effects',
|
||||||
|
scrollable: ['.effects-sections'],
|
||||||
template: 'systems/daggerheart/templates/sheets/actors/character/effects.hbs'
|
template: 'systems/daggerheart/templates/sheets/actors/character/effects.hbs'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -126,6 +132,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
});
|
});
|
||||||
htmlElement.querySelectorAll('.inventory-item-quantity').forEach(element => {
|
htmlElement.querySelectorAll('.inventory-item-quantity').forEach(element => {
|
||||||
element.addEventListener('change', this.updateItemQuantity.bind(this));
|
element.addEventListener('change', this.updateItemQuantity.bind(this));
|
||||||
|
element.addEventListener('click', e => e.stopPropagation());
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add listener for armor marks input
|
// Add listener for armor marks input
|
||||||
|
|
@ -142,6 +149,13 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
.querySelector('.level-value')
|
.querySelector('.level-value')
|
||||||
?.addEventListener('change', event => this.document.updateLevel(Number(event.currentTarget.value)));
|
?.addEventListener('change', event => this.document.updateLevel(Number(event.currentTarget.value)));
|
||||||
|
|
||||||
|
const observer = this.document.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER, {
|
||||||
|
exact: true
|
||||||
|
});
|
||||||
|
if (observer) {
|
||||||
|
this.element.querySelector('.window-content').classList.add('viewMode');
|
||||||
|
}
|
||||||
|
|
||||||
this._createFilterMenus();
|
this._createFilterMenus();
|
||||||
this._createSearchFilter();
|
this._createSearchFilter();
|
||||||
}
|
}
|
||||||
|
|
@ -218,7 +232,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
async _prepareLoadoutContext(context, _options) {
|
async _prepareLoadoutContext(context, _options) {
|
||||||
context.cardView = !game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsList);
|
context.cardView = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsCard);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -620,14 +634,22 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
const { key } = button.dataset;
|
const { key } = button.dataset;
|
||||||
|
|
||||||
const presets = {
|
const presets = {
|
||||||
compendium: 'daggerheart',
|
|
||||||
folder: key,
|
folder: key,
|
||||||
|
filter:
|
||||||
|
key === 'subclasses'
|
||||||
|
? {
|
||||||
|
'system.linkedClass.uuid': {
|
||||||
|
key: 'system.linkedClass.uuid',
|
||||||
|
value: this.document.system.class.value._stats.compendiumSource
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
render: {
|
render: {
|
||||||
noFolder: true
|
noFolder: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return new ItemBrowser({ presets }).render({ force: true });
|
ui.compendiumBrowser.open(presets);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -655,31 +677,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
this.consumeResource(result?.costs);
|
if (result) game.system.api.fields.ActionFields.CostField.execute.call(this, result);
|
||||||
}
|
|
||||||
|
|
||||||
// Remove when Action Refactor part #2 done
|
|
||||||
async consumeResource(costs) {
|
|
||||||
if (!costs?.length) return;
|
|
||||||
const usefulResources = {
|
|
||||||
...foundry.utils.deepClone(this.actor.system.resources),
|
|
||||||
fear: {
|
|
||||||
value: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
|
|
||||||
max: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear,
|
|
||||||
reversed: false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const resources = game.system.api.fields.ActionFields.CostField.getRealCosts(costs).map(c => {
|
|
||||||
const resource = usefulResources[c.key];
|
|
||||||
return {
|
|
||||||
key: c.key,
|
|
||||||
value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1),
|
|
||||||
target: resource.target,
|
|
||||||
keyIsID: resource.keyIsID
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.actor.modifyResource(resources);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: redo toggleEquipItem method
|
//TODO: redo toggleEquipItem method
|
||||||
|
|
@ -724,8 +722,8 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
* @type {ApplicationClickAction}
|
* @type {ApplicationClickAction}
|
||||||
*/
|
*/
|
||||||
static async #toggleLoadoutView(_, button) {
|
static async #toggleLoadoutView(_, button) {
|
||||||
const newAbilityView = button.dataset.value !== 'true';
|
const newAbilityView = button.dataset.value === 'true';
|
||||||
await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsList, newAbilityView);
|
await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsCard, newAbilityView);
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -768,13 +766,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Temp
|
|
||||||
*/
|
|
||||||
static async #tempBrowser(_, target) {
|
|
||||||
new ItemBrowser().render({ force: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle the roll values of resource dice.
|
* Handle the roll values of resource dice.
|
||||||
* @type {ApplicationClickAction}
|
* @type {ApplicationClickAction}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
|
||||||
classes: ['actor', 'companion'],
|
classes: ['actor', 'companion'],
|
||||||
position: { width: 340 },
|
position: { width: 340 },
|
||||||
actions: {
|
actions: {
|
||||||
|
actionRoll: DhCompanionSheet.#actionRoll,
|
||||||
levelManagement: DhCompanionSheet.#levelManagement
|
levelManagement: DhCompanionSheet.#levelManagement
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -15,7 +16,10 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
|
||||||
static PARTS = {
|
static PARTS = {
|
||||||
header: { template: 'systems/daggerheart/templates/sheets/actors/companion/header.hbs' },
|
header: { template: 'systems/daggerheart/templates/sheets/actors/companion/header.hbs' },
|
||||||
details: { template: 'systems/daggerheart/templates/sheets/actors/companion/details.hbs' },
|
details: { template: 'systems/daggerheart/templates/sheets/actors/companion/details.hbs' },
|
||||||
effects: { template: 'systems/daggerheart/templates/sheets/actors/companion/effects.hbs' }
|
effects: {
|
||||||
|
template: 'systems/daggerheart/templates/sheets/actors/companion/effects.hbs',
|
||||||
|
scrollable: ['.effects-sections']
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
@ -42,6 +46,51 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
|
||||||
/* Application Clicks Actions */
|
/* Application Clicks Actions */
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
static async #actionRoll(event) {
|
||||||
|
const partner = this.actor.system.partner;
|
||||||
|
const config = {
|
||||||
|
event,
|
||||||
|
title: `${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}: ${this.actor.name}`,
|
||||||
|
headerTitle: `Companion ${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}`,
|
||||||
|
roll: {
|
||||||
|
trait: partner.system.spellcastModifierTrait?.key
|
||||||
|
},
|
||||||
|
hasRoll: true,
|
||||||
|
data: partner.getRollData()
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await partner.diceRoll(config);
|
||||||
|
this.consumeResource(result?.costs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove when Action Refactor part #2 done
|
||||||
|
async consumeResource(costs) {
|
||||||
|
if (!costs?.length) return;
|
||||||
|
|
||||||
|
const partner = this.actor.system.partner;
|
||||||
|
const usefulResources = {
|
||||||
|
...foundry.utils.deepClone(partner.system.resources),
|
||||||
|
fear: {
|
||||||
|
value: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
|
||||||
|
max: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear,
|
||||||
|
reversed: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const resources = game.system.api.fields.ActionFields.CostField.getRealCosts(costs).map(c => {
|
||||||
|
const resource = usefulResources[c.key];
|
||||||
|
return {
|
||||||
|
key: c.key,
|
||||||
|
value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1),
|
||||||
|
target: resource.target
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await partner.modifyResource(resources);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the companions level management window.
|
* Opens the companions level management window.
|
||||||
* @type {ApplicationClickAction}
|
* @type {ApplicationClickAction}
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,13 @@ export default class DhpEnvironment extends DHBaseActorSheet {
|
||||||
/**@override */
|
/**@override */
|
||||||
static PARTS = {
|
static PARTS = {
|
||||||
header: { template: 'systems/daggerheart/templates/sheets/actors/environment/header.hbs' },
|
header: { template: 'systems/daggerheart/templates/sheets/actors/environment/header.hbs' },
|
||||||
features: { template: 'systems/daggerheart/templates/sheets/actors/environment/features.hbs' },
|
features: {
|
||||||
|
template: 'systems/daggerheart/templates/sheets/actors/environment/features.hbs',
|
||||||
|
scrollable: ['feature-section']
|
||||||
|
},
|
||||||
potentialAdversaries: {
|
potentialAdversaries: {
|
||||||
template: 'systems/daggerheart/templates/sheets/actors/environment/potentialAdversaries.hbs'
|
template: 'systems/daggerheart/templates/sheets/actors/environment/potentialAdversaries.hbs',
|
||||||
|
scrollable: ['items-sections']
|
||||||
},
|
},
|
||||||
notes: { template: 'systems/daggerheart/templates/sheets/actors/environment/notes.hbs' }
|
notes: { template: 'systems/daggerheart/templates/sheets/actors/environment/notes.hbs' }
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||||
import { getDocFromElement, getDocFromElementSync, tagifyElement } from '../../../helpers/utils.mjs';
|
import { getDocFromElement, getDocFromElementSync, tagifyElement } from '../../../helpers/utils.mjs';
|
||||||
import { ItemBrowser } from '../../ui/itemBrowser.mjs';
|
|
||||||
|
|
||||||
const typeSettingsMap = {
|
const typeSettingsMap = {
|
||||||
character: 'extendCharacterDescriptions',
|
character: 'extendCharacterDescriptions',
|
||||||
|
|
@ -412,6 +411,19 @@ export default function DHApplicationMixin(Base) {
|
||||||
];
|
];
|
||||||
|
|
||||||
if (usable) {
|
if (usable) {
|
||||||
|
options.unshift({
|
||||||
|
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.cancelBeastform',
|
||||||
|
icon: 'fa-solid fa-ban',
|
||||||
|
condition: target => {
|
||||||
|
const doc = getDocFromElementSync(target);
|
||||||
|
return doc && doc.system?.actions?.some(a => a.type === 'beastform');
|
||||||
|
},
|
||||||
|
callback: async target =>
|
||||||
|
game.system.api.fields.ActionFields.BeastformField.handleActiveTransformations.call(
|
||||||
|
await getDocFromElement(target)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
options.unshift({
|
options.unshift({
|
||||||
name: 'DAGGERHEART.GENERAL.damage',
|
name: 'DAGGERHEART.GENERAL.damage',
|
||||||
icon: 'fa-solid fa-explosion',
|
icon: 'fa-solid fa-explosion',
|
||||||
|
|
@ -422,7 +434,9 @@ export default function DHApplicationMixin(Base) {
|
||||||
callback: async (target, event) => {
|
callback: async (target, event) => {
|
||||||
const doc = await getDocFromElement(target),
|
const doc = await getDocFromElement(target),
|
||||||
action = doc?.system?.attack ?? doc;
|
action = doc?.system?.attack ?? doc;
|
||||||
return action && action.use(event, { byPassRoll: true });
|
const config = action.prepareConfig(event);
|
||||||
|
config.hasRoll = false;
|
||||||
|
return action && action.workflow.get('damage').execute(config, null, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -441,7 +455,7 @@ export default function DHApplicationMixin(Base) {
|
||||||
options.push({
|
options.push({
|
||||||
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
|
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
|
||||||
icon: 'fa-solid fa-message',
|
icon: 'fa-solid fa-message',
|
||||||
callback: async target => (await getDocFromElement(target)).toChat(this.document.id)
|
callback: async target => (await getDocFromElement(target)).toChat(this.document.uuid)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (deletable)
|
if (deletable)
|
||||||
|
|
@ -577,28 +591,27 @@ export default function DHApplicationMixin(Base) {
|
||||||
static async #browseItem(event, target) {
|
static async #browseItem(event, target) {
|
||||||
const type = target.dataset.compendium ?? target.dataset.type;
|
const type = target.dataset.compendium ?? target.dataset.type;
|
||||||
|
|
||||||
const presets = {};
|
const presets = {
|
||||||
|
render: {
|
||||||
|
noFolder: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'loot':
|
case 'loot':
|
||||||
|
presets.folder = 'equipments.folders.loots';
|
||||||
|
break;
|
||||||
case 'consumable':
|
case 'consumable':
|
||||||
|
presets.folder = 'equipments.folders.consumables';
|
||||||
|
break;
|
||||||
case 'armor':
|
case 'armor':
|
||||||
|
presets.folder = 'equipments.folders.armors';
|
||||||
|
break;
|
||||||
case 'weapon':
|
case 'weapon':
|
||||||
presets.compendium = 'daggerheart';
|
presets.folder = 'equipments.folders.weapons';
|
||||||
presets.folder = 'equipments';
|
|
||||||
presets.render = {
|
|
||||||
noFolder: true
|
|
||||||
};
|
|
||||||
presets.filter = {
|
|
||||||
type: { key: 'type', value: type, forced: true }
|
|
||||||
};
|
|
||||||
break;
|
break;
|
||||||
case 'domainCard':
|
case 'domainCard':
|
||||||
presets.compendium = 'daggerheart';
|
|
||||||
presets.folder = 'domains';
|
presets.folder = 'domains';
|
||||||
presets.render = {
|
|
||||||
noFolder: true
|
|
||||||
};
|
|
||||||
presets.filter = {
|
presets.filter = {
|
||||||
'level.max': { key: 'level.max', value: this.document.system.levelData.level.current },
|
'level.max': { key: 'level.max', value: this.document.system.levelData.level.current },
|
||||||
'system.domain': { key: 'system.domain', value: this.document.system.domains }
|
'system.domain': { key: 'system.domain', value: this.document.system.domains }
|
||||||
|
|
@ -608,7 +621,7 @@ export default function DHApplicationMixin(Base) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ItemBrowser({ presets }).render({ force: true });
|
ui.compendiumBrowser.open(presets);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -639,7 +652,6 @@ export default function DHApplicationMixin(Base) {
|
||||||
if (featureOnCharacter) {
|
if (featureOnCharacter) {
|
||||||
systemData = {
|
systemData = {
|
||||||
originItemType: this.document.type,
|
originItemType: this.document.type,
|
||||||
originId: this.document.id,
|
|
||||||
identifier: this.document.system.isMulticlass ? 'multiclass' : null
|
identifier: this.document.system.isMulticlass ? 'multiclass' : null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -167,12 +167,12 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
|
||||||
const { type } = target.dataset;
|
const { type } = target.dataset;
|
||||||
const cls = foundry.documents.Item.implementation;
|
const cls = foundry.documents.Item.implementation;
|
||||||
|
|
||||||
|
const multiclass = this.document.system.isMulticlass ? 'multiclass' : null;
|
||||||
let systemData = {};
|
let systemData = {};
|
||||||
if (this.document.parent?.type === 'character') {
|
if (this.document.parent?.type === 'character') {
|
||||||
systemData = {
|
systemData = {
|
||||||
originItemType: this.document.type,
|
originItemType: this.document.type,
|
||||||
originId: this.document.id,
|
identifier: multiclass ?? type
|
||||||
identifier: this.document.system.isMulticlass ? 'multiclass' : null
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -293,14 +293,15 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
|
||||||
|
|
||||||
if (this.document.parent?.type === 'character') {
|
if (this.document.parent?.type === 'character') {
|
||||||
const itemData = item.toObject();
|
const itemData = item.toObject();
|
||||||
|
const multiclass = this.document.system.isMulticlass ? 'multiclass' : null;
|
||||||
item = await cls.create(
|
item = await cls.create(
|
||||||
{
|
{
|
||||||
...itemData,
|
...itemData,
|
||||||
|
_stats: { compendiumSource: this.document.uuid },
|
||||||
system: {
|
system: {
|
||||||
...itemData.system,
|
...itemData.system,
|
||||||
originItemType: this.document.type,
|
originItemType: this.document.type,
|
||||||
originId: this.document.id,
|
identifier: multiclass ?? target.dataset.type
|
||||||
identifier: this.document.system.isMulticlass ? 'multiclass' : null
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ parent: this.document.parent }
|
{ parent: this.document.parent }
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
|
||||||
tagifyConfigs: [
|
tagifyConfigs: [
|
||||||
{
|
{
|
||||||
selector: '.features-input',
|
selector: '.features-input',
|
||||||
options: () => CONFIG.DH.ITEM.armorFeatures,
|
options: () => CONFIG.DH.ITEM.orderedArmorFeatures(),
|
||||||
callback: ArmorSheet.#onFeatureSelect
|
callback: ArmorSheet.#onFeatureSelect
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,10 @@ export default class ClassSheet extends DHBaseItemSheet {
|
||||||
template: 'systems/daggerheart/templates/sheets/items/class/settings.hbs',
|
template: 'systems/daggerheart/templates/sheets/items/class/settings.hbs',
|
||||||
scrollable: ['.settings']
|
scrollable: ['.settings']
|
||||||
},
|
},
|
||||||
|
questions: {
|
||||||
|
template: 'systems/daggerheart/templates/sheets/items/class/questions.hbs',
|
||||||
|
scrollable: ['.questions']
|
||||||
|
},
|
||||||
effects: {
|
effects: {
|
||||||
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs',
|
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs',
|
||||||
scrollable: ['.effects']
|
scrollable: ['.effects']
|
||||||
|
|
@ -55,7 +59,13 @@ export default class ClassSheet extends DHBaseItemSheet {
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
static TABS = {
|
static TABS = {
|
||||||
primary: {
|
primary: {
|
||||||
tabs: [{ id: 'description' }, { id: 'features' }, { id: 'settings' }, { id: 'effects' }],
|
tabs: [
|
||||||
|
{ id: 'description' },
|
||||||
|
{ id: 'features' },
|
||||||
|
{ id: 'settings' },
|
||||||
|
{ id: 'questions' },
|
||||||
|
{ id: 'effects' }
|
||||||
|
],
|
||||||
initial: 'description',
|
initial: 'description',
|
||||||
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
|
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
|
||||||
}
|
}
|
||||||
|
|
@ -119,6 +129,15 @@ export default class ClassSheet extends DHBaseItemSheet {
|
||||||
const itemType = data.data ? data.type : item.type;
|
const itemType = data.data ? data.type : item.type;
|
||||||
const target = event.target.closest('fieldset.drop-section');
|
const target = event.target.closest('fieldset.drop-section');
|
||||||
if (itemType === 'subclass') {
|
if (itemType === 'subclass') {
|
||||||
|
if (item.system.linkedClass) {
|
||||||
|
return ui.notifications.warn(
|
||||||
|
game.i18n.format('DAGGERHEART.UI.Notifications.subclassAlreadyLinked', {
|
||||||
|
name: item.name,
|
||||||
|
class: this.document.name
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await item.update({ 'system.linkedClass': this.document.uuid });
|
||||||
await this.document.update({
|
await this.document.update({
|
||||||
'system.subclasses': [...this.document.system.subclasses.map(x => x.uuid), item.uuid]
|
'system.subclasses': [...this.document.system.subclasses.map(x => x.uuid), item.uuid]
|
||||||
});
|
});
|
||||||
|
|
@ -181,6 +200,12 @@ export default class ClassSheet extends DHBaseItemSheet {
|
||||||
static async #removeItemFromCollection(_event, element) {
|
static async #removeItemFromCollection(_event, element) {
|
||||||
const { uuid, target } = element.dataset;
|
const { uuid, target } = element.dataset;
|
||||||
const prop = foundry.utils.getProperty(this.document.system, target);
|
const prop = foundry.utils.getProperty(this.document.system, target);
|
||||||
|
|
||||||
|
if (target === 'subclasses') {
|
||||||
|
const subclass = await foundry.utils.fromUuid(uuid);
|
||||||
|
await subclass.update({ 'system.linkedClass': null });
|
||||||
|
}
|
||||||
|
|
||||||
await this.document.update({ [`system.${target}`]: prop.filter(i => i.uuid !== uuid).map(x => x.uuid) });
|
await this.document.update({ [`system.${target}`]: prop.filter(i => i.uuid !== uuid).map(x => x.uuid) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export default class WeaponSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
|
||||||
tagifyConfigs: [
|
tagifyConfigs: [
|
||||||
{
|
{
|
||||||
selector: '.features-input',
|
selector: '.features-input',
|
||||||
options: () => CONFIG.DH.ITEM.weaponFeatures,
|
options: () => CONFIG.DH.ITEM.orderedWeaponFeatures(),
|
||||||
callback: WeaponSheet.#onFeatureSelect
|
callback: WeaponSheet.#onFeatureSelect
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
2
module/applications/sidebar/_module.mjs
Normal file
2
module/applications/sidebar/_module.mjs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as DaggerheartMenu } from './tabs/daggerheartMenu.mjs';
|
||||||
|
export { default as DhSidebar } from './sidebar.mjs';
|
||||||
33
module/applications/sidebar/sidebar.mjs
Normal file
33
module/applications/sidebar/sidebar.mjs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
export default class DhSidebar extends Sidebar {
|
||||||
|
/** @override */
|
||||||
|
static TABS = {
|
||||||
|
...super.TABS,
|
||||||
|
daggerheartMenu: {
|
||||||
|
tooltip: 'DAGGERHEART.UI.Sidebar.daggerheartMenu.title',
|
||||||
|
img: 'systems/daggerheart/assets/logos/FoundryBorneLogoWhite.svg'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
static PARTS = {
|
||||||
|
tabs: {
|
||||||
|
id: 'tabs',
|
||||||
|
template: 'systems/daggerheart/templates/sidebar/tabs.hbs'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
async _prepareTabContext(context, options) {
|
||||||
|
context.tabs = Object.entries(this.constructor.TABS).reduce((obj, [k, v]) => {
|
||||||
|
let { documentName, gmOnly, tooltip, icon, img } = v;
|
||||||
|
if (gmOnly && !game.user.isGM) return obj;
|
||||||
|
if (documentName) {
|
||||||
|
tooltip ??= getDocumentClass(documentName).metadata.labelPlural;
|
||||||
|
icon ??= CONFIG[documentName]?.sidebarIcon;
|
||||||
|
}
|
||||||
|
obj[k] = { tooltip, icon, img };
|
||||||
|
obj[k].active = this.tabGroups.primary === k;
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
160
module/applications/sidebar/tabs/daggerheartMenu.mjs
Normal file
160
module/applications/sidebar/tabs/daggerheartMenu.mjs
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||||
|
const { AbstractSidebarTab } = foundry.applications.sidebar;
|
||||||
|
/**
|
||||||
|
* The daggerheart menu tab.
|
||||||
|
* @extends {AbstractSidebarTab}
|
||||||
|
* @mixes HandlebarsApplication
|
||||||
|
*/
|
||||||
|
export default class DaggerheartMenu extends HandlebarsApplicationMixin(AbstractSidebarTab) {
|
||||||
|
constructor(options) {
|
||||||
|
super(options);
|
||||||
|
|
||||||
|
this.refreshSelections = DaggerheartMenu.#defaultRefreshSelections();
|
||||||
|
}
|
||||||
|
|
||||||
|
static #defaultRefreshSelections() {
|
||||||
|
return {
|
||||||
|
session: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.session') },
|
||||||
|
scene: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.scene') },
|
||||||
|
longRest: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.longrest') },
|
||||||
|
shortRest: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.shortrest') }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
classes: ['dh-style'],
|
||||||
|
window: {
|
||||||
|
title: 'SIDEBAR.TabSettings'
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
selectRefreshable: DaggerheartMenu.#selectRefreshable,
|
||||||
|
refreshActors: DaggerheartMenu.#refreshActors
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
static tabName = 'daggerheartMenu';
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
static PARTS = {
|
||||||
|
main: { template: 'systems/daggerheart/templates/sidebar/daggerheart-menu/main.hbs' }
|
||||||
|
};
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
|
async _prepareContext(options) {
|
||||||
|
const context = await super._prepareContext(options);
|
||||||
|
context.refreshables = this.refreshSelections;
|
||||||
|
context.disableRefresh = Object.values(this.refreshSelections).every(x => !x.selected);
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRefreshables(types) {
|
||||||
|
const refreshedActors = {};
|
||||||
|
for (let actor of game.actors) {
|
||||||
|
if (['character', 'adversary'].includes(actor.type) && actor.prototypeToken.actorLink) {
|
||||||
|
const updates = {};
|
||||||
|
for (let item of actor.items) {
|
||||||
|
if (item.system.metadata.hasResource && types.includes(item.system.resource?.recovery)) {
|
||||||
|
if (!refreshedActors[actor.id])
|
||||||
|
refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() };
|
||||||
|
refreshedActors[actor.id].refreshed.add(
|
||||||
|
game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[item.system.resource.recovery].label)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updates[item.id]?.system) updates[item.id] = { system: {} };
|
||||||
|
|
||||||
|
const increasing =
|
||||||
|
item.system.resource.progression === CONFIG.DH.ITEM.itemResourceProgression.increasing.id;
|
||||||
|
updates[item.id].system = {
|
||||||
|
...updates[item.id].system,
|
||||||
|
'resource.value': increasing
|
||||||
|
? 0
|
||||||
|
: Roll.replaceFormulaData(item.system.resource.max, actor.getRollData())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (item.system.metadata.hasActions) {
|
||||||
|
const refreshTypes = new Set();
|
||||||
|
const actions = item.system.actions.filter(action => {
|
||||||
|
if (types.includes(action.uses.recovery)) {
|
||||||
|
refreshTypes.add(action.uses.recovery);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (actions.length === 0) continue;
|
||||||
|
|
||||||
|
if (!refreshedActors[actor.id])
|
||||||
|
refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() };
|
||||||
|
refreshedActors[actor.id].refreshed.add(
|
||||||
|
...refreshTypes.map(type => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[type].label))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updates[item.id]?.system) updates[item.id] = { system: {} };
|
||||||
|
|
||||||
|
updates[item.id].system = {
|
||||||
|
...updates[item.id].system,
|
||||||
|
...actions.reduce(
|
||||||
|
(acc, action) => {
|
||||||
|
acc.actions[action.id] = { 'uses.value': 0 };
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ actions: updates[item.id].system.actions ?? {} }
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let key in updates) {
|
||||||
|
const update = updates[key];
|
||||||
|
await actor.items.get(key).update(update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return refreshedActors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
/* Application Clicks Actions */
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
static async #selectRefreshable(_event, button) {
|
||||||
|
const { type } = button.dataset;
|
||||||
|
this.refreshSelections[type].selected = !this.refreshSelections[type].selected;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #refreshActors() {
|
||||||
|
const refreshKeys = Object.keys(this.refreshSelections).filter(key => this.refreshSelections[key].selected);
|
||||||
|
await this.getRefreshables(refreshKeys);
|
||||||
|
const types = refreshKeys.map(x => this.refreshSelections[x].label).join(', ');
|
||||||
|
ui.notifications.info(
|
||||||
|
game.i18n.format('DAGGERHEART.UI.Notifications.gmMenuRefresh', {
|
||||||
|
types: `[${types}]`
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.refreshSelections = DaggerheartMenu.#defaultRefreshSelections();
|
||||||
|
|
||||||
|
const cls = getDocumentClass('ChatMessage');
|
||||||
|
const msg = {
|
||||||
|
user: game.user.id,
|
||||||
|
content: await foundry.applications.handlebars.renderTemplate(
|
||||||
|
'systems/daggerheart/templates/ui/chat/refreshMessage.hbs',
|
||||||
|
{
|
||||||
|
types: types
|
||||||
|
}
|
||||||
|
),
|
||||||
|
title: game.i18n.localize('DAGGERHEART.UI.Chat.refreshMessage.title'),
|
||||||
|
speaker: cls.getSpeaker()
|
||||||
|
};
|
||||||
|
|
||||||
|
cls.create(msg);
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,3 +3,4 @@ export { default as DhCombatTracker } from './combatTracker.mjs';
|
||||||
export * as DhCountdowns from './countdowns.mjs';
|
export * as DhCountdowns from './countdowns.mjs';
|
||||||
export { default as DhFearTracker } from './fearTracker.mjs';
|
export { default as DhFearTracker } from './fearTracker.mjs';
|
||||||
export { default as DhHotbar } from './hotbar.mjs';
|
export { default as DhHotbar } from './hotbar.mjs';
|
||||||
|
export { ItemBrowser } from './itemBrowser.mjs';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
|
|
||||||
|
|
||||||
export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog {
|
export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
@ -55,21 +53,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
||||||
}
|
}
|
||||||
|
|
||||||
addChatListeners = async (app, html, data) => {
|
addChatListeners = async (app, html, data) => {
|
||||||
html.querySelectorAll('.duality-action-damage').forEach(element =>
|
|
||||||
element.addEventListener('click', event => this.onRollDamage(event, data.message))
|
|
||||||
);
|
|
||||||
html.querySelectorAll('.target-save').forEach(element =>
|
|
||||||
element.addEventListener('click', event => this.onRollSave(event, data.message))
|
|
||||||
);
|
|
||||||
html.querySelectorAll('.roll-all-save-button').forEach(element =>
|
|
||||||
element.addEventListener('click', event => this.onRollAllSave(event, data.message))
|
|
||||||
);
|
|
||||||
html.querySelectorAll('.simple-roll-button').forEach(element =>
|
html.querySelectorAll('.simple-roll-button').forEach(element =>
|
||||||
element.addEventListener('click', event => this.onRollSimple(event, data.message))
|
element.addEventListener('click', event => this.onRollSimple(event, data.message))
|
||||||
);
|
);
|
||||||
html.querySelectorAll('.healing-button').forEach(element =>
|
|
||||||
element.addEventListener('click', event => this.onHealing(event, data.message))
|
|
||||||
);
|
|
||||||
html.querySelectorAll('.ability-use-button').forEach(element =>
|
html.querySelectorAll('.ability-use-button').forEach(element =>
|
||||||
element.addEventListener('click', event => this.abilityUseButton(event, data.message))
|
element.addEventListener('click', event => this.abilityUseButton(event, data.message))
|
||||||
);
|
);
|
||||||
|
|
@ -90,80 +76,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
||||||
super.close(options);
|
super.close(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getActor(uuid) {
|
|
||||||
return await foundry.utils.fromUuid(uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
getAction(actor, itemId, actionId) {
|
|
||||||
const item = actor.items.get(itemId),
|
|
||||||
action =
|
|
||||||
actor.system.attack?._id === actionId
|
|
||||||
? actor.system.attack
|
|
||||||
: item.system.attack?._id === actionId
|
|
||||||
? item.system.attack
|
|
||||||
: item?.system?.actions?.get(actionId);
|
|
||||||
return action;
|
|
||||||
}
|
|
||||||
|
|
||||||
async onRollDamage(event, message) {
|
|
||||||
event.stopPropagation();
|
|
||||||
const actor = await this.getActor(message.system.source.actor);
|
|
||||||
if(!actor.isOwner) return true;
|
|
||||||
if (message.system.source.item && message.system.source.action) {
|
|
||||||
const action = this.getAction(actor, message.system.source.item, message.system.source.action);
|
|
||||||
if (!action || !action?.rollDamage) return;
|
|
||||||
await action.rollDamage(event, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async onRollSave(event, message) {
|
|
||||||
event.stopPropagation();
|
|
||||||
const actor = await this.getActor(message.system.source.actor),
|
|
||||||
tokenId = event.target.closest('[data-token]')?.dataset.token,
|
|
||||||
token = game.canvas.tokens.get(tokenId);
|
|
||||||
if (!token?.actor || !token.isOwner) return true;
|
|
||||||
if (message.system.source.item && message.system.source.action) {
|
|
||||||
const action = this.getAction(actor, message.system.source.item, message.system.source.action);
|
|
||||||
if (!action || !action?.hasSave) return;
|
|
||||||
action.rollSave(token.actor, event, message).then(result =>
|
|
||||||
emitAsGM(
|
|
||||||
GMUpdateEvent.UpdateSaveMessage,
|
|
||||||
action.updateSaveMessage.bind(action, result, message, token.id),
|
|
||||||
{
|
|
||||||
action: action.uuid,
|
|
||||||
message: message._id,
|
|
||||||
token: token.id,
|
|
||||||
result
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async onRollAllSave(event, message) {
|
|
||||||
event.stopPropagation();
|
|
||||||
if (!game.user.isGM) return;
|
|
||||||
const targets = event.target.parentElement.querySelectorAll('[data-token] .target-save');
|
|
||||||
const actor = await this.getActor(message.system.source.actor),
|
|
||||||
action = this.getAction(actor, message.system.source.item, message.system.source.action);
|
|
||||||
targets.forEach(async el => {
|
|
||||||
const tokenId = el.closest('[data-token]')?.dataset.token,
|
|
||||||
token = game.canvas.tokens.get(tokenId);
|
|
||||||
if (!token.actor) return;
|
|
||||||
if (game.user === token.actor.owner) el.dispatchEvent(new PointerEvent('click', { shiftKey: true }));
|
|
||||||
else {
|
|
||||||
token.actor.owner
|
|
||||||
.query('reactionRoll', {
|
|
||||||
actionId: action.uuid,
|
|
||||||
actorId: token.actor.uuid,
|
|
||||||
event,
|
|
||||||
message
|
|
||||||
})
|
|
||||||
.then(result => action.updateSaveMessage(result, message, token.id));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async onRollSimple(event, message) {
|
async onRollSimple(event, message) {
|
||||||
const buttonType = event.target.dataset.type ?? 'damage',
|
const buttonType = event.target.dataset.type ?? 'damage',
|
||||||
total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0),
|
total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0),
|
||||||
|
|
@ -197,8 +109,11 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
||||||
item.system.attack?.id === event.currentTarget.id
|
item.system.attack?.id === event.currentTarget.id
|
||||||
? item.system.attack
|
? item.system.attack
|
||||||
: item.system.actions.get(event.currentTarget.id);
|
: item.system.actions.get(event.currentTarget.id);
|
||||||
if (event.currentTarget.dataset.directDamage) action.use(event, { byPassRoll: true });
|
if (event.currentTarget.dataset.directDamage) {
|
||||||
else action.use(event);
|
const config = action.prepareConfig(event);
|
||||||
|
config.hasRoll = false;
|
||||||
|
action.workflow.get('damage').execute(config, null, true);
|
||||||
|
} else action.use(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
async actionUseButton(event, message) {
|
async actionUseButton(event, message) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { emitAsGM, GMUpdateEvent, socketEvent } from "../../systemRegistration/socket.mjs";
|
import { emitAsGM, GMUpdateEvent, socketEvent } from '../../systemRegistration/socket.mjs';
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
||||||
|
|
@ -78,7 +78,7 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
|
||||||
|
|
||||||
/** @override */
|
/** @override */
|
||||||
async _preRender(context, options) {
|
async _preRender(context, options) {
|
||||||
if (this.currentFear > this.maxFear)
|
if (this.currentFear > this.maxFear && game.user.isGM)
|
||||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, this.maxFear);
|
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, this.maxFear);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,19 +106,10 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateFear(value) {
|
async updateFear(value) {
|
||||||
return emitAsGM(GMUpdateEvent.UpdateFear, game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), value);
|
return emitAsGM(
|
||||||
/* if(!game.user.isGM)
|
GMUpdateEvent.UpdateFear,
|
||||||
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
|
||||||
action: socketEvent.GMUpdate,
|
value
|
||||||
data: {
|
);
|
||||||
action: GMUpdateEvent.UpdateFear,
|
|
||||||
update: value
|
|
||||||
}
|
|
||||||
});
|
|
||||||
else
|
|
||||||
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, value); */
|
|
||||||
/* if (!game.user.isGM) return;
|
|
||||||
value = Math.max(0, Math.min(this.maxFear, value));
|
|
||||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, value); */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,16 +15,13 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
this.fieldFilter = [];
|
this.fieldFilter = [];
|
||||||
this.selectedMenu = { path: [], data: null };
|
this.selectedMenu = { path: [], data: null };
|
||||||
this.config = CONFIG.DH.ITEMBROWSER.compendiumConfig;
|
this.config = CONFIG.DH.ITEMBROWSER.compendiumConfig;
|
||||||
this.presets = options.presets;
|
this.presets = {};
|
||||||
|
|
||||||
if (this.presets?.compendium && this.presets?.folder)
|
|
||||||
ItemBrowser.selectFolder.call(this, null, null, this.presets.compendium, this.presets.folder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
static DEFAULT_OPTIONS = {
|
static DEFAULT_OPTIONS = {
|
||||||
id: 'itemBrowser',
|
id: 'itemBrowser',
|
||||||
classes: ['daggerheart', 'dh-style', 'dialog', 'compendium-browser'],
|
classes: ['daggerheart', 'dh-style', 'dialog', 'compendium-browser', 'loader'],
|
||||||
tag: 'div',
|
tag: 'div',
|
||||||
window: {
|
window: {
|
||||||
frame: true,
|
frame: true,
|
||||||
|
|
@ -84,17 +81,13 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @inheritDoc */
|
|
||||||
async _preFirstRender(context, options) {
|
|
||||||
if (context.presets?.render?.noFolder || context.presets?.render?.lite) options.position.width = 600;
|
|
||||||
|
|
||||||
await super._preFirstRender(context, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
async _preRender(context, options) {
|
async _preRender(context, options) {
|
||||||
if (context.presets?.render?.noFolder || context.presets?.render?.lite)
|
this.presets = options.presets ?? {};
|
||||||
options.parts.splice(options.parts.indexOf('sidebar'), 1);
|
|
||||||
|
const width = this.presets?.render?.noFolder === true || this.presets?.render?.lite === true ? 600 : 850;
|
||||||
|
if (this.rendered) this.setPosition({ width });
|
||||||
|
else options.position.width = width;
|
||||||
|
|
||||||
await super._preRender(context, options);
|
await super._preRender(context, options);
|
||||||
}
|
}
|
||||||
|
|
@ -103,32 +96,31 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
async _onRender(context, options) {
|
async _onRender(context, options) {
|
||||||
await super._onRender(context, options);
|
await super._onRender(context, options);
|
||||||
|
|
||||||
this._createSearchFilter();
|
this.element
|
||||||
this._createFilterInputs();
|
.querySelectorAll('[data-action="selectFolder"]')
|
||||||
this._createDragProcess();
|
.forEach(element =>
|
||||||
|
element.classList.toggle('is-selected', element.dataset.folderId === this.selectedMenu.path.join('.'))
|
||||||
if (context.presets?.render?.lite) this.element.classList.add('lite');
|
|
||||||
|
|
||||||
if (context.presets?.render?.noFolder) this.element.classList.add('no-folder');
|
|
||||||
|
|
||||||
if (context.presets?.render?.noFilter) this.element.classList.add('no-filter');
|
|
||||||
|
|
||||||
if (this.presets?.filter) {
|
|
||||||
Object.entries(this.presets.filter).forEach(
|
|
||||||
([k, v]) => (this.fieldFilter.find(c => c.name === k).value = v.value)
|
|
||||||
);
|
);
|
||||||
await this._onInputFilterBrowser();
|
|
||||||
}
|
this._createSearchFilter();
|
||||||
|
|
||||||
|
this.element.classList.toggle('lite', this.presets?.render?.lite === true);
|
||||||
|
this.element.classList.toggle('no-folder', this.presets?.render?.noFolder === true);
|
||||||
|
this.element.classList.toggle('no-filter', this.presets?.render?.noFilter === true);
|
||||||
|
this.element.querySelectorAll('.folder-list > [data-action="selectFolder"]').forEach(element => {
|
||||||
|
element.hidden =
|
||||||
|
this.presets.render?.folders?.length && !this.presets.render.folders.includes(element.dataset.folderId);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_attachPartListeners(partId, htmlElement, options) {
|
_attachPartListeners(partId, htmlElement, options) {
|
||||||
super._attachPartListeners(partId, htmlElement, options);
|
super._attachPartListeners(partId, htmlElement, options);
|
||||||
|
|
||||||
htmlElement
|
htmlElement.querySelectorAll('[data-action="selectFolder"]').forEach(element =>
|
||||||
.querySelectorAll('[data-action="selectFolder"]')
|
element.addEventListener('contextmenu', event => {
|
||||||
.forEach(element => element.addEventListener("contextmenu", (event) => {
|
|
||||||
event.target.classList.toggle('expanded');
|
event.target.classList.toggle('expanded');
|
||||||
}))
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
@ -139,22 +131,26 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
async _prepareContext(options) {
|
async _prepareContext(options) {
|
||||||
const context = await super._prepareContext(options);
|
const context = await super._prepareContext(options);
|
||||||
context.compendiums = this.getCompendiumFolders(foundry.utils.deepClone(this.config));
|
context.compendiums = this.getCompendiumFolders(foundry.utils.deepClone(this.config));
|
||||||
// context.pathTitle = this.pathTile;
|
|
||||||
context.menu = this.selectedMenu;
|
context.menu = this.selectedMenu;
|
||||||
context.formatLabel = this.formatLabel;
|
context.formatLabel = this.formatLabel;
|
||||||
context.formatChoices = this.formatChoices;
|
context.formatChoices = this.formatChoices;
|
||||||
context.fieldFilter = this.fieldFilter = this._createFieldFilter();
|
|
||||||
context.items = this.items;
|
context.items = this.items;
|
||||||
context.presets = this.presets;
|
context.presets = this.presets;
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open(presets = {}) {
|
||||||
|
this.presets = presets;
|
||||||
|
ItemBrowser.selectFolder.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
getCompendiumFolders(config, parent = null, depth = 0) {
|
getCompendiumFolders(config, parent = null, depth = 0) {
|
||||||
let folders = [];
|
let folders = [];
|
||||||
Object.values(config).forEach(c => {
|
Object.values(config).forEach(c => {
|
||||||
|
// if(this.presets.render?.folders?.length && !this.presets.render.folders.includes(c.id)) return;
|
||||||
const folder = {
|
const folder = {
|
||||||
id: c.id,
|
id: c.id,
|
||||||
label: c.label,
|
label: game.i18n.localize(c.label),
|
||||||
selected: (!parent || parent.selected) && this.selectedMenu.path[depth] === c.id
|
selected: (!parent || parent.selected) && this.selectedMenu.path[depth] === c.id
|
||||||
};
|
};
|
||||||
folder.folders = c.folders
|
folder.folders = c.folders
|
||||||
|
|
@ -162,47 +158,108 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
: [];
|
: [];
|
||||||
folders.push(folder);
|
folders.push(folder);
|
||||||
});
|
});
|
||||||
|
folders.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
|
|
||||||
return folders;
|
return folders;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async selectFolder(_, target, compend, folder) {
|
static async selectFolder(_, target) {
|
||||||
const config = foundry.utils.deepClone(this.config),
|
const folderId = target?.dataset?.folderId ?? this.presets.folder,
|
||||||
compendium = compend ?? target.closest('[data-compendium-id]').dataset.compendiumId,
|
folderData = foundry.utils.getProperty(this.config, folderId) ?? {};
|
||||||
folderId = folder ?? target.dataset.folderId,
|
|
||||||
folderPath = `${compendium}.folders.${folderId}`,
|
const columns = ItemBrowser.getFolderConfig(folderData).map(col => ({
|
||||||
folderData = foundry.utils.getProperty(config, folderPath);
|
...col,
|
||||||
|
label: game.i18n.localize(col.label)
|
||||||
|
}));
|
||||||
|
|
||||||
this.selectedMenu = {
|
this.selectedMenu = {
|
||||||
path: folderPath.split('.'),
|
path: folderId?.split('.') ?? [],
|
||||||
data: {
|
data: {
|
||||||
...folderData,
|
...folderData,
|
||||||
columns: ItemBrowser.getFolderConfig(folderData)
|
columns: columns
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let items = [];
|
await this.render({ force: true, presets: this.presets });
|
||||||
for (const key of folderData.keys) {
|
|
||||||
const comp = game.packs.get(`${compendium}.${key}`);
|
|
||||||
if (!comp) return;
|
|
||||||
items = items.concat(await comp.getDocuments({ type__in: folderData.type }));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.items = ItemBrowser.sortBy(items, 'name');
|
if (this.selectedMenu?.data?.type?.length) this.loadItems();
|
||||||
|
|
||||||
if(target) {
|
|
||||||
target.closest('.compendium-sidebar').querySelectorAll('[data-action="selectFolder"]').forEach(element => element.classList.remove("is-selected"))
|
|
||||||
target.classList.add('is-selected');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.render({ force: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_replaceHTML(result, content, options) {
|
_replaceHTML(result, content, options) {
|
||||||
if(!options.isFirstRender) delete result.sidebar;
|
if (!options.isFirstRender) delete result.sidebar;
|
||||||
super._replaceHTML(result, content, options);
|
super._replaceHTML(result, content, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadItems() {
|
||||||
|
let loadTimeout = this.toggleLoader(true);
|
||||||
|
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
game.packs.forEach(pack => {
|
||||||
|
promises.push(
|
||||||
|
new Promise(async resolve => {
|
||||||
|
const items = await pack.getDocuments({ type__in: this.selectedMenu?.data?.type });
|
||||||
|
resolve(items);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all(promises).then(async result => {
|
||||||
|
this.items = ItemBrowser.sortBy(
|
||||||
|
result.flatMap(r => r),
|
||||||
|
'name'
|
||||||
|
);
|
||||||
|
this.fieldFilter = this._createFieldFilter();
|
||||||
|
|
||||||
|
if (this.presets?.filter) {
|
||||||
|
Object.entries(this.presets.filter).forEach(([k, v]) => {
|
||||||
|
const filter = this.fieldFilter.find(c => c.name === k);
|
||||||
|
if (filter) filter.value = v.value;
|
||||||
|
});
|
||||||
|
// await this._onInputFilterBrowser();
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterList = await foundry.applications.handlebars.renderTemplate(
|
||||||
|
'systems/daggerheart/templates/ui/itemBrowser/filterContainer.hbs',
|
||||||
|
{
|
||||||
|
fieldFilter: this.fieldFilter,
|
||||||
|
presets: this.presets,
|
||||||
|
formatChoices: this.formatChoices
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.element.querySelector('.filter-content .wrapper').innerHTML = filterList;
|
||||||
|
const filterContainer = this.element.querySelector('.filter-header > [data-action="expandContent"]');
|
||||||
|
if (this.fieldFilter.length === 0) filterContainer.setAttribute('disabled', '');
|
||||||
|
else filterContainer.removeAttribute('disabled');
|
||||||
|
|
||||||
|
const itemList = await foundry.applications.handlebars.renderTemplate(
|
||||||
|
'systems/daggerheart/templates/ui/itemBrowser/itemContainer.hbs',
|
||||||
|
{
|
||||||
|
items: this.items,
|
||||||
|
menu: this.selectedMenu,
|
||||||
|
formatLabel: this.formatLabel
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.element.querySelector('.item-list').innerHTML = itemList;
|
||||||
|
|
||||||
|
this._createFilterInputs();
|
||||||
|
await this._onInputFilterBrowser();
|
||||||
|
this._createDragProcess();
|
||||||
|
|
||||||
|
clearTimeout(loadTimeout);
|
||||||
|
this.toggleLoader(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleLoader(state) {
|
||||||
|
const container = this.element.querySelector('.item-list');
|
||||||
|
return setTimeout(() => {
|
||||||
|
container.classList.toggle('loader', state);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
static expandContent(_, target) {
|
static expandContent(_, target) {
|
||||||
const parent = target.parentElement;
|
const parent = target.parentElement;
|
||||||
parent.classList.toggle('expanded');
|
parent.classList.toggle('expanded');
|
||||||
|
|
@ -235,8 +292,14 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
filters.forEach(f => {
|
filters.forEach(f => {
|
||||||
if (typeof f.field === 'string') f.field = foundry.utils.getProperty(game, f.field);
|
if (typeof f.field === 'string') f.field = foundry.utils.getProperty(game, f.field);
|
||||||
else if (typeof f.choices === 'function') {
|
else if (typeof f.choices === 'function') {
|
||||||
f.choices = f.choices();
|
f.choices = f.choices(this.items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear field label so template uses our custom label parameter
|
||||||
|
if (f.field && f.label) {
|
||||||
|
f.field.label = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
f.name ??= f.key;
|
f.name ??= f.key;
|
||||||
f.value = this.presets?.filter?.[f.name]?.value ?? null;
|
f.value = this.presets?.filter?.[f.name]?.value ?? null;
|
||||||
});
|
});
|
||||||
|
|
@ -248,11 +311,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create and initialize search filter instances for the inventory and loadout sections.
|
* Create and initialize search filter instance.
|
||||||
*
|
*
|
||||||
* Sets up two {@link foundry.applications.ux.SearchFilter} instances:
|
|
||||||
* - One for the inventory, which filters items in the inventory grid.
|
|
||||||
* - One for the loadout, which filters items in the loadout/card grid.
|
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_createSearchFilter() {
|
_createSearchFilter() {
|
||||||
|
|
@ -317,6 +377,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
for (const li of html.querySelectorAll('.item-container')) {
|
for (const li of html.querySelectorAll('.item-container')) {
|
||||||
const itemUUID = li.dataset.itemUuid,
|
const itemUUID = li.dataset.itemUuid,
|
||||||
item = this.items.find(i => i.uuid === itemUUID);
|
item = this.items.find(i => i.uuid === itemUUID);
|
||||||
|
if (!item) continue;
|
||||||
const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name);
|
const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name);
|
||||||
if (matchesSearch) this.#filteredItems.browser.search.add(item.id);
|
if (matchesSearch) this.#filteredItems.browser.search.add(item.id);
|
||||||
const { input } = this.#filteredItems.browser;
|
const { input } = this.#filteredItems.browser;
|
||||||
|
|
@ -408,11 +469,13 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
|
|
||||||
const newOrder = [...itemList].reverse().sort((a, b) => {
|
const newOrder = [...itemList].reverse().sort((a, b) => {
|
||||||
const aProp = a.querySelector(`[data-item-key="${key}"]`),
|
const aProp = a.querySelector(`[data-item-key="${key}"]`),
|
||||||
bProp = b.querySelector(`[data-item-key="${key}"]`);
|
bProp = b.querySelector(`[data-item-key="${key}"]`),
|
||||||
|
aValue = isNaN(aProp.innerText) ? aProp.innerText : Number(aProp.innerText),
|
||||||
|
bValue = isNaN(bProp.innerText) ? bProp.innerText : Number(bProp.innerText);
|
||||||
if (type === 'DESC') {
|
if (type === 'DESC') {
|
||||||
return aProp.innerText < bProp.innerText ? 1 : -1;
|
return aValue < bValue ? 1 : -1;
|
||||||
} else {
|
} else {
|
||||||
return aProp.innerText > bProp.innerText ? 1 : -1;
|
return aValue > bValue ? 1 : -1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -441,4 +504,41 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
_canDragStart() {
|
_canDragStart() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static injectSidebarButton(html) {
|
||||||
|
if (!game.user.isGM) return;
|
||||||
|
const sectionId = html.dataset.tab,
|
||||||
|
menus = {
|
||||||
|
actors: {
|
||||||
|
folder: 'adversaries',
|
||||||
|
render: {
|
||||||
|
folders: ['adversaries', 'characters', 'environments']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
folder: 'equipments',
|
||||||
|
render: {
|
||||||
|
noFolder: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
compendium: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Object.keys(menus).includes(sectionId)) {
|
||||||
|
const headerActions = html.querySelector('.header-actions');
|
||||||
|
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.type = 'button';
|
||||||
|
button.classList.add('open-compendium-browser');
|
||||||
|
button.innerHTML = `
|
||||||
|
<i class="fa-solid fa-book-atlas"></i>
|
||||||
|
${game.i18n.localize('DAGGERHEART.UI.Tooltip.compendiumBrowser')}
|
||||||
|
`;
|
||||||
|
button.addEventListener('click', event => {
|
||||||
|
ui.compendiumBrowser.open(menus[sectionId]);
|
||||||
|
});
|
||||||
|
|
||||||
|
headerActions.append(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,29 +10,41 @@ export default class DhMeasuredTemplate extends foundry.canvas.placeables.Measur
|
||||||
const splitRulerText = this.ruler.text.split(' ');
|
const splitRulerText = this.ruler.text.split(' ');
|
||||||
if (splitRulerText.length > 0) {
|
if (splitRulerText.length > 0) {
|
||||||
const rulerValue = Number(splitRulerText[0]);
|
const rulerValue = Number(splitRulerText[0]);
|
||||||
const vagueLabel = this.constructor.getDistanceLabel(rulerValue, rangeMeasurementSettings);
|
const result = DhMeasuredTemplate.getRangeLabels(rulerValue, rangeMeasurementSettings);
|
||||||
this.ruler.text = vagueLabel;
|
this.ruler.text = result.distance + (result.units ? ' ' + result.units : '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDistanceLabel(distance, settings) {
|
static getRangeLabels(distanceValue, settings) {
|
||||||
if (distance <= settings.melee) {
|
let result = { distance: distanceValue, units: '' };
|
||||||
return game.i18n.localize('DAGGERHEART.CONFIG.Range.melee.name');
|
const rangeMeasurementOverride = canvas.scene.flags.daggerheart?.rangeMeasurementOverride;
|
||||||
|
|
||||||
|
if (rangeMeasurementOverride === true) {
|
||||||
|
result.distance = distanceValue;
|
||||||
|
result.units = canvas.scene?.grid?.units;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
if (distance <= settings.veryClose) {
|
if (distanceValue <= settings.melee) {
|
||||||
return game.i18n.localize('DAGGERHEART.CONFIG.Range.veryClose.name');
|
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.melee.name');
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
if (distance <= settings.close) {
|
if (distanceValue <= settings.veryClose) {
|
||||||
return game.i18n.localize('DAGGERHEART.CONFIG.Range.close.name');
|
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.veryClose.name');
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
if (distance <= settings.far) {
|
if (distanceValue <= settings.close) {
|
||||||
return game.i18n.localize('DAGGERHEART.CONFIG.Range.far.name');
|
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.close.name');
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
if (distance > settings.far) {
|
if (distanceValue <= settings.far) {
|
||||||
return game.i18n.localize('DAGGERHEART.CONFIG.Range.veryFar.name');
|
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.far.name');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (distanceValue > settings.far) {
|
||||||
|
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.veryFar.name');
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ export default class DhpRuler extends foundry.canvas.interaction.Ruler {
|
||||||
const range = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement;
|
const range = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement;
|
||||||
|
|
||||||
if (range.enabled) {
|
if (range.enabled) {
|
||||||
const distance = DhMeasuredTemplate.getDistanceLabel(waypoint.measurement.distance.toNearest(0.01), range);
|
const result = DhMeasuredTemplate.getRangeLabels(waypoint.measurement.distance.toNearest(0.01), range);
|
||||||
context.cost = { total: distance, units: null };
|
context.cost = { total: result.distance, units: result.units };
|
||||||
context.distance = { total: distance, units: null };
|
context.distance = { total: result.distance, units: result.units };
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ export default class DhpTokenRuler extends foundry.canvas.placeables.tokens.Toke
|
||||||
const range = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement;
|
const range = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement;
|
||||||
|
|
||||||
if (range.enabled) {
|
if (range.enabled) {
|
||||||
const distance = DhMeasuredTemplate.getDistanceLabel(waypoint.measurement.distance.toNearest(0.01), range);
|
const result = DhMeasuredTemplate.getRangeLabels(waypoint.measurement.distance.toNearest(0.01), range);
|
||||||
context.cost = { total: distance, units: null };
|
context.cost = { total: result.distance, units: result.units };
|
||||||
context.distance = { total: distance, units: null };
|
context.distance = { total: result.distance, units: result.units };
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,11 @@ export const adversaryTypes = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const allAdversaryTypes = () => ({
|
||||||
|
...adversaryTypes,
|
||||||
|
...game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).adversaryTypes
|
||||||
|
});
|
||||||
|
|
||||||
export const environmentTypes = {
|
export const environmentTypes = {
|
||||||
exploration: {
|
exploration: {
|
||||||
label: 'DAGGERHEART.CONFIG.EnvironmentType.exploration.label',
|
label: 'DAGGERHEART.CONFIG.EnvironmentType.exploration.label',
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export const displayDomainCardsAsList = 'displayDomainCardsAsList';
|
export const displayDomainCardsAsCard = 'displayDomainCardsAsCard';
|
||||||
export const narrativeCountdown = {
|
export const narrativeCountdown = {
|
||||||
simple: 'countdown-narrative-simple',
|
simple: 'countdown-narrative-simple',
|
||||||
position: 'countdown-narrative-position'
|
position: 'countdown-narrative-position'
|
||||||
|
|
|
||||||
|
|
@ -90,22 +90,22 @@ export const rangeInclusion = {
|
||||||
export const otherTargetTypes = {
|
export const otherTargetTypes = {
|
||||||
friendly: {
|
friendly: {
|
||||||
id: 'friendly',
|
id: 'friendly',
|
||||||
label: 'Friendly'
|
label: 'DAGGERHEART.CONFIG.TargetTypes.friendly'
|
||||||
},
|
},
|
||||||
hostile: {
|
hostile: {
|
||||||
id: 'hostile',
|
id: 'hostile',
|
||||||
label: 'Hostile'
|
label: 'DAGGERHEART.CONFIG.TargetTypes.hostile'
|
||||||
},
|
},
|
||||||
any: {
|
any: {
|
||||||
id: 'any',
|
id: 'any',
|
||||||
label: 'Any'
|
label: 'DAGGERHEART.CONFIG.TargetTypes.any'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const targetTypes = {
|
export const targetTypes = {
|
||||||
self: {
|
self: {
|
||||||
id: 'self',
|
id: 'self',
|
||||||
label: 'Self'
|
label: 'DAGGERHEART.CONFIG.TargetTypes.self'
|
||||||
},
|
},
|
||||||
...otherTargetTypes
|
...otherTargetTypes
|
||||||
};
|
};
|
||||||
|
|
@ -561,6 +561,19 @@ export const refreshTypes = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const itemAbilityCosts = {
|
||||||
|
resource: {
|
||||||
|
id: 'resource',
|
||||||
|
label: 'DAGGERHEART.GENERAL.resource',
|
||||||
|
group: 'Global'
|
||||||
|
},
|
||||||
|
quantity: {
|
||||||
|
id: 'quantity',
|
||||||
|
label: 'DAGGERHEART.GENERAL.quantity',
|
||||||
|
group: 'Global'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const abilityCosts = {
|
export const abilityCosts = {
|
||||||
hitPoints: {
|
hitPoints: {
|
||||||
id: 'hitPoints',
|
id: 'hitPoints',
|
||||||
|
|
@ -574,19 +587,20 @@ export const abilityCosts = {
|
||||||
},
|
},
|
||||||
hope: {
|
hope: {
|
||||||
id: 'hope',
|
id: 'hope',
|
||||||
label: 'Hope',
|
label: 'DAGGERHEART.CONFIG.HealingType.hope.name',
|
||||||
group: 'TYPES.Actor.character'
|
group: 'TYPES.Actor.character'
|
||||||
},
|
},
|
||||||
armor: {
|
armor: {
|
||||||
id: 'armor',
|
id: 'armor',
|
||||||
label: 'Armor Slot',
|
label: 'DAGGERHEART.CONFIG.HealingType.armor.name',
|
||||||
group: 'TYPES.Actor.character'
|
group: 'TYPES.Actor.character'
|
||||||
},
|
},
|
||||||
fear: {
|
fear: {
|
||||||
id: 'fear',
|
id: 'fear',
|
||||||
label: 'Fear',
|
label: 'DAGGERHEART.CONFIG.HealingType.fear.name',
|
||||||
group: 'TYPES.Actor.adversary'
|
group: 'TYPES.Actor.adversary'
|
||||||
}
|
},
|
||||||
|
resource: itemAbilityCosts.resource
|
||||||
};
|
};
|
||||||
|
|
||||||
export const countdownTypes = {
|
export const countdownTypes = {
|
||||||
|
|
|
||||||
|
|
@ -2,270 +2,372 @@ export const typeConfig = {
|
||||||
adversaries: {
|
adversaries: {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
key: "system.tier",
|
key: 'system.tier',
|
||||||
label: "Tier"
|
label: 'DAGGERHEART.GENERAL.Tiers.singular'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.type",
|
key: 'system.type',
|
||||||
label: "Type"
|
label: 'DAGGERHEART.GENERAL.type'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
key: "system.tier",
|
key: 'system.tier',
|
||||||
label: "Tier",
|
label: 'DAGGERHEART.GENERAL.Tiers.singular',
|
||||||
field: 'system.api.models.actors.DhAdversary.schema.fields.tier'
|
field: 'system.api.models.actors.DhAdversary.schema.fields.tier'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.type",
|
key: 'system.type',
|
||||||
label: "Type",
|
label: 'DAGGERHEART.GENERAL.type',
|
||||||
field: 'system.api.models.actors.DhAdversary.schema.fields.type'
|
field: 'system.api.models.actors.DhAdversary.schema.fields.type'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.difficulty",
|
key: 'system.difficulty',
|
||||||
name: "difficulty.min",
|
name: 'difficulty.min',
|
||||||
label: "Difficulty (Min)",
|
label: 'DAGGERHEART.UI.ItemBrowser.difficultyMin',
|
||||||
field: 'system.api.models.actors.DhAdversary.schema.fields.difficulty',
|
field: 'system.api.models.actors.DhAdversary.schema.fields.difficulty',
|
||||||
operator: "gte"
|
operator: 'gte'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.difficulty",
|
key: 'system.difficulty',
|
||||||
name: "difficulty.max",
|
name: 'difficulty.max',
|
||||||
label: "Difficulty (Max)",
|
label: 'DAGGERHEART.UI.ItemBrowser.difficultyMax',
|
||||||
field: 'system.api.models.actors.DhAdversary.schema.fields.difficulty',
|
field: 'system.api.models.actors.DhAdversary.schema.fields.difficulty',
|
||||||
operator: "lte"
|
operator: 'lte'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.resources.hitPoints.max",
|
key: 'system.resources.hitPoints.max',
|
||||||
name: "hp.min",
|
name: 'hp.min',
|
||||||
label: "Hit Points (Min)",
|
label: 'DAGGERHEART.UI.ItemBrowser.hitPointsMin',
|
||||||
field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.hitPoints.fields.max',
|
field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.hitPoints.fields.max',
|
||||||
operator: "gte"
|
operator: 'gte'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.resources.hitPoints.max",
|
key: 'system.resources.hitPoints.max',
|
||||||
name: "hp.max",
|
name: 'hp.max',
|
||||||
label: "Hit Points (Max)",
|
label: 'DAGGERHEART.UI.ItemBrowser.hitPointsMax',
|
||||||
field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.hitPoints.fields.max',
|
field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.hitPoints.fields.max',
|
||||||
operator: "lte"
|
operator: 'lte'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.resources.stress.max",
|
key: 'system.resources.stress.max',
|
||||||
name: "stress.min",
|
name: 'stress.min',
|
||||||
label: "Stress (Min)",
|
label: 'DAGGERHEART.UI.ItemBrowser.stressMin',
|
||||||
field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.stress.fields.max',
|
field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.stress.fields.max',
|
||||||
operator: "gte"
|
operator: 'gte'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.resources.stress.max",
|
key: 'system.resources.stress.max',
|
||||||
name: "stress.max",
|
name: 'stress.max',
|
||||||
label: "Stress (Max)",
|
label: 'DAGGERHEART.UI.ItemBrowser.stressMax',
|
||||||
field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.stress.fields.max',
|
field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.stress.fields.max',
|
||||||
operator: "lte"
|
operator: 'lte'
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
items: {
|
items: {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
key: "type",
|
key: 'type',
|
||||||
label: "Type"
|
label: 'DAGGERHEART.GENERAL.type'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.secondary",
|
key: 'system.secondary',
|
||||||
label: "Subtype",
|
label: 'DAGGERHEART.UI.ItemBrowser.subtype',
|
||||||
format: (isSecondary) => isSecondary ? "secondary" : (isSecondary === false ? "primary" : '-')
|
format: isSecondary => (isSecondary ? 'secondary' : isSecondary === false ? 'primary' : '-')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.tier",
|
key: 'system.tier',
|
||||||
label: "Tier"
|
label: 'DAGGERHEART.GENERAL.Tiers.singular'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
key: "type",
|
key: 'type',
|
||||||
label: "Type",
|
label: 'DAGGERHEART.GENERAL.type',
|
||||||
choices: () => CONFIG.Item.documentClass.TYPES.filter(t => ["armor", "weapon", "consumable", "loot"].includes(t)).map(t => ({ value: t, label: t }))
|
choices: () =>
|
||||||
|
CONFIG.Item.documentClass.TYPES.filter(t =>
|
||||||
|
['armor', 'weapon', 'consumable', 'loot'].includes(t)
|
||||||
|
).map(t => ({ value: t, label: t }))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.secondary",
|
key: 'system.secondary',
|
||||||
label: "Subtype",
|
label: 'DAGGERHEART.UI.ItemBrowser.subtype',
|
||||||
choices: [
|
choices: [
|
||||||
{ value: false, label: "Primary Weapon"},
|
{ value: false, label: 'DAGGERHEART.ITEMS.Weapon.primaryWeapon' },
|
||||||
{ value: true, label: "Secondary Weapon"}
|
{ value: true, label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.tier",
|
key: 'system.tier',
|
||||||
label: "Tier",
|
label: 'DAGGERHEART.GENERAL.Tiers.singular',
|
||||||
choices: [{ value: "1", label: "1"}, { value: "2", label: "2"}, { value: "3", label: "3"}, { value: "4", label: "4"}]
|
choices: [
|
||||||
|
{ value: '1', label: '1' },
|
||||||
|
{ value: '2', label: '2' },
|
||||||
|
{ value: '3', label: '3' },
|
||||||
|
{ value: '4', label: '4' }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.burden",
|
key: 'system.burden',
|
||||||
label: "Burden",
|
label: 'DAGGERHEART.GENERAL.burden',
|
||||||
field: 'system.api.models.items.DHWeapon.schema.fields.burden'
|
field: 'system.api.models.items.DHWeapon.schema.fields.burden'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.attack.roll.trait",
|
key: 'system.attack.roll.trait',
|
||||||
label: "Trait",
|
label: 'DAGGERHEART.GENERAL.Trait.single',
|
||||||
field: 'system.api.models.actions.actionsTypes.attack.schema.fields.roll.fields.trait'
|
field: 'system.api.models.actions.actionsTypes.attack.schema.fields.roll.fields.trait'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.attack.range",
|
key: 'system.attack.range',
|
||||||
label: "Range",
|
label: 'DAGGERHEART.GENERAL.range',
|
||||||
field: 'system.api.models.actions.actionsTypes.attack.schema.fields.range'
|
field: 'system.api.models.actions.actionsTypes.attack.schema.fields.range'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.baseScore",
|
key: 'system.baseScore',
|
||||||
name: "armor.min",
|
name: 'armor.min',
|
||||||
label: "Armor Score (Min)",
|
label: 'DAGGERHEART.UI.ItemBrowser.armorScoreMin',
|
||||||
field: 'system.api.models.items.DHArmor.schema.fields.baseScore',
|
field: 'system.api.models.items.DHArmor.schema.fields.baseScore',
|
||||||
operator: "gte"
|
operator: 'gte'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.baseScore",
|
key: 'system.baseScore',
|
||||||
name: "armor.max",
|
name: 'armor.max',
|
||||||
label: "Armor Score (Max)",
|
label: 'DAGGERHEART.UI.ItemBrowser.armorScoreMax',
|
||||||
field: 'system.api.models.items.DHArmor.schema.fields.baseScore',
|
field: 'system.api.models.items.DHArmor.schema.fields.baseScore',
|
||||||
operator: "lte"
|
operator: 'lte'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.itemFeatures",
|
key: 'system.itemFeatures',
|
||||||
label: "Features",
|
label: 'DAGGERHEART.GENERAL.features',
|
||||||
choices: () => [...Object.entries(CONFIG.DH.ITEM.weaponFeatures), ...Object.entries(CONFIG.DH.ITEM.armorFeatures)].map(([k,v]) => ({ value: k, label: v.label})),
|
choices: () =>
|
||||||
operator: "contains3"
|
[
|
||||||
|
...Object.entries(CONFIG.DH.ITEM.weaponFeatures),
|
||||||
|
...Object.entries(CONFIG.DH.ITEM.armorFeatures)
|
||||||
|
].map(([k, v]) => ({ value: k, label: v.label })),
|
||||||
|
operator: 'contains3'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
weapons: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: 'system.secondary',
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.subtype',
|
||||||
|
format: isSecondary => (isSecondary ? 'secondary' : isSecondary === false ? 'primary' : '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'system.tier',
|
||||||
|
label: 'DAGGERHEART.GENERAL.Tiers.singular'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: 'system.secondary',
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.subtype',
|
||||||
|
choices: [
|
||||||
|
{ value: false, label: 'DAGGERHEART.ITEMS.Weapon.primaryWeapon' },
|
||||||
|
{ value: true, label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'system.tier',
|
||||||
|
label: 'DAGGERHEART.GENERAL.Tiers.singular',
|
||||||
|
choices: [
|
||||||
|
{ value: '1', label: '1' },
|
||||||
|
{ value: '2', label: '2' },
|
||||||
|
{ value: '3', label: '3' },
|
||||||
|
{ value: '4', label: '4' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'system.burden',
|
||||||
|
label: 'DAGGERHEART.GENERAL.burden',
|
||||||
|
field: 'system.api.models.items.DHWeapon.schema.fields.burden'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'system.attack.roll.trait',
|
||||||
|
label: 'DAGGERHEART.GENERAL.Trait.single',
|
||||||
|
field: 'system.api.models.actions.actionsTypes.attack.schema.fields.roll.fields.trait'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'system.attack.range',
|
||||||
|
label: 'DAGGERHEART.GENERAL.range',
|
||||||
|
field: 'system.api.models.actions.actionsTypes.attack.schema.fields.range'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'system.itemFeatures',
|
||||||
|
label: 'DAGGERHEART.GENERAL.features',
|
||||||
|
choices: () =>
|
||||||
|
Object.entries(CONFIG.DH.ITEM.weaponFeatures).map(([k, v]) => ({ value: k, label: v.label })),
|
||||||
|
operator: 'contains3'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
armors: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: 'system.tier',
|
||||||
|
label: 'DAGGERHEART.GENERAL.Tiers.singular'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: 'system.tier',
|
||||||
|
label: 'DAGGERHEART.GENERAL.Tiers.singular',
|
||||||
|
choices: [
|
||||||
|
{ value: '1', label: '1' },
|
||||||
|
{ value: '2', label: '2' },
|
||||||
|
{ value: '3', label: '3' },
|
||||||
|
{ value: '4', label: '4' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'system.baseScore',
|
||||||
|
name: 'armor.min',
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.armorScoreMin',
|
||||||
|
field: 'system.api.models.items.DHArmor.schema.fields.baseScore',
|
||||||
|
operator: 'gte'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'system.baseScore',
|
||||||
|
name: 'armor.max',
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.armorScoreMax',
|
||||||
|
field: 'system.api.models.items.DHArmor.schema.fields.baseScore',
|
||||||
|
operator: 'lte'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'system.itemFeatures',
|
||||||
|
label: 'DAGGERHEART.GENERAL.features',
|
||||||
|
choices: () =>
|
||||||
|
Object.entries(CONFIG.DH.ITEM.armorFeatures).map(([k, v]) => ({ value: k, label: v.label })),
|
||||||
|
operator: 'contains3'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
features: {
|
features: {
|
||||||
columns: [
|
columns: [],
|
||||||
|
filters: []
|
||||||
],
|
|
||||||
filters: [
|
|
||||||
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
cards: {
|
cards: {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
key: "system.type",
|
key: 'system.type',
|
||||||
label: "Type"
|
label: 'DAGGERHEART.GENERAL.type'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.domain",
|
key: 'system.domain',
|
||||||
label: "Domain"
|
label: 'DAGGERHEART.GENERAL.Domain.single'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.level",
|
key: 'system.level',
|
||||||
label: "Level"
|
label: 'DAGGERHEART.GENERAL.level'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
key: "system.type",
|
key: 'system.type',
|
||||||
label: "Type",
|
label: 'DAGGERHEART.GENERAL.type',
|
||||||
field: 'system.api.models.items.DHDomainCard.schema.fields.type'
|
field: 'system.api.models.items.DHDomainCard.schema.fields.type'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.domain",
|
key: 'system.domain',
|
||||||
label: "Domain",
|
label: 'DAGGERHEART.GENERAL.Domain.single',
|
||||||
field: 'system.api.models.items.DHDomainCard.schema.fields.domain',
|
field: 'system.api.models.items.DHDomainCard.schema.fields.domain',
|
||||||
operator: "contains2"
|
operator: 'contains2'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.level",
|
key: 'system.level',
|
||||||
name: "level.min",
|
name: 'level.min',
|
||||||
label: "Level (Min)",
|
label: 'DAGGERHEART.UI.ItemBrowser.levelMin',
|
||||||
field: 'system.api.models.items.DHDomainCard.schema.fields.level',
|
field: 'system.api.models.items.DHDomainCard.schema.fields.level',
|
||||||
operator: "gte"
|
operator: 'gte'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.level",
|
key: 'system.level',
|
||||||
name: "level.max",
|
name: 'level.max',
|
||||||
label: "Level (Max)",
|
label: 'DAGGERHEART.UI.ItemBrowser.levelMax',
|
||||||
field: 'system.api.models.items.DHDomainCard.schema.fields.level',
|
field: 'system.api.models.items.DHDomainCard.schema.fields.level',
|
||||||
operator: "lte"
|
operator: 'lte'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.recallCost",
|
key: 'system.recallCost',
|
||||||
name: "recall.min",
|
name: 'recall.min',
|
||||||
label: "Recall Cost (Min)",
|
label: 'DAGGERHEART.UI.ItemBrowser.recallCostMin',
|
||||||
field: 'system.api.models.items.DHDomainCard.schema.fields.recallCost',
|
field: 'system.api.models.items.DHDomainCard.schema.fields.recallCost',
|
||||||
operator: "gte"
|
operator: 'gte'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.recallCost",
|
key: 'system.recallCost',
|
||||||
name: "recall.max",
|
name: 'recall.max',
|
||||||
label: "Recall Cost (Max)",
|
label: 'DAGGERHEART.UI.ItemBrowser.recallCostMax',
|
||||||
field: 'system.api.models.items.DHDomainCard.schema.fields.recallCost',
|
field: 'system.api.models.items.DHDomainCard.schema.fields.recallCost',
|
||||||
operator: "lte"
|
operator: 'lte'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
classes: {
|
classes: {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
key: "system.evasion",
|
key: 'system.evasion',
|
||||||
label: "Evasion"
|
label: 'DAGGERHEART.GENERAL.evasion'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.hitPoints",
|
key: 'system.hitPoints',
|
||||||
label: "Hit Points"
|
label: 'DAGGERHEART.GENERAL.HitPoints.plural'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.domains",
|
key: 'system.domains',
|
||||||
label: "Domains"
|
label: 'DAGGERHEART.GENERAL.Domain.plural'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
key: "system.evasion",
|
key: 'system.evasion',
|
||||||
name: "evasion.min",
|
name: 'evasion.min',
|
||||||
label: "Evasion (Min)",
|
label: 'DAGGERHEART.UI.ItemBrowser.evasionMin',
|
||||||
field: 'system.api.models.items.DHClass.schema.fields.evasion',
|
field: 'system.api.models.items.DHClass.schema.fields.evasion',
|
||||||
operator: "gte"
|
operator: 'gte'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.evasion",
|
key: 'system.evasion',
|
||||||
name: "evasion.max",
|
name: 'evasion.max',
|
||||||
label: "Evasion (Max)",
|
label: 'DAGGERHEART.UI.ItemBrowser.evasionMax',
|
||||||
field: 'system.api.models.items.DHClass.schema.fields.evasion',
|
field: 'system.api.models.items.DHClass.schema.fields.evasion',
|
||||||
operator: "lte"
|
operator: 'lte'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.hitPoints",
|
key: 'system.hitPoints',
|
||||||
name: "hp.min",
|
name: 'hp.min',
|
||||||
label: "Hit Points (Min)",
|
label: 'DAGGERHEART.UI.ItemBrowser.hitPointsMin',
|
||||||
field: 'system.api.models.items.DHClass.schema.fields.hitPoints',
|
field: 'system.api.models.items.DHClass.schema.fields.hitPoints',
|
||||||
operator: "gte"
|
operator: 'gte'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.hitPoints",
|
key: 'system.hitPoints',
|
||||||
name: "hp.max",
|
name: 'hp.max',
|
||||||
label: "Hit Points (Max)",
|
label: 'DAGGERHEART.UI.ItemBrowser.hitPointsMax',
|
||||||
field: 'system.api.models.items.DHClass.schema.fields.hitPoints',
|
field: 'system.api.models.items.DHClass.schema.fields.hitPoints',
|
||||||
operator: "lte"
|
operator: 'lte'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.domains",
|
key: 'system.domains',
|
||||||
label: "Domains",
|
label: 'DAGGERHEART.GENERAL.Domain.plural',
|
||||||
choices: () => Object.values(CONFIG.DH.DOMAIN.domains).map(d => ({ value: d.id, label: d.label})),
|
choices: () => Object.values(CONFIG.DH.DOMAIN.allDomains()).map(d => ({ value: d.id, label: d.label })),
|
||||||
operator: "contains2"
|
operator: 'contains2'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
subclasses: {
|
subclasses: {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
key: "id",
|
key: 'system.linkedClass',
|
||||||
label: "Class",
|
label: 'Class',
|
||||||
format: (id) => {
|
format: linkedClass => linkedClass.name
|
||||||
return "";
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.spellcastingTrait",
|
key: 'system.spellcastingTrait',
|
||||||
label: "Spellcasting Trait"
|
label: 'DAGGERHEART.ITEMS.Subclass.spellcastingTrait'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
filters: []
|
filters: []
|
||||||
|
|
@ -273,133 +375,196 @@ export const typeConfig = {
|
||||||
beastforms: {
|
beastforms: {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
key: "system.tier",
|
key: 'system.tier',
|
||||||
label: "Tier"
|
label: 'DAGGERHEART.GENERAL.Tiers.singular'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.mainTrait",
|
key: 'system.mainTrait',
|
||||||
label: "Main Trait"
|
label: 'DAGGERHEART.GENERAL.Trait.single'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
key: "system.tier",
|
key: 'system.linkedClass.uuid',
|
||||||
label: "Tier",
|
label: 'Class',
|
||||||
|
choices: items => {
|
||||||
|
const list = items.map(item => ({
|
||||||
|
value: item.system.linkedClass.uuid,
|
||||||
|
label: item.system.linkedClass.name
|
||||||
|
}));
|
||||||
|
return list.reduce((a, c) => {
|
||||||
|
if (!a.find(i => i.value === c.value)) a.push(c);
|
||||||
|
return a;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
beastforms: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: 'system.tier',
|
||||||
|
label: 'DAGGERHEART.GENERAL.Tiers.singular'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'system.mainTrait',
|
||||||
|
label: 'DAGGERHEART.GENERAL.Trait.single'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: 'system.tier',
|
||||||
|
label: 'DAGGERHEART.GENERAL.Tiers.singular',
|
||||||
field: 'system.api.models.items.DHBeastform.schema.fields.tier'
|
field: 'system.api.models.items.DHBeastform.schema.fields.tier'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "system.mainTrait",
|
key: 'system.mainTrait',
|
||||||
label: "Main Trait",
|
label: 'DAGGERHEART.GENERAL.Trait.single',
|
||||||
field: 'system.api.models.items.DHBeastform.schema.fields.mainTrait'
|
field: 'system.api.models.items.DHBeastform.schema.fields.mainTrait'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const compendiumConfig = {
|
export const compendiumConfig = {
|
||||||
"daggerheart": {
|
characters: {
|
||||||
id: "daggerheart",
|
id: 'characters',
|
||||||
label: "DAGGERHEART",
|
keys: ['characters'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.characters',
|
||||||
|
type: ['character']
|
||||||
|
// listType: 'characters'
|
||||||
|
},
|
||||||
|
adversaries: {
|
||||||
|
id: 'adversaries',
|
||||||
|
keys: ['adversaries'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.adversaries',
|
||||||
|
type: ['adversary'],
|
||||||
|
listType: 'adversaries'
|
||||||
|
},
|
||||||
|
ancestries: {
|
||||||
|
id: 'ancestries',
|
||||||
|
keys: ['ancestries'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.ancestries',
|
||||||
|
type: ['ancestry']
|
||||||
|
/* folders: {
|
||||||
|
features: {
|
||||||
|
id: 'features',
|
||||||
|
keys: ['ancestries'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
|
||||||
|
type: ['feature']
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
},
|
||||||
|
equipments: {
|
||||||
|
id: 'equipments',
|
||||||
|
keys: ['armors', 'weapons', 'consumables', 'loot'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.equipment',
|
||||||
|
type: ['armor', 'weapon', 'consumable', 'loot'],
|
||||||
|
listType: 'items',
|
||||||
folders: {
|
folders: {
|
||||||
"adversaries": {
|
weapons: {
|
||||||
id: "adversaries",
|
id: 'weapons',
|
||||||
keys: ["adversaries"],
|
keys: ['weapons'],
|
||||||
label: "Adversaries",
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.weapons',
|
||||||
type: ["adversary"],
|
type: ['weapon'],
|
||||||
listType: "adversaries"
|
listType: 'weapons'
|
||||||
},
|
},
|
||||||
"ancestries": {
|
armors: {
|
||||||
id: "ancestries",
|
id: 'armors',
|
||||||
keys: ["ancestries"],
|
keys: ['armors'],
|
||||||
label: "Ancestries",
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.armors',
|
||||||
type: ["ancestry"],
|
type: ['armor'],
|
||||||
folders: {
|
listType: 'armors'
|
||||||
"features": {
|
|
||||||
id: "features",
|
|
||||||
keys: ["ancestries"],
|
|
||||||
label: "Features",
|
|
||||||
type: ["feature"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"equipments": {
|
consumables: {
|
||||||
id: "equipments",
|
id: 'consumables',
|
||||||
keys: ["armors", "weapons", "consumables", "loot"],
|
keys: ['consumables'],
|
||||||
label: "Equipment",
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.consumables',
|
||||||
type: ["armor", "weapon", "consumable", "loot"],
|
type: ['consumable']
|
||||||
listType: "items"
|
|
||||||
},
|
},
|
||||||
"classes": {
|
loots: {
|
||||||
id: "classes",
|
id: 'loots',
|
||||||
keys: ["classes"],
|
keys: ['loots'],
|
||||||
label: "Classes",
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.loots',
|
||||||
type: ["class"],
|
type: ['loot']
|
||||||
folders: {
|
|
||||||
"features": {
|
|
||||||
id: "features",
|
|
||||||
keys: ["classes"],
|
|
||||||
label: "Features",
|
|
||||||
type: ["feature"]
|
|
||||||
},
|
|
||||||
"items": {
|
|
||||||
id: "items",
|
|
||||||
keys: ["classes"],
|
|
||||||
label: "Items",
|
|
||||||
type: ["armor", "weapon", "consumable", "loot"],
|
|
||||||
listType: "items"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
listType: "classes"
|
|
||||||
},
|
|
||||||
"subclasses": {
|
|
||||||
id: "subclasses",
|
|
||||||
keys: ["subclasses"],
|
|
||||||
label: "Subclasses",
|
|
||||||
type: ["subclass"],
|
|
||||||
listType: "subclasses"
|
|
||||||
},
|
|
||||||
"domains": {
|
|
||||||
id: "domains",
|
|
||||||
keys: ["domains"],
|
|
||||||
label: "Domain Cards",
|
|
||||||
type: ["domainCard"],
|
|
||||||
listType: "cards"
|
|
||||||
},
|
|
||||||
"communities": {
|
|
||||||
id: "communities",
|
|
||||||
keys: ["communities"],
|
|
||||||
label: "Communities",
|
|
||||||
type: ["community"],
|
|
||||||
folders: {
|
|
||||||
"features": {
|
|
||||||
id: "features",
|
|
||||||
keys: ["communities"],
|
|
||||||
label: "Features",
|
|
||||||
type: ["feature"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"environments": {
|
|
||||||
id: "environments",
|
|
||||||
keys: ["environments"],
|
|
||||||
label: "Environments",
|
|
||||||
type: ["environment"]
|
|
||||||
},
|
|
||||||
"beastforms": {
|
|
||||||
id: "beastforms",
|
|
||||||
keys: ["beastforms"],
|
|
||||||
label: "Beastforms",
|
|
||||||
type: ["beastform"],
|
|
||||||
listType: "beastforms",
|
|
||||||
folders: {
|
|
||||||
"features": {
|
|
||||||
id: "features",
|
|
||||||
keys: ["beastforms"],
|
|
||||||
label: "Features",
|
|
||||||
type: ["feature"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
classes: {
|
||||||
|
id: 'classes',
|
||||||
|
keys: ['classes'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.classes',
|
||||||
|
type: ['class'],
|
||||||
|
/* folders: {
|
||||||
|
features: {
|
||||||
|
id: 'features',
|
||||||
|
keys: ['classes'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
|
||||||
|
type: ['feature']
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
id: 'items',
|
||||||
|
keys: ['classes'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.items',
|
||||||
|
type: ['armor', 'weapon', 'consumable', 'loot'],
|
||||||
|
listType: 'items'
|
||||||
|
}
|
||||||
|
}, */
|
||||||
|
listType: 'classes'
|
||||||
|
},
|
||||||
|
subclasses: {
|
||||||
|
id: 'subclasses',
|
||||||
|
keys: ['subclasses'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.subclasses',
|
||||||
|
type: ['subclass'],
|
||||||
|
listType: 'subclasses'
|
||||||
|
},
|
||||||
|
domains: {
|
||||||
|
id: 'domains',
|
||||||
|
keys: ['domains'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.domainCards',
|
||||||
|
type: ['domainCard'],
|
||||||
|
listType: 'cards'
|
||||||
|
},
|
||||||
|
communities: {
|
||||||
|
id: 'communities',
|
||||||
|
keys: ['communities'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.communities',
|
||||||
|
type: ['community']
|
||||||
|
/* folders: {
|
||||||
|
features: {
|
||||||
|
id: 'features',
|
||||||
|
keys: ['communities'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
|
||||||
|
type: ['feature']
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
},
|
||||||
|
environments: {
|
||||||
|
id: 'environments',
|
||||||
|
keys: ['environments'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.environments',
|
||||||
|
type: ['environment']
|
||||||
|
},
|
||||||
|
beastforms: {
|
||||||
|
id: 'beastforms',
|
||||||
|
keys: ['beastforms'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.beastforms',
|
||||||
|
type: ['beastform'],
|
||||||
|
listType: 'beastforms'
|
||||||
|
/* folders: {
|
||||||
|
features: {
|
||||||
|
id: 'features',
|
||||||
|
keys: ['beastforms'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
|
||||||
|
type: ['feature']
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
id: 'features',
|
||||||
|
keys: ['features'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
|
||||||
|
type: ['feature']
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -452,6 +452,34 @@ export const armorFeatures = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const allArmorFeatures = () => {
|
||||||
|
const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures
|
||||||
|
.armorFeatures;
|
||||||
|
return {
|
||||||
|
...armorFeatures,
|
||||||
|
...Object.keys(homebrewFeatures).reduce((acc, key) => {
|
||||||
|
const feature = homebrewFeatures[key];
|
||||||
|
acc[key] = { ...feature, label: feature.name };
|
||||||
|
return acc;
|
||||||
|
}, {})
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const orderedArmorFeatures = () => {
|
||||||
|
const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures
|
||||||
|
.armorFeatures;
|
||||||
|
const allFeatures = { ...armorFeatures, ...homebrewFeatures };
|
||||||
|
const all = Object.keys(allFeatures).map(key => {
|
||||||
|
const feature = allFeatures[key];
|
||||||
|
return {
|
||||||
|
...feature,
|
||||||
|
id: key,
|
||||||
|
label: feature.label ?? feature.name
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return Object.values(all).sort((a, b) => game.i18n.localize(a.label).localeCompare(game.i18n.localize(b.label)));
|
||||||
|
};
|
||||||
|
|
||||||
export const weaponFeatures = {
|
export const weaponFeatures = {
|
||||||
barrier: {
|
barrier: {
|
||||||
label: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.name',
|
label: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.name',
|
||||||
|
|
@ -865,6 +893,9 @@ export const weaponFeatures = {
|
||||||
name: 'DAGGERHEART.CONFIG.WeaponFeature.greedy.name',
|
name: 'DAGGERHEART.CONFIG.WeaponFeature.greedy.name',
|
||||||
description: 'DAGGERHEART.CONFIG.WeaponFeature.greedy.description',
|
description: 'DAGGERHEART.CONFIG.WeaponFeature.greedy.description',
|
||||||
img: 'icons/commodities/currency/coins-crown-stack-gold.webp',
|
img: 'icons/commodities/currency/coins-crown-stack-gold.webp',
|
||||||
|
target: {
|
||||||
|
type: 'self'
|
||||||
|
},
|
||||||
// Should cost handful of gold,
|
// Should cost handful of gold,
|
||||||
effects: [
|
effects: [
|
||||||
{
|
{
|
||||||
|
|
@ -1380,6 +1411,34 @@ export const weaponFeatures = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const allWeaponFeatures = () => {
|
||||||
|
const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures
|
||||||
|
.weaponFeatures;
|
||||||
|
return {
|
||||||
|
...weaponFeatures,
|
||||||
|
...Object.keys(homebrewFeatures).reduce((acc, key) => {
|
||||||
|
const feature = homebrewFeatures[key];
|
||||||
|
acc[key] = { ...feature, label: feature.name };
|
||||||
|
return acc;
|
||||||
|
}, {})
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const orderedWeaponFeatures = () => {
|
||||||
|
const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures
|
||||||
|
.weaponFeatures;
|
||||||
|
const allFeatures = { ...weaponFeatures, ...homebrewFeatures };
|
||||||
|
const all = Object.keys(allFeatures).map(key => {
|
||||||
|
const feature = allFeatures[key];
|
||||||
|
return {
|
||||||
|
...feature,
|
||||||
|
id: key,
|
||||||
|
label: feature.label ?? feature.name
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return Object.values(all).sort((a, b) => game.i18n.localize(a.label).localeCompare(game.i18n.localize(b.label)));
|
||||||
|
};
|
||||||
|
|
||||||
export const featureTypes = {
|
export const featureTypes = {
|
||||||
ancestry: {
|
ancestry: {
|
||||||
id: 'ancestry',
|
id: 'ancestry',
|
||||||
|
|
|
||||||
|
|
@ -26,5 +26,25 @@ export const gameSettings = {
|
||||||
Fear: 'ResourcesFear'
|
Fear: 'ResourcesFear'
|
||||||
},
|
},
|
||||||
LevelTiers: 'LevelTiers',
|
LevelTiers: 'LevelTiers',
|
||||||
Countdowns: 'Countdowns'
|
Countdowns: 'Countdowns',
|
||||||
|
LastMigrationVersion: 'LastMigrationVersion'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actionAutomationChoices = {
|
||||||
|
never: {
|
||||||
|
id: 'never',
|
||||||
|
label: 'Never'
|
||||||
|
},
|
||||||
|
showDialog: {
|
||||||
|
id: 'showDialog',
|
||||||
|
label: 'Show Dialog only'
|
||||||
|
},
|
||||||
|
// npcOnly: {
|
||||||
|
// id: "npcOnly",
|
||||||
|
// label: "Always for non-characters"
|
||||||
|
// },
|
||||||
|
always: {
|
||||||
|
id: 'always',
|
||||||
|
label: 'Always'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -51,11 +51,13 @@ export default class DHAttackAction extends DHDamageAction {
|
||||||
const labels = [];
|
const labels = [];
|
||||||
const { roll, range, damage } = this;
|
const { roll, range, damage } = this;
|
||||||
|
|
||||||
if (roll.trait) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${roll.trait}.short`))
|
if (roll.trait) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${roll.trait}.short`));
|
||||||
if (range) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Range.${range}.short`));
|
if (range) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Range.${range}.short`));
|
||||||
|
|
||||||
for (const { value, type } of damage.parts) {
|
const useAltDamage = this.actor?.effects?.find(x => x.type === 'horde')?.active;
|
||||||
const str = Roll.replaceFormulaData(value.getFormula(), this.actor?.getRollData() ?? {});
|
for (const { value, valueAlt, type } of damage.parts) {
|
||||||
|
const usedValue = useAltDamage ? valueAlt : value;
|
||||||
|
const str = Roll.replaceFormulaData(usedValue.getFormula(), this.actor?.getRollData() ?? {});
|
||||||
|
|
||||||
const icons = Array.from(type)
|
const icons = Array.from(type)
|
||||||
.map(t => CONFIG.DH.GENERAL.damageTypes[t]?.icon)
|
.map(t => CONFIG.DH.GENERAL.damageTypes[t]?.icon)
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
import DhpActor from '../../documents/actor.mjs';
|
import DhpActor from '../../documents/actor.mjs';
|
||||||
import D20RollDialog from '../../applications/dialogs/d20RollDialog.mjs';
|
import D20RollDialog from '../../applications/dialogs/d20RollDialog.mjs';
|
||||||
import { ActionMixin } from '../fields/actionField.mjs';
|
import { ActionMixin } from '../fields/actionField.mjs';
|
||||||
import { abilities } from '../../config/actorConfig.mjs';
|
|
||||||
|
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
/*
|
|
||||||
!!! I'm currently refactoring the whole Action thing, it's a WIP !!!
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
ToDo
|
ToDo
|
||||||
- Target Check / Target Picker
|
- Target Check / Target Picker
|
||||||
|
|
@ -20,6 +15,7 @@ const fields = foundry.data.fields;
|
||||||
export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel) {
|
export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel) {
|
||||||
static extraSchemas = ['cost', 'uses', 'range'];
|
static extraSchemas = ['cost', 'uses', 'range'];
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
static defineSchema() {
|
static defineSchema() {
|
||||||
const schemaFields = {
|
const schemaFields = {
|
||||||
_id: new fields.DocumentIdField({ initial: () => foundry.utils.randomID() }),
|
_id: new fields.DocumentIdField({ initial: () => foundry.utils.randomID() }),
|
||||||
|
|
@ -32,36 +28,82 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
actionType: new fields.StringField({
|
actionType: new fields.StringField({
|
||||||
choices: CONFIG.DH.ITEM.actionTypes,
|
choices: CONFIG.DH.ITEM.actionTypes,
|
||||||
initial: 'action',
|
initial: 'action',
|
||||||
nullable: true
|
nullable: false,
|
||||||
|
required: true
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
this.extraSchemas.forEach(s => {
|
this.extraSchemas.forEach(s => {
|
||||||
let clsField;
|
let clsField = this.getActionField(s);
|
||||||
if ((clsField = this.getActionField(s))) schemaFields[s] = new clsField();
|
if (clsField) schemaFields[s] = new clsField();
|
||||||
});
|
});
|
||||||
|
|
||||||
return schemaFields;
|
return schemaFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Map containing each Action step based on fields define in schema. Ordered by Fields order property.
|
||||||
|
*
|
||||||
|
* Each step can be called individually as long as needed config is provided.
|
||||||
|
* Ex: <action>.workflow.get("damage").execute(config)
|
||||||
|
* @returns {Map}
|
||||||
|
*/
|
||||||
|
defineWorkflow() {
|
||||||
|
const workflow = new Map();
|
||||||
|
this.constructor.extraSchemas.forEach(s => {
|
||||||
|
let clsField = this.constructor.getActionField(s);
|
||||||
|
if (clsField?.execute) {
|
||||||
|
workflow.set(s, { order: clsField.order, execute: clsField.execute.bind(this) });
|
||||||
|
if (s === 'damage')
|
||||||
|
workflow.set('applyDamage', { order: 75, execute: clsField.applyDamage.bind(this) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return new Map([...workflow.entries()].sort(([aKey, aValue], [bKey, bValue]) => aValue.order - bValue.order));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getter returning the workflow property or creating it the first time the property is called
|
||||||
|
*/
|
||||||
|
get workflow() {
|
||||||
|
if (this.hasOwnProperty('_workflow')) return this._workflow;
|
||||||
|
const workflow = Object.freeze(this.defineWorkflow());
|
||||||
|
Object.defineProperty(this, '_workflow', { value: workflow, writable: false });
|
||||||
|
return workflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Field class from ActionFields global config
|
||||||
|
* @param {string} name Field short name, equal to Action property
|
||||||
|
* @returns Action Field
|
||||||
|
*/
|
||||||
static getActionField(name) {
|
static getActionField(name) {
|
||||||
const field = game.system.api.fields.ActionFields[`${name.capitalize()}Field`];
|
const field = game.system.api.fields.ActionFields[`${name.capitalize()}Field`];
|
||||||
return fields.DataField.isPrototypeOf(field) && field;
|
return fields.DataField.isPrototypeOf(field) && field;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
prepareData() {
|
prepareData() {
|
||||||
this.name = this.name || game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[this.type].name);
|
this.name = this.name || game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[this.type].name);
|
||||||
this.img = this.img ?? this.parent?.parent?.img;
|
this.img = this.img ?? this.parent?.parent?.img;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Action ID
|
||||||
|
*/
|
||||||
get id() {
|
get id() {
|
||||||
return this._id;
|
return this._id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return Item the action is attached too.
|
||||||
|
*/
|
||||||
get item() {
|
get item() {
|
||||||
return this.parent.parent;
|
return this.parent.parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the first Actor parent found.
|
||||||
|
*/
|
||||||
get actor() {
|
get actor() {
|
||||||
return this.item instanceof DhpActor
|
return this.item instanceof DhpActor
|
||||||
? this.item
|
? this.item
|
||||||
|
|
@ -74,6 +116,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
return 'trait';
|
return 'trait';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare base data based on Action Type & Parent Type
|
||||||
|
* @param {object} parent
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
static getSourceConfig(parent) {
|
static getSourceConfig(parent) {
|
||||||
const updateSource = {};
|
const updateSource = {};
|
||||||
if (parent?.parent?.type === 'weapon' && this === game.system.api.models.actions.actionsTypes.attack) {
|
if (parent?.parent?.type === 'weapon' && this === game.system.api.models.actions.actionsTypes.attack) {
|
||||||
|
|
@ -96,6 +143,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
return updateSource;
|
return updateSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtain a data object used to evaluate any dice rolls associated with this particular Action
|
||||||
|
* @param {object} [data ={}] Optional data object from previous configuration/rolls
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
getRollData(data = {}) {
|
getRollData(data = {}) {
|
||||||
if (!this.actor) return null;
|
if (!this.actor) return null;
|
||||||
const actorData = this.actor.getRollData(false);
|
const actorData = this.actor.getRollData(false);
|
||||||
|
|
@ -111,19 +163,30 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
return actorData;
|
return actorData;
|
||||||
}
|
}
|
||||||
|
|
||||||
async use(event, options = {}) {
|
/**
|
||||||
|
* Execute each part of the Action workflow in order, calling a specific event before and after each part.
|
||||||
|
* @param {object} config Config object usually created from prepareConfig method
|
||||||
|
*/
|
||||||
|
async executeWorkflow(config) {
|
||||||
|
for (const [key, part] of this.workflow) {
|
||||||
|
if (Hooks.call(`${CONFIG.DH.id}.pre${key.capitalize()}Action`, this, config) === false) return;
|
||||||
|
if ((await part.execute(config)) === false) return;
|
||||||
|
if (Hooks.call(`${CONFIG.DH.id}.post${key.capitalize()}Action`, this, config) === false) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main method to use the Action
|
||||||
|
* @param {Event} event Event from the button used to trigger the Action
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
async use(event) {
|
||||||
if (!this.actor) throw new Error("An Action can't be used outside of an Actor context.");
|
if (!this.actor) throw new Error("An Action can't be used outside of an Actor context.");
|
||||||
|
|
||||||
if (this.chatDisplay) await this.toChat();
|
if (this.chatDisplay) await this.toChat();
|
||||||
let { byPassRoll } = options,
|
|
||||||
config = this.prepareConfig(event, byPassRoll);
|
let config = this.prepareConfig(event);
|
||||||
for (let i = 0; i < this.constructor.extraSchemas.length; i++) {
|
if (!config) return;
|
||||||
let clsField = this.constructor.getActionField(this.constructor.extraSchemas[i]);
|
|
||||||
if (clsField?.prepareConfig) {
|
|
||||||
const keep = clsField.prepareConfig.call(this, config);
|
|
||||||
if (config.isFastForward && !keep) return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return;
|
if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return;
|
||||||
|
|
||||||
|
|
@ -133,274 +196,116 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
if (!config) return;
|
if (!config) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.hasRoll) {
|
// Execute the Action Worflow in order based of schema fields
|
||||||
const rollConfig = this.prepareRoll(config);
|
await this.executeWorkflow(config);
|
||||||
config.roll = rollConfig;
|
|
||||||
config = await this.actor.diceRoll(config);
|
|
||||||
if (!config) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.doFollowUp(config)) {
|
|
||||||
if (this.rollDamage && this.damage.parts.length) await this.rollDamage(event, config);
|
|
||||||
else if (this.trigger) await this.trigger(event, config);
|
|
||||||
else if (this.hasSave || this.hasEffect) {
|
|
||||||
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
|
|
||||||
roll._evaluated = true;
|
|
||||||
await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Consume resources
|
|
||||||
await this.consume(config);
|
|
||||||
|
|
||||||
if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return;
|
if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return;
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* */
|
/**
|
||||||
prepareConfig(event, byPass = false) {
|
* Create the basic config common to every action type
|
||||||
const hasRoll = this.getUseHasRoll(byPass);
|
* @param {Event} event Event from the button used to trigger the Action
|
||||||
return {
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
prepareBaseConfig(event) {
|
||||||
|
const config = {
|
||||||
event,
|
event,
|
||||||
title: `${this.item.name}: ${game.i18n.localize(this.name)}`,
|
title: `${this.item instanceof CONFIG.Actor.documentClass ? '' : `${this.item.name}: `}${game.i18n.localize(this.name)}`,
|
||||||
source: {
|
source: {
|
||||||
item: this.item._id,
|
item: this.item._id,
|
||||||
action: this._id,
|
action: this._id,
|
||||||
actor: this.actor.uuid
|
actor: this.actor.uuid
|
||||||
},
|
},
|
||||||
dialog: {
|
dialog: {},
|
||||||
configure: hasRoll
|
actionType: this.actionType,
|
||||||
},
|
hasRoll: this.hasRoll,
|
||||||
type: this.type,
|
hasDamage: this.hasDamage,
|
||||||
hasRoll: hasRoll,
|
hasHealing: this.hasHealing,
|
||||||
hasDamage: this.damage?.parts?.length && this.type !== 'healing',
|
hasEffect: this.hasEffect,
|
||||||
hasHealing: this.damage?.parts?.length && this.type === 'healing',
|
|
||||||
hasEffect: !!this.effects?.length,
|
|
||||||
isDirect: !!this.damage?.direct,
|
|
||||||
hasSave: this.hasSave,
|
hasSave: this.hasSave,
|
||||||
|
isDirect: !!this.damage?.direct,
|
||||||
selectedRollMode: game.settings.get('core', 'rollMode'),
|
selectedRollMode: game.settings.get('core', 'rollMode'),
|
||||||
isFastForward: event.shiftKey,
|
|
||||||
data: this.getRollData(),
|
data: this.getRollData(),
|
||||||
evaluate: hasRoll
|
evaluate: this.hasRoll
|
||||||
};
|
};
|
||||||
|
DHBaseAction.applyKeybindings(config);
|
||||||
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the config for that action used for its workflow
|
||||||
|
* @param {Event} event Event from the button used to trigger the Action
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
prepareConfig(event) {
|
||||||
|
const config = this.prepareBaseConfig(event);
|
||||||
|
for (const clsField of Object.values(this.schema.fields)) {
|
||||||
|
if (clsField?.prepareConfig) if (clsField.prepareConfig.call(this, config) === false) return false;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method used to know if a configuration dialog must be shown or not when there is no roll.
|
||||||
|
* @param {*} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
requireConfigurationDialog(config) {
|
requireConfigurationDialog(config) {
|
||||||
return !config.event.shiftKey && !config.hasRoll && (config.costs?.length || config.uses);
|
return !config.event.shiftKey && !config.hasRoll && (config.costs?.length || config.uses);
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareRoll() {
|
/**
|
||||||
const roll = {
|
* Consume Action configured resources & uses.
|
||||||
baseModifiers: this.roll.getModifier(),
|
* That method is only used when we want those resources to be consumed outside of the use method workflow.
|
||||||
label: 'Attack',
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
type: this.actionType,
|
* @param {boolean} successCost
|
||||||
difficulty: this.roll?.difficulty,
|
*/
|
||||||
formula: this.roll.getFormula(),
|
|
||||||
advantage: CONFIG.DH.ACTIONS.advantageState[this.roll.advState].value
|
|
||||||
};
|
|
||||||
if (this.roll?.type === 'diceSet' || !this.hasRoll) roll.lite = true;
|
|
||||||
|
|
||||||
return roll;
|
|
||||||
}
|
|
||||||
|
|
||||||
doFollowUp(config) {
|
|
||||||
return !config.hasRoll;
|
|
||||||
}
|
|
||||||
|
|
||||||
async consume(config, successCost = false) {
|
async consume(config, successCost = false) {
|
||||||
const actor = this.actor.system.partner ?? this.actor,
|
await this.workflow.get('cost')?.execute(config, successCost);
|
||||||
usefulResources = {
|
await this.workflow.get('uses')?.execute(config, successCost);
|
||||||
...foundry.utils.deepClone(actor.system.resources),
|
|
||||||
fear: {
|
|
||||||
value: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
|
|
||||||
max: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear,
|
|
||||||
reversed: false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (var cost of config.costs) {
|
if (config.roll && !config.roll.success && successCost) {
|
||||||
if (cost.keyIsID) {
|
|
||||||
usefulResources[cost.key] = {
|
|
||||||
value: cost.value,
|
|
||||||
target: this.parent.parent,
|
|
||||||
keyIsID: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resources = game.system.api.fields.ActionFields.CostField.getRealCosts(config.costs)
|
|
||||||
.filter(
|
|
||||||
c =>
|
|
||||||
(!successCost && (!c.consumeOnSuccess || config.roll?.success)) ||
|
|
||||||
(successCost && c.consumeOnSuccess)
|
|
||||||
)
|
|
||||||
.reduce((a, c) => {
|
|
||||||
const resource = usefulResources[c.key];
|
|
||||||
if (resource) {
|
|
||||||
a.push({
|
|
||||||
key: c.key,
|
|
||||||
value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1),
|
|
||||||
target: resource.target,
|
|
||||||
keyIsID: resource.keyIsID
|
|
||||||
});
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
await actor.modifyResource(resources);
|
|
||||||
if (
|
|
||||||
config.uses?.enabled &&
|
|
||||||
((!successCost && (!config.uses?.consumeOnSuccess || config.roll?.success)) ||
|
|
||||||
(successCost && config.uses?.consumeOnSuccess))
|
|
||||||
)
|
|
||||||
this.update({ 'uses.value': this.uses.value + 1 });
|
|
||||||
|
|
||||||
if (config.roll?.success || successCost) {
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
(config.message ?? config.parent).update({ 'system.successConsumed': true });
|
(config.message ?? config.parent).update({ 'system.successConsumed': true });
|
||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* */
|
|
||||||
|
|
||||||
/* ROLL */
|
/**
|
||||||
getUseHasRoll(byPass = false) {
|
* Set if a configuration dialog must be shown or not if a special keyboard key is pressed.
|
||||||
return this.hasRoll && !byPass;
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
*/
|
||||||
|
static applyKeybindings(config) {
|
||||||
|
config.dialog.configure ??= !(config.event.shiftKey || config.event.altKey || config.event.ctrlKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getters to know which parts the action is composed of. A field can exist but configured to not be used.
|
||||||
|
* @returns {boolean} If that part is in the action.
|
||||||
|
*/
|
||||||
|
|
||||||
get hasRoll() {
|
get hasRoll() {
|
||||||
return !!this.roll?.type;
|
return !!this.roll?.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
get modifiers() {
|
get hasDamage() {
|
||||||
if (!this.actor) return [];
|
return this.damage?.parts?.length && this.type !== 'healing';
|
||||||
const modifiers = [];
|
}
|
||||||
/** Placeholder for specific bonuses **/
|
|
||||||
return modifiers;
|
get hasHealing() {
|
||||||
|
return this.damage?.parts?.length && this.type === 'healing';
|
||||||
}
|
}
|
||||||
/* ROLL */
|
|
||||||
|
|
||||||
/* SAVE */
|
|
||||||
get hasSave() {
|
get hasSave() {
|
||||||
return !!this.save?.trait;
|
return !!this.save?.trait;
|
||||||
}
|
}
|
||||||
/* SAVE */
|
|
||||||
|
|
||||||
/* EFFECTS */
|
|
||||||
get hasEffect() {
|
get hasEffect() {
|
||||||
return this.effects?.length > 0;
|
return this.effects?.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async applyEffects(event, data, targets) {
|
|
||||||
targets ??= data.system.targets;
|
|
||||||
const force = true; /* Where should this come from? */
|
|
||||||
if (!this.effects?.length || !targets.length) return;
|
|
||||||
let effects = this.effects;
|
|
||||||
targets.forEach(async token => {
|
|
||||||
if (!token.hit && !force) return;
|
|
||||||
if (this.hasSave && token.saved.success === true) {
|
|
||||||
effects = this.effects.filter(e => e.onSave === true);
|
|
||||||
}
|
|
||||||
if (!effects.length) return;
|
|
||||||
effects.forEach(async e => {
|
|
||||||
const actor = canvas.tokens.get(token.id)?.actor,
|
|
||||||
effect = this.item.effects.get(e._id);
|
|
||||||
if (!actor || !effect) return;
|
|
||||||
await this.applyEffect(effect, actor);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async applyEffect(effect, actor) {
|
|
||||||
const existingEffect = actor.effects.find(e => e.origin === effect.uuid);
|
|
||||||
if (existingEffect) {
|
|
||||||
return effect.update(
|
|
||||||
foundry.utils.mergeObject({
|
|
||||||
...effect.constructor.getInitialDuration(),
|
|
||||||
disabled: false
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, create a new effect on the target
|
|
||||||
const effectData = foundry.utils.mergeObject({
|
|
||||||
...effect.toObject(),
|
|
||||||
disabled: false,
|
|
||||||
transfer: false,
|
|
||||||
origin: effect.uuid
|
|
||||||
});
|
|
||||||
await ActiveEffect.implementation.create(effectData, { parent: actor });
|
|
||||||
}
|
|
||||||
/* EFFECTS */
|
|
||||||
|
|
||||||
/* SAVE */
|
|
||||||
async rollSave(actor, event, message) {
|
|
||||||
if (!actor) return;
|
|
||||||
const title = actor.isNPC
|
|
||||||
? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll')
|
|
||||||
: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
|
|
||||||
ability: game.i18n.localize(abilities[this.save.trait]?.label)
|
|
||||||
});
|
|
||||||
return actor.diceRoll({
|
|
||||||
event,
|
|
||||||
title,
|
|
||||||
roll: {
|
|
||||||
trait: this.save.trait,
|
|
||||||
difficulty: this.save.difficulty ?? this.actor?.baseSaveDifficulty,
|
|
||||||
type: 'reaction'
|
|
||||||
},
|
|
||||||
type: 'trait',
|
|
||||||
hasRoll: true,
|
|
||||||
data: actor.getRollData()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSaveMessage(result, message, targetId) {
|
|
||||||
if (!result) return;
|
|
||||||
const updateMsg = this.updateChatMessage.bind(this, message, targetId, {
|
|
||||||
result: result.roll.total,
|
|
||||||
success: result.roll.success
|
|
||||||
});
|
|
||||||
if (game.modules.get('dice-so-nice')?.active)
|
|
||||||
game.dice3d.waitFor3DAnimationByMessageID(result.message.id ?? result.message._id).then(() => updateMsg());
|
|
||||||
else updateMsg();
|
|
||||||
}
|
|
||||||
|
|
||||||
static rollSaveQuery({ actionId, actorId, event, message }) {
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
const actor = await fromUuid(actorId),
|
|
||||||
action = await fromUuid(actionId);
|
|
||||||
if (!actor || !actor?.isOwner) reject();
|
|
||||||
action.rollSave(actor, event, message).then(result => resolve(result));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
/* SAVE */
|
|
||||||
|
|
||||||
async updateChatMessage(message, targetId, changes, chain = true) {
|
|
||||||
setTimeout(async () => {
|
|
||||||
const chatMessage = ui.chat.collection.get(message._id);
|
|
||||||
|
|
||||||
await chatMessage.update({
|
|
||||||
flags: {
|
|
||||||
[game.system.id]: {
|
|
||||||
reactionRolls: {
|
|
||||||
[targetId]: changes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 100);
|
|
||||||
if (chain) {
|
|
||||||
if (message.system.source.message)
|
|
||||||
this.updateChatMessage(ui.chat.collection.get(message.system.source.message), targetId, changes, false);
|
|
||||||
const relatedChatMessages = ui.chat.collection.filter(c => c.system.source?.message === message._id);
|
|
||||||
relatedChatMessages.forEach(c => {
|
|
||||||
this.updateChatMessage(c, targetId, changes, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a list of localized tags for this action.
|
* Generates a list of localized tags for this action.
|
||||||
* @returns {string[]} An array of localized tag strings.
|
* @returns {string[]} An array of localized tag strings.
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import BeastformDialog from '../../applications/dialogs/beastformDialog.mjs';
|
|
||||||
import DHBaseAction from './baseAction.mjs';
|
import DHBaseAction from './baseAction.mjs';
|
||||||
|
|
||||||
export default class DhBeastformAction extends DHBaseAction {
|
export default class DhBeastformAction extends DHBaseAction {
|
||||||
static extraSchemas = [...super.extraSchemas, 'beastform'];
|
static extraSchemas = [...super.extraSchemas, 'beastform'];
|
||||||
|
|
||||||
async use(event, options) {
|
/* async use(event, options) {
|
||||||
const beastformConfig = this.prepareBeastformConfig();
|
const beastformConfig = this.prepareBeastformConfig();
|
||||||
|
|
||||||
const abort = await this.handleActiveTransformations();
|
const abort = await this.handleActiveTransformations();
|
||||||
|
|
@ -82,5 +81,5 @@ export default class DhBeastformAction extends DHBaseAction {
|
||||||
beastformEffects.map(x => x.id)
|
beastformEffects.map(x => x.id)
|
||||||
);
|
);
|
||||||
return existingEffects;
|
return existingEffects;
|
||||||
}
|
} */
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,5 @@
|
||||||
import { setsEqual } from '../../helpers/utils.mjs';
|
|
||||||
import DHBaseAction from './baseAction.mjs';
|
import DHBaseAction from './baseAction.mjs';
|
||||||
|
|
||||||
export default class DHDamageAction extends DHBaseAction {
|
export default class DHDamageAction extends DHBaseAction {
|
||||||
static extraSchemas = [...super.extraSchemas, 'damage', 'target', 'effects'];
|
static extraSchemas = [...super.extraSchemas, 'damage', 'target', 'effects'];
|
||||||
|
|
||||||
getFormulaValue(part, data) {
|
|
||||||
let formulaValue = part.value;
|
|
||||||
|
|
||||||
if (data.hasRoll && part.resultBased && data.roll.result.duality === -1) return part.valueAlt;
|
|
||||||
|
|
||||||
const isAdversary = this.actor.type === 'adversary';
|
|
||||||
if (isAdversary && this.actor.system.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id) {
|
|
||||||
const hasHordeDamage = this.actor.effects.find(x => x.type === 'horde');
|
|
||||||
if (hasHordeDamage && !hasHordeDamage.disabled) return part.valueAlt;
|
|
||||||
}
|
|
||||||
|
|
||||||
return formulaValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
formatFormulas(formulas, systemData) {
|
|
||||||
const formattedFormulas = [];
|
|
||||||
formulas.forEach(formula => {
|
|
||||||
if (isNaN(formula.formula))
|
|
||||||
formula.formula = Roll.replaceFormulaData(formula.formula, this.getRollData(systemData));
|
|
||||||
const same = formattedFormulas.find(
|
|
||||||
f => setsEqual(f.damageTypes, formula.damageTypes) && f.applyTo === formula.applyTo
|
|
||||||
);
|
|
||||||
if (same) same.formula += ` + ${formula.formula}`;
|
|
||||||
else formattedFormulas.push(formula);
|
|
||||||
});
|
|
||||||
return formattedFormulas;
|
|
||||||
}
|
|
||||||
|
|
||||||
async rollDamage(event, data) {
|
|
||||||
const systemData = data.system ?? data;
|
|
||||||
|
|
||||||
let formulas = this.damage.parts.map(p => ({
|
|
||||||
formula: this.getFormulaValue(p, systemData).getFormula(this.actor),
|
|
||||||
damageTypes: p.applyTo === 'hitPoints' && !p.type.size ? new Set(['physical']) : p.type,
|
|
||||||
applyTo: p.applyTo
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!formulas.length) return;
|
|
||||||
|
|
||||||
formulas = this.formatFormulas(formulas, systemData);
|
|
||||||
|
|
||||||
delete systemData.evaluate;
|
|
||||||
const config = {
|
|
||||||
...systemData,
|
|
||||||
roll: formulas,
|
|
||||||
dialog: {},
|
|
||||||
data: this.getRollData()
|
|
||||||
};
|
|
||||||
if (this.hasSave) config.onSave = this.save.damageMod;
|
|
||||||
if (data.system) {
|
|
||||||
config.source.message = data._id;
|
|
||||||
config.directDamage = false;
|
|
||||||
} else {
|
|
||||||
config.directDamage = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return CONFIG.Dice.daggerheart.DamageRoll.build(config);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,4 @@ import DHBaseAction from './baseAction.mjs';
|
||||||
|
|
||||||
export default class DHMacroAction extends DHBaseAction {
|
export default class DHMacroAction extends DHBaseAction {
|
||||||
static extraSchemas = [...super.extraSchemas, 'macro'];
|
static extraSchemas = [...super.extraSchemas, 'macro'];
|
||||||
|
|
||||||
async trigger(event, ...args) {
|
|
||||||
const fixUUID = !this.macro.includes('Macro.') ? `Macro.${this.macro}` : this.macro,
|
|
||||||
macro = await fromUuid(fixUUID);
|
|
||||||
try {
|
|
||||||
if (!macro) throw new Error(`No macro found for the UUID: ${this.macro}.`);
|
|
||||||
macro.execute();
|
|
||||||
} catch (error) {
|
|
||||||
ui.notifications.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,4 +30,24 @@ export default class BaseEffect extends foundry.abstract.TypeDataModel {
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getDefaultObject() {
|
||||||
|
return {
|
||||||
|
name: 'New Effect',
|
||||||
|
id: foundry.utils.randomID(),
|
||||||
|
disabled: false,
|
||||||
|
img: 'icons/magic/life/heart-cross-blue.webp',
|
||||||
|
description: '',
|
||||||
|
statuses: [],
|
||||||
|
changes: [],
|
||||||
|
system: {
|
||||||
|
rangeDependence: {
|
||||||
|
enabled: false,
|
||||||
|
type: CONFIG.DH.GENERAL.rangeInclusion.withinRange.id,
|
||||||
|
target: CONFIG.DH.GENERAL.otherTargetTypes.hostile.id,
|
||||||
|
range: CONFIG.DH.GENERAL.range.melee.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export default class DhpAdversary extends BaseDataActor {
|
||||||
}),
|
}),
|
||||||
type: new fields.StringField({
|
type: new fields.StringField({
|
||||||
required: true,
|
required: true,
|
||||||
choices: CONFIG.DH.ACTOR.adversaryTypes,
|
choices: CONFIG.DH.ACTOR.allAdversaryTypes,
|
||||||
initial: CONFIG.DH.ACTOR.adversaryTypes.standard.id
|
initial: CONFIG.DH.ACTOR.adversaryTypes.standard.id
|
||||||
}),
|
}),
|
||||||
motivesAndTactics: new fields.StringField(),
|
motivesAndTactics: new fields.StringField(),
|
||||||
|
|
@ -130,7 +130,7 @@ export default class DhpAdversary extends BaseDataActor {
|
||||||
CONFIG.DH.id,
|
CONFIG.DH.id,
|
||||||
CONFIG.DH.SETTINGS.gameSettings.Automation
|
CONFIG.DH.SETTINGS.gameSettings.Automation
|
||||||
).hordeDamage;
|
).hordeDamage;
|
||||||
if (autoHordeDamage && changes.system?.resources?.hitPoints?.value) {
|
if (autoHordeDamage && changes.system?.resources?.hitPoints?.value !== undefined) {
|
||||||
const hordeActiveEffect = this.parent.effects.find(x => x.type === 'horde');
|
const hordeActiveEffect = this.parent.effects.find(x => x.type === 'horde');
|
||||||
if (hordeActiveEffect) {
|
if (hordeActiveEffect) {
|
||||||
const halfHP = Math.ceil(this.resources.hitPoints.max / 2);
|
const halfHP = Math.ceil(this.resources.hitPoints.max / 2);
|
||||||
|
|
|
||||||
|
|
@ -130,11 +130,16 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
|
||||||
const typeForDefeated = ['character', 'adversary', 'companion'].find(x => x === this.parent.type);
|
const typeForDefeated = ['character', 'adversary', 'companion'].find(x => x === this.parent.type);
|
||||||
if (defeatedSettings.enabled && typeForDefeated) {
|
if (defeatedSettings.enabled && typeForDefeated) {
|
||||||
const resource = typeForDefeated === 'companion' ? 'stress' : 'hitPoints';
|
const resource = typeForDefeated === 'companion' ? 'stress' : 'hitPoints';
|
||||||
if (changes.system.resources[resource]) {
|
const resourceValue = changes.system.resources[resource];
|
||||||
const becameMax = changes.system.resources[resource].value === this.resources[resource].max;
|
if (
|
||||||
|
resourceValue &&
|
||||||
|
this.resources[resource].max &&
|
||||||
|
resourceValue.value !== this.resources[resource].value
|
||||||
|
) {
|
||||||
|
const becameMax = resourceValue.value === this.resources[resource].max;
|
||||||
const wasMax =
|
const wasMax =
|
||||||
this.resources[resource].value === this.resources[resource].max &&
|
this.resources[resource].value === this.resources[resource].max &&
|
||||||
this.resources[resource].value !== changes.system.resources[resource].value;
|
this.resources[resource].value !== resourceValue.value;
|
||||||
if (becameMax) {
|
if (becameMax) {
|
||||||
this.parent.toggleDefeated(true);
|
this.parent.toggleDefeated(true);
|
||||||
} else if (wasMax) {
|
} else if (wasMax) {
|
||||||
|
|
|
||||||
|
|
@ -317,7 +317,7 @@ export default class DhCharacter extends BaseDataActor {
|
||||||
}
|
}
|
||||||
|
|
||||||
get multiclass() {
|
get multiclass() {
|
||||||
const value = this.parent.items.find(x => x.type === 'Class' && x.system.isMulticlass);
|
const value = this.parent.items.find(x => x.type === 'class' && x.system.isMulticlass);
|
||||||
const subclass = this.parent.items.find(x => x.type === 'subclass' && x.system.isMulticlass);
|
const subclass = this.parent.items.find(x => x.type === 'subclass' && x.system.isMulticlass);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -443,17 +443,15 @@ export default class DhCharacter extends BaseDataActor {
|
||||||
classFeatures.push(item);
|
classFeatures.push(item);
|
||||||
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.subclass.id) {
|
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.subclass.id) {
|
||||||
if (this.class.subclass) {
|
if (this.class.subclass) {
|
||||||
const subclassState = this.class.subclass.system.featureState;
|
const prop = item.system.multiclassOrigin ? 'multiclass' : 'class';
|
||||||
const subclass =
|
const subclassState = this[prop].subclass?.system?.featureState;
|
||||||
item.system.identifier === 'multiclass' ? this.multiclass.subclass : this.class.subclass;
|
if (!subclassState) continue;
|
||||||
const featureType = subclass
|
|
||||||
? (subclass.system.features.find(x => x.item?.uuid === item.uuid)?.type ?? null)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
featureType === CONFIG.DH.ITEM.featureSubTypes.foundation ||
|
item.system.identifier === CONFIG.DH.ITEM.featureSubTypes.foundation ||
|
||||||
(featureType === CONFIG.DH.ITEM.featureSubTypes.specialization && subclassState >= 2) ||
|
(item.system.identifier === CONFIG.DH.ITEM.featureSubTypes.specialization &&
|
||||||
(featureType === CONFIG.DH.ITEM.featureSubTypes.mastery && subclassState >= 3)
|
subclassState >= 2) ||
|
||||||
|
(item.system.identifier === CONFIG.DH.ITEM.featureSubTypes.mastery && subclassState >= 3)
|
||||||
) {
|
) {
|
||||||
subclassFeatures.push(item);
|
subclassFeatures.push(item);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import DHAbilityUse from "./abilityUse.mjs";
|
import DHAbilityUse from './abilityUse.mjs';
|
||||||
import DHActorRoll from "./adversaryRoll.mjs";
|
import DHActorRoll from './actorRoll.mjs';
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
abilityUse: DHAbilityUse,
|
abilityUse: DHAbilityUse,
|
||||||
adversaryRoll: DHActorRoll,
|
adversaryRoll: DHActorRoll,
|
||||||
damageRoll: DHActorRoll,
|
damageRoll: DHActorRoll,
|
||||||
dualityRoll: DHActorRoll
|
dualityRoll: DHActorRoll
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -55,9 +55,12 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
get action() {
|
get action() {
|
||||||
const actionItem = this.actionItem;
|
const actionActor = this.actionActor,
|
||||||
if (!actionItem || !this.source.action) return null;
|
actionItem = this.actionItem;
|
||||||
return actionItem.system.actionsList?.find(a => a.id === this.source.action);
|
if (!this.source.action) return null;
|
||||||
|
if (actionItem) return actionItem.system.actionsList?.find(a => a.id === this.source.action);
|
||||||
|
else if (actionActor?.system.attack?._id === this.source.action) return actionActor.system.attack;
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get targetMode() {
|
get targetMode() {
|
||||||
|
|
@ -95,7 +98,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
registerTargetHook() {
|
registerTargetHook() {
|
||||||
if (!this.parent.isAuthor) return;
|
if (!this.parent.isAuthor || !this.hasTarget) return;
|
||||||
if (this.targetMode && this.parent.targetHook !== null) {
|
if (this.targetMode && this.parent.targetHook !== null) {
|
||||||
Hooks.off('targetToken', this.parent.targetHook);
|
Hooks.off('targetToken', this.parent.targetHook);
|
||||||
return (this.parent.targetHook = null);
|
return (this.parent.targetHook = null);
|
||||||
|
|
@ -113,7 +116,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
|
||||||
this.currentTargets = this.getTargetList();
|
this.currentTargets = this.getTargetList();
|
||||||
// this.registerTargetHook();
|
// this.registerTargetHook();
|
||||||
|
|
||||||
if (this.targetMode === true && this.hasRoll) {
|
if (this.hasRoll) {
|
||||||
this.targetShort = this.targets.reduce(
|
this.targetShort = this.targets.reduce(
|
||||||
(a, c) => {
|
(a, c) => {
|
||||||
if (c.hit) a.hit += 1;
|
if (c.hit) a.hit += 1;
|
||||||
|
|
@ -127,7 +130,8 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.canViewSecret = this.parent.speakerActor?.testUserPermission(game.user, 'OBSERVER');
|
this.canViewSecret = this.parent.speakerActor?.testUserPermission(game.user, 'OBSERVER');
|
||||||
this.canButtonApply = game.user.isGM;
|
this.canButtonApply = game.user.isGM; //temp
|
||||||
|
this.isGM = game.user.isGM; //temp
|
||||||
}
|
}
|
||||||
|
|
||||||
getTargetList() {
|
getTargetList() {
|
||||||
|
|
@ -6,6 +6,5 @@ export { default as EffectsField } from './effectsField.mjs';
|
||||||
export { default as SaveField } from './saveField.mjs';
|
export { default as SaveField } from './saveField.mjs';
|
||||||
export { default as BeastformField } from './beastformField.mjs';
|
export { default as BeastformField } from './beastformField.mjs';
|
||||||
export { default as DamageField } from './damageField.mjs';
|
export { default as DamageField } from './damageField.mjs';
|
||||||
export { default as HealingField } from './healingField.mjs';
|
|
||||||
export { default as RollField } from './rollField.mjs';
|
export { default as RollField } from './rollField.mjs';
|
||||||
export { default as MacroField } from './macroField.mjs';
|
export { default as MacroField } from './macroField.mjs';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,13 @@
|
||||||
|
import BeastformDialog from '../../../applications/dialogs/beastformDialog.mjs';
|
||||||
|
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
export default class BeastformField extends fields.SchemaField {
|
export default class BeastformField extends fields.SchemaField {
|
||||||
|
/**
|
||||||
|
* Action Workflow order
|
||||||
|
*/
|
||||||
|
static order = 90;
|
||||||
|
|
||||||
constructor(options = {}, context = {}) {
|
constructor(options = {}, context = {}) {
|
||||||
const beastformFields = {
|
const beastformFields = {
|
||||||
tierAccess: new fields.SchemaField({
|
tierAccess: new fields.SchemaField({
|
||||||
|
|
@ -27,4 +34,96 @@ export default class BeastformField extends fields.SchemaField {
|
||||||
};
|
};
|
||||||
super(beastformFields, options, context);
|
super(beastformFields, options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Beastform Transformation Action Workflow part.
|
||||||
|
* Must be called within Action context or similar.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
*/
|
||||||
|
static async execute(config) {
|
||||||
|
// Should not be useful anymore here
|
||||||
|
await BeastformField.handleActiveTransformations.call(this);
|
||||||
|
|
||||||
|
const { selected, evolved, hybrid } = await BeastformDialog.configure(config, this.item);
|
||||||
|
if (!selected) return false;
|
||||||
|
|
||||||
|
return await BeastformField.transform.call(this, selected, evolved, hybrid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Action Workflow config object.
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
*/
|
||||||
|
prepareConfig(config) {
|
||||||
|
if (this.actor.effects.find(x => x.type === 'beastform')) {
|
||||||
|
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.beastformAlreadyApplied'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers;
|
||||||
|
const actorLevel = this.actor.system.levelData.level.current;
|
||||||
|
const actorTier =
|
||||||
|
Object.values(settingsTiers).find(
|
||||||
|
tier => actorLevel >= tier.levels.start && actorLevel <= tier.levels.end
|
||||||
|
) ?? 1;
|
||||||
|
|
||||||
|
config.tierLimit = this.beastform.tierAccess.exact ?? actorTier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO by Harry
|
||||||
|
* @param {*} selectedForm
|
||||||
|
* @param {*} evolvedData
|
||||||
|
* @param {*} hybridData
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
static async transform(selectedForm, evolvedData, hybridData) {
|
||||||
|
const formData = evolvedData?.form ? evolvedData.form.toObject() : selectedForm.toObject();
|
||||||
|
const beastformEffect = formData.effects.find(x => x.type === 'beastform');
|
||||||
|
if (!beastformEffect) {
|
||||||
|
ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evolvedData?.form) {
|
||||||
|
const evolvedForm = selectedForm.effects.find(x => x.type === 'beastform');
|
||||||
|
if (!evolvedForm) {
|
||||||
|
ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
beastformEffect.changes = [...beastformEffect.changes, ...evolvedForm.changes];
|
||||||
|
formData.system.features = [...formData.system.features, ...selectedForm.system.features.map(x => x.uuid)];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedForm.system.beastformType === CONFIG.DH.ITEM.beastformTypes.hybrid.id) {
|
||||||
|
formData.system.advantageOn = Object.values(hybridData.advantages).reduce((advantages, formCategory) => {
|
||||||
|
Object.keys(formCategory).forEach(advantageKey => {
|
||||||
|
advantages[advantageKey] = formCategory[advantageKey];
|
||||||
|
});
|
||||||
|
return advantages;
|
||||||
|
}, {});
|
||||||
|
formData.system.features = [
|
||||||
|
...formData.system.features,
|
||||||
|
...Object.values(hybridData.features).flatMap(x => Object.keys(x))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.actor.createEmbeddedDocuments('Item', [formData]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove existing beastform effect and return true if there was one
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
static async handleActiveTransformations() {
|
||||||
|
const beastformEffects = this.actor.effects.filter(x => x.type === 'beastform');
|
||||||
|
const existingEffects = beastformEffects.length > 0;
|
||||||
|
await this.actor.deleteEmbeddedDocuments(
|
||||||
|
'ActiveEffect',
|
||||||
|
beastformEffects.map(x => x.id)
|
||||||
|
);
|
||||||
|
return existingEffects;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
export default class CostField extends fields.ArrayField {
|
export default class CostField extends fields.ArrayField {
|
||||||
|
/**
|
||||||
|
* Action Workflow order
|
||||||
|
*/
|
||||||
|
static order = 150;
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
constructor(options = {}, context = {}) {
|
constructor(options = {}, context = {}) {
|
||||||
const element = new fields.SchemaField({
|
const element = new fields.SchemaField({
|
||||||
key: new fields.StringField({
|
key: new fields.StringField({
|
||||||
|
|
@ -8,7 +14,7 @@ export default class CostField extends fields.ArrayField {
|
||||||
required: true,
|
required: true,
|
||||||
initial: 'hope'
|
initial: 'hope'
|
||||||
}),
|
}),
|
||||||
keyIsID: new fields.BooleanField(),
|
itemId: new fields.StringField({ nullable: true, initial: null }),
|
||||||
value: new fields.NumberField({ nullable: true, initial: 1, min: 0 }),
|
value: new fields.NumberField({ nullable: true, initial: 1, min: 0 }),
|
||||||
scalable: new fields.BooleanField({ initial: false }),
|
scalable: new fields.BooleanField({ initial: false }),
|
||||||
step: new fields.NumberField({ nullable: true, initial: null }),
|
step: new fields.NumberField({ nullable: true, initial: null }),
|
||||||
|
|
@ -20,18 +26,88 @@ export default class CostField extends fields.ArrayField {
|
||||||
super(element, options, context);
|
super(element, options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
static prepareConfig(config) {
|
/**
|
||||||
|
* Cost Consumption Action Workflow part.
|
||||||
|
* Consume configured action resources.
|
||||||
|
* Must be called within Action context or similar.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
* @param {boolean} [successCost=false] Consume only resources configured as "On Success only" if not already consumed.
|
||||||
|
*/
|
||||||
|
static async execute(config, successCost = false) {
|
||||||
|
const actor = this.actor.system.partner ?? this.actor,
|
||||||
|
usefulResources = {
|
||||||
|
...foundry.utils.deepClone(actor.system.resources),
|
||||||
|
fear: {
|
||||||
|
value: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
|
||||||
|
max: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear,
|
||||||
|
reversed: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.parent?.parent) {
|
||||||
|
for (var cost of config.costs) {
|
||||||
|
if (cost.itemId) {
|
||||||
|
usefulResources[cost.key] = {
|
||||||
|
value: cost.value,
|
||||||
|
target: this.parent.parent,
|
||||||
|
itemId: cost.itemId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resources = CostField.getRealCosts(config.costs)
|
||||||
|
.filter(
|
||||||
|
c =>
|
||||||
|
(!successCost && (!c.consumeOnSuccess || config.roll?.success)) ||
|
||||||
|
(successCost && c.consumeOnSuccess)
|
||||||
|
)
|
||||||
|
.reduce((a, c) => {
|
||||||
|
const resource = usefulResources[c.key];
|
||||||
|
if (resource) {
|
||||||
|
a.push({
|
||||||
|
key: c.key,
|
||||||
|
value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1),
|
||||||
|
target: resource.target,
|
||||||
|
itemId: resource.itemId
|
||||||
|
});
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
await actor.modifyResource(resources);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Action Workflow config object.
|
||||||
|
* Must be called within Action context or similar.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
* @returns {boolean} Return false if fast-forwarded and no more uses.
|
||||||
|
*/
|
||||||
|
prepareConfig(config) {
|
||||||
const costs = this.cost?.length ? foundry.utils.deepClone(this.cost) : [];
|
const costs = this.cost?.length ? foundry.utils.deepClone(this.cost) : [];
|
||||||
config.costs = CostField.calcCosts.call(this, costs);
|
config.costs = CostField.calcCosts.call(this, costs);
|
||||||
const hasCost = CostField.hasCost.call(this, config.costs);
|
const hasCost = CostField.hasCost.call(this, config.costs);
|
||||||
if (config.isFastForward && !hasCost)
|
if (config.dialog.configure === false && !hasCost) {
|
||||||
return ui.notifications.warn("You don't have the resources to use that action.");
|
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.insufficientResources'));
|
||||||
return hasCost;
|
return hasCost;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {*} costs
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
static calcCosts(costs) {
|
static calcCosts(costs) {
|
||||||
const resources = CostField.getResources.call(this, costs);
|
const resources = CostField.getResources.call(this, costs);
|
||||||
return costs.map(c => {
|
let filteredCosts = costs;
|
||||||
|
if (this.parent.metadata.isQuantifiable && this.parent.consumeOnUse === false) {
|
||||||
|
filteredCosts = filteredCosts.filter(c => c.key !== 'quantity');
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredCosts.map(c => {
|
||||||
c.scale = c.scale ?? 0;
|
c.scale = c.scale ?? 0;
|
||||||
c.step = c.step ?? 1;
|
c.step = c.step ?? 1;
|
||||||
c.total = c.value + c.scale * c.step;
|
c.total = c.value + c.scale * c.step;
|
||||||
|
|
@ -40,13 +116,19 @@ export default class CostField extends fields.ArrayField {
|
||||||
c.key === 'fear'
|
c.key === 'fear'
|
||||||
? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear)
|
? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear)
|
||||||
: resources[c.key].isReversed
|
: resources[c.key].isReversed
|
||||||
? resources[c.key].max
|
? resources[c.key].max - resources[c.key].value
|
||||||
: resources[c.key].value;
|
: resources[c.key].value;
|
||||||
if (c.scalable) c.maxStep = Math.floor((c.max - c.value) / c.step);
|
if (c.scalable) c.maxStep = Math.floor((c.max - c.value) / c.step);
|
||||||
return c;
|
return c;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current Actor currently has all needed resources.
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {*} costs
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
static hasCost(costs) {
|
static hasCost(costs) {
|
||||||
const realCosts = CostField.getRealCosts.call(this, costs),
|
const realCosts = CostField.getRealCosts.call(this, costs),
|
||||||
hasFearCost = realCosts.findIndex(c => c.key === 'fear');
|
hasFearCost = realCosts.findIndex(c => c.key === 'fear');
|
||||||
|
|
@ -73,17 +155,20 @@ export default class CostField extends fields.ArrayField {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all Actor resources + parent Item potential one.
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {*} costs
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
static getResources(costs) {
|
static getResources(costs) {
|
||||||
const actorResources = foundry.utils.deepClone(this.actor.system.resources);
|
const actorResources = foundry.utils.deepClone(this.actor.system.resources);
|
||||||
if (this.actor.system.partner)
|
if (this.actor.system.partner)
|
||||||
actorResources.hope = foundry.utils.deepClone(this.actor.system.partner.system.resources.hope);
|
actorResources.hope = foundry.utils.deepClone(this.actor.system.partner.system.resources.hope);
|
||||||
const itemResources = {};
|
const itemResources = {};
|
||||||
for (let itemResource of costs) {
|
for (let itemResource of costs) {
|
||||||
if (itemResource.keyIsID) {
|
if (itemResource.itemId) {
|
||||||
itemResources[itemResource.key] = {
|
itemResources[itemResource.key] = CostField.getItemIdCostResource.bind(this)(itemResource);
|
||||||
value: this.parent.resource.value ?? 0,
|
|
||||||
max: CostField.formatMax.call(this, this.parent?.resource?.max)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,8 +178,48 @@ export default class CostField extends fields.ArrayField {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getItemIdCostResource(itemResource) {
|
||||||
|
switch (itemResource.key) {
|
||||||
|
case CONFIG.DH.GENERAL.itemAbilityCosts.resource.id:
|
||||||
|
return {
|
||||||
|
value: this.parent.resource.value ?? 0,
|
||||||
|
max: CostField.formatMax.call(this, this.parent?.resource?.max)
|
||||||
|
};
|
||||||
|
case CONFIG.DH.GENERAL.itemAbilityCosts.quantity.id:
|
||||||
|
return {
|
||||||
|
value: this.parent.quantity ?? 0,
|
||||||
|
max: this.parent.quantity ?? 0
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return { value: 0, max: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getItemIdCostUpdate(r) {
|
||||||
|
switch (r.key) {
|
||||||
|
case CONFIG.DH.GENERAL.itemAbilityCosts.resource.id:
|
||||||
|
return {
|
||||||
|
path: 'system.resource.value',
|
||||||
|
value: r.target.system.resource.value + r.value
|
||||||
|
};
|
||||||
|
case CONFIG.DH.GENERAL.itemAbilityCosts.quantity.id:
|
||||||
|
return {
|
||||||
|
path: 'system.quantity',
|
||||||
|
value: r.target.system.quantity + r.value
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return { path: '', value: undefined };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} costs
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
static getRealCosts(costs) {
|
static getRealCosts(costs) {
|
||||||
const realCosts = costs?.length ? costs.filter(c => c.enabled) : [];
|
const cloneCosts = foundry.utils.deepClone(costs),
|
||||||
|
realCosts = cloneCosts?.length ? cloneCosts.filter(c => c.enabled) : [];
|
||||||
let mergedCosts = [];
|
let mergedCosts = [];
|
||||||
realCosts.forEach(c => {
|
realCosts.forEach(c => {
|
||||||
const getCost = Object.values(mergedCosts).find(gc => gc.key === c.key);
|
const getCost = Object.values(mergedCosts).find(gc => gc.key === c.key);
|
||||||
|
|
@ -104,6 +229,12 @@ export default class CostField extends fields.ArrayField {
|
||||||
return mergedCosts;
|
return mergedCosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format scalable max cost, inject Action datas if it's a formula.
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {number|string} max Configured maximum for that resource.
|
||||||
|
* @returns {number} The max cost value.
|
||||||
|
*/
|
||||||
static formatMax(max) {
|
static formatMax(max) {
|
||||||
max ??= 0;
|
max ??= 0;
|
||||||
if (isNaN(max)) {
|
if (isNaN(max)) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,15 @@
|
||||||
import FormulaField from '../formulaField.mjs';
|
import FormulaField from '../formulaField.mjs';
|
||||||
|
import { setsEqual } from '../../../helpers/utils.mjs';
|
||||||
|
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
export default class DamageField extends fields.SchemaField {
|
export default class DamageField extends fields.SchemaField {
|
||||||
|
/**
|
||||||
|
* Action Workflow order
|
||||||
|
*/
|
||||||
|
static order = 20;
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
constructor(options, context = {}) {
|
constructor(options, context = {}) {
|
||||||
const damageFields = {
|
const damageFields = {
|
||||||
parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)),
|
parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)),
|
||||||
|
|
@ -14,6 +21,152 @@ export default class DamageField extends fields.SchemaField {
|
||||||
};
|
};
|
||||||
super(damageFields, options, context);
|
super(damageFields, options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roll Damage/Healing Action Workflow part.
|
||||||
|
* Must be called within Action context or similar.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
* @param {string} [messageId=null] ChatMessage Id where the clicked button belong.
|
||||||
|
* @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example.
|
||||||
|
*/
|
||||||
|
static async execute(config, messageId = null, force = false) {
|
||||||
|
if (!this.hasDamage && !this.hasHealing) return;
|
||||||
|
if (
|
||||||
|
this.hasRoll &&
|
||||||
|
DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.never.id &&
|
||||||
|
!force
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let formulas = this.damage.parts.map(p => ({
|
||||||
|
formula: DamageField.getFormulaValue.call(this, p, config).getFormula(this.actor),
|
||||||
|
damageTypes: p.applyTo === 'hitPoints' && !p.type.size ? new Set(['physical']) : p.type,
|
||||||
|
applyTo: p.applyTo
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!formulas.length) return false;
|
||||||
|
|
||||||
|
formulas = DamageField.formatFormulas.call(this, formulas, config);
|
||||||
|
|
||||||
|
const damageConfig = {
|
||||||
|
...config,
|
||||||
|
roll: formulas,
|
||||||
|
dialog: {},
|
||||||
|
data: this.getRollData()
|
||||||
|
};
|
||||||
|
delete damageConfig.evaluate;
|
||||||
|
|
||||||
|
if (DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.always.id)
|
||||||
|
damageConfig.dialog.configure = false;
|
||||||
|
if (config.hasSave) config.onSave = damageConfig.onSave = this.save.damageMod;
|
||||||
|
|
||||||
|
damageConfig.source.message = config.message?._id ?? messageId;
|
||||||
|
damageConfig.directDamage = !!damageConfig.source?.message;
|
||||||
|
|
||||||
|
// if(damageConfig.source?.message && game.modules.get('dice-so-nice')?.active)
|
||||||
|
// await game.dice3d.waitFor3DAnimationByMessageID(damageConfig.source.message);
|
||||||
|
|
||||||
|
const damageResult = await CONFIG.Dice.daggerheart.DamageRoll.build(damageConfig);
|
||||||
|
if (!damageResult) return false;
|
||||||
|
config.damage = damageResult.damage;
|
||||||
|
config.message ??= damageConfig.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply Damage/Healing Action Worflow part.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
* @param {*[]} targets Arrays of targets to bypass pre-selected ones.
|
||||||
|
* @param {boolean} force If the method should be executed outside of Action workflow, for ChatMessage button for example.
|
||||||
|
*/
|
||||||
|
static async applyDamage(config, targets = null, force = false) {
|
||||||
|
targets ??= config.targets.filter(target => target.hit);
|
||||||
|
if (!config.damage || !targets?.length || (!DamageField.getApplyAutomation() && !force)) return;
|
||||||
|
for (let target of targets) {
|
||||||
|
const actor = fromUuidSync(target.actorId);
|
||||||
|
if (!actor) continue;
|
||||||
|
if (!config.hasHealing && config.onSave && target.saved?.success === true) {
|
||||||
|
const mod = CONFIG.DH.ACTIONS.damageOnSave[config.onSave]?.mod ?? 1;
|
||||||
|
Object.entries(config.damage).forEach(([k, v]) => {
|
||||||
|
v.total = 0;
|
||||||
|
v.parts.forEach(part => {
|
||||||
|
part.total = Math.ceil(part.total * mod);
|
||||||
|
v.total += part.total;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.hasHealing) actor.takeHealing(config.damage);
|
||||||
|
else actor.takeDamage(config.damage, config.isDirect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return value or valueAlt from damage part
|
||||||
|
* Must be called within Action context or similar.
|
||||||
|
* @param {object} part Damage Part
|
||||||
|
* @param {object} data Action getRollData
|
||||||
|
* @returns Formula value object
|
||||||
|
*/
|
||||||
|
static getFormulaValue(part, data) {
|
||||||
|
let formulaValue = part.value;
|
||||||
|
|
||||||
|
if (data.hasRoll && part.resultBased && data.roll.result.duality === -1) return part.valueAlt;
|
||||||
|
|
||||||
|
const isAdversary = this.actor.type === 'adversary';
|
||||||
|
if (isAdversary && this.actor.system.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id) {
|
||||||
|
const hasHordeDamage = this.actor.effects.find(x => x.type === 'horde');
|
||||||
|
if (hasHordeDamage && !hasHordeDamage.disabled) return part.valueAlt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formulaValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare formulas for Damage Roll
|
||||||
|
* Must be called within Action context or similar.
|
||||||
|
* @param {object[]} formulas Array of formatted formulas object
|
||||||
|
* @param {object} data Action getRollData
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
static formatFormulas(formulas, data) {
|
||||||
|
const formattedFormulas = [];
|
||||||
|
formulas.forEach(formula => {
|
||||||
|
if (isNaN(formula.formula))
|
||||||
|
formula.formula = Roll.replaceFormulaData(formula.formula, this.getRollData(data));
|
||||||
|
const same = formattedFormulas.find(
|
||||||
|
f => setsEqual(f.damageTypes, formula.damageTypes) && f.applyTo === formula.applyTo
|
||||||
|
);
|
||||||
|
if (same) same.formula += ` + ${formula.formula}`;
|
||||||
|
else formattedFormulas.push(formula);
|
||||||
|
});
|
||||||
|
return formattedFormulas;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the automation setting for execute method for current user role
|
||||||
|
* @returns {string} Id from settingsConfig.mjs actionAutomationChoices
|
||||||
|
*/
|
||||||
|
static getAutomation() {
|
||||||
|
return (
|
||||||
|
(game.user.isGM &&
|
||||||
|
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.gm) ||
|
||||||
|
(!game.user.isGM &&
|
||||||
|
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.players)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the automation setting for applyDamage method for current user role
|
||||||
|
* @returns {boolean} If applyDamage should be triggered automatically
|
||||||
|
*/
|
||||||
|
static getApplyAutomation() {
|
||||||
|
return (
|
||||||
|
(game.user.isGM &&
|
||||||
|
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.gm) ||
|
||||||
|
(!game.user.isGM &&
|
||||||
|
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.players)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DHActionDiceData extends foundry.abstract.DataModel {
|
export class DHActionDiceData extends foundry.abstract.DataModel {
|
||||||
|
|
@ -23,14 +176,26 @@ export class DHActionDiceData extends foundry.abstract.DataModel {
|
||||||
multiplier: new fields.StringField({
|
multiplier: new fields.StringField({
|
||||||
choices: CONFIG.DH.GENERAL.multiplierTypes,
|
choices: CONFIG.DH.GENERAL.multiplierTypes,
|
||||||
initial: 'prof',
|
initial: 'prof',
|
||||||
label: 'Multiplier'
|
label: 'DAGGERHEART.ACTIONS.Config.damage.multiplier',
|
||||||
|
nullable: false,
|
||||||
|
required: true
|
||||||
}),
|
}),
|
||||||
flatMultiplier: new fields.NumberField({ nullable: true, initial: 1, label: 'Flat Multiplier' }),
|
flatMultiplier: new fields.NumberField({
|
||||||
dice: new fields.StringField({ choices: CONFIG.DH.GENERAL.diceTypes, initial: 'd6', label: 'Dice' }),
|
nullable: true,
|
||||||
bonus: new fields.NumberField({ nullable: true, initial: null, label: 'Bonus' }),
|
initial: 1,
|
||||||
|
label: 'DAGGERHEART.ACTIONS.Config.damage.flatMultiplier'
|
||||||
|
}),
|
||||||
|
dice: new fields.StringField({
|
||||||
|
choices: CONFIG.DH.GENERAL.diceTypes,
|
||||||
|
initial: 'd6',
|
||||||
|
label: 'DAGGERHEART.GENERAL.Dice.single',
|
||||||
|
nullable: false,
|
||||||
|
required: true
|
||||||
|
}),
|
||||||
|
bonus: new fields.NumberField({ nullable: true, initial: null, label: 'DAGGERHEART.GENERAL.bonus' }),
|
||||||
custom: new fields.SchemaField({
|
custom: new fields.SchemaField({
|
||||||
enabled: new fields.BooleanField({ label: 'Custom Formula' }),
|
enabled: new fields.BooleanField({ label: 'DAGGERHEART.ACTIONS.Config.general.customFormula' }),
|
||||||
formula: new FormulaField({ label: 'Formula', initial: '' })
|
formula: new FormulaField({ label: 'DAGGERHEART.ACTIONS.Config.general.formula', initial: '' })
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
|
import { emitAsGM, GMUpdateEvent } from '../../../systemRegistration/socket.mjs';
|
||||||
|
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
export default class EffectsField extends fields.ArrayField {
|
export default class EffectsField extends fields.ArrayField {
|
||||||
|
/**
|
||||||
|
* Action Workflow order
|
||||||
|
*/
|
||||||
|
static order = 100;
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
constructor(options = {}, context = {}) {
|
constructor(options = {}, context = {}) {
|
||||||
const element = new fields.SchemaField({
|
const element = new fields.SchemaField({
|
||||||
_id: new fields.DocumentIdField(),
|
_id: new fields.DocumentIdField(),
|
||||||
|
|
@ -8,4 +16,85 @@ export default class EffectsField extends fields.ArrayField {
|
||||||
});
|
});
|
||||||
super(element, options, context);
|
super(element, options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply Effects Action Workflow part.
|
||||||
|
* Must be called within Action context or similar.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
* @param {object[]} [targets=null] Array of targets to override pre-selected ones.
|
||||||
|
* @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example.
|
||||||
|
*/
|
||||||
|
static async execute(config, targets = null, force = false) {
|
||||||
|
if (!config.hasEffect) return;
|
||||||
|
let message = config.message ?? ui.chat.collection.get(config.parent?._id);
|
||||||
|
if (!message) {
|
||||||
|
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
|
||||||
|
roll._evaluated = true;
|
||||||
|
message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);
|
||||||
|
}
|
||||||
|
if (EffectsField.getAutomation() || force) {
|
||||||
|
targets ??= (message.system?.targets ?? config.targets).filter(t => !config.hasRoll || t.hit);
|
||||||
|
await emitAsGM(GMUpdateEvent.UpdateEffect, EffectsField.applyEffects.bind(this), targets, this.uuid);
|
||||||
|
// EffectsField.applyEffects.call(this, config.targets.filter(t => !config.hasRoll || t.hit));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply Action Effects to a list of Targets
|
||||||
|
* Must be called within Action context or similar.
|
||||||
|
* @param {object[]} targets Array of formatted targets
|
||||||
|
*/
|
||||||
|
static async applyEffects(targets) {
|
||||||
|
if (!this.effects?.length || !targets?.length) return;
|
||||||
|
let effects = this.effects;
|
||||||
|
targets.forEach(async token => {
|
||||||
|
if (this.hasSave && token.saved.success === true) effects = this.effects.filter(e => e.onSave === true);
|
||||||
|
if (!effects.length) return;
|
||||||
|
effects.forEach(async e => {
|
||||||
|
const actor = canvas.tokens.get(token.id)?.actor,
|
||||||
|
effect = this.item.effects.get(e._id);
|
||||||
|
if (!actor || !effect) return;
|
||||||
|
await EffectsField.applyEffect(effect, actor);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply an Effect to a target or enable it if already on it
|
||||||
|
* @param {object} effect Effect object containing ActiveEffect UUID
|
||||||
|
* @param {object} actor Actor Document
|
||||||
|
*/
|
||||||
|
static async applyEffect(effect, actor) {
|
||||||
|
const existingEffect = actor.effects.find(e => e.origin === effect.uuid);
|
||||||
|
if (existingEffect) {
|
||||||
|
return effect.update(
|
||||||
|
foundry.utils.mergeObject({
|
||||||
|
...effect.constructor.getInitialDuration(),
|
||||||
|
disabled: false
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, create a new effect on the target
|
||||||
|
const effectData = foundry.utils.mergeObject({
|
||||||
|
...effect.toObject(),
|
||||||
|
disabled: false,
|
||||||
|
transfer: false,
|
||||||
|
origin: effect.uuid
|
||||||
|
});
|
||||||
|
await ActiveEffect.implementation.create(effectData, { parent: actor });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the automation setting for execute method for current user role
|
||||||
|
* @returns {boolean} If execute should be triggered automatically
|
||||||
|
*/
|
||||||
|
static getAutomation() {
|
||||||
|
return (
|
||||||
|
(game.user.isGM &&
|
||||||
|
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.effect.gm) ||
|
||||||
|
(!game.user.isGM &&
|
||||||
|
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.effect.players)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { DHDamageData } from './damageField.mjs';
|
|
||||||
|
|
||||||
const fields = foundry.data.fields;
|
|
||||||
|
|
||||||
export default class HealingField extends fields.SchemaField {
|
|
||||||
constructor(options, context = {}) {
|
|
||||||
const healingFields = {
|
|
||||||
parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData))
|
|
||||||
};
|
|
||||||
super(healingFields, options, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,29 @@
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
export default class MacroField extends fields.DocumentUUIDField {
|
export default class MacroField extends fields.DocumentUUIDField {
|
||||||
|
/**
|
||||||
|
* Action Workflow order
|
||||||
|
*/
|
||||||
|
static order = 70;
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
constructor(context = {}) {
|
constructor(context = {}) {
|
||||||
super({ type: "Macro" }, context);
|
super({ type: 'Macro' }, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Macro Action Workflow part.
|
||||||
|
* Must be called within Action context or similar or similar.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. Currently not used.
|
||||||
|
*/
|
||||||
|
static async execute(config) {
|
||||||
|
const fixUUID = !this.macro.includes('Macro.') ? `Macro.${this.macro}` : this.macro,
|
||||||
|
macro = await fromUuid(fixUUID);
|
||||||
|
try {
|
||||||
|
if (!macro) throw new Error(`No macro found for the UUID: ${this.macro}.`);
|
||||||
|
macro.execute();
|
||||||
|
} catch (error) {
|
||||||
|
ui.notifications.error(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,23 @@
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
export default class RangeField extends fields.StringField {
|
export default class RangeField extends fields.StringField {
|
||||||
|
/** @inheritDoc */
|
||||||
constructor(context = {}) {
|
constructor(context = {}) {
|
||||||
const options = {
|
const options = {
|
||||||
choices: CONFIG.DH.GENERAL.range,
|
choices: CONFIG.DH.GENERAL.range,
|
||||||
required: false,
|
required: false,
|
||||||
blank: true,
|
blank: true,
|
||||||
label: "DAGGERHEART.GENERAL.range"
|
label: 'DAGGERHEART.GENERAL.range'
|
||||||
};
|
};
|
||||||
super(options, context);
|
super(options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
static prepareConfig(config) {
|
/**
|
||||||
return true;
|
* Update Action Workflow config object.
|
||||||
|
* NOT YET IMPLEMENTED.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
*/
|
||||||
|
prepareConfig(config) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,18 +5,27 @@ export class DHActionRollData extends foundry.abstract.DataModel {
|
||||||
static defineSchema() {
|
static defineSchema() {
|
||||||
return {
|
return {
|
||||||
type: new fields.StringField({ nullable: true, initial: null, choices: CONFIG.DH.GENERAL.rollTypes }),
|
type: new fields.StringField({ nullable: true, initial: null, choices: CONFIG.DH.GENERAL.rollTypes }),
|
||||||
trait: new fields.StringField({ nullable: true, initial: null, choices: CONFIG.DH.ACTOR.abilities, label: "DAGGERHEART.GENERAL.Trait.single" }),
|
trait: new fields.StringField({
|
||||||
|
nullable: true,
|
||||||
|
initial: null,
|
||||||
|
choices: CONFIG.DH.ACTOR.abilities,
|
||||||
|
label: 'DAGGERHEART.GENERAL.Trait.single'
|
||||||
|
}),
|
||||||
difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }),
|
difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }),
|
||||||
bonus: new fields.NumberField({ nullable: true, initial: null, integer: true }),
|
bonus: new fields.NumberField({ nullable: true, initial: null, integer: true }),
|
||||||
advState: new fields.StringField({
|
advState: new fields.StringField({
|
||||||
choices: CONFIG.DH.ACTIONS.advantageState,
|
choices: CONFIG.DH.ACTIONS.advantageState,
|
||||||
initial: 'neutral'
|
initial: 'neutral',
|
||||||
|
nullable: false,
|
||||||
|
required: true
|
||||||
}),
|
}),
|
||||||
diceRolling: new fields.SchemaField({
|
diceRolling: new fields.SchemaField({
|
||||||
multiplier: new fields.StringField({
|
multiplier: new fields.StringField({
|
||||||
choices: CONFIG.DH.GENERAL.diceSetNumbers,
|
choices: CONFIG.DH.GENERAL.diceSetNumbers,
|
||||||
initial: 'prof',
|
initial: 'prof',
|
||||||
label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.multiplier'
|
label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.multiplier',
|
||||||
|
nullable: false,
|
||||||
|
required: true
|
||||||
}),
|
}),
|
||||||
flatMultiplier: new fields.NumberField({
|
flatMultiplier: new fields.NumberField({
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
|
@ -26,7 +35,9 @@ export class DHActionRollData extends foundry.abstract.DataModel {
|
||||||
dice: new fields.StringField({
|
dice: new fields.StringField({
|
||||||
choices: CONFIG.DH.GENERAL.diceTypes,
|
choices: CONFIG.DH.GENERAL.diceTypes,
|
||||||
initial: CONFIG.DH.GENERAL.diceTypes.d6,
|
initial: CONFIG.DH.GENERAL.diceTypes.d6,
|
||||||
label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.dice'
|
label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.dice',
|
||||||
|
nullable: false,
|
||||||
|
required: true
|
||||||
}),
|
}),
|
||||||
compare: new fields.StringField({
|
compare: new fields.StringField({
|
||||||
choices: CONFIG.DH.ACTIONS.diceCompare,
|
choices: CONFIG.DH.ACTIONS.diceCompare,
|
||||||
|
|
@ -71,29 +82,6 @@ export class DHActionRollData extends foundry.abstract.DataModel {
|
||||||
const modifiers = [];
|
const modifiers = [];
|
||||||
if (!this.parent?.actor) return modifiers;
|
if (!this.parent?.actor) return modifiers;
|
||||||
switch (this.parent.actor.type) {
|
switch (this.parent.actor.type) {
|
||||||
case 'character':
|
|
||||||
const spellcastingTrait =
|
|
||||||
this.type === 'spellcast'
|
|
||||||
? (this.parent.actor?.system?.spellcastModifierTrait?.key ?? 'agility')
|
|
||||||
: null;
|
|
||||||
const trait =
|
|
||||||
this.useDefault || !this.trait
|
|
||||||
? (spellcastingTrait ?? this.parent.item.system.attack?.roll?.trait ?? 'agility')
|
|
||||||
: this.trait;
|
|
||||||
if (
|
|
||||||
this.type === CONFIG.DH.GENERAL.rollTypes.attack.id ||
|
|
||||||
this.type === CONFIG.DH.GENERAL.rollTypes.trait.id
|
|
||||||
)
|
|
||||||
modifiers.push({
|
|
||||||
label: `DAGGERHEART.CONFIG.Traits.${trait}.name`,
|
|
||||||
value: this.parent.actor.system.traits[trait].value
|
|
||||||
});
|
|
||||||
else if (this.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id)
|
|
||||||
modifiers.push({
|
|
||||||
label: `DAGGERHEART.CONFIG.RollTypes.spellcast.name`,
|
|
||||||
value: this.parent.actor.system.spellcastModifier
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'companion':
|
case 'companion':
|
||||||
case 'adversary':
|
case 'adversary':
|
||||||
if (this.type === CONFIG.DH.GENERAL.rollTypes.attack.id)
|
if (this.type === CONFIG.DH.GENERAL.rollTypes.attack.id)
|
||||||
|
|
@ -107,10 +95,79 @@ export class DHActionRollData extends foundry.abstract.DataModel {
|
||||||
}
|
}
|
||||||
return modifiers;
|
return modifiers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get rollTrait() {
|
||||||
|
if (this.parent?.actor?.type !== 'character') return null;
|
||||||
|
switch (this.type) {
|
||||||
|
case CONFIG.DH.GENERAL.rollTypes.spellcast.id:
|
||||||
|
return this.parent.actor?.system?.spellcastModifierTrait?.key ?? 'agility';
|
||||||
|
case CONFIG.DH.GENERAL.rollTypes.attack.id:
|
||||||
|
case CONFIG.DH.GENERAL.rollTypes.trait.id:
|
||||||
|
return this.useDefault || !this.trait
|
||||||
|
? (this.parent.item.system.attack?.roll?.trait ?? 'agility')
|
||||||
|
: this.trait;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RollField extends fields.EmbeddedDataField {
|
export default class RollField extends fields.EmbeddedDataField {
|
||||||
|
/**
|
||||||
|
* Action Workflow order
|
||||||
|
*/
|
||||||
|
static order = 10;
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
constructor(options, context = {}) {
|
constructor(options, context = {}) {
|
||||||
super(DHActionRollData, options, context);
|
super(DHActionRollData, options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roll Action Workflow part.
|
||||||
|
* Must be called within Action context or similar.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
*/
|
||||||
|
static async execute(config) {
|
||||||
|
if (!config.hasRoll) return;
|
||||||
|
config = await this.actor.diceRoll(config);
|
||||||
|
if (!config) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Action Workflow config object.
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
*/
|
||||||
|
prepareConfig(config) {
|
||||||
|
if (!config.hasRoll) return;
|
||||||
|
|
||||||
|
config.dialog.configure = RollField.getAutomation() ? !config.dialog.configure : config.dialog.configure;
|
||||||
|
|
||||||
|
const roll = {
|
||||||
|
baseModifiers: this.roll.getModifier(),
|
||||||
|
label: 'Attack',
|
||||||
|
type: this.roll?.type,
|
||||||
|
trait: this.roll?.rollTrait,
|
||||||
|
difficulty: this.roll?.difficulty,
|
||||||
|
formula: this.roll.getFormula(),
|
||||||
|
advantage: CONFIG.DH.ACTIONS.advantageState[this.roll.advState].value
|
||||||
|
};
|
||||||
|
if (this.roll.type === 'diceSet' || !this.hasRoll) roll.lite = true;
|
||||||
|
|
||||||
|
config.roll = roll;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the automation setting for execute method for current user role
|
||||||
|
* @returns {boolean} If execute should be triggered automatically
|
||||||
|
*/
|
||||||
|
static getAutomation() {
|
||||||
|
return (
|
||||||
|
(game.user.isGM &&
|
||||||
|
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.roll.gm) ||
|
||||||
|
(!game.user.isGM &&
|
||||||
|
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.roll.players)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
|
import { abilities } from '../../../config/actorConfig.mjs';
|
||||||
|
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
export default class SaveField extends fields.SchemaField {
|
export default class SaveField extends fields.SchemaField {
|
||||||
|
/**
|
||||||
|
* Action Workflow order
|
||||||
|
*/
|
||||||
|
static order = 50;
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
constructor(options = {}, context = {}) {
|
constructor(options = {}, context = {}) {
|
||||||
const saveFields = {
|
const saveFields = {
|
||||||
trait: new fields.StringField({
|
trait: new fields.StringField({
|
||||||
|
|
@ -11,9 +19,164 @@ export default class SaveField extends fields.SchemaField {
|
||||||
difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }),
|
difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }),
|
||||||
damageMod: new fields.StringField({
|
damageMod: new fields.StringField({
|
||||||
initial: CONFIG.DH.ACTIONS.damageOnSave.none.id,
|
initial: CONFIG.DH.ACTIONS.damageOnSave.none.id,
|
||||||
choices: CONFIG.DH.ACTIONS.damageOnSave
|
choices: CONFIG.DH.ACTIONS.damageOnSave,
|
||||||
|
nullable: false,
|
||||||
|
required: true
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
super(saveFields, options, context);
|
super(saveFields, options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reaction Roll Action Workflow part.
|
||||||
|
* Must be called within Action context or similar.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
* @param {object[]} [targets=null] Array of targets to override pre-selected ones.
|
||||||
|
* @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example.
|
||||||
|
*/
|
||||||
|
static async execute(config, targets = null, force = false) {
|
||||||
|
if (!config.hasSave) return;
|
||||||
|
let message = config.message ?? ui.chat.collection.get(config.parent?._id);
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
|
||||||
|
roll._evaluated = true;
|
||||||
|
message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);
|
||||||
|
}
|
||||||
|
if (SaveField.getAutomation() !== CONFIG.DH.SETTINGS.actionAutomationChoices.never.id || force) {
|
||||||
|
targets ??= config.targets.filter(t => !config.hasRoll || t.hit);
|
||||||
|
await SaveField.rollAllSave.call(this, targets, config.event, message);
|
||||||
|
} else return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roll a Reaction Roll for all targets. Send a query to the owner if the User is not.
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {object[]} targets Array of formatted targets.
|
||||||
|
* @param {Event} event Triggering event
|
||||||
|
* @param {ChatMessage} message The ChatMessage the triggered button comes from.
|
||||||
|
*/
|
||||||
|
static async rollAllSave(targets, event, message) {
|
||||||
|
if (!targets) return;
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const aPromise = [];
|
||||||
|
targets.forEach(target => {
|
||||||
|
aPromise.push(
|
||||||
|
new Promise(async subResolve => {
|
||||||
|
const actor = fromUuidSync(target.actorId);
|
||||||
|
if (actor) {
|
||||||
|
const rollSave =
|
||||||
|
game.user === actor.owner
|
||||||
|
? SaveField.rollSave.call(this, actor, event)
|
||||||
|
: actor.owner.query('reactionRoll', {
|
||||||
|
actionId: this.uuid,
|
||||||
|
actorId: actor.uuid,
|
||||||
|
event,
|
||||||
|
message
|
||||||
|
});
|
||||||
|
const result = await rollSave;
|
||||||
|
await SaveField.updateSaveMessage.call(this, result, message, target.id);
|
||||||
|
subResolve();
|
||||||
|
} else subResolve();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Promise.all(aPromise).then(result => resolve());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roll a Reaction Roll for the specified Actor against the Action difficulty.
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {*} actor Actor document
|
||||||
|
* @param {Event} event Triggering event
|
||||||
|
* @returns {object} Actor diceRoll config result.
|
||||||
|
*/
|
||||||
|
static async rollSave(actor, event) {
|
||||||
|
if (!actor) return;
|
||||||
|
const title = actor.isNPC
|
||||||
|
? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll')
|
||||||
|
: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
|
||||||
|
ability: game.i18n.localize(abilities[this.save.trait]?.label)
|
||||||
|
}),
|
||||||
|
rollConfig = {
|
||||||
|
event,
|
||||||
|
title,
|
||||||
|
roll: {
|
||||||
|
trait: this.save.trait,
|
||||||
|
difficulty: this.save.difficulty ?? this.actor?.baseSaveDifficulty,
|
||||||
|
type: 'trait'
|
||||||
|
},
|
||||||
|
actionType: 'reaction',
|
||||||
|
hasRoll: true,
|
||||||
|
data: actor.getRollData()
|
||||||
|
};
|
||||||
|
if (SaveField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.always.id)
|
||||||
|
rollConfig.dialog = { configure: false };
|
||||||
|
return actor.diceRoll(rollConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a Roll ChatMessage for a token according to his Reaction Roll result.
|
||||||
|
* @param {object} result Result from the Reaction Roll
|
||||||
|
* @param {object} message ChatMessage to update
|
||||||
|
* @param {string} targetId Token ID
|
||||||
|
*/
|
||||||
|
static async updateSaveMessage(result, message, targetId) {
|
||||||
|
if (!result) return;
|
||||||
|
const updateMsg = async function (message, targetId, result) {
|
||||||
|
// setTimeout(async () => {
|
||||||
|
const chatMessage = ui.chat.collection.get(message._id),
|
||||||
|
changes = {
|
||||||
|
flags: {
|
||||||
|
[game.system.id]: {
|
||||||
|
reactionRolls: {
|
||||||
|
[targetId]: {
|
||||||
|
result: result.roll.total,
|
||||||
|
success: result.roll.success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await chatMessage.update(changes);
|
||||||
|
// }, 100);
|
||||||
|
};
|
||||||
|
if (game.modules.get('dice-so-nice')?.active)
|
||||||
|
game.dice3d
|
||||||
|
.waitFor3DAnimationByMessageID(result.message.id ?? result.message._id)
|
||||||
|
.then(async () => await updateMsg(message, targetId, result));
|
||||||
|
else await updateMsg(message, targetId, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the automation setting for execute method for current user role
|
||||||
|
* @returns {string} Id from settingsConfig.mjs actionAutomationChoices
|
||||||
|
*/
|
||||||
|
static getAutomation() {
|
||||||
|
return (
|
||||||
|
(game.user.isGM &&
|
||||||
|
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.save.gm) ||
|
||||||
|
(!game.user.isGM &&
|
||||||
|
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.save.players)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a query to an Actor owner to roll a Reaction Roll then send back the result.
|
||||||
|
* @param {object} param0
|
||||||
|
* @param {string} param0.actionId Action ID
|
||||||
|
* @param {string} param0.actorId Actor ID
|
||||||
|
* @param {Event} param0.event Triggering event
|
||||||
|
* @param {ChatMessage} param0.message Chat Message to update
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
static rollSaveQuery({ actionId, actorId, event, message }) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
const actor = await fromUuid(actorId),
|
||||||
|
action = await fromUuid(actionId);
|
||||||
|
if (!actor || !actor?.isOwner) reject();
|
||||||
|
SaveField.rollSave.call(action, actor, event, message).then(result => resolve(result));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,80 @@
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
export default class TargetField extends fields.SchemaField {
|
export default class TargetField extends fields.SchemaField {
|
||||||
|
/** @inheritDoc */
|
||||||
constructor(options = {}, context = {}) {
|
constructor(options = {}, context = {}) {
|
||||||
const targetFields = {
|
const targetFields = {
|
||||||
type: new fields.StringField({
|
type: new fields.StringField({
|
||||||
choices: CONFIG.DH.GENERAL.targetTypes,
|
choices: CONFIG.DH.GENERAL.targetTypes,
|
||||||
initial: CONFIG.DH.GENERAL.targetTypes.any.id,
|
initial: CONFIG.DH.GENERAL.targetTypes.any.id,
|
||||||
nullable: true
|
nullable: true,
|
||||||
|
blank: true
|
||||||
}),
|
}),
|
||||||
amount: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 })
|
amount: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 })
|
||||||
};
|
};
|
||||||
super(targetFields, options, context);
|
super(targetFields, options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
static prepareConfig(config) {
|
/**
|
||||||
if (!this.target?.type) return [];
|
* Update Action Workflow config object.
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
*/
|
||||||
|
prepareConfig(config) {
|
||||||
|
if (!this.target?.type) return (config.targets = []);
|
||||||
config.hasTarget = true;
|
config.hasTarget = true;
|
||||||
let targets;
|
let targets;
|
||||||
|
// If the Action is configured as self-targeted, set targets as the owner.
|
||||||
if (this.target?.type === CONFIG.DH.GENERAL.targetTypes.self.id)
|
if (this.target?.type === CONFIG.DH.GENERAL.targetTypes.self.id)
|
||||||
targets = [this.actor.token ?? this.actor.prototypeToken];
|
targets = [this.actor.token ?? this.actor.prototypeToken];
|
||||||
else {
|
else {
|
||||||
targets = Array.from(game.user.targets);
|
targets = Array.from(game.user.targets);
|
||||||
if (this.target.type !== CONFIG.DH.GENERAL.targetTypes.any.id) {
|
if (this.target.type !== CONFIG.DH.GENERAL.targetTypes.any.id) {
|
||||||
targets = targets.filter(t => TargetField.isTargetFriendly.call(this, t));
|
targets = targets.filter(target => TargetField.isTargetFriendly(this.actor, target, this.target.type));
|
||||||
if (this.target.amount && targets.length > this.target.amount) targets = [];
|
if (this.target.amount && targets.length > this.target.amount) targets = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
config.targets = targets.map(t => TargetField.formatTarget.call(this, t));
|
config.targets = targets.map(t => TargetField.formatTarget.call(this, t));
|
||||||
const hasTargets = TargetField.checkTargets.call(this, this.target.amount, config.targets);
|
const hasTargets = TargetField.checkTargets.call(this, this.target.amount, config.targets);
|
||||||
if (config.isFastForward && !hasTargets)
|
if (config.dialog.configure === false && !hasTargets) {
|
||||||
return ui.notifications.warn('Too many targets selected for that actions.');
|
ui.notifications.warn('Too many targets selected for that actions.');
|
||||||
return hasTargets;
|
return hasTargets;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the number of selected targets respect the amount set in the Action.
|
||||||
|
* NOT YET IMPLEMENTED. Will be with Target Picker.
|
||||||
|
* @param {number} amount Max amount of targets configured in the action.
|
||||||
|
* @param {*[]} targets Array of targeted tokens.
|
||||||
|
* @returns {boolean} If the amount of targeted tokens does not exceed action configured one.
|
||||||
|
*/
|
||||||
static checkTargets(amount, targets) {
|
static checkTargets(amount, targets) {
|
||||||
return true;
|
return true;
|
||||||
// return !amount || (targets.length > amount);
|
// return !amount || (targets.length > amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
static isTargetFriendly(target) {
|
/**
|
||||||
const actorDisposition = this.actor.token
|
* Compare 2 Actors disposition between each other
|
||||||
? this.actor.token.disposition
|
* @param {*} actor First actor document.
|
||||||
: this.actor.prototypeToken.disposition,
|
* @param {*} target Second actor document.
|
||||||
|
* @param {string} type Disposition id to compare (friendly/hostile).
|
||||||
|
* @returns {boolean} If both actors respect the provided type.
|
||||||
|
*/
|
||||||
|
static isTargetFriendly(actor, target, type) {
|
||||||
|
const actorDisposition = actor.token ? actor.token.disposition : actor.prototypeToken.disposition,
|
||||||
targetDisposition = target.document.disposition;
|
targetDisposition = target.document.disposition;
|
||||||
return (
|
return (
|
||||||
(this.target.type === CONFIG.DH.GENERAL.targetTypes.friendly.id &&
|
(type === CONFIG.DH.GENERAL.targetTypes.friendly.id && actorDisposition === targetDisposition) ||
|
||||||
actorDisposition === targetDisposition) ||
|
(type === CONFIG.DH.GENERAL.targetTypes.hostile.id && actorDisposition + targetDisposition === 0)
|
||||||
(this.target.type === CONFIG.DH.GENERAL.targetTypes.hostile.id &&
|
|
||||||
actorDisposition + targetDisposition === 0)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format actor to useful datas for Action roll workflow.
|
||||||
|
* @param {*} actor Actor object to format.
|
||||||
|
* @returns {*} Formatted Actor.
|
||||||
|
*/
|
||||||
static formatTarget(actor) {
|
static formatTarget(actor) {
|
||||||
return {
|
return {
|
||||||
id: actor.id,
|
id: actor.id,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,12 @@ import FormulaField from '../formulaField.mjs';
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
export default class UsesField extends fields.SchemaField {
|
export default class UsesField extends fields.SchemaField {
|
||||||
|
/**
|
||||||
|
* Action Workflow order
|
||||||
|
*/
|
||||||
|
static order = 160;
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
constructor(options = {}, context = {}) {
|
constructor(options = {}, context = {}) {
|
||||||
const usesFields = {
|
const usesFields = {
|
||||||
value: new fields.NumberField({ nullable: true, initial: null }),
|
value: new fields.NumberField({ nullable: true, initial: null }),
|
||||||
|
|
@ -20,15 +26,45 @@ export default class UsesField extends fields.SchemaField {
|
||||||
super(usesFields, options, context);
|
super(usesFields, options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
static prepareConfig(config) {
|
/**
|
||||||
|
* Uses Consumption Action Workflow part.
|
||||||
|
* Increment Action spent uses by 1.
|
||||||
|
* Must be called within Action context or similar or similar.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
* @param {boolean} [successCost=false] Consume only resources configured as "On Success only" if not already consumed.
|
||||||
|
*/
|
||||||
|
static async execute(config, successCost = false) {
|
||||||
|
if (
|
||||||
|
config.uses?.enabled &&
|
||||||
|
((!successCost && (!config.uses?.consumeOnSuccess || config.roll?.success)) ||
|
||||||
|
(successCost && config.uses?.consumeOnSuccess))
|
||||||
|
)
|
||||||
|
this.update({ 'uses.value': this.uses.value + 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Action Workflow config object.
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
* @returns {boolean} Return false if fast-forwarded and no more uses.
|
||||||
|
*/
|
||||||
|
prepareConfig(config) {
|
||||||
const uses = this.uses?.max ? foundry.utils.deepClone(this.uses) : null;
|
const uses = this.uses?.max ? foundry.utils.deepClone(this.uses) : null;
|
||||||
if (uses && !uses.value) uses.value = 0;
|
if (uses && !uses.value) uses.value = 0;
|
||||||
config.uses = uses;
|
config.uses = uses;
|
||||||
const hasUses = UsesField.hasUses.call(this, config.uses);
|
const hasUses = UsesField.hasUses.call(this, config.uses);
|
||||||
if (config.isFastForward && !hasUses) return ui.notifications.warn("That action doesn't have remaining uses.");
|
if (config.dialog.configure === false && !hasUses) {
|
||||||
return hasUses;
|
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.actionNoUsesRemaining'));
|
||||||
|
return hasUses;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare Uses object for Action Workflow
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {object} uses
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
static calcUses(uses) {
|
static calcUses(uses) {
|
||||||
if (!uses) return null;
|
if (!uses) return null;
|
||||||
return {
|
return {
|
||||||
|
|
@ -38,6 +74,12 @@ export default class UsesField extends fields.SchemaField {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the Action still get atleast one unspent uses.
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {*} uses
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
static hasUses(uses) {
|
static hasUses(uses) {
|
||||||
if (!uses) return true;
|
if (!uses) return true;
|
||||||
let max = uses.max ?? 0;
|
let max = uses.max ?? 0;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import AttachableItem from './attachableItem.mjs';
|
import AttachableItem from './attachableItem.mjs';
|
||||||
import { armorFeatures } from '../../config/itemConfig.mjs';
|
|
||||||
|
|
||||||
export default class DHArmor extends AttachableItem {
|
export default class DHArmor extends AttachableItem {
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
|
|
@ -25,7 +24,7 @@ export default class DHArmor extends AttachableItem {
|
||||||
new fields.SchemaField({
|
new fields.SchemaField({
|
||||||
value: new fields.StringField({
|
value: new fields.StringField({
|
||||||
required: true,
|
required: true,
|
||||||
choices: CONFIG.DH.ITEM.armorFeatures,
|
choices: CONFIG.DH.ITEM.allArmorFeatures,
|
||||||
blank: true
|
blank: true
|
||||||
}),
|
}),
|
||||||
effectIds: new fields.ArrayField(new fields.StringField({ required: true })),
|
effectIds: new fields.ArrayField(new fields.StringField({ required: true })),
|
||||||
|
|
@ -60,13 +59,14 @@ export default class DHArmor extends AttachableItem {
|
||||||
const allowed = await super._preUpdate(changes, options, user);
|
const allowed = await super._preUpdate(changes, options, user);
|
||||||
if (allowed === false) return false;
|
if (allowed === false) return false;
|
||||||
|
|
||||||
|
const changedArmorFeatures = changes.system?.armorFeatures ?? [];
|
||||||
|
const removedFeatures = this.armorFeatures.filter(x => changedArmorFeatures.every(y => y.value !== x.value));
|
||||||
if (changes.system?.armorFeatures) {
|
if (changes.system?.armorFeatures) {
|
||||||
const removed = this.armorFeatures.filter(x => !changes.system.armorFeatures.includes(x));
|
const added = changedArmorFeatures.filter(x => this.armorFeatures.every(y => y.value !== x.value));
|
||||||
const added = changes.system.armorFeatures.filter(x => !this.armorFeatures.includes(x));
|
|
||||||
|
|
||||||
const effectIds = [];
|
const effectIds = [];
|
||||||
const actionIds = [];
|
const actionIds = [];
|
||||||
for (var feature of removed) {
|
for (var feature of removedFeatures) {
|
||||||
effectIds.push(...feature.effectIds);
|
effectIds.push(...feature.effectIds);
|
||||||
actionIds.push(...feature.actionIds);
|
actionIds.push(...feature.actionIds);
|
||||||
}
|
}
|
||||||
|
|
@ -76,8 +76,9 @@ export default class DHArmor extends AttachableItem {
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
const allFeatures = CONFIG.DH.ITEM.allArmorFeatures();
|
||||||
for (const feature of added) {
|
for (const feature of added) {
|
||||||
const featureData = armorFeatures[feature.value];
|
const featureData = allFeatures[feature.value];
|
||||||
if (featureData.effects?.length > 0) {
|
if (featureData.effects?.length > 0) {
|
||||||
const embeddedItems = await this.parent.createEmbeddedDocuments(
|
const embeddedItems = await this.parent.createEmbeddedDocuments(
|
||||||
'ActiveEffect',
|
'ActiveEffect',
|
||||||
|
|
@ -91,7 +92,7 @@ export default class DHArmor extends AttachableItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
const newActions = {};
|
const newActions = {};
|
||||||
if (featureData.actions?.length > 0) {
|
if (featureData.actions?.length > 0 || featureData.actions?.size > 0) {
|
||||||
for (let action of featureData.actions) {
|
for (let action of featureData.actions) {
|
||||||
const embeddedEffects = await this.parent.createEmbeddedDocuments(
|
const embeddedEffects = await this.parent.createEmbeddedDocuments(
|
||||||
'ActiveEffect',
|
'ActiveEffect',
|
||||||
|
|
@ -110,10 +111,12 @@ export default class DHArmor extends AttachableItem {
|
||||||
{
|
{
|
||||||
...cls.getSourceConfig(this),
|
...cls.getSourceConfig(this),
|
||||||
...action,
|
...action,
|
||||||
|
type: action.type,
|
||||||
_id: actionId,
|
_id: actionId,
|
||||||
name: game.i18n.localize(action.name),
|
name: game.i18n.localize(action.name),
|
||||||
description: game.i18n.localize(action.description),
|
description: game.i18n.localize(action.description),
|
||||||
effects: embeddedEffects.map(x => ({ _id: x.id }))
|
effects: embeddedEffects.map(x => ({ _id: x.id })),
|
||||||
|
systemPath: 'actions'
|
||||||
},
|
},
|
||||||
{ parent: this }
|
{ parent: this }
|
||||||
);
|
);
|
||||||
|
|
@ -126,6 +129,10 @@ export default class DHArmor extends AttachableItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onUpdate(a, b, c) {
|
||||||
|
super._onUpdate(a, b, c);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a list of localized tags based on this item's type-specific properties.
|
* Generates a list of localized tags based on this item's type-specific properties.
|
||||||
* @returns {string[]} An array of localized tag strings.
|
* @returns {string[]} An array of localized tag strings.
|
||||||
|
|
@ -145,7 +152,8 @@ export default class DHArmor extends AttachableItem {
|
||||||
*/
|
*/
|
||||||
_getLabels() {
|
_getLabels() {
|
||||||
const labels = [];
|
const labels = [];
|
||||||
if(this.baseScore) labels.push(`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.baseScore}`)
|
if (this.baseScore)
|
||||||
|
labels.push(`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.baseScore}`);
|
||||||
return labels;
|
return labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -158,50 +158,30 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.actor && this.actor.type === 'character' && this.features) {
|
if (this.actor && this.actor.type === 'character' && this.features) {
|
||||||
const featureUpdates = {};
|
const features = [];
|
||||||
for (let f of this.features) {
|
for (let f of this.features) {
|
||||||
const fBase = f.item ?? f;
|
const fBase = f.item ?? f;
|
||||||
const feature = fBase.system ? fBase : await foundry.utils.fromUuid(fBase.uuid);
|
const feature = fBase.system ? fBase : await foundry.utils.fromUuid(fBase.uuid);
|
||||||
const createData = foundry.utils.mergeObject(
|
features.push(
|
||||||
feature.toObject(),
|
foundry.utils.mergeObject(
|
||||||
{
|
feature.toObject(),
|
||||||
system: {
|
{
|
||||||
originItemType: this.parent.type,
|
_stats: { compendiumSource: fBase.uuid },
|
||||||
originId: data._id,
|
system: {
|
||||||
identifier: this.isMulticlass ? 'multiclass' : null
|
originItemType: this.parent.type,
|
||||||
}
|
identifier: f.item ? f.type : null,
|
||||||
},
|
multiclassOrigin: this.isMulticlass
|
||||||
{ inplace: false }
|
}
|
||||||
|
},
|
||||||
|
{ inplace: false }
|
||||||
|
)
|
||||||
);
|
);
|
||||||
const [doc] = await this.actor.createEmbeddedDocuments('Item', [createData]);
|
|
||||||
|
|
||||||
if (!featureUpdates.features)
|
|
||||||
featureUpdates.features = this.features.map(x => (x.item ? { ...x, item: x.item.uuid } : x.uuid));
|
|
||||||
|
|
||||||
if (f.item) {
|
|
||||||
const existingFeature = featureUpdates.features.find(x => x.item === f.item.uuid);
|
|
||||||
existingFeature.item = doc.uuid;
|
|
||||||
} else {
|
|
||||||
const replaceIndex = featureUpdates.features.findIndex(x => x === f.uuid);
|
|
||||||
featureUpdates.features.splice(replaceIndex, 1, doc.uuid);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.updateSource(featureUpdates);
|
await this.actor.createEmbeddedDocuments('Item', features);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _preDelete() {
|
|
||||||
if (!this.actor || this.actor.type !== 'character') return;
|
|
||||||
|
|
||||||
const items = this.actor.items.filter(item => item.system.originId === this.parent.id);
|
|
||||||
if (items.length > 0)
|
|
||||||
await this.actor.deleteEmbeddedDocuments(
|
|
||||||
'Item',
|
|
||||||
items.map(x => x.id)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async _preUpdate(changed, options, userId) {
|
async _preUpdate(changed, options, userId) {
|
||||||
const allowed = await super._preUpdate(changed, options, userId);
|
const allowed = await super._preUpdate(changed, options, userId);
|
||||||
if (allowed === false) return false;
|
if (allowed === false) return false;
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,8 @@ export default class DHClass extends BaseDataItem {
|
||||||
suggestedSecondaryWeapon: new ForeignDocumentUUIDField({ type: 'Item' }),
|
suggestedSecondaryWeapon: new ForeignDocumentUUIDField({ type: 'Item' }),
|
||||||
suggestedArmor: new ForeignDocumentUUIDField({ type: 'Item' })
|
suggestedArmor: new ForeignDocumentUUIDField({ type: 'Item' })
|
||||||
}),
|
}),
|
||||||
|
backgroundQuestions: new fields.ArrayField(new fields.StringField(), { initial: ['', '', ''] }),
|
||||||
|
connections: new fields.ArrayField(new fields.StringField(), { initial: ['', '', ''] }),
|
||||||
isMulticlass: new fields.BooleanField({ initial: false })
|
isMulticlass: new fields.BooleanField({ initial: false })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -96,6 +98,20 @@ export default class DHClass extends BaseDataItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!data.system.isMulticlass) {
|
||||||
|
const addQuestions = (base, questions) => {
|
||||||
|
return `${base}${questions.map(q => `<p><strong>${q}</strong></p>`).join('<br/>')}`;
|
||||||
|
};
|
||||||
|
const backgroundQuestions = data.system.backgroundQuestions.filter(x => x);
|
||||||
|
const connections = data.system.connections.filter(x => x);
|
||||||
|
await this.actor.update({
|
||||||
|
'system.biography': {
|
||||||
|
background: addQuestions(this.actor.system.biography.background, backgroundQuestions),
|
||||||
|
connections: addQuestions(this.actor.system.biography.connections, connections)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowed = await super._preCreate(data, options, user);
|
const allowed = await super._preCreate(data, options, user);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import BaseDataItem from './base.mjs';
|
import BaseDataItem from './base.mjs';
|
||||||
import { ActionField } from '../fields/actionField.mjs';
|
|
||||||
|
|
||||||
export default class DHConsumable extends BaseDataItem {
|
export default class DHConsumable extends BaseDataItem {
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
|
|
@ -19,7 +18,8 @@ export default class DHConsumable extends BaseDataItem {
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
return {
|
return {
|
||||||
...super.defineSchema(),
|
...super.defineSchema(),
|
||||||
consumeOnUse: new fields.BooleanField({ initial: false })
|
consumeOnUse: new fields.BooleanField({ initial: true }),
|
||||||
|
destroyOnEmpty: new fields.BooleanField({ initial: true })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,5 +27,4 @@ export default class DHConsumable extends BaseDataItem {
|
||||||
|
|
||||||
/**@override */
|
/**@override */
|
||||||
static DEFAULT_ICON = 'systems/daggerheart/assets/icons/documents/items/round-potion.svg';
|
static DEFAULT_ICON = 'systems/daggerheart/assets/icons/documents/items/round-potion.svg';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import BaseDataItem from './base.mjs';
|
import BaseDataItem from './base.mjs';
|
||||||
import { ActionField, ActionsField } from '../fields/actionField.mjs';
|
|
||||||
|
|
||||||
export default class DHFeature extends BaseDataItem {
|
export default class DHFeature extends BaseDataItem {
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
|
|
@ -30,24 +29,8 @@ export default class DHFeature extends BaseDataItem {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
initial: null
|
initial: null
|
||||||
}),
|
}),
|
||||||
originId: new fields.StringField({ nullable: true, initial: null }),
|
multiclassOrigin: new fields.BooleanField({ initial: false }),
|
||||||
identifier: new fields.StringField()
|
identifier: new fields.StringField()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
get spellcastingModifier() {
|
|
||||||
let traitValue = 0;
|
|
||||||
if (this.actor && this.originId && ['class', 'subclass'].includes(this.originItemType)) {
|
|
||||||
if (this.originItemType === 'subclass') {
|
|
||||||
traitValue =
|
|
||||||
this.actor.system.traits[this.actor.items.get(this.originId).system.spellcastingTrait]?.value ?? 0;
|
|
||||||
} else {
|
|
||||||
const { value: multiclass, subclass } = this.actor.system.multiclass;
|
|
||||||
const selectedSubclass = multiclass?.id === this.originId ? subclass : this.actor.system.class.subclass;
|
|
||||||
traitValue = this.actor.system.traits[selectedSubclass.system.spellcastingTrait]?.value ?? 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return traitValue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
|
||||||
import ItemLinkFields from '../fields/itemLinkFields.mjs';
|
import ItemLinkFields from '../fields/itemLinkFields.mjs';
|
||||||
import BaseDataItem from './base.mjs';
|
import BaseDataItem from './base.mjs';
|
||||||
|
|
||||||
|
|
@ -25,7 +26,8 @@ export default class DHSubclass extends BaseDataItem {
|
||||||
}),
|
}),
|
||||||
features: new ItemLinkFields(),
|
features: new ItemLinkFields(),
|
||||||
featureState: new fields.NumberField({ required: true, initial: 1, min: 1 }),
|
featureState: new fields.NumberField({ required: true, initial: 1, min: 1 }),
|
||||||
isMulticlass: new fields.BooleanField({ initial: false })
|
isMulticlass: new fields.BooleanField({ initial: false }),
|
||||||
|
linkedClass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true, initial: null })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,8 +52,7 @@ export default class DHSubclass extends BaseDataItem {
|
||||||
|
|
||||||
async _preCreate(data, options, user) {
|
async _preCreate(data, options, user) {
|
||||||
if (this.actor?.type === 'character') {
|
if (this.actor?.type === 'character') {
|
||||||
const dataUuid =
|
const dataUuid = data.uuid ?? data._stats.compendiumSource ?? `Item.${data._id}`;
|
||||||
data.uuid ?? (data.folder ? `Compendium.daggerheart.subclasses.Item.${data._id}` : `Item.${data._id}`);
|
|
||||||
if (this.actor.system.class.subclass) {
|
if (this.actor.system.class.subclass) {
|
||||||
if (this.actor.system.multiclass.subclass) {
|
if (this.actor.system.multiclass.subclass) {
|
||||||
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassesAlreadyPresent'));
|
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassesAlreadyPresent'));
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ export default class DHWeapon extends AttachableItem {
|
||||||
new fields.SchemaField({
|
new fields.SchemaField({
|
||||||
value: new fields.StringField({
|
value: new fields.StringField({
|
||||||
required: true,
|
required: true,
|
||||||
choices: CONFIG.DH.ITEM.weaponFeatures,
|
choices: CONFIG.DH.ITEM.allWeaponFeatures,
|
||||||
blank: true
|
blank: true
|
||||||
}),
|
}),
|
||||||
effectIds: new fields.ArrayField(new fields.StringField({ required: true })),
|
effectIds: new fields.ArrayField(new fields.StringField({ required: true })),
|
||||||
|
|
@ -116,13 +116,14 @@ export default class DHWeapon extends AttachableItem {
|
||||||
const allowed = await super._preUpdate(changes, options, user);
|
const allowed = await super._preUpdate(changes, options, user);
|
||||||
if (allowed === false) return false;
|
if (allowed === false) return false;
|
||||||
|
|
||||||
|
const changedWeaponFeatures = changes.system?.weaponFeatures ?? [];
|
||||||
|
const removedFeatures = this.weaponFeatures.filter(x => changedWeaponFeatures.every(y => y.value !== x.value));
|
||||||
if (changes.system?.weaponFeatures) {
|
if (changes.system?.weaponFeatures) {
|
||||||
const removed = this.weaponFeatures.filter(x => !changes.system.weaponFeatures.includes(x));
|
const added = changedWeaponFeatures.filter(x => this.weaponFeatures.every(y => y.value !== x.value));
|
||||||
const added = changes.system.weaponFeatures.filter(x => !this.weaponFeatures.includes(x));
|
|
||||||
|
|
||||||
const removedEffectsUpdate = [];
|
const removedEffectsUpdate = [];
|
||||||
const removedActionsUpdate = [];
|
const removedActionsUpdate = [];
|
||||||
for (let weaponFeature of removed) {
|
for (let weaponFeature of removedFeatures) {
|
||||||
removedEffectsUpdate.push(...weaponFeature.effectIds);
|
removedEffectsUpdate.push(...weaponFeature.effectIds);
|
||||||
removedActionsUpdate.push(...weaponFeature.actionIds);
|
removedActionsUpdate.push(...weaponFeature.actionIds);
|
||||||
}
|
}
|
||||||
|
|
@ -133,8 +134,9 @@ export default class DHWeapon extends AttachableItem {
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
const allFeatures = CONFIG.DH.ITEM.allWeaponFeatures();
|
||||||
for (let weaponFeature of added) {
|
for (let weaponFeature of added) {
|
||||||
const featureData = CONFIG.DH.ITEM.weaponFeatures[weaponFeature.value];
|
const featureData = allFeatures[weaponFeature.value];
|
||||||
if (featureData.effects?.length > 0) {
|
if (featureData.effects?.length > 0) {
|
||||||
const embeddedItems = await this.parent.createEmbeddedDocuments(
|
const embeddedItems = await this.parent.createEmbeddedDocuments(
|
||||||
'ActiveEffect',
|
'ActiveEffect',
|
||||||
|
|
@ -148,7 +150,7 @@ export default class DHWeapon extends AttachableItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
const newActions = {};
|
const newActions = {};
|
||||||
if (featureData.actions?.length > 0) {
|
if (featureData.actions?.length > 0 || featureData.actions?.size > 0) {
|
||||||
for (let action of featureData.actions) {
|
for (let action of featureData.actions) {
|
||||||
const embeddedEffects = await this.parent.createEmbeddedDocuments(
|
const embeddedEffects = await this.parent.createEmbeddedDocuments(
|
||||||
'ActiveEffect',
|
'ActiveEffect',
|
||||||
|
|
@ -170,10 +172,12 @@ export default class DHWeapon extends AttachableItem {
|
||||||
{
|
{
|
||||||
...cls.getSourceConfig(this),
|
...cls.getSourceConfig(this),
|
||||||
...action,
|
...action,
|
||||||
|
type: action.type,
|
||||||
_id: actionId,
|
_id: actionId,
|
||||||
name: game.i18n.localize(action.name),
|
name: game.i18n.localize(action.name),
|
||||||
description: game.i18n.localize(action.description),
|
description: game.i18n.localize(action.description),
|
||||||
effects: embeddedEffects.map(x => ({ _id: x.id }))
|
effects: embeddedEffects.map(x => ({ _id: x.id })),
|
||||||
|
systemPath: 'actions'
|
||||||
},
|
},
|
||||||
{ parent: this }
|
{ parent: this }
|
||||||
);
|
);
|
||||||
|
|
@ -199,8 +203,8 @@ export default class DHWeapon extends AttachableItem {
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const { value, type } of attack.damage.parts) {
|
for (const { value, type } of attack.damage.parts) {
|
||||||
const parts = [value.dice];
|
const parts = value.custom.enabled ? [game.i18n.localize('DAGGERHEART.GENERAL.custom')] : [value.dice];
|
||||||
if (value.bonus) parts.push(value.bonus.signedString());
|
if (!value.custom.enabled && value.bonus) parts.push(value.bonus.signedString());
|
||||||
|
|
||||||
if (type.size > 0) {
|
if (type.size > 0) {
|
||||||
const typeTags = Array.from(type)
|
const typeTags = Array.from(type)
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ export const defaultLevelTiers = {
|
||||||
tiers: {
|
tiers: {
|
||||||
2: {
|
2: {
|
||||||
tier: 2,
|
tier: 2,
|
||||||
name: 'Tier 2',
|
name: 'DAGGERHEART.APPLICATIONS.Levelup.tier2.name',
|
||||||
levels: {
|
levels: {
|
||||||
start: 2,
|
start: 2,
|
||||||
end: 4
|
end: 4
|
||||||
|
|
@ -232,7 +232,7 @@ export const defaultLevelTiers = {
|
||||||
},
|
},
|
||||||
3: {
|
3: {
|
||||||
tier: 3,
|
tier: 3,
|
||||||
name: 'Tier 3',
|
name: 'DAGGERHEART.APPLICATIONS.Levelup.tier3.name',
|
||||||
levels: {
|
levels: {
|
||||||
start: 5,
|
start: 5,
|
||||||
end: 7
|
end: 7
|
||||||
|
|
@ -313,7 +313,7 @@ export const defaultLevelTiers = {
|
||||||
},
|
},
|
||||||
4: {
|
4: {
|
||||||
tier: 4,
|
tier: 4,
|
||||||
name: 'Tier 4',
|
name: 'DAGGERHEART.APPLICATIONS.Levelup.tier4.name',
|
||||||
levels: {
|
levels: {
|
||||||
start: 8,
|
start: 8,
|
||||||
end: 10
|
end: 10
|
||||||
|
|
|
||||||
|
|
@ -234,7 +234,7 @@ export class DhLevelup extends foundry.abstract.DataModel {
|
||||||
const subclassInTier = subclasses.some(x => x.tier === Number(tierKey));
|
const subclassInTier = subclasses.some(x => x.tier === Number(tierKey));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: tier.name,
|
name: game.i18n.localize(tier.name),
|
||||||
active: this.currentLevel >= Math.min(...tier.belongingLevels),
|
active: this.currentLevel >= Math.min(...tier.belongingLevels),
|
||||||
groups: Object.keys(tier.options).map(optionKey => {
|
groups: Object.keys(tier.options).map(optionKey => {
|
||||||
const option = tier.options[optionKey];
|
const option = tier.options[optionKey];
|
||||||
|
|
|
||||||
|
|
@ -1,100 +1,46 @@
|
||||||
import { fearDisplay } from '../../config/generalConfig.mjs';
|
|
||||||
|
|
||||||
export default class DhAppearance extends foundry.abstract.DataModel {
|
export default class DhAppearance extends foundry.abstract.DataModel {
|
||||||
|
static LOCALIZATION_PREFIXES = ['DAGGERHEART.SETTINGS.Appearance'];
|
||||||
|
|
||||||
static defineSchema() {
|
static defineSchema() {
|
||||||
const fields = foundry.data.fields;
|
const { StringField, ColorField, BooleanField, SchemaField } = foundry.data.fields;
|
||||||
|
|
||||||
|
// helper to create dice style schema
|
||||||
|
const diceStyle = ({ fg, bg, outline, edge }) =>
|
||||||
|
new SchemaField({
|
||||||
|
foreground: new ColorField({ required: true, initial: fg }),
|
||||||
|
background: new ColorField({ required: true, initial: bg }),
|
||||||
|
outline: new ColorField({ required: true, initial: outline }),
|
||||||
|
edge: new ColorField({ required: true, initial: edge }),
|
||||||
|
texture: new StringField({ initial: 'astralsea', required: true, blank: false }),
|
||||||
|
colorset: new StringField({ initial: 'inspired', required: true, blank: false }),
|
||||||
|
material: new StringField({ initial: 'metal', required: true, blank: false }),
|
||||||
|
system: new StringField({ initial: 'standard', required: true, blank: false })
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
displayFear: new fields.StringField({
|
displayFear: new StringField({
|
||||||
required: true,
|
required: true,
|
||||||
choices: fearDisplay,
|
choices: CONFIG.DH.GENERAL.fearDisplay,
|
||||||
initial: fearDisplay.token.value,
|
initial: CONFIG.DH.GENERAL.fearDisplay.token.value
|
||||||
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.displayFear.label'
|
|
||||||
}),
|
}),
|
||||||
diceSoNice: new fields.SchemaField({
|
diceSoNice: new SchemaField({
|
||||||
hope: new fields.SchemaField({
|
hope: diceStyle({ fg: '#ffffff', bg: '#ffe760', outline: '#000000', edge: '#ffffff' }),
|
||||||
foreground: new fields.ColorField({ required: true, initial: '#ffffff' }),
|
fear: diceStyle({ fg: '#000000', bg: '#0032b1', outline: '#ffffff', edge: '#000000' }),
|
||||||
background: new fields.ColorField({ required: true, initial: '#ffe760' }),
|
advantage: diceStyle({ fg: '#ffffff', bg: '#008000', outline: '#000000', edge: '#ffffff' }),
|
||||||
outline: new fields.ColorField({ required: true, initial: '#000000' }),
|
disadvantage: diceStyle({ fg: '#000000', bg: '#b30000', outline: '#ffffff', edge: '#000000' })
|
||||||
edge: new fields.ColorField({ required: true, initial: '#ffffff' }),
|
|
||||||
texture: new fields.StringField({ initial: 'astralsea' }),
|
|
||||||
colorset: new fields.StringField({ initial: 'inspired' }),
|
|
||||||
material: new fields.StringField({ initial: 'metal' }),
|
|
||||||
system: new fields.StringField({ initial: 'standard' })
|
|
||||||
}),
|
|
||||||
fear: new fields.SchemaField({
|
|
||||||
foreground: new fields.ColorField({ required: true, initial: '#000000' }),
|
|
||||||
background: new fields.ColorField({ required: true, initial: '#0032b1' }),
|
|
||||||
outline: new fields.ColorField({ required: true, initial: '#ffffff' }),
|
|
||||||
edge: new fields.ColorField({ required: true, initial: '#000000' }),
|
|
||||||
texture: new fields.StringField({ initial: 'astralsea' }),
|
|
||||||
colorset: new fields.StringField({ initial: 'inspired' }),
|
|
||||||
material: new fields.StringField({ initial: 'metal' }),
|
|
||||||
system: new fields.StringField({ initial: 'standard' })
|
|
||||||
}),
|
|
||||||
advantage: new fields.SchemaField({
|
|
||||||
foreground: new fields.ColorField({ required: true, initial: '#ffffff' }),
|
|
||||||
background: new fields.ColorField({ required: true, initial: '#008000' }),
|
|
||||||
outline: new fields.ColorField({ required: true, initial: '#000000' }),
|
|
||||||
edge: new fields.ColorField({ required: true, initial: '#ffffff' }),
|
|
||||||
texture: new fields.StringField({ initial: 'astralsea' }),
|
|
||||||
colorset: new fields.StringField({ initial: 'inspired' }),
|
|
||||||
material: new fields.StringField({ initial: 'metal' }),
|
|
||||||
system: new fields.StringField({ initial: 'standard' })
|
|
||||||
}),
|
|
||||||
disadvantage: new fields.SchemaField({
|
|
||||||
foreground: new fields.ColorField({ required: true, initial: '#000000' }),
|
|
||||||
background: new fields.ColorField({ required: true, initial: '#b30000' }),
|
|
||||||
outline: new fields.ColorField({ required: true, initial: '#ffffff' }),
|
|
||||||
edge: new fields.ColorField({ required: true, initial: '#000000' }),
|
|
||||||
texture: new fields.StringField({ initial: 'astralsea' }),
|
|
||||||
colorset: new fields.StringField({ initial: 'inspired' }),
|
|
||||||
material: new fields.StringField({ initial: 'metal' }),
|
|
||||||
system: new fields.StringField({ initial: 'standard' })
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
showGenericStatusEffects: new fields.BooleanField({
|
extendCharacterDescriptions: new BooleanField(),
|
||||||
initial: true,
|
extendAdversaryDescriptions: new BooleanField(),
|
||||||
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.showGenericStatusEffects.label'
|
extendEnvironmentDescriptions: new BooleanField(),
|
||||||
|
extendItemDescriptions: new BooleanField(),
|
||||||
|
expandRollMessage: new SchemaField({
|
||||||
|
desc: new BooleanField(),
|
||||||
|
roll: new BooleanField(),
|
||||||
|
damage: new BooleanField(),
|
||||||
|
target: new BooleanField()
|
||||||
}),
|
}),
|
||||||
extendCharacterDescriptions: new fields.BooleanField({
|
hideAttribution: new BooleanField(),
|
||||||
initial: false,
|
showGenericStatusEffects: new BooleanField({ initial: true })
|
||||||
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.extendCharacterDescriptions.label'
|
|
||||||
}),
|
|
||||||
extendAdversaryDescriptions: new fields.BooleanField({
|
|
||||||
initial: false,
|
|
||||||
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.extendAdversaryDescriptions.label'
|
|
||||||
}),
|
|
||||||
extendEnvironmentDescriptions: new fields.BooleanField({
|
|
||||||
initial: false,
|
|
||||||
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.extendEnvironmentDescriptions.label'
|
|
||||||
}),
|
|
||||||
extendItemDescriptions: new fields.BooleanField({
|
|
||||||
initial: false,
|
|
||||||
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.extendItemDescriptions.label'
|
|
||||||
}),
|
|
||||||
expandRollMessage: new fields.SchemaField({
|
|
||||||
desc: new fields.BooleanField({
|
|
||||||
initial: false,
|
|
||||||
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.expandRollMessageDesc.label'
|
|
||||||
}),
|
|
||||||
roll: new fields.BooleanField({
|
|
||||||
initial: false,
|
|
||||||
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.expandRollMessageRoll.label'
|
|
||||||
}),
|
|
||||||
damage: new fields.BooleanField({
|
|
||||||
initial: false,
|
|
||||||
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.expandRollMessageDamage.label'
|
|
||||||
}),
|
|
||||||
target: new fields.BooleanField({
|
|
||||||
initial: false,
|
|
||||||
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.expandRollMessageTarget.label'
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
hideAttribution: new fields.BooleanField({
|
|
||||||
required: true,
|
|
||||||
initial: false,
|
|
||||||
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.hideAttribution.label'
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,72 @@ export default class DhAutomation extends foundry.abstract.DataModel {
|
||||||
initial: CONFIG.DH.GENERAL.defeatedConditions.defeated.id,
|
initial: CONFIG.DH.GENERAL.defeatedConditions.defeated.id,
|
||||||
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.companionDefault.label'
|
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.companionDefault.label'
|
||||||
})
|
})
|
||||||
|
}),
|
||||||
|
roll: new fields.SchemaField({
|
||||||
|
roll: new fields.SchemaField({
|
||||||
|
gm: new fields.BooleanField({
|
||||||
|
required: true,
|
||||||
|
initial: false,
|
||||||
|
label: 'DAGGERHEART.GENERAL.gm'
|
||||||
|
}),
|
||||||
|
players: new fields.BooleanField({
|
||||||
|
required: true,
|
||||||
|
initial: false,
|
||||||
|
label: 'DAGGERHEART.GENERAL.player.plurial'
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
damage: new fields.SchemaField({
|
||||||
|
gm: new fields.StringField({
|
||||||
|
required: true,
|
||||||
|
initial: 'never',
|
||||||
|
choices: CONFIG.DH.SETTINGS.actionAutomationChoices,
|
||||||
|
label: 'DAGGERHEART.GENERAL.gm'
|
||||||
|
}),
|
||||||
|
players: new fields.StringField({
|
||||||
|
required: true,
|
||||||
|
initial: 'never',
|
||||||
|
choices: CONFIG.DH.SETTINGS.actionAutomationChoices,
|
||||||
|
label: 'DAGGERHEART.GENERAL.player.plurial'
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
save: new fields.SchemaField({
|
||||||
|
gm: new fields.StringField({
|
||||||
|
required: true,
|
||||||
|
initial: 'never',
|
||||||
|
choices: CONFIG.DH.SETTINGS.actionAutomationChoices,
|
||||||
|
label: 'DAGGERHEART.GENERAL.gm'
|
||||||
|
}),
|
||||||
|
players: new fields.StringField({
|
||||||
|
required: true,
|
||||||
|
initial: 'never',
|
||||||
|
choices: CONFIG.DH.SETTINGS.actionAutomationChoices,
|
||||||
|
label: 'DAGGERHEART.GENERAL.player.plurial'
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
damageApply: new fields.SchemaField({
|
||||||
|
gm: new fields.BooleanField({
|
||||||
|
required: true,
|
||||||
|
initial: false,
|
||||||
|
label: 'DAGGERHEART.GENERAL.gm'
|
||||||
|
}),
|
||||||
|
players: new fields.BooleanField({
|
||||||
|
required: true,
|
||||||
|
initial: false,
|
||||||
|
label: 'DAGGERHEART.GENERAL.player.plurial'
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
effect: new fields.SchemaField({
|
||||||
|
gm: new fields.BooleanField({
|
||||||
|
required: true,
|
||||||
|
initial: false,
|
||||||
|
label: 'DAGGERHEART.GENERAL.gm'
|
||||||
|
}),
|
||||||
|
players: new fields.BooleanField({
|
||||||
|
required: true,
|
||||||
|
initial: false,
|
||||||
|
label: 'DAGGERHEART.GENERAL.player.plurial'
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,42 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
|
||||||
}),
|
}),
|
||||||
description: new fields.HTMLField()
|
description: new fields.HTMLField()
|
||||||
})
|
})
|
||||||
)
|
),
|
||||||
|
adversaryTypes: new fields.TypedObjectField(
|
||||||
|
new fields.SchemaField({
|
||||||
|
id: new fields.StringField({ required: true }),
|
||||||
|
label: new fields.StringField({ required: true, label: 'DAGGERHEART.GENERAL.label' }),
|
||||||
|
description: new fields.StringField()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
itemFeatures: new fields.SchemaField({
|
||||||
|
weaponFeatures: new fields.TypedObjectField(
|
||||||
|
new fields.SchemaField({
|
||||||
|
name: new fields.StringField({ required: true }),
|
||||||
|
img: new fields.FilePathField({
|
||||||
|
initial: 'icons/magic/life/cross-worn-green.webp',
|
||||||
|
categories: ['IMAGE'],
|
||||||
|
base64: false
|
||||||
|
}),
|
||||||
|
description: new fields.HTMLField(),
|
||||||
|
actions: new ActionsField(),
|
||||||
|
effects: new fields.ArrayField(new fields.ObjectField())
|
||||||
|
})
|
||||||
|
),
|
||||||
|
armorFeatures: new fields.TypedObjectField(
|
||||||
|
new fields.SchemaField({
|
||||||
|
name: new fields.StringField({ required: true }),
|
||||||
|
img: new fields.FilePathField({
|
||||||
|
initial: 'icons/magic/life/cross-worn-green.webp',
|
||||||
|
categories: ['IMAGE'],
|
||||||
|
base64: false
|
||||||
|
}),
|
||||||
|
description: new fields.HTMLField(),
|
||||||
|
actions: new ActionsField(),
|
||||||
|
effects: new fields.ArrayField(new fields.ObjectField())
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,9 @@ export default class D20Roll extends DHRoll {
|
||||||
applyBaseBonus() {
|
applyBaseBonus() {
|
||||||
const modifiers = foundry.utils.deepClone(this.options.roll.baseModifiers) ?? [];
|
const modifiers = foundry.utils.deepClone(this.options.roll.baseModifiers) ?? [];
|
||||||
|
|
||||||
modifiers.push(...this.getBonus(`roll.${this.options.type}`, `${this.options.type?.capitalize()} Bonus`));
|
modifiers.push(
|
||||||
|
...this.getBonus(`roll.${this.options.actionType}`, `${this.options.actionType?.capitalize()} Bonus`)
|
||||||
|
);
|
||||||
modifiers.push(
|
modifiers.push(
|
||||||
...this.getBonus(`roll.${this.options.roll.type}`, `${this.options.roll.type?.capitalize()} Bonus`)
|
...this.getBonus(`roll.${this.options.roll.type}`, `${this.options.roll.type?.capitalize()} Bonus`)
|
||||||
);
|
);
|
||||||
|
|
@ -138,7 +140,7 @@ export default class D20Roll extends DHRoll {
|
||||||
|
|
||||||
static postEvaluate(roll, config = {}) {
|
static postEvaluate(roll, config = {}) {
|
||||||
const data = super.postEvaluate(roll, config);
|
const data = super.postEvaluate(roll, config);
|
||||||
data.type = config.roll?.type;
|
data.type = config.actionType;
|
||||||
data.difficulty = config.roll.difficulty;
|
data.difficulty = config.roll.difficulty;
|
||||||
if (config.targets?.length) {
|
if (config.targets?.length) {
|
||||||
config.targets.forEach(target => {
|
config.targets.forEach(target => {
|
||||||
|
|
@ -147,6 +149,7 @@ export default class D20Roll extends DHRoll {
|
||||||
});
|
});
|
||||||
data.success = config.targets.some(target => target.hit);
|
data.success = config.targets.some(target => target.hit);
|
||||||
} else if (config.roll.difficulty) data.success = roll.isCritical || roll.total >= config.roll.difficulty;
|
} else if (config.roll.difficulty) data.success = roll.isCritical || roll.total >= config.roll.difficulty;
|
||||||
|
config.successConsumed = data.success;
|
||||||
|
|
||||||
data.advantage = {
|
data.advantage = {
|
||||||
type: config.roll.advantage,
|
type: config.roll.advantage,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ export default class DamageRoll extends DHRoll {
|
||||||
static DefaultDialog = DamageDialog;
|
static DefaultDialog = DamageDialog;
|
||||||
|
|
||||||
static async buildEvaluate(roll, config = {}, message = {}) {
|
static async buildEvaluate(roll, config = {}, message = {}) {
|
||||||
|
if (config.dialog.configure === false) roll.constructFormula(config);
|
||||||
if (config.evaluate !== false) for (const roll of config.roll) await roll.roll.evaluate();
|
if (config.evaluate !== false) for (const roll of config.roll) await roll.roll.evaluate();
|
||||||
|
|
||||||
roll._evaluated = true;
|
roll._evaluated = true;
|
||||||
|
|
@ -37,12 +38,16 @@ export default class DamageRoll extends DHRoll {
|
||||||
Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll))
|
Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll))
|
||||||
),
|
),
|
||||||
diceRoll = Roll.fromTerms([pool]);
|
diceRoll = Roll.fromTerms([pool]);
|
||||||
await game.dice3d.showForRoll(diceRoll, game.user, true, chatMessage.whisper, chatMessage.blind);
|
await game.dice3d.showForRoll(
|
||||||
|
diceRoll,
|
||||||
|
game.user,
|
||||||
|
true,
|
||||||
|
chatMessage.whisper?.length > 0 ? chatMessage.whisper : null,
|
||||||
|
chatMessage.blind
|
||||||
|
);
|
||||||
}
|
}
|
||||||
await super.buildPost(roll, config, message);
|
await super.buildPost(roll, config, message);
|
||||||
if (config.source?.message) {
|
if (config.source?.message) chatMessage.update({ 'system.damage': config.damage });
|
||||||
chatMessage.update({ 'system.damage': config.damage });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static unifyDamageRoll(rolls) {
|
static unifyDamageRoll(rolls) {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ export default class DHRoll extends Roll {
|
||||||
static async buildConfigure(config = {}, message = {}) {
|
static async buildConfigure(config = {}, message = {}) {
|
||||||
config.hooks = [...this.getHooks(), ''];
|
config.hooks = [...this.getHooks(), ''];
|
||||||
config.dialog ??= {};
|
config.dialog ??= {};
|
||||||
|
|
||||||
for (const hook of config.hooks) {
|
for (const hook of config.hooks) {
|
||||||
if (Hooks.call(`${CONFIG.DH.id}.preRoll${hook.capitalize()}`, config, message) === false) return null;
|
if (Hooks.call(`${CONFIG.DH.id}.preRoll${hook.capitalize()}`, config, message) === false) return null;
|
||||||
}
|
}
|
||||||
|
|
@ -84,18 +85,27 @@ export default class DHRoll extends Roll {
|
||||||
|
|
||||||
static async toMessage(roll, config) {
|
static async toMessage(roll, config) {
|
||||||
const cls = getDocumentClass('ChatMessage'),
|
const cls = getDocumentClass('ChatMessage'),
|
||||||
msg = {
|
msgData = {
|
||||||
type: this.messageType,
|
type: this.messageType,
|
||||||
user: game.user.id,
|
user: game.user.id,
|
||||||
title: roll.title,
|
title: roll.title,
|
||||||
speaker: cls.getSpeaker(),
|
speaker: cls.getSpeaker({ actor: roll.data?.parent }),
|
||||||
sound: config.mute ? null : CONFIG.sounds.dice,
|
sound: config.mute ? null : CONFIG.sounds.dice,
|
||||||
system: config,
|
system: config,
|
||||||
rolls: [roll]
|
rolls: [roll]
|
||||||
};
|
};
|
||||||
|
|
||||||
config.selectedRollMode ??= game.settings.get('core', 'rollMode');
|
config.selectedRollMode ??= game.settings.get('core', 'rollMode');
|
||||||
if (roll._evaluated) return await cls.create(msg, { rollMode: config.selectedRollMode });
|
|
||||||
return msg;
|
if (roll._evaluated) {
|
||||||
|
const message = await cls.create(msgData, { rollMode: config.selectedRollMode });
|
||||||
|
|
||||||
|
if (game.modules.get('dice-so-nice')?.active) {
|
||||||
|
await game.dice3d.waitFor3DAnimationByMessageID(message.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
} else return msgData;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
|
|
@ -218,7 +228,7 @@ export const registerRollDiceHooks = () => {
|
||||||
if (
|
if (
|
||||||
!config.source?.actor ||
|
!config.source?.actor ||
|
||||||
(game.user.isGM ? !hopeFearAutomation.gm : !hopeFearAutomation.players) ||
|
(game.user.isGM ? !hopeFearAutomation.gm : !hopeFearAutomation.players) ||
|
||||||
config.roll.type === 'reaction'
|
config.actionType === 'reaction'
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export default class DualityRoll extends D20Roll {
|
||||||
|
|
||||||
get title() {
|
get title() {
|
||||||
return game.i18n.localize(
|
return game.i18n.localize(
|
||||||
`DAGGERHEART.GENERAL.${this.options?.roll?.type === CONFIG.DH.ITEM.actionTypes.reaction.id ? 'reactionRoll' : 'dualityRoll'}`
|
`DAGGERHEART.GENERAL.${this.options?.actionType === CONFIG.DH.ITEM.actionTypes.reaction.id ? 'reactionRoll' : 'dualityRoll'}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,9 +154,12 @@ export default class DualityRoll extends D20Roll {
|
||||||
applyBaseBonus() {
|
applyBaseBonus() {
|
||||||
const modifiers = super.applyBaseBonus();
|
const modifiers = super.applyBaseBonus();
|
||||||
|
|
||||||
if (this.options.roll.trait && this.data.traits[this.options.roll.trait])
|
if (this.options.roll.trait && this.data.traits?.[this.options.roll.trait])
|
||||||
modifiers.unshift({
|
modifiers.unshift({
|
||||||
label: `DAGGERHEART.CONFIG.Traits.${this.options.roll.trait}.name`,
|
label:
|
||||||
|
this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id
|
||||||
|
? 'DAGGERHEART.CONFIG.RollTypes.spellcast.name'
|
||||||
|
: `DAGGERHEART.CONFIG.Traits.${this.options.roll.trait}.name`,
|
||||||
value: this.data.traits[this.options.roll.trait].value
|
value: this.data.traits[this.options.roll.trait].value
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -167,13 +167,11 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
|
||||||
|
|
||||||
if (subclass) {
|
if (subclass) {
|
||||||
const featureState = subclass.system.featureState;
|
const featureState = subclass.system.featureState;
|
||||||
const featureType = subclass
|
|
||||||
? (subclass.system.features.find(x => x.item?.uuid === this.parent.uuid)?.type ?? null)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(featureType === CONFIG.DH.ITEM.featureSubTypes.specialization && featureState < 2) ||
|
(this.parent.system.identifier === CONFIG.DH.ITEM.featureSubTypes.specialization &&
|
||||||
(featureType === CONFIG.DH.ITEM.featureSubTypes.mastery && featureState < 3)
|
featureState < 2) ||
|
||||||
|
(this.parent.system.identifier === CONFIG.DH.ITEM.featureSubTypes.mastery && featureState < 3)
|
||||||
) {
|
) {
|
||||||
this.transfer = false;
|
this.transfer = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export default class DhpActor extends Actor {
|
||||||
get owner() {
|
get owner() {
|
||||||
const user =
|
const user =
|
||||||
this.hasPlayerOwner && game.users.players.find(u => this.testUserPermission(u, 'OWNER') && u.active);
|
this.hasPlayerOwner && game.users.players.find(u => this.testUserPermission(u, 'OWNER') && u.active);
|
||||||
if (!user) return game.user.isGM ? game.user : null;
|
if (!user) return game.users.activeGM;
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,10 +167,10 @@ export default class DhpActor extends Actor {
|
||||||
if (multiclass) {
|
if (multiclass) {
|
||||||
const multiclassItem = this.items.find(x => x.uuid === multiclass.itemUuid);
|
const multiclassItem = this.items.find(x => x.uuid === multiclass.itemUuid);
|
||||||
const multiclassFeatures = this.items.filter(
|
const multiclassFeatures = this.items.filter(
|
||||||
x => x.system.originItemType === 'class' && x.system.identifier === 'multiclass'
|
x => x.system.originItemType === 'class' && x.system.multiclassOrigin
|
||||||
);
|
);
|
||||||
const subclassFeatures = this.items.filter(
|
const subclassFeatures = this.items.filter(
|
||||||
x => x.system.originItemType === 'subclass' && x.system.identifier === 'multiclass'
|
x => x.system.originItemType === 'subclass' && x.system.multiclassOrigin
|
||||||
);
|
);
|
||||||
|
|
||||||
this.deleteEmbeddedDocuments(
|
this.deleteEmbeddedDocuments(
|
||||||
|
|
@ -659,13 +659,22 @@ export default class DhpActor extends Actor {
|
||||||
};
|
};
|
||||||
|
|
||||||
resources.forEach(r => {
|
resources.forEach(r => {
|
||||||
if (r.keyIsID) {
|
if (r.itemId) {
|
||||||
updates.items[r.key] = {
|
const { path, value } = game.system.api.fields.ActionFields.CostField.getItemIdCostUpdate(r);
|
||||||
target: r.target,
|
|
||||||
resources: {
|
if (
|
||||||
'system.resource.value': r.target.system.resource.value + r.value
|
r.key === 'quantity' &&
|
||||||
}
|
r.target.type === 'consumable' &&
|
||||||
};
|
value === 0 &&
|
||||||
|
r.target.system.destroyOnEmpty
|
||||||
|
) {
|
||||||
|
r.target.delete();
|
||||||
|
} else {
|
||||||
|
updates.items[r.key] = {
|
||||||
|
target: r.target,
|
||||||
|
resources: { [path]: value }
|
||||||
|
};
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
switch (r.key) {
|
switch (r.key) {
|
||||||
case 'fear':
|
case 'fear':
|
||||||
|
|
@ -773,6 +782,28 @@ export default class DhpActor extends Actor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
async importFromJSON(json) {
|
||||||
|
if (!this.type === 'character') return await super.importFromJSON(json);
|
||||||
|
|
||||||
|
if (!CONST.WORLD_DOCUMENT_TYPES.includes(this.documentName)) {
|
||||||
|
throw new Error('Only world Documents may be imported');
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedJSON = JSON.parse(json);
|
||||||
|
if (foundry.utils.isNewerVersion('1.1.0', parsedJSON._stats.systemVersion)) {
|
||||||
|
const confirmed = await foundry.applications.api.DialogV2.confirm({
|
||||||
|
window: {
|
||||||
|
title: game.i18n.localize('DAGGERHEART.ACTORS.Character.InvalidOldCharacterImportTitle')
|
||||||
|
},
|
||||||
|
content: game.i18n.localize('DAGGERHEART.ACTORS.Character.InvalidOldCharacterImportText')
|
||||||
|
});
|
||||||
|
if (!confirmed) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await super.importFromJSON(json);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate an array of localized tag.
|
* Generate an array of localized tag.
|
||||||
* @returns {string[]} An array of localized tag strings.
|
* @returns {string[]} An array of localized tag strings.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { emitAsGM, GMUpdateEvent } from '../systemRegistration/socket.mjs';
|
||||||
|
|
||||||
export default class DhpChatMessage extends foundry.documents.ChatMessage {
|
export default class DhpChatMessage extends foundry.documents.ChatMessage {
|
||||||
targetHook = null;
|
targetHook = null;
|
||||||
|
|
||||||
|
|
@ -103,19 +105,29 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
|
||||||
if (itemDesc && autoExpandRoll.desc) itemDesc.setAttribute('open', '');
|
if (itemDesc && autoExpandRoll.desc) itemDesc.setAttribute('open', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!game.user.isGM) {
|
if (!this.isAuthor && !this.speakerActor?.isOwner) {
|
||||||
const applyButtons = html.querySelector('.apply-buttons');
|
const applyButtons = html.querySelector('.apply-buttons');
|
||||||
applyButtons?.remove();
|
applyButtons?.remove();
|
||||||
if (!this.isAuthor && !this.speakerActor?.isOwner) {
|
const buttons = html.querySelectorAll('.ability-card-footer > .ability-use-button');
|
||||||
const buttons = html.querySelectorAll('.ability-card-footer > .ability-use-button');
|
buttons.forEach(b => b.remove());
|
||||||
buttons.forEach(b => b.remove());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addChatListeners(html) {
|
addChatListeners(html) {
|
||||||
|
html.querySelectorAll('.duality-action-damage').forEach(element =>
|
||||||
|
element.addEventListener('click', this.onRollDamage.bind(this))
|
||||||
|
);
|
||||||
|
|
||||||
html.querySelectorAll('.damage-button').forEach(element =>
|
html.querySelectorAll('.damage-button').forEach(element =>
|
||||||
element.addEventListener('click', this.onDamage.bind(this))
|
element.addEventListener('click', this.onApplyDamage.bind(this))
|
||||||
|
);
|
||||||
|
|
||||||
|
html.querySelectorAll('.target-save').forEach(element =>
|
||||||
|
element.addEventListener('click', this.onRollSave.bind(this))
|
||||||
|
);
|
||||||
|
|
||||||
|
html.querySelectorAll('.roll-all-save-button').forEach(element =>
|
||||||
|
element.addEventListener('click', this.onRollAllSave.bind(this))
|
||||||
);
|
);
|
||||||
|
|
||||||
html.querySelectorAll('.duality-action-effect').forEach(element =>
|
html.querySelectorAll('.duality-action-effect').forEach(element =>
|
||||||
|
|
@ -133,17 +145,21 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getTargetList() {
|
async onRollDamage(event) {
|
||||||
const targets = this.system.hitTargets ?? [];
|
event.stopPropagation();
|
||||||
return targets.map(target => game.canvas.tokens.documentCollection.find(t => t.actor?.uuid === target.actorId));
|
const config = foundry.utils.deepClone(this.system);
|
||||||
|
config.event = event;
|
||||||
|
this.system.action?.workflow.get('damage')?.execute(config, this._id, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onDamage(event) {
|
async onApplyDamage(event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const targets = this.getTargetList();
|
const targets = this.filterPermTargets(this.system.hitTargets),
|
||||||
|
config = foundry.utils.deepClone(this.system);
|
||||||
|
config.event = event;
|
||||||
|
|
||||||
if (this.system.onSave) {
|
if (this.system.onSave) {
|
||||||
const pendingingSaves = this.system.hitTargets.filter(t => t.saved.success === null);
|
const pendingingSaves = targets.filter(t => t.saved.success === null);
|
||||||
if (pendingingSaves.length) {
|
if (pendingingSaves.length) {
|
||||||
const confirm = await foundry.applications.api.DialogV2.confirm({
|
const confirm = await foundry.applications.api.DialogV2.confirm({
|
||||||
window: { title: 'Pending Reaction Rolls found' },
|
window: { title: 'Pending Reaction Rolls found' },
|
||||||
|
|
@ -154,62 +170,66 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targets.length === 0)
|
if (targets.length === 0)
|
||||||
return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected'));
|
return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm'));
|
||||||
|
|
||||||
for (let target of targets) {
|
this.consumeOnSuccess();
|
||||||
let damages = foundry.utils.deepClone(this.system.damage);
|
this.system.action?.workflow.get('applyDamage')?.execute(config, targets, true);
|
||||||
if (
|
}
|
||||||
!this.system.hasHealing &&
|
|
||||||
this.system.onSave &&
|
|
||||||
this.system.hitTargets.find(t => t.id === target.id)?.saved?.success === true
|
|
||||||
) {
|
|
||||||
const mod = CONFIG.DH.ACTIONS.damageOnSave[this.system.onSave]?.mod ?? 1;
|
|
||||||
Object.entries(damages).forEach(([k, v]) => {
|
|
||||||
v.total = 0;
|
|
||||||
v.parts.forEach(part => {
|
|
||||||
part.total = Math.ceil(part.total * mod);
|
|
||||||
v.total += part.total;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.consumeOnSuccess();
|
async onRollSave(event) {
|
||||||
if (this.system.hasHealing) target.actor.takeHealing(damages);
|
event.stopPropagation();
|
||||||
else target.actor.takeDamage(damages, this.system.isDirect);
|
const tokenId = event.target.closest('[data-token]')?.dataset.token,
|
||||||
|
token = game.canvas.tokens.get(tokenId);
|
||||||
|
if (!token?.actor || !token.isOwner) return true;
|
||||||
|
if (this.system.source.item && this.system.source.action) {
|
||||||
|
const action = this.system.action;
|
||||||
|
if (!action || !action?.hasSave) return;
|
||||||
|
game.system.api.fields.ActionFields.SaveField.rollSave.call(action, token.actor, event).then(result =>
|
||||||
|
emitAsGM(
|
||||||
|
GMUpdateEvent.UpdateSaveMessage,
|
||||||
|
game.system.api.fields.ActionFields.SaveField.updateSaveMessage.bind(
|
||||||
|
action,
|
||||||
|
result,
|
||||||
|
this,
|
||||||
|
token.id
|
||||||
|
),
|
||||||
|
{
|
||||||
|
action: action.uuid,
|
||||||
|
message: this._id,
|
||||||
|
token: token.id,
|
||||||
|
result
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAction(actor, itemId, actionId) {
|
async onRollAllSave(event) {
|
||||||
const item = actor.items.get(itemId),
|
event.stopPropagation();
|
||||||
action =
|
if (!game.user.isGM) return;
|
||||||
actor.system.attack?._id === actionId
|
const targets = this.system.hitTargets,
|
||||||
? actor.system.attack
|
config = foundry.utils.deepClone(this.system);
|
||||||
: item.system.attack?._id === actionId
|
config.event = event;
|
||||||
? item.system.attack
|
this.system.action?.workflow.get('save')?.execute(config, targets, true);
|
||||||
: item?.system?.actions?.get(actionId);
|
|
||||||
return action;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onApplyEffect(event) {
|
async onApplyEffect(event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const actor = await foundry.utils.fromUuid(this.system.source.actor);
|
const targets = this.filterPermTargets(this.system.hitTargets),
|
||||||
if (!actor || !game.user.isGM) return true;
|
config = foundry.utils.deepClone(this.system);
|
||||||
if (this.system.source.item && this.system.source.action) {
|
config.event = event;
|
||||||
const action = this.getAction(actor, this.system.source.item, this.system.source.action);
|
if (targets.length === 0)
|
||||||
if (!action || !action?.applyEffects) return;
|
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm'));
|
||||||
const targets = this.getTargetList();
|
this.consumeOnSuccess();
|
||||||
if (targets.length === 0)
|
this.system.action?.workflow.get('effects')?.execute(config, targets, true);
|
||||||
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected'));
|
}
|
||||||
this.consumeOnSuccess();
|
|
||||||
await action.applyEffects(event, this, targets);
|
filterPermTargets(targets) {
|
||||||
}
|
return targets.filter(t => fromUuidSync(t.actorId)?.canUserModify(game.user, 'update'));
|
||||||
}
|
}
|
||||||
|
|
||||||
consumeOnSuccess() {
|
consumeOnSuccess() {
|
||||||
if (!this.system.successConsumed && !this.targetSelection) {
|
if (!this.system.successConsumed && !this.targetSelection) this.system.action?.consume(this.system, true);
|
||||||
const action = this.system.action;
|
|
||||||
if (action) action.consume(this.system, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hoverTarget(event) {
|
hoverTarget(event) {
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export default class DHItem extends foundry.documents.Item {
|
||||||
|
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
static migrateData(source) {
|
static migrateData(source) {
|
||||||
if(source.system?.attack && !source.system.attack.type) source.system.attack.type = "attack";
|
if (source.system?.attack && !source.system.attack.type) source.system.attack.type = 'attack';
|
||||||
return super.migrateData(source);
|
return super.migrateData(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,10 @@ export default class DhTemplateManager {
|
||||||
* @param {wheel Event} event
|
* @param {wheel Event} event
|
||||||
*/
|
*/
|
||||||
#onMouseWheel(event) {
|
#onMouseWheel(event) {
|
||||||
if (!event.shiftKey) return;
|
if (!this.#activePreview) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!event.shiftKey && !event.ctrlKey) return;
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const { moveTime, object } = this.#activePreview;
|
const { moveTime, object } = this.#activePreview;
|
||||||
|
|
@ -66,8 +69,10 @@ export default class DhTemplateManager {
|
||||||
if (now - (moveTime || 0) <= 16) return;
|
if (now - (moveTime || 0) <= 16) return;
|
||||||
this.#activePreview.moveTime = now;
|
this.#activePreview.moveTime = now;
|
||||||
|
|
||||||
|
const multiplier = event.shiftKey ? 0.2 : 0.1;
|
||||||
|
|
||||||
object.document.updateSource({
|
object.document.updateSource({
|
||||||
direction: object.document.direction + event.deltaY * 0.2
|
direction: object.document.direction + event.deltaY * multiplier
|
||||||
});
|
});
|
||||||
object.renderFlags.set({ refresh: true });
|
object.renderFlags.set({ refresh: true });
|
||||||
}
|
}
|
||||||
|
|
@ -77,12 +82,13 @@ export default class DhTemplateManager {
|
||||||
* @param {contextmenu Event} event
|
* @param {contextmenu Event} event
|
||||||
*/
|
*/
|
||||||
#cancelTemplate(event) {
|
#cancelTemplate(event) {
|
||||||
const { mousemove, mousedown, contextmenu } = this.#activePreview.events;
|
const { mousemove, mousedown, contextmenu, wheel } = this.#activePreview.events;
|
||||||
canvas.templates._onDragLeftCancel(event);
|
canvas.templates._onDragLeftCancel(event);
|
||||||
|
|
||||||
canvas.stage.off('mousemove', mousemove);
|
canvas.stage.off('mousemove', mousemove);
|
||||||
canvas.stage.off('mousedown', mousedown);
|
canvas.stage.off('mousedown', mousedown);
|
||||||
canvas.app.view.removeEventListener('contextmenu', contextmenu);
|
canvas.app.view.removeEventListener('contextmenu', contextmenu);
|
||||||
|
canvas.app.view.removeEventListener('wheel', wheel);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -91,9 +97,9 @@ export default class DhTemplateManager {
|
||||||
*/
|
*/
|
||||||
#confirmTemplate(event) {
|
#confirmTemplate(event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
this.#cancelTemplate(event);
|
||||||
|
|
||||||
canvas.scene.createEmbeddedDocuments('MeasuredTemplate', [this.#activePreview.document.toObject()]);
|
canvas.scene.createEmbeddedDocuments('MeasuredTemplate', [this.#activePreview.document.toObject()]);
|
||||||
|
this.#activePreview = undefined;
|
||||||
this.#cancelTemplate(event);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ export default function DhDamageEnricher(match, _options) {
|
||||||
const parts = match[1].split('|').map(x => x.trim());
|
const parts = match[1].split('|').map(x => x.trim());
|
||||||
|
|
||||||
let value = null,
|
let value = null,
|
||||||
type = null;
|
type = null,
|
||||||
|
inline = false;
|
||||||
|
|
||||||
parts.forEach(part => {
|
parts.forEach(part => {
|
||||||
const split = part.split(':').map(x => x.toLowerCase().trim());
|
const split = part.split(':').map(x => x.toLowerCase().trim());
|
||||||
|
|
@ -14,16 +15,19 @@ export default function DhDamageEnricher(match, _options) {
|
||||||
case 'type':
|
case 'type':
|
||||||
type = split[1];
|
type = split[1];
|
||||||
break;
|
break;
|
||||||
|
case 'inline':
|
||||||
|
inline = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!value || !value) return match[0];
|
if (!value || !value) return match[0];
|
||||||
|
|
||||||
return getDamageMessage(value, type, match[0]);
|
return getDamageMessage(value, type, inline, match[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDamageMessage(damage, type, defaultElement) {
|
function getDamageMessage(damage, type, inline, defaultElement) {
|
||||||
const typeIcons = type
|
const typeIcons = type
|
||||||
.replace('[', '')
|
.replace('[', '')
|
||||||
.replace(']', '')
|
.replace(']', '')
|
||||||
|
|
@ -40,7 +44,7 @@ function getDamageMessage(damage, type, defaultElement) {
|
||||||
|
|
||||||
const dualityElement = document.createElement('span');
|
const dualityElement = document.createElement('span');
|
||||||
dualityElement.innerHTML = `
|
dualityElement.innerHTML = `
|
||||||
<button class="enriched-damage-button"
|
<button type="button" class="enriched-damage-button${inline ? ' inline' : ''}"
|
||||||
data-value="${damage}"
|
data-value="${damage}"
|
||||||
data-type="${type}"
|
data-type="${type}"
|
||||||
data-tooltip="${game.i18n.localize('DAGGERHEART.GENERAL.damage')}"
|
data-tooltip="${game.i18n.localize('DAGGERHEART.GENERAL.damage')}"
|
||||||
|
|
@ -67,6 +71,11 @@ export const renderDamageButton = async event => {
|
||||||
title: game.i18n.localize('Damage Roll'),
|
title: game.i18n.localize('Damage Roll'),
|
||||||
data: { bonuses: [] },
|
data: { bonuses: [] },
|
||||||
source: {},
|
source: {},
|
||||||
|
hasDamage: true,
|
||||||
|
hasTarget: true,
|
||||||
|
targets: Array.from(game.user.targets).map(t =>
|
||||||
|
game.system.api.fields.ActionFields.TargetField.formatTarget(t)
|
||||||
|
),
|
||||||
roll: [
|
roll: [
|
||||||
{
|
{
|
||||||
formula: value,
|
formula: value,
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,12 @@ export default function DhDualityRollEnricher(match, _options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDualityMessage(roll, flavor) {
|
function getDualityMessage(roll, flavor) {
|
||||||
const trait = roll.trait && abilities[roll.trait] ? game.i18n.localize(abilities[roll.trait].label) : null;
|
const trait = roll?.trait && abilities[roll.trait] ? game.i18n.localize(abilities[roll.trait].label) : null;
|
||||||
const label =
|
const label =
|
||||||
flavor ??
|
flavor ??
|
||||||
(roll.trait
|
(roll?.trait
|
||||||
? game.i18n.format('DAGGERHEART.GENERAL.rollWith', { roll: trait })
|
? game.i18n.format('DAGGERHEART.GENERAL.rollWith', { roll: trait })
|
||||||
: roll.reaction
|
: roll?.reaction
|
||||||
? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll')
|
? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll')
|
||||||
: game.i18n.localize('DAGGERHEART.GENERAL.duality'));
|
: game.i18n.localize('DAGGERHEART.GENERAL.duality'));
|
||||||
|
|
||||||
|
|
@ -22,9 +22,9 @@ function getDualityMessage(roll, flavor) {
|
||||||
? game.i18n.localize(abilities[roll.trait].label)
|
? game.i18n.localize(abilities[roll.trait].label)
|
||||||
: game.i18n.localize('DAGGERHEART.GENERAL.duality');
|
: game.i18n.localize('DAGGERHEART.GENERAL.duality');
|
||||||
|
|
||||||
const advantage = roll.advantage
|
const advantage = roll?.advantage
|
||||||
? CONFIG.DH.ACTIONS.advantageState.advantage.value
|
? CONFIG.DH.ACTIONS.advantageState.advantage.value
|
||||||
: roll.disadvantage
|
: roll?.disadvantage
|
||||||
? CONFIG.DH.ACTIONS.advantageState.disadvantage.value
|
? CONFIG.DH.ACTIONS.advantageState.disadvantage.value
|
||||||
: undefined;
|
: undefined;
|
||||||
const advantageLabel =
|
const advantageLabel =
|
||||||
|
|
@ -36,21 +36,21 @@ function getDualityMessage(roll, flavor) {
|
||||||
|
|
||||||
const dualityElement = document.createElement('span');
|
const dualityElement = document.createElement('span');
|
||||||
dualityElement.innerHTML = `
|
dualityElement.innerHTML = `
|
||||||
<button class="duality-roll-button"
|
<button type="button" class="duality-roll-button${roll?.inline ? ' inline' : ''}"
|
||||||
data-title="${label}"
|
data-title="${label}"
|
||||||
data-label="${dataLabel}"
|
data-label="${dataLabel}"
|
||||||
data-reaction="${roll.reaction ? 'true' : 'false'}"
|
data-reaction="${roll?.reaction ? 'true' : 'false'}"
|
||||||
data-hope="${roll.hope ?? 'd12'}"
|
data-hope="${roll?.hope ?? 'd12'}"
|
||||||
data-fear="${roll.fear ?? 'd12'}"
|
data-fear="${roll?.fear ?? 'd12'}"
|
||||||
${advantage ? `data-advantage="${advantage}"` : ''}
|
${advantage ? `data-advantage="${advantage}"` : ''}
|
||||||
${roll.difficulty !== undefined ? `data-difficulty="${roll.difficulty}"` : ''}
|
${roll?.difficulty !== undefined ? `data-difficulty="${roll.difficulty}"` : ''}
|
||||||
${roll.trait && abilities[roll.trait] ? `data-trait="${roll.trait}"` : ''}
|
${roll?.trait && abilities[roll.trait] ? `data-trait="${roll.trait}"` : ''}
|
||||||
${roll.advantage ? 'data-advantage="true"' : ''}
|
${roll?.advantage ? 'data-advantage="true"' : ''}
|
||||||
${roll.disadvantage ? 'data-disadvantage="true"' : ''}
|
${roll?.disadvantage ? 'data-disadvantage="true"' : ''}
|
||||||
>
|
>
|
||||||
${roll.reaction ? '<i class="fa-solid fa-reply"></i>' : '<i class="fa-solid fa-circle-half-stroke"></i>'}
|
${roll?.reaction ? '<i class="fa-solid fa-reply"></i>' : '<i class="fa-solid fa-circle-half-stroke"></i>'}
|
||||||
${label}
|
${label}
|
||||||
${!flavor && (roll.difficulty || advantageLabel) ? `(${[roll.difficulty, advantageLabel ? game.i18n.localize(`DAGGERHEART.GENERAL.${advantageLabel}.short`) : null].filter(x => x).join(' ')})` : ''}
|
${!flavor && (roll?.difficulty || advantageLabel) ? `(${[roll.difficulty, advantageLabel ? game.i18n.localize(`DAGGERHEART.GENERAL.${advantageLabel}.short`) : null].filter(x => x).join(' ')})` : ''}
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@ export default function DhTemplateEnricher(match, _options) {
|
||||||
const parts = match[1].split('|').map(x => x.trim());
|
const parts = match[1].split('|').map(x => x.trim());
|
||||||
|
|
||||||
let type = null,
|
let type = null,
|
||||||
range = null;
|
range = null,
|
||||||
|
angle = CONFIG.MeasuredTemplate.defaults.angle,
|
||||||
|
direction = 0,
|
||||||
|
inline = false;
|
||||||
|
|
||||||
parts.forEach(part => {
|
parts.forEach(part => {
|
||||||
const split = part.split(':').map(x => x.toLowerCase().trim());
|
const split = part.split(':').map(x => x.toLowerCase().trim());
|
||||||
|
|
@ -15,10 +18,23 @@ export default function DhTemplateEnricher(match, _options) {
|
||||||
type = matchedType;
|
type = matchedType;
|
||||||
break;
|
break;
|
||||||
case 'range':
|
case 'range':
|
||||||
const matchedRange = Object.values(CONFIG.DH.GENERAL.templateRanges).find(
|
if (Number.isNaN(Number(split[1]))) {
|
||||||
x => x.id.toLowerCase() === split[1] || x.short === split[1]
|
const matchedRange = Object.values(CONFIG.DH.GENERAL.templateRanges).find(
|
||||||
);
|
x => x.id.toLowerCase() === split[1] || x.short === split[1]
|
||||||
range = matchedRange?.id;
|
);
|
||||||
|
range = matchedRange?.id;
|
||||||
|
} else {
|
||||||
|
range = split[1];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'inline':
|
||||||
|
inline = true;
|
||||||
|
break;
|
||||||
|
case 'angle':
|
||||||
|
angle = split[1];
|
||||||
|
break;
|
||||||
|
case 'direction':
|
||||||
|
direction = split[1];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -28,10 +44,32 @@ export default function DhTemplateEnricher(match, _options) {
|
||||||
|
|
||||||
const label = game.i18n.localize(`DAGGERHEART.CONFIG.TemplateTypes.${type}`);
|
const label = game.i18n.localize(`DAGGERHEART.CONFIG.TemplateTypes.${type}`);
|
||||||
|
|
||||||
|
const rangeDisplay = Number.isNaN(Number(range)) ? game.i18n.localize(`DAGGERHEART.CONFIG.Range.${range}.name`) : range;
|
||||||
|
|
||||||
|
let angleDisplay = '';
|
||||||
|
if (angle != CONFIG.MeasuredTemplate.defaults.angle) {
|
||||||
|
angleDisplay = 'angle:' + angle;
|
||||||
|
|
||||||
|
}
|
||||||
|
let directionDisplay = '';
|
||||||
|
if (direction != 0) {
|
||||||
|
directionDisplay = 'direction:' + direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
let extraDisplay = '';
|
||||||
|
if (angleDisplay != '' && directionDisplay != '') {
|
||||||
|
extraDisplay = ' (' + angleDisplay + '|' + directionDisplay + ')';
|
||||||
|
} else if (angleDisplay != '') {
|
||||||
|
extraDisplay = ' (' + angleDisplay + ')';
|
||||||
|
} else if (directionDisplay != '') {
|
||||||
|
extraDisplay = ' (' + directionDisplay + ')';
|
||||||
|
}
|
||||||
|
|
||||||
const templateElement = document.createElement('span');
|
const templateElement = document.createElement('span');
|
||||||
templateElement.innerHTML = `
|
templateElement.innerHTML = `
|
||||||
<button class="measured-template-button" data-type="${type}" data-range="${range}">
|
<button type="button" class="measured-template-button${inline ? ' inline' : ''}"
|
||||||
${label} - ${game.i18n.localize(`DAGGERHEART.CONFIG.Range.${range}.name`)}
|
data-type="${type}" data-range="${range}" data-angle="${angle}" data-direction="${direction}">
|
||||||
|
${label} - ${rangeDisplay}${extraDisplay}
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -41,21 +79,25 @@ export default function DhTemplateEnricher(match, _options) {
|
||||||
export const renderMeasuredTemplate = async event => {
|
export const renderMeasuredTemplate = async event => {
|
||||||
const button = event.currentTarget,
|
const button = event.currentTarget,
|
||||||
type = button.dataset.type,
|
type = button.dataset.type,
|
||||||
range = button.dataset.range;
|
range = button.dataset.range,
|
||||||
|
angle = button.dataset.angle,
|
||||||
|
direction = button.dataset.direction;
|
||||||
|
|
||||||
if (!type || !range || !game.canvas.scene) return;
|
if (!type || !range || !game.canvas.scene) return;
|
||||||
|
|
||||||
const usedType = type === 'inFront' ? 'cone' : type === 'emanation' ? 'circle' : type;
|
const usedType = type === 'inFront' ? 'cone' : type === 'emanation' ? 'circle' : type;
|
||||||
const angle =
|
const usedAngle =
|
||||||
type === CONST.MEASURED_TEMPLATE_TYPES.CONE
|
type === CONST.MEASURED_TEMPLATE_TYPES.CONE
|
||||||
? CONFIG.MeasuredTemplate.defaults.angle
|
? (angle ?? CONFIG.MeasuredTemplate.defaults.angle)
|
||||||
: type === CONFIG.DH.GENERAL.templateTypes.INFRONT
|
: type === CONFIG.DH.GENERAL.templateTypes.INFRONT
|
||||||
? '180'
|
? '180'
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const baseDistance = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement[
|
let baseDistance = range;
|
||||||
range
|
if (Number.isNaN(Number(range))) {
|
||||||
];
|
baseDistance =
|
||||||
|
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement[range];
|
||||||
|
}
|
||||||
const distance = type === CONFIG.DH.GENERAL.templateTypes.EMANATION ? baseDistance + 2.5 : baseDistance;
|
const distance = type === CONFIG.DH.GENERAL.templateTypes.EMANATION ? baseDistance + 2.5 : baseDistance;
|
||||||
|
|
||||||
const { width, height } = game.canvas.scene.dimensions;
|
const { width, height } = game.canvas.scene.dimensions;
|
||||||
|
|
@ -65,7 +107,8 @@ export const renderMeasuredTemplate = async event => {
|
||||||
t: usedType,
|
t: usedType,
|
||||||
distance: distance,
|
distance: distance,
|
||||||
width: type === CONST.MEASURED_TEMPLATE_TYPES.RAY ? 5 : undefined,
|
width: type === CONST.MEASURED_TEMPLATE_TYPES.RAY ? 5 : undefined,
|
||||||
angle: angle
|
angle: usedAngle,
|
||||||
|
direction: direction
|
||||||
};
|
};
|
||||||
|
|
||||||
CONFIG.ux.TemplateManager.createPreview(data);
|
CONFIG.ux.TemplateManager.createPreview(data);
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ export default class RegisterHandlebarsHelpers {
|
||||||
hasProperty: foundry.utils.hasProperty,
|
hasProperty: foundry.utils.hasProperty,
|
||||||
getProperty: foundry.utils.getProperty,
|
getProperty: foundry.utils.getProperty,
|
||||||
setVar: this.setVar,
|
setVar: this.setVar,
|
||||||
empty: this.empty
|
empty: this.empty,
|
||||||
|
pluralize: this.pluralize
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
static add(a, b) {
|
static add(a, b) {
|
||||||
|
|
@ -64,7 +65,7 @@ export default class RegisterHandlebarsHelpers {
|
||||||
return isNumerical ? (!result ? 0 : Number(result)) : result;
|
return isNumerical ? (!result ? 0 : Number(result)) : result;
|
||||||
}
|
}
|
||||||
|
|
||||||
static setVar(name, value, context) {
|
static setVar(name, value) {
|
||||||
this[name] = value;
|
this[name] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,4 +73,20 @@ export default class RegisterHandlebarsHelpers {
|
||||||
if (!(typeof object === 'object')) return true;
|
if (!(typeof object === 'object')) return true;
|
||||||
return Object.keys(object).length === 0;
|
return Object.keys(object).length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pluralize helper that returns the appropriate localized string based on count
|
||||||
|
* @param {number} count - The number to check for plurality
|
||||||
|
* @param {string} baseKey - The base localization key (e.g., "DAGGERHEART.GENERAL.Target")
|
||||||
|
* @returns {string} The localized singular or plural string
|
||||||
|
*
|
||||||
|
* Usage: {{pluralize currentTargets.length "DAGGERHEART.GENERAL.Target"}}
|
||||||
|
* Returns: "Target" if count is exactly 1, "Targets" if count is 0, 2+, or invalid
|
||||||
|
*/
|
||||||
|
static pluralize(count, baseKey) {
|
||||||
|
const numericCount = Number(count);
|
||||||
|
const isSingular = !isNaN(numericCount) && numericCount === 1;
|
||||||
|
const key = isSingular ? `${baseKey}.single` : `${baseKey}.plural`;
|
||||||
|
return game.i18n.localize(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export { preloadHandlebarsTemplates as handlebarsRegistration } from './handlebars.mjs';
|
export { preloadHandlebarsTemplates as handlebarsRegistration } from './handlebars.mjs';
|
||||||
export * as settingsRegistration from './settings.mjs';
|
export * as settingsRegistration from './settings.mjs';
|
||||||
export * as socketRegistration from './socket.mjs';
|
export * as socketRegistration from './socket.mjs';
|
||||||
|
export { runMigrations } from './migrations.mjs';
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,11 @@ export const preloadHandlebarsTemplates = async function () {
|
||||||
'systems/daggerheart/templates/dialogs/downtime/activities.hbs',
|
'systems/daggerheart/templates/dialogs/downtime/activities.hbs',
|
||||||
'systems/daggerheart/templates/dialogs/dice-roll/costSelection.hbs',
|
'systems/daggerheart/templates/dialogs/dice-roll/costSelection.hbs',
|
||||||
|
|
||||||
|
|
||||||
'systems/daggerheart/templates/ui/chat/parts/roll-part.hbs',
|
'systems/daggerheart/templates/ui/chat/parts/roll-part.hbs',
|
||||||
'systems/daggerheart/templates/ui/chat/parts/damage-part.hbs',
|
'systems/daggerheart/templates/ui/chat/parts/damage-part.hbs',
|
||||||
'systems/daggerheart/templates/ui/chat/parts/target-part.hbs',
|
'systems/daggerheart/templates/ui/chat/parts/target-part.hbs',
|
||||||
'systems/daggerheart/templates/ui/chat/parts/button-part.hbs',
|
'systems/daggerheart/templates/ui/chat/parts/button-part.hbs',
|
||||||
|
'systems/daggerheart/templates/ui/itemBrowser/itemContainer.hbs',
|
||||||
|
'systems/daggerheart/templates/scene/dh-config.hbs'
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
155
module/systemRegistration/migrations.mjs
Normal file
155
module/systemRegistration/migrations.mjs
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
export async function runMigrations() {
|
||||||
|
let lastMigrationVersion = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion);
|
||||||
|
if (!lastMigrationVersion) lastMigrationVersion = '1.0.6';
|
||||||
|
|
||||||
|
if (foundry.utils.isNewerVersion('1.1.0', lastMigrationVersion)) {
|
||||||
|
const lockedPacks = [];
|
||||||
|
const compendiumActors = [];
|
||||||
|
for (let pack of game.packs) {
|
||||||
|
if (pack.locked) {
|
||||||
|
lockedPacks.push(pack.collection);
|
||||||
|
await pack.configure({ locked: false });
|
||||||
|
}
|
||||||
|
const documents = await pack.getDocuments();
|
||||||
|
compendiumActors.push(...documents.filter(x => x.type === 'character'));
|
||||||
|
}
|
||||||
|
|
||||||
|
[...compendiumActors, ...game.actors].forEach(actor => {
|
||||||
|
const items = actor.items.reduce((acc, item) => {
|
||||||
|
if (item.type === 'feature') {
|
||||||
|
const { originItemType, isMulticlass, identifier } = item.system;
|
||||||
|
const base = originItemType
|
||||||
|
? actor.items.find(
|
||||||
|
x => x.type === originItemType && Boolean(isMulticlass) === Boolean(x.system.isMulticlass)
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
if (base) {
|
||||||
|
const feature = base.system.features.find(x => x.item && x.item.uuid === item.uuid);
|
||||||
|
if (feature && identifier !== 'multiclass') {
|
||||||
|
acc.push({ _id: item.id, system: { identifier: feature.type } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
actor.updateEmbeddedDocuments('Item', items);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let packId of lockedPacks) {
|
||||||
|
const pack = game.packs.get(packId);
|
||||||
|
await pack.configure({ locked: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMigrationVersion = '1.1.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundry.utils.isNewerVersion('1.1.1', lastMigrationVersion)) {
|
||||||
|
const lockedPacks = [];
|
||||||
|
const compendiumClasses = [];
|
||||||
|
const compendiumActors = [];
|
||||||
|
for (let pack of game.packs) {
|
||||||
|
if (pack.locked) {
|
||||||
|
lockedPacks.push(pack.collection);
|
||||||
|
await pack.configure({ locked: false });
|
||||||
|
}
|
||||||
|
const documents = await pack.getDocuments();
|
||||||
|
compendiumClasses.push(...documents.filter(x => x.type === 'class'));
|
||||||
|
compendiumActors.push(...documents.filter(x => x.type === 'character'));
|
||||||
|
}
|
||||||
|
|
||||||
|
[...compendiumActors, ...game.actors.filter(x => x.type === 'character')].forEach(char => {
|
||||||
|
const multiclass = char.items.find(x => x.type === 'class' && x.system.isMulticlass);
|
||||||
|
const multiclassSubclass =
|
||||||
|
multiclass?.system?.subclasses?.length > 0 ? multiclass.system.subclasses[0] : null;
|
||||||
|
char.items.forEach(item => {
|
||||||
|
if (item.type === 'feature' && item.system.identifier === 'multiclass') {
|
||||||
|
const base = item.system.originItemType === 'class' ? multiclass : multiclassSubclass;
|
||||||
|
if (base) {
|
||||||
|
const baseFeature = base.system.features.find(x => x.item.name === item.name);
|
||||||
|
if (baseFeature) {
|
||||||
|
item.update({
|
||||||
|
system: {
|
||||||
|
multiclassOrigin: true,
|
||||||
|
identifier: baseFeature.type
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const worldClasses = game.items.filter(x => x.type === 'class');
|
||||||
|
for (let classVal of [...compendiumClasses, ...worldClasses]) {
|
||||||
|
for (let subclass of classVal.system.subclasses) {
|
||||||
|
await subclass.update({ 'system.linkedClass': classVal.uuid });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let packId of lockedPacks) {
|
||||||
|
const pack = game.packs.get(packId);
|
||||||
|
await pack.configure({ locked: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMigrationVersion = '1.1.1';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundry.utils.isNewerVersion('1.2.0', lastMigrationVersion)) {
|
||||||
|
const lockedPacks = [];
|
||||||
|
const compendiumItems = [];
|
||||||
|
for (let pack of game.packs) {
|
||||||
|
if (pack.locked) {
|
||||||
|
lockedPacks.push(pack.collection);
|
||||||
|
await pack.configure({ locked: false });
|
||||||
|
}
|
||||||
|
const documents = await pack.getDocuments();
|
||||||
|
|
||||||
|
compendiumItems.push(...documents.filter(x => x.system?.metadata?.hasActions));
|
||||||
|
compendiumItems.push(
|
||||||
|
...documents
|
||||||
|
.filter(x => x.items)
|
||||||
|
.flatMap(actor => actor.items.filter(x => x.system?.metadata?.hasActions))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const worldItems = game.items.filter(x => x.system.metadata.hasActions);
|
||||||
|
const worldActorItems = Array.from(game.actors).flatMap(actor =>
|
||||||
|
actor.items.filter(x => x.system.metadata.hasActions)
|
||||||
|
);
|
||||||
|
|
||||||
|
const validCostKeys = Object.keys(CONFIG.DH.GENERAL.abilityCosts);
|
||||||
|
for (let item of [...worldItems, ...worldActorItems, ...compendiumItems]) {
|
||||||
|
for (let action of item.system.actions) {
|
||||||
|
const resourceCostIndexes = Object.keys(action.cost).reduce(
|
||||||
|
(acc, index) => (!validCostKeys.includes(action.cost[index].key) ? [...acc, Number(index)] : acc),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
if (resourceCostIndexes.length === 0) continue;
|
||||||
|
|
||||||
|
await action.update({
|
||||||
|
cost: action.cost.map((cost, index) => {
|
||||||
|
const { keyIsID, ...rest } = cost;
|
||||||
|
if (!resourceCostIndexes.includes(index)) return { ...rest };
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
key: 'resource',
|
||||||
|
itemId: cost.key
|
||||||
|
};
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let packId of lockedPacks) {
|
||||||
|
const pack = game.packs.get(packId);
|
||||||
|
await pack.configure({ locked: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMigrationVersion = '1.2.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion, lastMigrationVersion);
|
||||||
|
}
|
||||||
|
|
@ -72,7 +72,7 @@ const registerMenus = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
game.settings.registerMenu(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance, {
|
game.settings.registerMenu(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance, {
|
||||||
name: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.appearance.title'),
|
name: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.appearance.label'),
|
||||||
label: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.appearance.label'),
|
label: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.appearance.label'),
|
||||||
hint: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.appearance.hint'),
|
hint: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.appearance.hint'),
|
||||||
icon: 'fa-solid fa-palette',
|
icon: 'fa-solid fa-palette',
|
||||||
|
|
@ -91,6 +91,12 @@ const registerMenus = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const registerNonConfigSettings = () => {
|
const registerNonConfigSettings = () => {
|
||||||
|
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion, {
|
||||||
|
scope: 'world',
|
||||||
|
config: false,
|
||||||
|
type: String
|
||||||
|
});
|
||||||
|
|
||||||
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers, {
|
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers, {
|
||||||
scope: 'world',
|
scope: 'world',
|
||||||
config: false,
|
config: false,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ export const socketEvent = {
|
||||||
|
|
||||||
export const GMUpdateEvent = {
|
export const GMUpdateEvent = {
|
||||||
UpdateDocument: 'DhGMUpdateDocument',
|
UpdateDocument: 'DhGMUpdateDocument',
|
||||||
|
UpdateEffect: 'DhGMUpdateEffect',
|
||||||
UpdateSetting: 'DhGMUpdateSetting',
|
UpdateSetting: 'DhGMUpdateSetting',
|
||||||
UpdateFear: 'DhGMUpdateFear',
|
UpdateFear: 'DhGMUpdateFear',
|
||||||
UpdateSaveMessage: 'DhGMUpdateSaveMessage'
|
UpdateSaveMessage: 'DhGMUpdateSaveMessage'
|
||||||
|
|
@ -37,9 +38,11 @@ export const registerSocketHooks = () => {
|
||||||
const document = data.uuid ? await fromUuid(data.uuid) : null;
|
const document = data.uuid ? await fromUuid(data.uuid) : null;
|
||||||
switch (data.action) {
|
switch (data.action) {
|
||||||
case GMUpdateEvent.UpdateDocument:
|
case GMUpdateEvent.UpdateDocument:
|
||||||
if (document && data.update) {
|
if (document && data.update) await document.update(data.update);
|
||||||
await document.update(data.update);
|
break;
|
||||||
}
|
case GMUpdateEvent.UpdateEffect:
|
||||||
|
if (document && data.update)
|
||||||
|
await game.system.api.fields.ActionFields.EffectsField.applyEffects.call(document, data.update);
|
||||||
break;
|
break;
|
||||||
case GMUpdateEvent.UpdateSetting:
|
case GMUpdateEvent.UpdateSetting:
|
||||||
await game.settings.set(CONFIG.DH.id, data.uuid, data.update);
|
await game.settings.set(CONFIG.DH.id, data.uuid, data.update);
|
||||||
|
|
@ -78,7 +81,7 @@ export const registerSocketHooks = () => {
|
||||||
|
|
||||||
export const registerUserQueries = () => {
|
export const registerUserQueries = () => {
|
||||||
CONFIG.queries.armorSlot = DamageReductionDialog.armorSlotQuery;
|
CONFIG.queries.armorSlot = DamageReductionDialog.armorSlotQuery;
|
||||||
CONFIG.queries.reactionRoll = game.system.api.models.actions.actionsTypes.base.rollSaveQuery;
|
CONFIG.queries.reactionRoll = game.system.api.fields.ActionFields.SaveField.rollSaveQuery;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const emitAsGM = async (eventName, callback, update, uuid = null) => {
|
export const emitAsGM = async (eventName, callback, update, uuid = null) => {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
---
|
---
|
||||||
name: Pull Request
|
name: Pull Request
|
||||||
about: Create a new pull request
|
about: Create a new pull request
|
||||||
title: "[PR] <Insert Title here>"
|
title: '[PR] <Insert Title here>'
|
||||||
labels: pr
|
labels: pr
|
||||||
assignees: ''
|
assignees: ''
|
||||||
---
|
---
|
||||||
|
|
||||||
Is this a community PR? Please go to preview tab and click [here](?expand=1&template=community_pull_request_template.md). If not, delete this line.
|
Is this a community PR? Please go to preview tab and click [here](?expand=1&template=community_pull_request_template.md). If not, delete this line.
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
|
||||||
|
|
@ -316,7 +316,6 @@
|
||||||
"scalable": false,
|
"scalable": false,
|
||||||
"key": "stress",
|
"key": "stress",
|
||||||
"value": 1,
|
"value": 1,
|
||||||
"keyIsID": false,
|
|
||||||
"step": null
|
"step": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
@ -439,14 +438,14 @@
|
||||||
"_id": "UpFsnlbZkyvM2Ftv",
|
"_id": "UpFsnlbZkyvM2Ftv",
|
||||||
"img": "icons/magic/acid/projectile-smoke-glowing.webp",
|
"img": "icons/magic/acid/projectile-smoke-glowing.webp",
|
||||||
"system": {
|
"system": {
|
||||||
"description": "<p>Make an attack against all targets in front of the Burrower within Close range. Targets the Burrower succeeds against take <strong>2d6</strong> physical damage and must mark an Armor Slot without receiving its benefi ts (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP and you gain a Fear.</p><p>@Template[type:inFront|range:c]</p>",
|
"description": "<p>Make an attack against all targets in front of the Burrower within Close range. Targets the Burrower succeeds against take <strong>2d6</strong> physical damage and must mark an Armor Slot without receiving its benefits (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP and you gain a Fear.</p><p>@Template[type:inFront|range:c]</p>",
|
||||||
"resource": null,
|
"resource": null,
|
||||||
"actions": {
|
"actions": {
|
||||||
"yd10HwK6Wa3OEvv2": {
|
"yd10HwK6Wa3OEvv2": {
|
||||||
"type": "attack",
|
"type": "attack",
|
||||||
"_id": "yd10HwK6Wa3OEvv2",
|
"_id": "yd10HwK6Wa3OEvv2",
|
||||||
"systemPath": "actions",
|
"systemPath": "actions",
|
||||||
"description": "<p>Make an attack against all targets in front of the Burrower within Close range. Targets the Burrower succeeds against take <strong>2d6</strong> physical damage and must mark an Armor Slot without receiving its benefi ts (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP and you gain a Fear.</p><p>@Template[type:inFront|range:c]</p>",
|
"description": "<p>Make an attack against all targets in front of the Burrower within Close range. Targets the Burrower succeeds against take <strong>2d6</strong> physical damage and must mark an Armor Slot without receiving its benefits (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP and you gain a Fear.</p><p>@Template[type:inFront|range:c]</p>",
|
||||||
"chatDisplay": true,
|
"chatDisplay": true,
|
||||||
"actionType": "action",
|
"actionType": "action",
|
||||||
"cost": [],
|
"cost": [],
|
||||||
|
|
@ -558,11 +557,11 @@
|
||||||
"compendiumSource": null,
|
"compendiumSource": null,
|
||||||
"duplicateSource": null,
|
"duplicateSource": null,
|
||||||
"exportSource": null,
|
"exportSource": null,
|
||||||
"coreVersion": "13.346",
|
"coreVersion": "13.348",
|
||||||
"systemId": "daggerheart",
|
"systemId": "daggerheart",
|
||||||
"systemVersion": "0.0.1",
|
"systemVersion": "1.1.2",
|
||||||
"lastModifiedBy": "MQSznptE5yLT7kj8",
|
"lastModifiedBy": "mdk78Q6pOyHh6aBg",
|
||||||
"modifiedTime": 1754143653876
|
"modifiedTime": 1756510879809
|
||||||
},
|
},
|
||||||
"_key": "!actors.items!89yAh30vaNQOALlz.UpFsnlbZkyvM2Ftv"
|
"_key": "!actors.items!89yAh30vaNQOALlz.UpFsnlbZkyvM2Ftv"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -392,7 +392,6 @@
|
||||||
"scalable": false,
|
"scalable": false,
|
||||||
"key": "fear",
|
"key": "fear",
|
||||||
"value": 1,
|
"value": 1,
|
||||||
"keyIsID": false,
|
|
||||||
"step": null
|
"step": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
@ -507,7 +506,6 @@
|
||||||
"scalable": false,
|
"scalable": false,
|
||||||
"key": "stress",
|
"key": "stress",
|
||||||
"value": 1,
|
"value": 1,
|
||||||
"keyIsID": false,
|
|
||||||
"step": null
|
"step": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
@ -727,7 +725,6 @@
|
||||||
"scalable": false,
|
"scalable": false,
|
||||||
"key": "stress",
|
"key": "stress",
|
||||||
"value": 1,
|
"value": 1,
|
||||||
"keyIsID": false,
|
|
||||||
"step": null
|
"step": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue