diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..8bbc2b52 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +[*] +indent_size = 4 +indent_style = spaces diff --git a/daggerheart.mjs b/daggerheart.mjs index f75ff1da..05b57ac9 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -242,6 +242,41 @@ Hooks.on('setup', () => { systemEffect: true })) ]; + + const damageThresholds = ['damageThresholds.major', 'damageThresholds.severe']; + const traits = Object.keys(game.system.api.data.actors.DhCharacter.schema.fields.traits.fields).map( + trait => `traits.${trait}.value` + ); + const resistance = Object.values(game.system.api.data.actors.DhCharacter.schema.fields.resistance.fields).flatMap( + type => Object.keys(type.fields).map(x => `resistance.${type.name}.${x}`) + ); + const actorCommon = { + bar: ['resources.stress'], + value: [...resistance, 'advantageSources', 'disadvantageSources'] + }; + CONFIG.Actor.trackableAttributes = { + character: { + bar: [...actorCommon.bar, 'resources.hitPoints', 'resources.hope'], + value: [ + ...actorCommon.value, + ...traits, + ...damageThresholds, + 'proficiency', + 'evasion', + 'armorScore', + 'scars', + 'levelData.level.current' + ] + }, + adversary: { + bar: [...actorCommon.bar, 'resources.hitPoints'], + value: [...actorCommon.value, ...damageThresholds, 'criticalThreshold', 'difficulty'] + }, + companion: { + bar: [...actorCommon.bar], + value: [...actorCommon.value, 'evasion', 'levelData.level.current'] + } + }; }); Hooks.on('ready', async () => { @@ -385,10 +420,7 @@ const updateActorsRangeDependentEffects = async token => { // Get required distance and special case 5 feet to test adjacency const required = rangeMeasurement[range]; const reverse = type === CONFIG.DH.GENERAL.rangeInclusion.outsideRange.id; - const inRange = - required === 5 - ? userTarget.isAdjacentWith(token.object) - : userTarget.distanceTo(token.object) <= required; + const inRange = userTarget.distanceTo(token.object) <= required; if (reverse ? inRange : !inRange) { enabledEffect = false; break; diff --git a/lang/en.json b/lang/en.json index 0186ae3e..c9d21944 100755 --- a/lang/en.json +++ b/lang/en.json @@ -192,6 +192,9 @@ }, "age": "Age", "backgroundQuestions": "Backgrounds", + "burden": { + "ignore": { "label": "Burden: Ignore", "hint": "Ignore burden rules" } + }, "companionFeatures": "Companion Features", "connections": "Connections", "contextMenu": { @@ -214,6 +217,12 @@ "maxEvasionBonus": "Max Evasion Increase", "maxHPBonus": "Max HP Increase", "pronouns": "Pronouns", + "roll": { + "guaranteedCritical": { + "label": "Guaranteed Critical", + "hint": "Set to 1 to always roll a critical" + } + }, "story": { "backgroundTitle": "Background", "characteristics": "Characteristics", @@ -343,6 +352,12 @@ "requestSpotlight": "Request The Spotlight", "openCountdowns": "Countdowns" }, + "CompendiumBrowserSettings": { + "title": "Enable Compendiums", + "enableSource": "Enable Source", + "disableSource": "Disable Source", + "worldCompendiums": "World Compendiums" + }, "ContextMenu": { "disableEffect": "Disable Effect", "enableEffect": "Enable Effect", @@ -443,9 +458,13 @@ "name": "Clear Stress" }, "prepare": { - "description": "Describe how you are preparing for the next day's adventure, then gain a Hope. If you choose to Prepare with one or more members of your party, you may each take two Hope.", + "description": "Describe how you are preparing for the next day's adventure, then gain a Hope.", "name": "Prepare" }, + "prepareWithFriends": { + "description": "You prepare with one or more members of your party, and you each gain 2 Hope.", + "name": "Prepare (together)" + }, "repairArmor": { "description": "Describe how you spend time repairing your armor and clear all of its Armor Slots. You may also do this to an ally's armor instead.", "name": "Repair Armor" @@ -476,7 +495,11 @@ }, "prepare": { "name": "Prepare", - "description": "Describe how you prepare yourself for the path ahead, then gain a Hope. If you choose to Prepare with one or more members of your party, you each gain 2 Hope." + "description": "Describe how you prepare yourself for the path ahead, then gain a Hope." + }, + "prepareWithFriends": { + "name": "Prepare (together)", + "description": "You prepare with one or more members of your party, and you each gain 2 Hope." } }, "refreshable": { @@ -1008,7 +1031,8 @@ }, "vulnerable": { "name": "Vulnerable", - "description": "While a creature is Vulnerable, all rolls targeting them have advantage.\nA creature who is already Vulnerable can’t be made to take the condition again." + "description": "While a creature is Vulnerable, all rolls targeting them have advantage.\nA creature who is already Vulnerable can’t be made to take the condition again.", + "autoAppliedByLabel": "Max Stress" } }, "CountdownType": { @@ -1143,12 +1167,12 @@ }, "far": { "name": "Far", - "description": "means a distance where one can see the appearance of a person or object, but probably not in great detail-- across a small battlefield or down a large corridor. This is usually about 30-100 feet away. While under danger, a PC will likely have to make an Agility check to get here safely. Anything on a battle map that is within the length of a standard piece of paper (~10-11 inches) can usually be considered far.", + "description": "means a distance where one can see the appearance of a person or object, but probably not in great detail-- across a small battlefield or down a large corridor. This is usually about 30-100 feet away. While under danger, a PC will likely have to make an Agility roll to get here safely. Anything on a battle map that is within the length of a standard piece of paper (~10-11 inches) can usually be considered far.", "short": "Far" }, "veryFar": { "name": "Very Far", - "description": "means a distance where you can see the shape of a person or object, but probably not make outany details-- across a large battlefield or down a long street, generally about 100-300 feet away. While under danger, a PC likely has to make an Agility check to get here safely. Anything on a battle map that is beyond far distance, but still within sight of the characters can usually be considered very far.", + "description": "means a distance where you can see the shape of a person or object, but probably not make outany details-- across a large battlefield or down a long street, generally about 100-300 feet away. While under danger, a PC likely has to make an Agility roll to get here safely. Anything on a battle map that is beyond far distance, but still within sight of the characters can usually be considered very far.", "short": "V. Far" } }, @@ -1271,6 +1295,7 @@ "triggerTexts": { "strangePatternsContentTitle": "Matched {nr} times.", "strangePatternsContentSubTitle": "Increase hope and stress to a total of {nr}.", + "strangePatternsActionExplanation": "Left click to increase, right click to decrease", "ferocityContent": "Spend 2 Hope to gain {bonus} bonus Evasion until after the next attack against you?", "ferocityEffectDescription": "Your evasion is increased by {bonus}. This bonus lasts until after the next attack made against you." }, @@ -1840,6 +1865,16 @@ "singular": "Adversary", "plural": "Adversaries" }, + "Attack": { + "hpDamageMultiplier": { + "label": "HP Damage Multiplier", + "hint": "Multiply any damage you deal by this number" + }, + "hpDamageTakenMultiplier": { + "label": "HP Damage Taken Multiplier", + "hint": "Multiply any damage dealt to you by this number" + } + }, "Bonuses": { "rest": { "downtimeAction": "Downtime Action", @@ -2024,16 +2059,40 @@ "reaction": "Reaction Roll" }, "Rules": { + "conditionImmunities": { + "hidden": "Condition Immunity: Hidden", + "restrained": "Condition Immunity: Restrained", + "vulnerable": "Condition Immunity: Vulnerable" + }, "damageReduction": { + "disabledArmor": { "label": "Disabled Armorslots" }, "increasePerArmorMark": { "label": "Damage Reduction per Armor Slot", "hint": "A used armor slot normally reduces damage by one step. This value increases the number of steps damage is reduced by." }, + "magical": { + "label": "Daamge Reduction: Only Magical", + "hint": "Armor can only be used to reduce magical damage" + }, "maxArmorMarkedBonus": "Max Armor Used", "maxArmorMarkedStress": { "label": "Max Armor Used With Stress", "hint": "If this value is set you can use up to that much stress to spend additional Armor Marks beyond your normal maximum." }, + "reduceSeverity": { + "magical": { + "label": "Reduce Damage Severity: Magical", + "hint": "Lowers any magical damage received by the set amount of severity degrees" + }, + "physical": { + "label": "Reduce Damage Severity: Physical", + "hint": "Lowers any physical damage received by the set amount of severity degrees" + } + }, + "physical": { + "label": "Damage Reduction: Only Physical", + "hint": "Armor can only be used to reduce physical damage" + }, "stress": { "any": { "label": "Stress Damage Reduction: Any", @@ -2051,6 +2110,12 @@ "label": "Stress Damage Reduction: Minor", "hint": "The cost in stress you can pay to reduce minor damage to none." } + }, + "thresholdImmunities": { + "minor": { + "label": "Threshold Immunities: Minor", + "hint": "Automatically ignores minor damage when set to 1" + } } }, "attack": { @@ -2120,7 +2185,9 @@ "configuration": "Configuration", "base": "Base", "triggers": "Triggers", - "deathMoves": "Deathmoves" + "deathMoves": "Deathmoves", + "sources": "Sources", + "packs": "Packs" }, "Tiers": { "singular": "Tier", @@ -2152,6 +2219,7 @@ "continue": "Continue", "criticalSuccess": "Critical Success", "criticalShort": "Critical", + "currentLevel": "Current Level", "custom": "Custom", "d20Roll": "D20 Roll", "damage": "Damage", @@ -2257,6 +2325,7 @@ "single": "Target", "plural": "Targets" }, + "thingsAndThing": "{things} and {thing}", "title": "Title", "tokenSize": "Token Size", "total": "Total", @@ -2295,7 +2364,8 @@ }, "Ancestry": { "primaryFeature": "Primary Feature", - "secondaryFeature": "Secondary Feature" + "secondaryFeature": "Secondary Feature", + "featuresLabel": "Ancestry Features" }, "Armor": { "baseScore": "Base Score", @@ -2348,7 +2418,12 @@ "evolvedImagePlaceholder": "The image for the form selected for evolution will be used" }, "Class": { + "startingEvasionScore": "Starting Evasion Score", + "startingHitPoints": "Starting Hit Points", + "classItems": "Class Items", + "hopeFeatureLabel": "{class}'s Hope Feature", "hopeFeatures": "Hope Features", + "classFeature": "Class Feature", "classFeatures": "Class Features", "guide": { "suggestedEquipment": "Suggested Equipments", @@ -2361,6 +2436,9 @@ } } }, + "Community": { + "featuresLabel": "Community Feature" + }, "Consumable": { "consumeOnUse": "Consume On Use", "destroyOnEmpty": "Destroy On Empty" @@ -2376,7 +2454,11 @@ "masteryTitle": "Mastery" }, "Subclass": { - "spellcastingTrait": "Spellcasting Trait" + "spellcastingTrait": "Spellcasting Trait", + "spellcastTrait": "Spellcast Trait", + "foundationFeatures": "Foundation Features", + "specializationFeature": "Specialization Feature", + "masteryFeature": "Mastery Feature" }, "Weapon": { "weaponType": "Weapon Type", @@ -2475,6 +2557,10 @@ "gm": { "label": "GM" }, "players": { "label": "Players" } }, + "vulnerableAutomation": { + "label": "Vulnerable Automation", + "hint": "Automatically apply the Vulnerable condition when a actor reaches max stress" + }, "countdownAutomation": { "label": "Countdown Automation", "hint": "Automatically progress countdowns based on their progression settings" @@ -2555,6 +2641,8 @@ "resetMovesTitle": "Reset {type} Downtime Moves", "resetItemFeaturesTitle": "Reset {type}", "resetMovesText": "Are you sure you want to reset?", + "deleteItemTitle": "Delete Homebrew Item", + "deleteItemText": "Are you sure you want to delete the item?", "FIELDS": { "maxFear": { "label": "Max Fear" }, "maxHope": { "label": "Max Hope" }, @@ -2723,7 +2811,7 @@ "title": "Domain Card" }, "dualityRoll": { - "abilityCheckTitle": "{ability} Check" + "abilityCheckTitle": "{ability} Roll" }, "effectSummary": { "title": "Effects Applied", @@ -2738,7 +2826,7 @@ "selectLeader": "Select a Leader", "selectMember": "Select a Member", "rerollTitle": "Reroll Group Roll", - "rerollContent": "Are you sure you want to reroll your {trait} check?", + "rerollContent": "Are you sure you want to reroll your {trait} roll?", "rerollTooltip": "Reroll", "wholePartySelected": "The whole party is selected" }, @@ -2786,6 +2874,7 @@ "ItemBrowser": { "title": "Daggerheart Compendium Browser", "hint": "Select a Folder in sidebar to start browsing through the compendium", + "browserSettings": "Browser Settings", "searchPlaceholder": "Search...", "columnName": "Name", "tooltipFilters": "Filters", @@ -2903,14 +2992,17 @@ "tokenActorMissing": "{name} is missing an Actor", "tokenActorsMissing": "[{names}] missing Actors", "domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used", - "knowTheTide": "Know The Tide gained a token" + "knowTheTide": "Know The Tide gained a token", + "lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}" }, "Sidebar": { "actorDirectory": { "tier": "Tier {tier} {type}", "character": "Level {level} Character", "companion": "Level {level} - {partner}", - "companionNoPartner": "No Partner" + "companionNoPartner": "No Partner", + "duplicateToNewTier": "Duplicate to New Tier", + "pickTierTitle": "Pick a new tier for this adversary" }, "daggerheartMenu": { "title": "Daggerheart Menu", @@ -2942,7 +3034,7 @@ "rulesOn": "Rules On", "rulesOff": "Rules Off", "remainingUses": "Uses refresh on {type}", - "rightClickExtand": "Right-Click to extand", + "rightClickExtend": "Right-Click to extend", "companionPartnerLevelBlock": "The companion needs an assigned partner to level up.", "configureAttribution": "Configure Attribution", "deleteItem": "Delete Item", diff --git a/module/applications/dialogs/CompendiumBrowserSettings.mjs b/module/applications/dialogs/CompendiumBrowserSettings.mjs new file mode 100644 index 00000000..bef54a6f --- /dev/null +++ b/module/applications/dialogs/CompendiumBrowserSettings.mjs @@ -0,0 +1,143 @@ +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; + +export default class CompendiumBrowserSettings extends HandlebarsApplicationMixin(ApplicationV2) { + constructor() { + super(); + + this.browserSettings = game.settings + .get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings) + .toObject(); + } + + static DEFAULT_OPTIONS = { + tag: 'div', + classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'compendium-brower-settings'], + window: { + icon: 'fa-solid fa-book', + title: 'DAGGERHEART.APPLICATIONS.CompendiumBrowserSettings.title' + }, + position: { + width: 500 + }, + actions: { + toggleSource: CompendiumBrowserSettings.#toggleSource, + finish: CompendiumBrowserSettings.#finish + } + }; + + /** @override */ + static PARTS = { + packs: { + id: 'packs', + template: 'systems/daggerheart/templates/dialogs/compendiumBrowserSettingsDialog/packs.hbs' + }, + footer: { template: 'systems/daggerheart/templates/dialogs/compendiumBrowserSettingsDialog/footer.hbs' } + }; + + static #browserPackTypes = ['Actor', 'Item']; + + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + for (const element of htmlElement.querySelectorAll('.pack-checkbox')) + element.addEventListener('change', this.toggleTypedPack.bind(this)); + } + + /**@inheritdoc */ + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + + const excludedSourceData = this.browserSettings.excludedSources; + const excludedPackData = this.browserSettings.excludedPacks; + context.typePackCollections = game.packs.reduce((acc, pack) => { + const { type, label, packageType, packageName: basePackageName, id } = pack.metadata; + if (!CompendiumBrowserSettings.#browserPackTypes.includes(type)) return acc; + + const isWorldPack = packageType === 'world'; + const packageName = isWorldPack ? 'world' : basePackageName; + const sourceChecked = + !excludedSourceData[packageName] || + !excludedSourceData[packageName].excludedDocumentTypes.includes(type); + + const sourceLabel = + game.modules.get(packageName)?.title ?? + (isWorldPack + ? game.i18n.localize('DAGGERHEART.APPLICATIONS.CompendiumBrowserSettings.worldCompendiums') + : game.system.title); + if (!acc[type]) acc[type] = { label: game.i18n.localize(`DOCUMENT.${type}s`), sources: {} }; + if (!acc[type].sources[packageName]) + acc[type].sources[packageName] = { label: sourceLabel, checked: sourceChecked, packs: [] }; + + const checked = !excludedPackData[id] || !excludedPackData[id].excludedDocumentTypes.includes(type); + + acc[type].sources[packageName].packs.push({ + pack: id, + type, + label: id === game.system.id ? game.system.title : game.i18n.localize(label), + checked: checked + }); + + return acc; + }, {}); + + return context; + } + + static #toggleSource(event, button) { + event.stopPropagation(); + + const { type, source } = button.dataset; + const currentlyExcluded = this.browserSettings.excludedSources[source] + ? this.browserSettings.excludedSources[source].excludedDocumentTypes.includes(type) + : false; + + if (!this.browserSettings.excludedSources[source]) + this.browserSettings.excludedSources[source] = { excludedDocumentTypes: [] }; + this.browserSettings.excludedSources[source].excludedDocumentTypes = currentlyExcluded + ? this.browserSettings.excludedSources[source].excludedDocumentTypes.filter(x => x !== type) + : [...(this.browserSettings.excludedSources[source]?.excludedDocumentTypes ?? []), type]; + + const toggleIcon = button.querySelector('a > i'); + toggleIcon.classList.toggle('fa-toggle-off'); + toggleIcon.classList.toggle('fa-toggle-on'); + button.closest('.source-container').querySelector('.checks-container').classList.toggle('collapsed'); + } + + toggleTypedPack(event) { + event.stopPropagation(); + + const { type, pack } = event.target.dataset; + const currentlyExcluded = this.browserSettings.excludedPacks[pack] + ? this.browserSettings.excludedPacks[pack].excludedDocumentTypes.includes(type) + : false; + + if (!this.browserSettings.excludedPacks[pack]) + this.browserSettings.excludedPacks[pack] = { excludedDocumentTypes: [] }; + this.browserSettings.excludedPacks[pack].excludedDocumentTypes = currentlyExcluded + ? this.browserSettings.excludedPacks[pack].excludedDocumentTypes.filter(x => x !== type) + : [...(this.browserSettings.excludedPacks[pack]?.excludedDocumentTypes ?? []), type]; + + this.render(); + } + + static async #finish() { + const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings); + await settings.updateSource(this.browserSettings); + await game.settings.set( + CONFIG.DH.id, + CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings, + settings.toObject() + ); + + this.updated = true; + this.close(); + } + + static async configure() { + return new Promise(resolve => { + const app = new this(); + app.addEventListener('close', () => resolve(app.updated), { once: true }); + app.render({ force: true }); + }); + } +} diff --git a/module/applications/dialogs/_module.mjs b/module/applications/dialogs/_module.mjs index 4eda8579..a479100a 100644 --- a/module/applications/dialogs/_module.mjs +++ b/module/applications/dialogs/_module.mjs @@ -16,3 +16,4 @@ export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs'; export { default as GroupRollDialog } from './group-roll-dialog.mjs'; export { default as TagTeamDialog } from './tagTeamDialog.mjs'; export { default as RiskItAllDialog } from './riskItAllDialog.mjs'; +export { default as CompendiumBrowserSettingsDialog } from './CompendiumBrowserSettings.mjs'; diff --git a/module/applications/dialogs/attributionDialog.mjs b/module/applications/dialogs/attributionDialog.mjs index 99ff261a..7f3f8bb2 100644 --- a/module/applications/dialogs/attributionDialog.mjs +++ b/module/applications/dialogs/attributionDialog.mjs @@ -54,7 +54,11 @@ export default class AttributionDialog extends HandlebarsApplicationMixin(Applic const after = label.slice(matchIndex + search.length, label.length); const element = document.createElement('li'); - element.innerHTML = `${beforeText}${matchText ? `${matchText}` : ''}${after}`; + element.innerHTML = + `${beforeText}${matchText ? `${matchText}` : ''}${after}`.replaceAll( + ' ', + ' ' + ); if (item.hint) { element.dataset.tooltip = game.i18n.localize(item.hint); } diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index 4a4b1556..8e79ba58 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -165,9 +165,10 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio } 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) - }); + if (!this.config.source.item) + 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.render(); diff --git a/module/applications/dialogs/deathMove.mjs b/module/applications/dialogs/deathMove.mjs index a9141158..69ff758e 100644 --- a/module/applications/dialogs/deathMove.mjs +++ b/module/applications/dialogs/deathMove.mjs @@ -200,7 +200,6 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV description: game.i18n.localize(this.selectedMove.description), result: result, open: autoExpandDescription ? 'open' : '', - chevron: autoExpandDescription ? 'fa-chevron-up' : 'fa-chevron-down', showRiskItAllButton: this.showRiskItAllButton, riskItAllButtonLabel: this.riskItAllButtonLabel, riskItAllHope: this.riskItAllHope diff --git a/module/applications/dialogs/downtime.mjs b/module/applications/dialogs/downtime.mjs index 9a9a9ddb..4c01c2a9 100644 --- a/module/applications/dialogs/downtime.mjs +++ b/module/applications/dialogs/downtime.mjs @@ -196,6 +196,9 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV .filter(x => x.testUserPermission(game.user, 'LIMITED')) .filter(x => x.uuid !== this.actor.uuid); + const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance) + .expandRollMessage?.desc; + const cls = getDocumentClass('ChatMessage'); const msg = { user: game.user.id, @@ -216,7 +219,8 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV actor: { name: this.actor.name, img: this.actor.img }, moves: moves, characters: characters, - selfId: this.actor.uuid + selfId: this.actor.uuid, + open: autoExpandDescription ? 'open' : '' } ), flags: { diff --git a/module/applications/dialogs/group-roll-dialog.mjs b/module/applications/dialogs/group-roll-dialog.mjs index 2cb79563..8a3c43d6 100644 --- a/module/applications/dialogs/group-roll-dialog.mjs +++ b/module/applications/dialogs/group-roll-dialog.mjs @@ -70,7 +70,11 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat element.appendChild(img); const label = document.createElement('span'); - label.innerHTML = `${beforeText}${matchText ? `${matchText}` : ''}${after}`; + label.innerHTML = + `${beforeText}${matchText ? `${matchText}` : ''}${after}`.replaceAll( + ' ', + ' ' + ); element.appendChild(label); return element; @@ -119,7 +123,11 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat element.appendChild(img); const label = document.createElement('span'); - label.innerHTML = `${beforeText}${matchText ? `${matchText}` : ''}${after}`; + label.innerHTML = + `${beforeText}${matchText ? `${matchText}` : ''}${after}`.replaceAll( + ' ', + ' ' + ); element.appendChild(label); return element; diff --git a/module/applications/scene/sceneConfigSettings.mjs b/module/applications/scene/sceneConfigSettings.mjs index 5335be16..57a2bda4 100644 --- a/module/applications/scene/sceneConfigSettings.mjs +++ b/module/applications/scene/sceneConfigSettings.mjs @@ -104,7 +104,10 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S /** @override */ async _processSubmitData(event, form, submitData, options) { if (!submitData.flags) submitData.flags = {}; - submitData.flags.daggerheart = this.daggerheartFlag.toObject(); + submitData.flags.daggerheart = foundry.utils.mergeObject( + this.daggerheartFlag.toObject(), + submitData.flags.daggerheart + ); submitData.flags.daggerheart.sceneEnvironments = submitData.flags.daggerheart.sceneEnvironments.filter(x => foundry.utils.fromUuidSync(x) ); diff --git a/module/applications/settings/homebrewSettings.mjs b/module/applications/settings/homebrewSettings.mjs index add3c9e1..083c468f 100644 --- a/module/applications/settings/homebrewSettings.mjs +++ b/module/applications/settings/homebrewSettings.mjs @@ -103,6 +103,12 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli ? { id: this.selected.adversaryType, ...this.settings.adversaryTypes[this.selected.adversaryType] } : null; break; + case 'downtime': + context.restOptions = { + shortRest: CONFIG.DH.GENERAL.defaultRestOptions.shortRest(), + longRest: CONFIG.DH.GENERAL.defaultRestOptions.longRest() + }; + break; } return context; @@ -181,6 +187,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli }); } + game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject()); this.render(); } @@ -221,16 +228,28 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli } }); + game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject()); this.render(); } static async removeItem(_, target) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.localize(`DAGGERHEART.SETTINGS.Homebrew.deleteItemTitle`) + }, + content: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.deleteItemText') + }); + + if (!confirmed) return; + const { type, id } = target.dataset; const isDowntime = ['shortRest', 'longRest'].includes(type); const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`; await this.settings.updateSource({ [`${path}.-=${id}`]: null }); + + game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject()); this.render(); } diff --git a/module/applications/sheets-configs/activeEffectConfig.mjs b/module/applications/sheets-configs/activeEffectConfig.mjs index d7b1b536..1f2ba5e0 100644 --- a/module/applications/sheets-configs/activeEffectConfig.mjs +++ b/module/applications/sheets-configs/activeEffectConfig.mjs @@ -4,22 +4,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac constructor(options) { super(options); - 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); - // As per DHToken._getTrackedAttributesFromSchema, attributes.bar have a max version as well. - const maxAttributes = attributes.bar.map(x => [...x, 'max']); - attributes.value.push(...maxAttributes); - const group = game.i18n.localize(model.metadata.label); - const choices = CONFIG.Token.documentClass - .getTrackedAttributeChoices(attributes, model) - .map(x => ({ ...x, group: group })); - acc.push(...choices); - } - return acc; - }, []); + this.changeChoices = DhActiveEffectConfig.getChangeChoices(); } static DEFAULT_OPTIONS = { @@ -50,6 +35,69 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac } }; + /** + * Get ChangeChoices for the changes autocomplete. Static for use in this class aswell as in settings-active-effect-config.mjs + * @returns {ChangeChoice { value: string, label: string, hint: string, group: string }[]} + */ + static getChangeChoices() { + const ignoredActorKeys = ['config', 'DhEnvironment', 'DhParty']; + + const getAllLeaves = (root, group, parentPath = '') => { + const leaves = []; + const rootKey = `${parentPath ? `${parentPath}.` : ''}${root.name}`; + for (const field of Object.values(root.fields)) { + if (field instanceof foundry.data.fields.SchemaField) + leaves.push(...getAllLeaves(field, group, rootKey)); + else + leaves.push({ + value: `${rootKey}.${field.name}`, + label: game.i18n.localize(field.label), + hint: game.i18n.localize(field.hint), + group + }); + } + + return leaves; + }; + return Object.keys(game.system.api.models.actors).reduce((acc, key) => { + if (ignoredActorKeys.includes(key)) return acc; + + const model = game.system.api.models.actors[key]; + const group = game.i18n.localize(model.metadata.label); + const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model.metadata.type); + + const getTranslations = path => { + if (path === 'resources.hope.max') + return { + label: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxHope.label'), + hint: '' + }; + + const field = model.schema.getField(path); + return { + label: field ? game.i18n.localize(field.label) : path, + hint: field ? game.i18n.localize(field.hint) : '' + }; + }; + + const bars = attributes.bar.flatMap(x => { + const joined = `${x.join('.')}.max`; + return { value: joined, ...getTranslations(joined), group }; + }); + const values = attributes.value.flatMap(x => { + const joined = x.join('.'); + return { value: joined, ...getTranslations(joined), group }; + }); + + const bonuses = getAllLeaves(model.schema.fields.bonuses, group); + const rules = getAllLeaves(model.schema.fields.rules, group); + + acc.push(...bars, ...values, ...rules, ...bonuses); + + return acc; + }, []); + } + _attachPartListeners(partId, htmlElement, options) { super._attachPartListeners(partId, htmlElement, options); const changeChoices = this.changeChoices; @@ -68,14 +116,18 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac }, render: function (item, search) { const label = game.i18n.localize(item.label); - const matchIndex = label.toLowerCase().indexOf(search); + const matchIndex = label.toLowerCase().indexOf(search.toLowerCase()); const beforeText = label.slice(0, matchIndex); const 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 ? `${matchText}` : ''}${after}`; + element.innerHTML = + `${beforeText}${matchText ? `${matchText}` : ''}${after}`.replaceAll( + ' ', + ' ' + ); if (item.hint) { element.dataset.tooltip = game.i18n.localize(item.hint); } diff --git a/module/applications/sheets-configs/setting-active-effect-config.mjs b/module/applications/sheets-configs/setting-active-effect-config.mjs index ca0d56e3..12ac90d1 100644 --- a/module/applications/sheets-configs/setting-active-effect-config.mjs +++ b/module/applications/sheets-configs/setting-active-effect-config.mjs @@ -7,19 +7,7 @@ export default class SettingActiveEffectConfig extends HandlebarsApplicationMixi 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; - }, []); + this.changeChoices = game.system.api.applications.sheetConfigs.ActiveEffectConfig.getChangeChoices(); } static DEFAULT_OPTIONS = { @@ -103,7 +91,11 @@ export default class SettingActiveEffectConfig extends HandlebarsApplicationMixi const after = label.slice(matchIndex + search.length, label.length); const element = document.createElement('li'); - element.innerHTML = `${beforeText}${matchText ? `${matchText}` : ''}${after}`; + element.innerHTML = + `${beforeText}${matchText ? `${matchText}` : ''}${after}`.replaceAll( + ' ', + ' ' + ); if (item.hint) { element.dataset.tooltip = game.i18n.localize(item.hint); } diff --git a/module/applications/sheets-configs/setting-feature-config.mjs b/module/applications/sheets-configs/setting-feature-config.mjs index e8bf6109..28e686ea 100644 --- a/module/applications/sheets-configs/setting-feature-config.mjs +++ b/module/applications/sheets-configs/setting-feature-config.mjs @@ -73,9 +73,11 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App return context; } - static async updateData(event, element, formData) { + static async updateData(_event, _element, formData) { const data = foundry.utils.expandObject(formData.object); - foundry.utils.mergeObject(this.move, data); + await this.updateMove({ + [`${this.movePath}`]: data + }); this.render(); } @@ -135,9 +137,7 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App } ); - await this.settings.updateSource({ [`${this.actionsPath}.${action.id}`]: action }); - this.move = foundry.utils.getProperty(this.settings, this.movePath); - + await this.updateMove({ [`${this.actionsPath}.${action.id}`]: action }); this.render(); } @@ -150,13 +150,12 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect); if (!updatedEffect) return; - await this.settings.updateSource({ + await this.updateMove({ [`${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.render(); } else { const action = this.move.actions.get(id); @@ -171,13 +170,13 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App : existingEffectIndex === -1 ? [...currentEffects, effectData] : currentEffects.with(existingEffectIndex, effectData); - await this.settings.updateSource({ + await this.updateMove({ [`${this.movePath}.effects`]: updatedEffects }); } - await this.settings.updateSource({ [`${this.actionsPath}.${id}`]: updatedMove }); - this.move = foundry.utils.getProperty(this.settings, this.movePath); + await this.updateMove({ [`${this.actionsPath}.${id}`]: updatedMove }); + this.render(); return updatedEffects; }).render(true); @@ -199,33 +198,36 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App }); } } - await this.settings.updateSource({ + await this.updateMove({ [this.movePath]: { effects: move.effects.filter(x => x.id !== id), actions: move.actions } }); } else { - await this.settings.updateSource({ [`${this.actionsPath}.-=${target.dataset.id}`]: null }); + await this.updateMove({ [`${this.actionsPath}.-=${target.dataset.id}`]: null }); } - this.move = foundry.utils.getProperty(this.settings, this.movePath); this.render(); } - static async addEffect(_, target) { + static async addEffect() { const currentEffects = foundry.utils.getProperty(this.settings, `${this.movePath}.effects`); - await this.settings.updateSource({ + + await this.updateMove({ [`${this.movePath}.effects`]: [ ...currentEffects, game.system.api.data.activeEffects.BaseEffect.getDefaultObject() ] }); - - this.move = foundry.utils.getProperty(this.settings, this.movePath); this.render(); } + async updateMove(update) { + await this.settings.updateSource(update); + this.move = foundry.utils.getProperty(this.settings, this.movePath); + } + static resetMoves() {} _filterTabs(tabs) { diff --git a/module/applications/sheets/actors/party.mjs b/module/applications/sheets/actors/party.mjs index d78519cb..1b1722db 100644 --- a/module/applications/sheets/actors/party.mjs +++ b/module/applications/sheets/actors/party.mjs @@ -6,7 +6,6 @@ import DaggerheartMenu from '../../sidebar/tabs/daggerheartMenu.mjs'; import { socketEvent } from '../../../systemRegistration/socket.mjs'; import GroupRollDialog from '../../dialogs/group-roll-dialog.mjs'; import DhpActor from '../../../documents/actor.mjs'; -import DHItem from '../../../documents/item.mjs'; export default class Party extends DHBaseActorSheet { constructor(options) { @@ -269,15 +268,6 @@ export default class Party extends DHBaseActorSheet { ).render({ force: true }); } - /** - * Get the set of ContextMenu options for Consumable and Loot. - * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance - * @this {CharacterSheet} - * @protected - */ - static #getItemContextOptions() { - return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true }); - } /* -------------------------------------------- */ /* Filter Tracking */ /* -------------------------------------------- */ diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index 3c0444eb..449880fb 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -431,18 +431,18 @@ export default function DHApplicationMixin(Base) { { name: 'disableEffect', icon: 'fa-solid fa-lightbulb', - condition: target => { - const doc = getDocFromElementSync(target); - return doc && !doc.disabled; + condition: element => { + const target = element.closest('[data-item-uuid]'); + return !target.dataset.disabled && target.dataset.itemType !== 'beastform'; }, callback: async target => (await getDocFromElement(target)).update({ disabled: true }) }, { name: 'enableEffect', icon: 'fa-regular fa-lightbulb', - condition: target => { - const doc = getDocFromElementSync(target); - return doc && doc.disabled; + condition: element => { + const target = element.closest('[data-item-uuid]'); + return target.dataset.disabled && target.dataset.itemType !== 'beastform'; }, callback: async target => (await getDocFromElement(target)).update({ disabled: false }) } @@ -536,6 +536,10 @@ export default function DHApplicationMixin(Base) { options.push({ name: 'CONTROLS.CommonDelete', icon: 'fa-solid fa-trash', + condition: element => { + const target = element.closest('[data-item-uuid]'); + return target.dataset.itemType !== 'beastform'; + }, callback: async (target, event) => { const doc = await getDocFromElement(target); if (event.shiftKey) return doc.delete(); diff --git a/module/applications/sheets/api/base-actor.mjs b/module/applications/sheets/api/base-actor.mjs index 85ecd616..6f994faf 100644 --- a/module/applications/sheets/api/base-actor.mjs +++ b/module/applications/sheets/api/base-actor.mjs @@ -36,7 +36,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { ], dragDrop: [ { dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null }, - { dragSelector: ".currency[data-currency] .drag-handle", dropSelector: null } + { dragSelector: '.currency[data-currency] .drag-handle', dropSelector: null } ] }; @@ -92,7 +92,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { value: context.source.system.gold[key] }; } - context.inventory.hasCurrency = Object.values(context.inventory.currencies).some((c) => c.enabled); + context.inventory.hasCurrency = Object.values(context.inventory.currencies).some(c => c.enabled); } return context; @@ -270,7 +270,9 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { currency }); if (quantity) { - originActor.update({ [`system.gold.${currency}`]: Math.max(0, originActor.system.gold[currency] - quantity) }); + originActor.update({ + [`system.gold.${currency}`]: Math.max(0, originActor.system.gold[currency] - quantity) + }); this.document.update({ [`system.gold.${currency}`]: this.document.system.gold[currency] + quantity }); } return; @@ -292,6 +294,15 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { /* Handling transfer of inventoryItems */ if (item.system.metadata.isInventoryItem) { + if (!this.document.testUserPermission(game.user, 'OWNER', { exact: true })) { + return ui.notifications.error( + game.i18n.format('DAGGERHEART.UI.Notifications.lackingItemTransferPermission', { + user: game.user.name, + target: this.document.name + }) + ); + } + if (item.system.metadata.isQuantifiable) { const actorItem = originActor.items.get(data.originId); const quantityTransfered = await game.system.api.applications.dialogs.ItemTransferDialog.configure({ @@ -300,14 +311,6 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { }); if (quantityTransfered) { - if (quantityTransfered === actorItem.system.quantity) { - await originActor.deleteEmbeddedDocuments('Item', [data.originId]); - } else { - await actorItem.update({ - 'system.quantity': actorItem.system.quantity - quantityTransfered - }); - } - const existingItem = this.document.items.find(x => itemIsIdentical(x, item)); if (existingItem) { await existingItem.update({ @@ -325,10 +328,18 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { } ]); } + + if (quantityTransfered === actorItem.system.quantity) { + await originActor.deleteEmbeddedDocuments('Item', [data.originId]); + } else { + await actorItem.update({ + 'system.quantity': actorItem.system.quantity - quantityTransfered + }); + } } } else { - await originActor.deleteEmbeddedDocuments('Item', [data.originId]); await this.document.createEmbeddedDocuments('Item', [item.toObject()]); + await originActor.deleteEmbeddedDocuments('Item', [data.originId]); } } } @@ -339,7 +350,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { */ async _onDragStart(event) { // Handle drag/dropping currencies - const currencyEl = event.currentTarget.closest(".currency[data-currency]"); + const currencyEl = event.currentTarget.closest('.currency[data-currency]'); if (currencyEl) { const currency = currencyEl.dataset.currency; const data = { type: 'Currency', currency, originActor: this.document.uuid }; @@ -359,8 +370,8 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { event.dataTransfer.setData('text/plain', JSON.stringify(attackData)); event.dataTransfer.setDragImage(attackItem.querySelector('img'), 60, 0); return; - } - + } + const item = await getDocFromElement(event.target); if (item) { const dragData = { diff --git a/module/applications/sheets/api/item-attachment-sheet.mjs b/module/applications/sheets/api/item-attachment-sheet.mjs index 2898f5ac..bcf2fc3c 100644 --- a/module/applications/sheets/api/item-attachment-sheet.mjs +++ b/module/applications/sheets/api/item-attachment-sheet.mjs @@ -1,7 +1,6 @@ export default function ItemAttachmentSheet(Base) { return class extends Base { static DEFAULT_OPTIONS = { - ...super.DEFAULT_OPTIONS, dragDrop: [ ...(super.DEFAULT_OPTIONS.dragDrop || []), { dragSelector: null, dropSelector: '.attachments-section' } diff --git a/module/applications/sidebar/tabs/actorDirectory.mjs b/module/applications/sidebar/tabs/actorDirectory.mjs index d40443a0..9d8f16e1 100644 --- a/module/applications/sidebar/tabs/actorDirectory.mjs +++ b/module/applications/sidebar/tabs/actorDirectory.mjs @@ -43,4 +43,54 @@ export default class DhActorDirectory extends foundry.applications.sidebar.tabs. event.dataTransfer.setDragImage(preview, w / 2, h / 2); } } + + _getEntryContextOptions() { + const options = super._getEntryContextOptions(); + options.push({ + name: 'DAGGERHEART.UI.Sidebar.actorDirectory.duplicateToNewTier', + icon: ``, + condition: li => { + const actor = game.actors.get(li.dataset.entryId); + return actor?.type === 'adversary' && actor.system.type !== 'social'; + }, + callback: async li => { + const actor = game.actors.get(li.dataset.entryId); + if (!actor) throw new Error('Unexpected missing actor'); + + const tiers = [1, 2, 3, 4].filter(t => t !== actor.system.tier); + const content = document.createElement('div'); + const select = document.createElement('select'); + select.name = 'tier'; + select.append( + ...tiers.map(t => { + const option = document.createElement('option'); + option.value = t; + option.textContent = game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${t}`); + return option; + }) + ); + content.append(select); + + const tier = await foundry.applications.api.Dialog.input({ + classes: ['dh-style', 'dialog'], + window: { title: 'DAGGERHEART.UI.Sidebar.actorDirectory.pickTierTitle' }, + content, + ok: { + label: 'Create Adversary', + callback: (event, button, dialog) => Number(button.form.elements.tier.value) + } + }); + + if (tier === actor.system.tier) { + ui.notifications.warn('This actor is already at this tier'); + } else if (tier) { + const source = actor.system.adjustForTier(tier); + await Actor.create(source); + ui.notifications.info(`Tier ${tier} ${actor.name} created`); + } + } + }); + + return options; + } } diff --git a/module/applications/sidebar/tabs/daggerheartMenu.mjs b/module/applications/sidebar/tabs/daggerheartMenu.mjs index b29437bf..26ae484b 100644 --- a/module/applications/sidebar/tabs/daggerheartMenu.mjs +++ b/module/applications/sidebar/tabs/daggerheartMenu.mjs @@ -1,4 +1,4 @@ -import { refreshIsAllowed } from '../../../helpers/utils.mjs'; +import { RefreshFeatures } from '../../../helpers/utils.mjs'; const { HandlebarsApplicationMixin } = foundry.applications.api; const { AbstractSidebarTab } = foundry.applications.sidebar; @@ -54,73 +54,6 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract return context; } - async getRefreshables(types) { - const refreshedActors = {}; - for (let actor of game.actors) { - if (['character', 'adversary'].includes(actor.type) && actor.prototypeToken.actorLink) { - const updates = {}; - for (let item of actor.items) { - if (item.system.metadata?.hasResource && refreshIsAllowed(types, item.system.resource?.recovery)) { - if (!refreshedActors[actor.id]) - refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() }; - refreshedActors[actor.id].refreshed.add( - game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[item.system.resource.recovery].label) - ); - - if (!updates[item.id]?.system) updates[item.id] = { system: {} }; - - const increasing = - item.system.resource.progression === CONFIG.DH.ITEM.itemResourceProgression.increasing.id; - updates[item.id].system = { - ...updates[item.id].system, - 'resource.value': increasing - ? 0 - : Roll.replaceFormulaData(item.system.resource.max, actor.getRollData()) - }; - } - if (item.system.metadata?.hasActions) { - const refreshTypes = new Set(); - const actions = item.system.actions.filter(action => { - if (refreshIsAllowed(types, action.uses.recovery)) { - refreshTypes.add(action.uses.recovery); - return true; - } - - return false; - }); - if (actions.length === 0) continue; - - if (!refreshedActors[actor.id]) - refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() }; - refreshedActors[actor.id].refreshed.add( - ...refreshTypes.map(type => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[type].label)) - ); - - if (!updates[item.id]?.system) updates[item.id] = { system: {} }; - - updates[item.id].system = { - ...updates[item.id].system, - ...actions.reduce( - (acc, action) => { - acc.actions[action.id] = { 'uses.value': 0 }; - return acc; - }, - { actions: updates[item.id].system.actions ?? {} } - ) - }; - } - } - - for (let key in updates) { - const update = updates[key]; - await actor.items.get(key).update(update); - } - } - } - - return refreshedActors; - } - /* -------------------------------------------- */ /* Application Clicks Actions */ /* -------------------------------------------- */ @@ -133,30 +66,9 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract 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}]` - }) - ); + await RefreshFeatures(refreshKeys); + 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(); } } diff --git a/module/applications/ui/fearTracker.mjs b/module/applications/ui/fearTracker.mjs index e9c816db..82dda215 100644 --- a/module/applications/ui/fearTracker.mjs +++ b/module/applications/ui/fearTracker.mjs @@ -34,8 +34,6 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV position: { width: 222, height: 222 - // top: "200px", - // left: "120px" } }; @@ -66,7 +64,7 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV max = this.maxFear, percent = (current / max) * 100, isGM = game.user.isGM; - // Return the data for rendering + return { display, current, max, percent, isGM }; } diff --git a/module/applications/ui/itemBrowser.mjs b/module/applications/ui/itemBrowser.mjs index b35573f7..2d882eba 100644 --- a/module/applications/ui/itemBrowser.mjs +++ b/module/applications/ui/itemBrowser.mjs @@ -1,3 +1,5 @@ +import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; + const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; /** @@ -17,6 +19,15 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { this.config = CONFIG.DH.ITEMBROWSER.compendiumConfig; this.presets = {}; this.compendiumBrowserTypeKey = 'compendiumBrowserDefault'; + + this.setupHooks = Hooks.on(socketEvent.Refresh, ({ refreshType }) => { + if (refreshType === RefreshType.CompendiumBrowser) { + if (this.rendered) { + this.render(); + this.loadItems(); + } + } + }); } /** @inheritDoc */ @@ -35,7 +46,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { selectFolder: this.selectFolder, expandContent: this.expandContent, resetFilters: this.resetFilters, - sortList: this.sortList + sortList: this.sortList, + openSettings: this.openSettings }, position: { left: 100, @@ -157,6 +169,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { context.formatChoices = this.formatChoices; context.items = this.items; context.presets = this.presets; + context.isGM = game.user.isGM; + return context; } @@ -214,6 +228,10 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { loadItems() { let loadTimeout = this.toggleLoader(true); + const browserSettings = game.settings.get( + CONFIG.DH.id, + CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings + ); const promises = []; game.packs.forEach(pack => { @@ -227,7 +245,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { Promise.all(promises).then(async result => { this.items = ItemBrowser.sortBy( - result.flatMap(r => r), + result.flatMap(r => r).filter(r => !browserSettings.isEntryExcluded.bind(browserSettings)(r)), 'name' ); @@ -512,6 +530,22 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { itemListContainer.replaceChildren(...newOrder); } + static async openSettings() { + const settingsUpdated = await game.system.api.applications.dialogs.CompendiumBrowserSettingsDialog.configure(); + if (settingsUpdated) { + if (this.rendered) { + this.render(); + this.loadItems(); + } + await game.socket.emit(`system.${CONFIG.DH.id}`, { + action: socketEvent.Refresh, + data: { + refreshType: RefreshType.CompendiumBrowser + } + }); + } + } + _createDragProcess() { new foundry.applications.ux.DragDrop.implementation({ dragSelector: '.item-container', @@ -571,4 +605,9 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { headerActions.append(button); } } + + async close(options = {}) { + Hooks.off(socketEvent.Refresh, this.setupHooks); + await super.close(options); + } } diff --git a/module/applications/ui/sceneNavigation.mjs b/module/applications/ui/sceneNavigation.mjs index 0a3e08a5..67bfe0b4 100644 --- a/module/applications/ui/sceneNavigation.mjs +++ b/module/applications/ui/sceneNavigation.mjs @@ -63,7 +63,8 @@ export default class DhSceneNavigation extends foundry.applications.ui.SceneNavi if (scene.flags.daggerheart.sceneEnvironments[0] !== environment.uuid) { const newEnvironments = scene.flags.daggerheart.sceneEnvironments; const newFirst = newEnvironments.splice( - newEnvironments.findIndex(x => x === environment.uuid) + newEnvironments.findIndex(x => x === environment.uuid), + 1 )[0]; newEnvironments.unshift(newFirst); emitAsGM( diff --git a/module/canvas/placeables/token.mjs b/module/canvas/placeables/token.mjs index bc5c2a01..148466c1 100644 --- a/module/canvas/placeables/token.mjs +++ b/module/canvas/placeables/token.mjs @@ -1,4 +1,4 @@ -import DhMeasuredTemplate from "./measuredTemplate.mjs"; +import DhMeasuredTemplate from './measuredTemplate.mjs'; export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { /** @inheritdoc */ @@ -54,30 +54,58 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { if (this === target) return 0; const originPoint = this.center; - const destinationPoint = target.center; + const targetPoint = target.center; + const thisBounds = this.bounds; + const targetBounds = target.bounds; + const adjacencyBuffer = canvas.grid.distance * 1.75; // handles diagonals with one square elevation difference + + // Figure out the elevation difference. + // This intends to return "grid distance" for adjacent ones, so we add that number if not overlapping. + const sizePerUnit = canvas.grid.size / canvas.grid.distance; + const thisHeight = Math.max(thisBounds.width, thisBounds.height) / sizePerUnit; + const targetHeight = Math.max(targetBounds.width, targetBounds.height) / sizePerUnit; + const thisElevation = [this.document.elevation, this.document.elevation + thisHeight]; + const targetElevation = [target.document.elevation, target.document.elevation + targetHeight]; + const isSameAltitude = + thisElevation[0] < targetElevation[1] && // bottom of this must be at or below the top of target + thisElevation[1] > targetElevation[0]; // top of this must be at or above the bottom of target + const [lower, higher] = [targetElevation, thisElevation].sort((a, b) => a[1] - b[1]); + const elevation = isSameAltitude ? 0 : higher[0] - lower[1] + canvas.grid.distance; // Compute for gridless. This version returns circular edge to edge + grid distance, // so that tokens that are touching return 5. if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) { const boundsCorrection = canvas.grid.distance / canvas.grid.size; - const originRadius = (this.bounds.width * boundsCorrection) / 2; - const targetRadius = (target.bounds.width * boundsCorrection) / 2; - const distance = canvas.grid.measurePath([originPoint, destinationPoint]).distance; - return distance - originRadius - targetRadius + canvas.grid.distance; + const originRadius = (thisBounds.width * boundsCorrection) / 2; + const targetRadius = (targetBounds.width * boundsCorrection) / 2; + const measuredDistance = canvas.grid.measurePath([ + { ...originPoint, elevation: 0 }, + { ...targetPoint, elevation } + ]).distance; + const distance = Math.floor(measuredDistance - originRadius - targetRadius + canvas.grid.distance); + return Math.min(distance, distance > adjacencyBuffer ? Infinity : canvas.grid.distance); } // Compute what the closest grid space of each token is, then compute that distance - const originEdge = this.#getEdgeBoundary(this.bounds, originPoint, destinationPoint); - const targetEdge = this.#getEdgeBoundary(target.bounds, originPoint, destinationPoint); - const adjustedOriginPoint = canvas.grid.getTopLeftPoint({ - x: originEdge.x + Math.sign(originPoint.x - originEdge.x), - y: originEdge.y + Math.sign(originPoint.y - originEdge.y) - }); - const adjustDestinationPoint = canvas.grid.getTopLeftPoint({ - x: targetEdge.x + Math.sign(destinationPoint.x - targetEdge.x), - y: targetEdge.y + Math.sign(destinationPoint.y - targetEdge.y) - }); - return canvas.grid.measurePath([adjustedOriginPoint, adjustDestinationPoint]).distance; + const originEdge = this.#getEdgeBoundary(thisBounds, originPoint, targetPoint); + const targetEdge = this.#getEdgeBoundary(targetBounds, originPoint, targetPoint); + const adjustedOriginPoint = originEdge + ? canvas.grid.getTopLeftPoint({ + x: originEdge.x + Math.sign(originPoint.x - originEdge.x), + y: originEdge.y + Math.sign(originPoint.y - originEdge.y) + }) + : originPoint; + const adjustDestinationPoint = targetEdge + ? canvas.grid.getTopLeftPoint({ + x: targetEdge.x + Math.sign(targetPoint.x - targetEdge.x), + y: targetEdge.y + Math.sign(targetPoint.y - targetEdge.y) + }) + : targetPoint; + const distance = canvas.grid.measurePath([ + { ...adjustedOriginPoint, elevation: 0 }, + { ...adjustDestinationPoint, elevation } + ]).distance; + return Math.min(distance, distance > adjacencyBuffer ? Infinity : canvas.grid.distance); } _onHoverIn(event, options) { @@ -85,7 +113,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { // Check if the setting is enabled const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance).showTokenDistance; - if (setting === "never" || (setting === "encounters" && !game.combat?.started)) return; + if (setting === 'never' || (setting === 'encounters' && !game.combat?.started)) return; // Check if this token isn't invisible and is actually being hovered const isTokenValid = @@ -103,8 +131,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { // Determine the actual range const ranges = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement; - const distanceNum = originToken.distanceTo(this); - const distanceResult = DhMeasuredTemplate.getRangeLabels(distanceNum, ranges); + const distanceResult = DhMeasuredTemplate.getRangeLabels(originToken.distanceTo(this), ranges); const distanceLabel = `${distanceResult.distance} ${distanceResult.units}`.trim(); // Create the element @@ -156,11 +183,6 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { return null; } - /** Tests if the token is at least adjacent with another, with some leeway for diagonals */ - isAdjacentWith(token) { - return this.distanceTo(token) <= canvas.grid.distance * 1.5; - } - /** @inheritDoc */ _drawBar(number, bar, data) { const val = Number(data.value); diff --git a/module/config/actorConfig.mjs b/module/config/actorConfig.mjs index fdef7d03..ac55117a 100644 --- a/module/config/actorConfig.mjs +++ b/module/config/actorConfig.mjs @@ -494,3 +494,275 @@ export const subclassFeatureLabels = { 2: 'DAGGERHEART.ITEMS.DomainCard.specializationTitle', 3: 'DAGGERHEART.ITEMS.DomainCard.masteryTitle' }; + +/** + * @typedef {Object} TierData + * @property {number} difficulty + * @property {number} majorThreshold + * @property {number} severeThreshold + * @property {number} hp + * @property {number} stress + * @property {number} attack + * @property {number[]} damage + */ + +/** + * @type {Record} + * Scaling data used to change an adversary's tier. Each rank is applied incrementally. + */ +export const adversaryScalingData = { + bruiser: { + 2: { + difficulty: 2, + majorThreshold: 5, + severeThreshold: 10, + hp: 1, + stress: 2, + attack: 2, + }, + 3: { + difficulty: 2, + majorThreshold: 7, + severeThreshold: 15, + hp: 1, + stress: 0, + attack: 2, + }, + 4: { + difficulty: 2, + majorThreshold: 12, + severeThreshold: 25, + hp: 1, + stress: 0, + attack: 2, + } + }, + horde: { + 2: { + difficulty: 2, + majorThreshold: 5, + severeThreshold: 8, + hp: 2, + stress: 0, + attack: 0, + }, + 3: { + difficulty: 2, + majorThreshold: 5, + severeThreshold: 12, + hp: 0, + stress: 1, + attack: 1, + }, + 4: { + difficulty: 2, + majorThreshold: 10, + severeThreshold: 15, + hp: 2, + stress: 0, + attack: 0, + } + }, + leader: { + 2: { + difficulty: 2, + majorThreshold: 6, + severeThreshold: 10, + hp: 0, + stress: 0, + attack: 1, + }, + 3: { + difficulty: 2, + majorThreshold: 6, + severeThreshold: 15, + hp: 1, + stress: 0, + attack: 2, + }, + 4: { + difficulty: 2, + majorThreshold: 12, + severeThreshold: 25, + hp: 1, + stress: 1, + attack: 3, + } + }, + minion: { + 2: { + difficulty: 2, + majorThreshold: 0, + severeThreshold: 0, + hp: 0, + stress: 0, + attack: 1, + }, + 3: { + difficulty: 2, + majorThreshold: 0, + severeThreshold: 0, + hp: 0, + stress: 1, + attack: 1, + }, + 4: { + difficulty: 2, + majorThreshold: 0, + severeThreshold: 0, + hp: 0, + stress: 0, + attack: 1, + } + }, + ranged: { + 2: { + difficulty: 2, + majorThreshold: 3, + severeThreshold: 6, + hp: 1, + stress: 0, + attack: 1, + }, + 3: { + difficulty: 2, + majorThreshold: 7, + severeThreshold: 14, + hp: 1, + stress: 1, + attack: 2, + }, + 4: { + difficulty: 2, + majorThreshold: 5, + severeThreshold: 10, + hp: 1, + stress: 1, + attack: 1, + } + }, + skulk: { + 2: { + difficulty: 2, + majorThreshold: 3, + severeThreshold: 8, + hp: 1, + stress: 1, + attack: 1, + }, + 3: { + difficulty: 2, + majorThreshold: 8, + severeThreshold: 12, + hp: 1, + stress: 1, + attack: 1, + }, + 4: { + difficulty: 2, + majorThreshold: 8, + severeThreshold: 10, + hp: 1, + stress: 1, + attack: 1, + } + }, + solo: { + 2: { + difficulty: 2, + majorThreshold: 5, + severeThreshold: 10, + hp: 0, + stress: 1, + attack: 2, + }, + 3: { + difficulty: 2, + majorThreshold: 7, + severeThreshold: 15, + hp: 2, + stress: 1, + attack: 2, + }, + 4: { + difficulty: 2, + majorThreshold: 12, + severeThreshold: 25, + hp: 0, + stress: 1, + attack: 3, + } + }, + standard: { + 2: { + difficulty: 2, + majorThreshold: 3, + severeThreshold: 8, + hp: 0, + stress: 0, + attack: 1, + }, + 3: { + difficulty: 2, + majorThreshold: 7, + severeThreshold: 15, + hp: 1, + stress: 1, + attack: 1, + }, + 4: { + difficulty: 2, + majorThreshold: 10, + severeThreshold: 15, + hp: 0, + stress: 1, + attack: 1, + } + }, + support: { + 2: { + difficulty: 2, + majorThreshold: 3, + severeThreshold: 8, + hp: 1, + stress: 1, + attack: 1, + }, + 3: { + difficulty: 2, + majorThreshold: 7, + severeThreshold: 12, + hp: 0, + stress: 0, + attack: 1, + }, + 4: { + difficulty: 2, + majorThreshold: 8, + severeThreshold: 10, + hp: 1, + stress: 1, + attack: 1, + } + } +}; + +/** + * Scaling data used for an adversary's damage. + * Tier 4 is missing certain adversary types and therefore skews upwards. + * We manually set tier 4 data to hopefully lead to better results + */ +export const adversaryExpectedDamage = { + basic: { + 1: { mean: 7.321428571428571, deviation: 1.962519002770912 }, + 2: { mean: 12.444444444444445, deviation: 2.0631069425529676 }, + 3: { mean: 15.722222222222221, deviation: 2.486565208464823 }, + 4: { mean: 26, deviation: 5.2 } + }, + minion: { + 1: { mean: 2.142857142857143, deviation: 1.0690449676496976 }, + 2: { mean: 5, deviation: 0.816496580927726 }, + 3: { mean: 6.5, deviation: 2.1213203435596424 }, + 4: { mean: 11, deviation: 1 } + } +}; diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index ae49c73e..d46db23a 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -202,7 +202,8 @@ export const conditions = () => ({ id: 'vulnerable', name: 'DAGGERHEART.CONFIG.Condition.vulnerable.name', img: 'icons/magic/control/silhouette-fall-slip-prone.webp', - description: 'DAGGERHEART.CONFIG.Condition.vulnerable.description' + description: 'DAGGERHEART.CONFIG.Condition.vulnerable.description', + autoApplyFlagId: 'auto-vulnerable' }, hidden: { id: 'hidden', @@ -236,6 +237,7 @@ export const defaultRestOptions = { actionType: 'action', chatDisplay: false, target: { + amount: 1, type: 'friendly' }, damage: { @@ -304,6 +306,7 @@ export const defaultRestOptions = { actionType: 'action', chatDisplay: false, target: { + amount: 1, type: 'friendly' }, damage: { @@ -329,7 +332,56 @@ export const defaultRestOptions = { icon: 'fa-solid fa-dumbbell', img: 'icons/skills/trades/academics-merchant-scribe.webp', description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepare.description'), - actions: {}, + actions: { + prepare: { + type: 'healing', + systemPath: 'restMoves.shortRest.moves.prepare.actions', + name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepare.name'), + img: 'icons/skills/trades/academics-merchant-scribe.webp', + actionType: 'action', + chatDisplay: false, + target: { + type: 'self' + }, + damage: { + parts: [ + { + applyTo: healingTypes.hope.id, + value: { + custom: { + enabled: true, + formula: '1' + } + } + } + ] + } + }, + prepareWithFriends: { + type: 'healing', + systemPath: 'restMoves.shortRest.moves.prepare.actions', + name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepareWithFriends.name'), + img: 'icons/skills/trades/academics-merchant-scribe.webp', + actionType: 'action', + chatDisplay: false, + target: { + type: 'self' + }, + damage: { + parts: [ + { + applyTo: healingTypes.hope.id, + value: { + custom: { + enabled: true, + formula: '2' + } + } + } + ] + } + } + }, effects: [] } }), @@ -349,6 +401,7 @@ export const defaultRestOptions = { actionType: 'action', chatDisplay: false, target: { + amount: 1, type: 'friendly' }, damage: { @@ -417,6 +470,7 @@ export const defaultRestOptions = { actionType: 'action', chatDisplay: false, target: { + amount: 1, type: 'friendly' }, damage: { @@ -442,7 +496,56 @@ export const defaultRestOptions = { icon: 'fa-solid fa-dumbbell', img: 'icons/skills/trades/academics-merchant-scribe.webp', description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepare.description'), - actions: {}, + actions: { + prepare: { + type: 'healing', + systemPath: 'restMoves.longRest.moves.prepare.actions', + name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepare.name'), + img: 'icons/skills/trades/academics-merchant-scribe.webp', + actionType: 'action', + chatDisplay: false, + target: { + type: 'self' + }, + damage: { + parts: [ + { + applyTo: healingTypes.hope.id, + value: { + custom: { + enabled: true, + formula: '1' + } + } + } + ] + } + }, + prepareWithFriends: { + type: 'healing', + systemPath: 'restMoves.longRest.moves.prepare.actions', + name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepareWithFriends.name'), + img: 'icons/skills/trades/academics-merchant-scribe.webp', + actionType: 'action', + chatDisplay: false, + target: { + type: 'self' + }, + damage: { + parts: [ + { + applyTo: healingTypes.hope.id, + value: { + custom: { + enabled: true, + formula: '2' + } + } + } + ] + } + } + }, effects: [] }, workOnAProject: { diff --git a/module/config/itemConfig.mjs b/module/config/itemConfig.mjs index 7d80e597..77328987 100644 --- a/module/config/itemConfig.mjs +++ b/module/config/itemConfig.mjs @@ -467,9 +467,7 @@ export const allArmorFeatures = () => { }; export const orderedArmorFeatures = () => { - const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures - .armorFeatures; - const allFeatures = { ...armorFeatures, ...homebrewFeatures }; + const allFeatures = allArmorFeatures(); const all = Object.keys(allFeatures).map(key => { const feature = allFeatures[key]; return { @@ -1404,9 +1402,7 @@ export const allWeaponFeatures = () => { }; export const orderedWeaponFeatures = () => { - const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures - .weaponFeatures; - const allFeatures = { ...weaponFeatures, ...homebrewFeatures }; + const allFeatures = allWeaponFeatures(); const all = Object.keys(allFeatures).map(key => { const feature = allFeatures[key]; return { diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs index 3d993949..d3f752bb 100644 --- a/module/config/settingsConfig.mjs +++ b/module/config/settingsConfig.mjs @@ -30,6 +30,7 @@ export const gameSettings = { LastMigrationVersion: 'LastMigrationVersion', TagTeamRoll: 'TagTeamRoll', SpotlightRequestQueue: 'SpotlightRequestQueue', + CompendiumBrowserSettings: 'CompendiumBrowserSettings' }; export const actionAutomationChoices = { diff --git a/module/data/_module.mjs b/module/data/_module.mjs index f7e25a4e..52fa689e 100644 --- a/module/data/_module.mjs +++ b/module/data/_module.mjs @@ -3,6 +3,7 @@ export { default as DhCombatant } from './combatant.mjs'; export { default as DhTagTeamRoll } from './tagTeamRoll.mjs'; export { default as DhRollTable } from './rollTable.mjs'; export { default as RegisteredTriggers } from './registeredTriggers.mjs'; +export { default as CompendiumBrowserSettings } from './compendiumBrowserSettings.mjs'; export * as countdowns from './countdowns.mjs'; export * as actions from './action/_module.mjs'; diff --git a/module/data/action/attackAction.mjs b/module/data/action/attackAction.mjs index 7be7461d..60112c40 100644 --- a/module/data/action/attackAction.mjs +++ b/module/data/action/attackAction.mjs @@ -34,6 +34,20 @@ export default class DHAttackAction extends DHDamageAction { }; } + get damageFormula() { + const hitPointsPart = this.damage.parts.find(x => x.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id); + if (!hitPointsPart) return '0'; + + return hitPointsPart.value.getFormula(); + } + + get altDamageFormula() { + const hitPointsPart = this.damage.parts.find(x => x.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id); + if (!hitPointsPart) return '0'; + + return hitPointsPart.valueAlt.getFormula(); + } + async use(event, options) { const result = await super.use(event, options); if (!result.message) return; diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index e843027b..f6ffe75f 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -229,7 +229,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return; - if (this.chatDisplay) await this.toChat(); + if (this.chatDisplay && !config.actionChatMessageHandled) await this.toChat(); return config; } @@ -240,9 +240,13 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel * @returns {object} */ prepareBaseConfig(event) { + const isActor = this.item instanceof CONFIG.Actor.documentClass; + const actionTitle = game.i18n.localize(this.name); + const itemTitle = isActor || this.item.name === actionTitle ? '' : `${this.item.name} - `; + const config = { event, - title: `${this.item instanceof CONFIG.Actor.documentClass ? '' : `${this.item.name}: `}${game.i18n.localize(this.name)}`, + title: `${itemTitle}${actionTitle}`, source: { item: this.item._id, originItem: this.originItem, diff --git a/module/data/actor/adversary.mjs b/module/data/actor/adversary.mjs index 16e7e37a..0a446c15 100644 --- a/module/data/actor/adversary.mjs +++ b/module/data/actor/adversary.mjs @@ -1,9 +1,12 @@ import DHAdversarySettings from '../../applications/sheets-configs/adversary-settings.mjs'; import { ActionField } from '../fields/actionField.mjs'; -import BaseDataActor, { commonActorRules } from './base.mjs'; +import { commonActorRules } from './base.mjs'; +import DhCreature from './creature.mjs'; import { resourceField, bonusField } from '../fields/actorField.mjs'; +import { calculateExpectedValue, parseTermsFromSimpleFormula } from '../../helpers/utils.mjs'; +import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs'; -export default class DhpAdversary extends BaseDataActor { +export default class DhpAdversary extends DhCreature { static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Adversary']; static get metadata() { @@ -40,7 +43,14 @@ export default class DhpAdversary extends BaseDataActor { integer: true, label: 'DAGGERHEART.GENERAL.hordeHp' }), - criticalThreshold: new fields.NumberField({ required: true, integer: true, min: 1, max: 20, initial: 20 }), + criticalThreshold: new fields.NumberField({ + required: true, + integer: true, + min: 1, + max: 20, + initial: 20, + label: 'DAGGERHEART.ACTIONS.Settings.criticalThreshold' + }), damageThresholds: new fields.SchemaField({ major: new fields.NumberField({ required: true, @@ -180,6 +190,10 @@ export default class DhpAdversary extends BaseDataActor { } } + prepareDerivedData() { + this.attack.roll.isStandardAttack = true; + } + _getTags() { const tags = [ game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${this.tier}`), @@ -188,4 +202,211 @@ export default class DhpAdversary extends BaseDataActor { ]; return tags; } + + /** Returns source data for this actor adjusted to a new tier, which can be used to create a new actor. */ + adjustForTier(tier) { + const source = this.parent.toObject(true); + + /** @type {(2 | 3 | 4)[]} */ + const tiers = new Array(Math.abs(tier - this.tier)) + .fill(0) + .map((_, idx) => idx + Math.min(tier, this.tier) + 1); + if (tier < this.tier) tiers.reverse(); + const typeData = adversaryScalingData[source.system.type] ?? adversaryScalingData[source.system.standard]; + const tierEntries = tiers.map(t => ({ tier: t, ...typeData[t] })); + + // Apply simple tier changes + const scale = tier > this.tier ? 1 : -1; + for (const entry of tierEntries) { + source.system.difficulty += scale * entry.difficulty; + source.system.damageThresholds.major += scale * entry.majorThreshold; + source.system.damageThresholds.severe += scale * entry.severeThreshold; + source.system.resources.hitPoints.max += scale * entry.hp; + source.system.resources.stress.max += scale * entry.stress; + source.system.attack.roll.bonus += scale * entry.attack; + } + + // Get the mean and standard deviation of expected damage in the previous and new tier + // The data we have is for attack scaling, but we reuse this for action scaling later + const expectedDamageData = adversaryExpectedDamage[source.system.type] ?? adversaryExpectedDamage.basic; + const damageMeta = { + currentDamageRange: { tier: source.system.tier, ...expectedDamageData[source.system.tier] }, + newDamageRange: { tier, ...expectedDamageData[tier] }, + type: 'attack' + }; + + // Update damage of base attack + try { + this.#adjustActionDamage(source.system.attack, damageMeta); + } catch (err) { + ui.notifications.warn('Failed to convert attack damage of adversary'); + console.error(err); + } + + // Update damage of each item action, making sure to also update the description if possible + const damageRegex = /@Damage\[([^\[\]]*)\]({[^}]*})?/g; + for (const item of source.items) { + // Replace damage inlines with new formulas + for (const withDescription of [item.system, ...Object.values(item.system.actions)]) { + withDescription.description = withDescription.description.replace(damageRegex, (match, inner) => { + const { value: formula } = parseInlineParams(inner); + if (!formula || !type) return match; + + try { + const adjusted = this.#calculateAdjustedDamage(formula, { ...damageMeta, type: 'action' }); + const newFormula = [ + adjusted.diceQuantity ? `${adjusted.diceQuantity}d${adjusted.faces}` : null, + adjusted.bonus + ] + .filter(p => !!p) + .join('+'); + return match.replace(formula, newFormula); + } catch { + return match; + } + }); + } + + // Update damage in item actions + for (const action of Object.values(item.system.actions)) { + if (!action.damage) continue; + + // Parse damage, and convert all formula matches in the descriptions to the new damage + try { + const result = this.#adjustActionDamage(action, { ...damageMeta, type: 'action' }); + for (const { previousFormula, formula } of Object.values(result)) { + const oldFormulaRegexp = new RegExp( + previousFormula.replace(' ', '').replace('+', '(?:\\s)?\\+(?:\\s)?') + ); + item.system.description = item.system.description.replace(oldFormulaRegexp, formula); + action.description = action.description.replace(oldFormulaRegexp, formula); + } + } catch (err) { + ui.notifications.warn(`Failed to convert action damage for item ${item.name}`); + console.error(err); + } + } + } + + // Finally set the tier of the source data, now that everything is complete + source.system.tier = tier; + return source; + } + + /** + * Converts a damage object to a new damage range + * @returns {{ diceQuantity: number; faces: number; bonus: number }} the adjusted result as a combined term + * @throws error if the formula is the wrong type + */ + #calculateAdjustedDamage(formula, { currentDamageRange, newDamageRange, type }) { + const terms = parseTermsFromSimpleFormula(formula); + const flatTerms = terms.filter(t => t.diceQuantity === 0); + const diceTerms = terms.filter(t => t.diceQuantity > 0); + if (flatTerms.length > 1 || diceTerms.length > 1) { + throw new Error('invalid formula for conversion'); + } + const value = { + ...(diceTerms[0] ?? { diceQuantity: 0, faces: 1 }), + bonus: flatTerms[0]?.bonus ?? 0 + }; + const previousExpected = calculateExpectedValue(value); + if (previousExpected === 0) return value; // nothing to do + + const dieSizes = [4, 6, 8, 10, 12, 20]; + const steps = newDamageRange.tier - currentDamageRange.tier; + const increasing = steps > 0; + const deviation = (previousExpected - currentDamageRange.mean) / currentDamageRange.deviation; + const expected = Math.max(1, newDamageRange.mean + newDamageRange.deviation * deviation); + + // If this was just a flat number, convert to the expected damage and exit + if (value.diceQuantity === 0) { + value.bonus = Math.round(expected); + return value; + } + + const getExpectedDie = () => calculateExpectedValue({ diceQuantity: 1, faces: value.faces }) || 1; + const getBaseAverage = () => calculateExpectedValue({ ...value, bonus: 0 }); + + // Check the number of base overages over the expected die. In the end, if the bonus inflates too much, we add a die + const baseOverages = Math.floor(value.bonus / getExpectedDie()); + + // Prestep. Change number of dice for attacks, bump up/down for actions + // We never bump up to d20, though we might bump down from it + if (type === 'attack') { + const minimum = increasing ? value.diceQuantity : 0; + value.diceQuantity = Math.max(minimum, newDamageRange.tier); + } else { + const currentIdx = dieSizes.indexOf(value.faces); + value.faces = dieSizes[Math.clamp(currentIdx + steps, 0, 4)]; + } + + value.bonus = Math.round(expected - getBaseAverage()); + + // Attempt to handle negative values. + // If we can do it with only step downs, do so. Otherwise remove tier dice, and try again + if (value.bonus < 0) { + let stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity); + const currentIdx = dieSizes.indexOf(value.faces); + + // If step downs alone don't suffice, change the flat modifier, then calculate steps required again + // If this isn't sufficient, the result will be slightly off. This is unlikely to happen + if (type !== 'attack' && stepsRequired > currentIdx && value.diceQuantity > 0) { + value.diceQuantity -= increasing ? 1 : Math.abs(steps); + value.bonus = Math.round(expected - getBaseAverage()); + if (value.bonus >= 0) return value; // complete + } + + stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity); + value.faces = dieSizes[Math.max(0, currentIdx - stepsRequired)]; + value.bonus = Math.max(0, Math.round(expected - getBaseAverage())); + } + + // If value is really high, we add a number of dice based on the number of overages + // This attempts to preserve a similar amount of variance when increasing an action + const overagesToRemove = Math.floor(value.bonus / getExpectedDie()) - baseOverages; + if (type !== 'attack' && increasing && overagesToRemove > 0) { + value.diceQuantity += overagesToRemove; + value.bonus = Math.round(expected - getBaseAverage()); + } + + return value; + } + + /** + * Updates damage to reflect a specific value. + * @throws if damage structure is invalid for conversion + * @returns the converted formula and value as a simplified term + */ + #adjustActionDamage(action, damageMeta) { + // The current algorithm only returns a value if there is a single damage part + const hpDamageParts = action.damage.parts.filter(d => d.applyTo === 'hitPoints'); + if (hpDamageParts.length !== 1) throw new Error('incorrect number of hp parts'); + + const result = {}; + for (const property of ['value', 'valueAlt']) { + const data = hpDamageParts[0][property]; + const previousFormula = data.custom.enabled + ? data.custom.formula + : [data.flatMultiplier ? `${data.flatMultiplier}${data.dice}` : 0, data.bonus ?? 0] + .filter(p => !!p) + .join('+'); + const value = this.#calculateAdjustedDamage(previousFormula, damageMeta); + const formula = [value.diceQuantity ? `${value.diceQuantity}d${value.faces}` : null, value.bonus] + .filter(p => !!p) + .join('+'); + if (value.diceQuantity) { + data.custom.enabled = false; + data.bonus = value.bonus; + data.dice = `d${value.faces}`; + data.flatMultiplier = value.diceQuantity; + } else if (!value.diceQuantity) { + data.custom.enabled = true; + data.custom.formula = formula; + } + + result[property] = { previousFormula, formula, value }; + } + + return result; + } } diff --git a/module/data/actor/base.mjs b/module/data/actor/base.mjs index 08308eab..5e16bac9 100644 --- a/module/data/actor/base.mjs +++ b/module/data/actor/base.mjs @@ -29,17 +29,40 @@ const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) => /* Common rules applying to Characters and Adversaries */ export const commonActorRules = (extendedData = { damageReduction: {}, attack: { damage: {} } }) => ({ conditionImmunities: new fields.SchemaField({ - hidden: new fields.BooleanField({ initial: false }), - restrained: new fields.BooleanField({ initial: false }), - vulnerable: new fields.BooleanField({ initial: false }) + hidden: new fields.BooleanField({ + initial: false, + label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.hidden' + }), + restrained: new fields.BooleanField({ + initial: false, + label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.restrained' + }), + vulnerable: new fields.BooleanField({ + initial: false, + label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.vulnerable' + }) }), damageReduction: new fields.SchemaField({ thresholdImmunities: new fields.SchemaField({ - minor: new fields.BooleanField({ initial: false }) + minor: new fields.BooleanField({ + initial: false, + label: 'DAGGERHEART.GENERAL.Rules.damageReduction.thresholdImmunities.minor.label', + hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.thresholdImmunities.minor.hint' + }) }), reduceSeverity: new fields.SchemaField({ - magical: new fields.NumberField({ initial: 0, min: 0 }), - physical: new fields.NumberField({ initial: 0, min: 0 }) + magical: new fields.NumberField({ + initial: 0, + min: 0, + label: 'DAGGERHEART.GENERAL.Rules.damageReduction.reduceSeverity.magical.label', + hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.reduceSeverity.magical.hint' + }), + physical: new fields.NumberField({ + initial: 0, + min: 0, + label: 'DAGGERHEART.GENERAL.Rules.damageReduction.reduceSeverity.physical.label', + hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.reduceSeverity.physical.hint' + }) }), ...(extendedData.damageReduction ?? {}) }), @@ -49,12 +72,16 @@ export const commonActorRules = (extendedData = { damageReduction: {}, attack: { hpDamageMultiplier: new fields.NumberField({ required: true, nullable: false, - initial: 1 + initial: 1, + label: 'DAGGERHEART.GENERAL.Attack.hpDamageMultiplier.label', + hint: 'DAGGERHEART.GENERAL.Attack.hpDamageMultiplier.hint' }), hpDamageTakenMultiplier: new fields.NumberField({ required: true, nullable: false, - initial: 1 + initial: 1, + label: 'DAGGERHEART.GENERAL.Attack.hpDamageTakenMultiplier.label', + hint: 'DAGGERHEART.GENERAL.Attack.hpDamageTakenMultiplier.hint' }), ...(extendedData.attack?.damage ?? {}) }) diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 8af4c74c..10fba63c 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -1,12 +1,13 @@ import { burden } from '../../config/generalConfig.mjs'; import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; import DhLevelData from '../levelData.mjs'; -import BaseDataActor, { commonActorRules } from './base.mjs'; +import { commonActorRules } from './base.mjs'; +import DhCreature from './creature.mjs'; import { attributeField, resourceField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs'; import { ActionField } from '../fields/actionField.mjs'; import DHCharacterSettings from '../../applications/sheets-configs/character-settings.mjs'; -export default class DhCharacter extends BaseDataActor { +export default class DhCharacter extends DhCreature { /**@override */ static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Character']; @@ -35,15 +36,18 @@ export default class DhCharacter extends BaseDataActor { 'DAGGERHEART.ACTORS.Character.maxHPBonus' ), stress: resourceField(6, 0, 'DAGGERHEART.GENERAL.stress', true), - hope: new fields.SchemaField({ - value: new fields.NumberField({ - initial: 2, - min: 0, - integer: true, - label: 'DAGGERHEART.GENERAL.hope' - }), - isReversed: new fields.BooleanField({ initial: false }) - }) + hope: new fields.SchemaField( + { + value: new fields.NumberField({ + initial: 2, + min: 0, + integer: true, + label: 'DAGGERHEART.GENERAL.hope' + }), + isReversed: new fields.BooleanField({ initial: false }) + }, + { label: 'DAGGERHEART.GENERAL.hope' } + ) }), traits: new fields.SchemaField({ agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'), @@ -128,14 +132,6 @@ export default class DhCharacter extends BaseDataActor { } } }), - advantageSources: new fields.ArrayField(new fields.StringField(), { - label: 'DAGGERHEART.ACTORS.Character.advantageSources.label', - hint: 'DAGGERHEART.ACTORS.Character.advantageSources.hint' - }), - disadvantageSources: new fields.ArrayField(new fields.StringField(), { - label: 'DAGGERHEART.ACTORS.Character.disadvantageSources.label', - hint: 'DAGGERHEART.ACTORS.Character.disadvantageSources.hint' - }), levelData: new fields.EmbeddedDataField(DhLevelData), bonuses: new fields.SchemaField({ roll: new fields.SchemaField({ @@ -222,8 +218,16 @@ export default class DhCharacter extends BaseDataActor { rules: new fields.SchemaField({ ...commonActorRules({ damageReduction: { - magical: new fields.BooleanField({ initial: false }), - physical: new fields.BooleanField({ initial: false }), + magical: new fields.BooleanField({ + initial: false, + label: 'DAGGERHEART.GENERAL.Rules.damageReduction.magical.label', + hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.magical.hint' + }), + physical: new fields.BooleanField({ + initial: false, + label: 'DAGGERHEART.GENERAL.Rules.damageReduction.physical.label', + hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.physical.hint' + }), maxArmorMarked: new fields.SchemaField({ value: new fields.NumberField({ required: true, @@ -253,7 +257,10 @@ export default class DhCharacter extends BaseDataActor { label: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.label', hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint' }), - disabledArmor: new fields.BooleanField({ intial: false }) + disabledArmor: new fields.BooleanField({ + intial: false, + label: 'DAGGERHEART.GENERAL.Rules.damageReduction.disabledArmor.label' + }) }, attack: { damage: { @@ -301,12 +308,14 @@ export default class DhCharacter extends BaseDataActor { label: 'DAGGERHEART.ACTORS.Character.defaultFearDice' }) }), - runeWard: new fields.BooleanField({ initial: false }), burden: new fields.SchemaField({ - ignore: new fields.BooleanField() + ignore: new fields.BooleanField({ label: 'DAGGERHEART.ACTORS.Character.burden.ignore.label' }) }), roll: new fields.SchemaField({ - guaranteedCritical: new fields.BooleanField() + guaranteedCritical: new fields.BooleanField({ + label: 'DAGGERHEART.ACTORS.Character.roll.guaranteedCritical.label', + hint: 'DAGGERHEART.ACTORS.Character.roll.guaranteedCritical.hint' + }) }) }) }; @@ -661,7 +670,7 @@ export default class DhCharacter extends BaseDataActor { }; const globalHopeMax = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxHope; - this.resources.hope.max = globalHopeMax - this.scars; + this.resources.hope.max = globalHopeMax; this.resources.hitPoints.max += this.class.value?.system?.hitPoints ?? 0; /* Companion Related Data */ @@ -685,6 +694,7 @@ export default class DhCharacter extends BaseDataActor { } } + this.resources.hope.max -= this.scars; this.resources.hope.value = Math.min(baseHope, this.resources.hope.max); this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait; diff --git a/module/data/actor/companion.mjs b/module/data/actor/companion.mjs index 1c25b48c..6f51b593 100644 --- a/module/data/actor/companion.mjs +++ b/module/data/actor/companion.mjs @@ -1,4 +1,4 @@ -import BaseDataActor from './base.mjs'; +import DhCreature from './creature.mjs'; import DhLevelData from '../levelData.mjs'; import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; import { ActionField } from '../fields/actionField.mjs'; @@ -6,7 +6,7 @@ import { adjustDice, adjustRange } from '../../helpers/utils.mjs'; import DHCompanionSettings from '../../applications/sheets-configs/companion-settings.mjs'; import { resourceField, bonusField } from '../fields/actorField.mjs'; -export default class DhCompanion extends BaseDataActor { +export default class DhCompanion extends DhCreature { static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Companion']; /**@inheritdoc */ @@ -53,9 +53,18 @@ export default class DhCompanion extends BaseDataActor { ), rules: new fields.SchemaField({ conditionImmunities: new fields.SchemaField({ - hidden: new fields.BooleanField({ initial: false }), - restrained: new fields.BooleanField({ initial: false }), - vulnerable: new fields.BooleanField({ initial: false }) + hidden: new fields.BooleanField({ + initial: false, + label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.hidden' + }), + restrained: new fields.BooleanField({ + initial: false, + label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.restrained' + }), + vulnerable: new fields.BooleanField({ + initial: false, + label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.vulnerable' + }) }) }), attack: new ActionField({ diff --git a/module/data/actor/creature.mjs b/module/data/actor/creature.mjs new file mode 100644 index 00000000..c8bf8448 --- /dev/null +++ b/module/data/actor/creature.mjs @@ -0,0 +1,61 @@ +import BaseDataActor from './base.mjs'; + +export default class DhCreature extends BaseDataActor { + /**@inheritdoc */ + static defineSchema() { + const fields = foundry.data.fields; + + return { + ...super.defineSchema(), + advantageSources: new fields.ArrayField(new fields.StringField(), { + label: 'DAGGERHEART.ACTORS.Character.advantageSources.label', + hint: 'DAGGERHEART.ACTORS.Character.advantageSources.hint' + }), + disadvantageSources: new fields.ArrayField(new fields.StringField(), { + label: 'DAGGERHEART.ACTORS.Character.disadvantageSources.label', + hint: 'DAGGERHEART.ACTORS.Character.disadvantageSources.hint' + }) + }; + } + + get isAutoVulnerableActive() { + const vulnerableAppliedByOther = this.parent.effects.some( + x => x.statuses.has('vulnerable') && !x.flags.daggerheart?.autoApplyFlagId + ); + return !vulnerableAppliedByOther; + } + + async _preUpdate(changes, options, userId) { + const allowed = await super._preUpdate(changes, options, userId); + if (allowed === false) return; + + const automationSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation); + if ( + automationSettings.vulnerableAutomation && + this.parent.type !== 'companion' && + changes.system?.resources?.stress?.value + ) { + const { name, description, img, autoApplyFlagId } = CONFIG.DH.GENERAL.conditions().vulnerable; + const autoEffects = this.parent.effects.filter( + x => x.flags.daggerheart?.autoApplyFlagId === autoApplyFlagId + ); + if (changes.system.resources.stress.value >= this.resources.stress.max) { + if (!autoEffects.length) + this.parent.createEmbeddedDocuments('ActiveEffect', [ + { + name: game.i18n.localize(name), + description: game.i18n.localize(description), + img: img, + statuses: ['vulnerable'], + flags: { daggerheart: { autoApplyFlagId } } + } + ]); + } else if (this.resources.stress.value >= this.resources.stress.max) { + this.parent.deleteEmbeddedDocuments( + 'ActiveEffect', + autoEffects.map(x => x.id) + ); + } + } + } +} diff --git a/module/data/chat-message/actorRoll.mjs b/module/data/chat-message/actorRoll.mjs index 61262529..1ea7ff93 100644 --- a/module/data/chat-message/actorRoll.mjs +++ b/module/data/chat-message/actorRoll.mjs @@ -31,6 +31,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { static defineSchema() { return { title: new fields.StringField(), + actionDescription: new fields.HTMLField(), roll: new fields.ObjectField(), targets: targetsField(), hasRoll: new fields.BooleanField({ initial: false }), diff --git a/module/data/compendiumBrowserSettings.mjs b/module/data/compendiumBrowserSettings.mjs new file mode 100644 index 00000000..ea71c439 --- /dev/null +++ b/module/data/compendiumBrowserSettings.mjs @@ -0,0 +1,36 @@ +export default class CompendiumBrowserSettings extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + excludedSources: new fields.TypedObjectField( + new fields.SchemaField({ + excludedDocumentTypes: new fields.ArrayField( + new fields.StringField({ required: true, choices: CONST.SYSTEM_SPECIFIC_COMPENDIUM_TYPES }) + ) + }) + ), + excludedPacks: new fields.TypedObjectField( + new fields.SchemaField({ + excludedDocumentTypes: new fields.ArrayField( + new fields.StringField({ required: true, choices: CONST.SYSTEM_SPECIFIC_COMPENDIUM_TYPES }) + ) + }) + ) + }; + } + + isEntryExcluded(item) { + const pack = game.packs.get(item.pack); + if (!pack) return false; + + const packageName = pack.metadata.packageType === 'world' ? 'world' : pack.metadata.packageName; + const excludedSourceData = this.excludedSources[packageName]; + if (excludedSourceData && excludedSourceData.excludedDocumentTypes.includes(pack.metadata.type)) return true; + + const excludedPackData = this.excludedPacks[item.pack]; + if (excludedPackData && excludedPackData.excludedDocumentTypes.includes(pack.metadata.type)) return true; + + return false; + } +} diff --git a/module/data/fields/action/damageField.mjs b/module/data/fields/action/damageField.mjs index ef91c64e..6439344b 100644 --- a/module/data/fields/action/damageField.mjs +++ b/module/data/fields/action/damageField.mjs @@ -68,6 +68,8 @@ export default class DamageField extends fields.SchemaField { const damageResult = await CONFIG.Dice.daggerheart.DamageRoll.build(damageConfig); if (!damageResult) return false; + if (damageResult.actionChatMessageHandled) config.actionChatMessageHandled = true; + config.damage = damageResult.damage; config.message ??= damageConfig.message; } @@ -107,8 +109,8 @@ export default class DamageField extends fields.SchemaField { ); else { const configDamage = foundry.utils.deepClone(config.damage); - const hpDamageMultiplier = config.actionActor?.system.rules.attack.damage.hpDamageMultiplier ?? 1; - const hpDamageTakenMultiplier = actor.system.rules.attack.damage.hpDamageTakenMultiplier; + const hpDamageMultiplier = config.actionActor?.system.rules?.attack?.damage?.hpDamageMultiplier ?? 1; + const hpDamageTakenMultiplier = actor.system.rules?.attack?.damage?.hpDamageTakenMultiplier; if (configDamage.hitPoints) { for (const part of configDamage.hitPoints.parts) { part.total = Math.ceil(part.total * hpDamageMultiplier * hpDamageTakenMultiplier); @@ -163,7 +165,8 @@ export default class DamageField extends fields.SchemaField { if (data.hasRoll && part.resultBased && data.roll.result.duality === -1) return part.valueAlt; const isAdversary = this.actor.type === 'adversary'; - if (isAdversary && this.actor.system.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id) { + const isHorde = this.actor.system.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id; + if (isAdversary && isHorde && this.roll?.isStandardAttack) { const hasHordeDamage = this.actor.effects.find(x => x.type === 'horde'); if (hasHordeDamage && !hasHordeDamage.disabled) return part.valueAlt; } diff --git a/module/data/fields/actionField.mjs b/module/data/fields/actionField.mjs index 4cadeac4..89c3c287 100644 --- a/module/data/fields/actionField.mjs +++ b/module/data/fields/actionField.mjs @@ -262,6 +262,9 @@ export function ActionMixin(Base) { } async toChat(origin) { + const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance) + .expandRollMessage?.desc; + const cls = getDocumentClass('ChatMessage'); const systemData = { title: game.i18n.localize('DAGGERHEART.CONFIG.FeatureForm.action'), @@ -290,7 +293,7 @@ export function ActionMixin(Base) { system: systemData, content: await foundry.applications.handlebars.renderTemplate( 'systems/daggerheart/templates/ui/chat/action.hbs', - systemData + { ...systemData, open: autoExpandDescription ? 'open' : '' } ), flags: { daggerheart: { diff --git a/module/data/fields/actorField.mjs b/module/data/fields/actorField.mjs index f9eeeb90..db1faad4 100644 --- a/module/data/fields/actorField.mjs +++ b/module/data/fields/actorField.mjs @@ -7,16 +7,20 @@ const attributeField = label => }); const resourceField = (max = 0, initial = 0, label, reverse = false, maxLabel) => - new fields.SchemaField({ - value: new fields.NumberField({ initial: initial, min: 0, integer: true, label }), - max: new fields.NumberField({ - initial: max, - integer: true, - label: - maxLabel ?? game.i18n.format('DAGGERHEART.GENERAL.maxWithThing', { thing: game.i18n.localize(label) }) - }), - isReversed: new fields.BooleanField({ initial: reverse }) - }); + new fields.SchemaField( + { + value: new fields.NumberField({ initial: initial, min: 0, integer: true, label }), + max: new fields.NumberField({ + initial: max, + integer: true, + label: + maxLabel ?? + game.i18n.format('DAGGERHEART.GENERAL.maxWithThing', { thing: game.i18n.localize(label) }) + }), + isReversed: new fields.BooleanField({ initial: reverse }) + }, + { label } + ); const stressDamageReductionRule = localizationPath => new fields.SchemaField({ diff --git a/module/data/item/ancestry.mjs b/module/data/item/ancestry.mjs index 6abdd334..b9253a3c 100644 --- a/module/data/item/ancestry.mjs +++ b/module/data/item/ancestry.mjs @@ -1,5 +1,6 @@ import BaseDataItem from './base.mjs'; import ItemLinkFields from '../../data/fields/itemLinkFields.mjs'; +import { getFeaturesHTMLData } from '../../helpers/utils.mjs'; export default class DHAncestry extends BaseDataItem { /** @inheritDoc */ @@ -19,7 +20,6 @@ export default class DHAncestry extends BaseDataItem { }; } - /* -------------------------------------------- */ /**@override */ @@ -42,4 +42,18 @@ export default class DHAncestry extends BaseDataItem { get secondaryFeature() { return this.features.find(x => x.type === CONFIG.DH.ITEM.featureSubTypes.secondary)?.item; } + + /**@inheritdoc */ + async getDescriptionData() { + const baseDescription = this.description; + const features = await getFeaturesHTMLData(this.features); + + if (!features.length) return { prefix: null, value: baseDescription, suffix: null }; + const suffix = await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/sheets/items/description.hbs', + { label: 'DAGGERHEART.ITEMS.Ancestry.featuresLabel', features } + ); + + return { prefix: null, value: baseDescription, suffix }; + } } diff --git a/module/data/item/armor.mjs b/module/data/item/armor.mjs index 3d4a62fa..0958a9f3 100644 --- a/module/data/item/armor.mjs +++ b/module/data/item/armor.mjs @@ -23,9 +23,7 @@ export default class DHArmor extends AttachableItem { armorFeatures: new fields.ArrayField( new fields.SchemaField({ value: new fields.StringField({ - required: true, - choices: CONFIG.DH.ITEM.allArmorFeatures, - blank: true + required: true }), effectIds: new fields.ArrayField(new fields.StringField({ required: true })), actionIds: new fields.ArrayField(new fields.StringField({ required: true })) @@ -58,12 +56,11 @@ export default class DHArmor extends AttachableItem { async getDescriptionData() { const baseDescription = this.description; const allFeatures = CONFIG.DH.ITEM.allArmorFeatures(); - const features = this.armorFeatures.map(x => allFeatures[x.value]); - if (!features.length) return { prefix: null, value: baseDescription, suffix: null }; + const features = this.armorFeatures.map(x => allFeatures[x.value]).filter(x => x); const prefix = await foundry.applications.handlebars.renderTemplate( 'systems/daggerheart/templates/sheets/items/armor/description.hbs', - { features } + { item: this.parent, features } ); return { prefix, value: baseDescription, suffix: null }; diff --git a/module/data/item/beastform.mjs b/module/data/item/beastform.mjs index dd491169..3a41aa7e 100644 --- a/module/data/item/beastform.mjs +++ b/module/data/item/beastform.mjs @@ -253,4 +253,20 @@ export default class DHBeastform extends BaseDataItem { return false; } + + _onCreate(_data, _options, userId) { + if (!this.actor && game.user.id === userId) { + const hasBeastformEffect = this.parent.effects.some(x => x.type === 'beastform'); + if (!hasBeastformEffect) + this.parent.createEmbeddedDocuments('ActiveEffect', [ + { + type: 'beastform', + name: game.i18n.localize('DAGGERHEART.ITEMS.Beastform.beastformEffect'), + img: 'icons/creatures/abilities/paw-print-pair-purple.webp' + } + ]); + + return; + } + } } diff --git a/module/data/item/class.mjs b/module/data/item/class.mjs index c233a31b..d3738318 100644 --- a/module/data/item/class.mjs +++ b/module/data/item/class.mjs @@ -2,7 +2,7 @@ import BaseDataItem from './base.mjs'; import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; import ItemLinkFields from '../fields/itemLinkFields.mjs'; -import { addLinkedItemsDiff, updateLinkedItemApps } from '../../helpers/utils.mjs'; +import { addLinkedItemsDiff, getFeaturesHTMLData, updateLinkedItemApps } from '../../helpers/utils.mjs'; export default class DHClass extends BaseDataItem { /** @inheritDoc */ @@ -163,4 +163,56 @@ export default class DHClass extends BaseDataItem { updateLinkedItemApps(options, this.parent.sheet); } + + /**@inheritdoc */ + async getDescriptionData() { + const baseDescription = this.description; + + const getDomainLabel = domain => { + const data = CONFIG.DH.DOMAIN.allDomains()[domain]; + return data ? game.i18n.localize(data.label) : ''; + }; + let domainsLabel = ''; + if (this.domains.length) { + if (this.domains.length === 1) domainsLabel = getDomainLabel(this.domains[0]); + else { + const firstDomains = this.domains + .slice(0, this.domains.length - 1) + .map(getDomainLabel) + .join(', '); + const lastDomain = getDomainLabel(this.domains[this.domains.length - 1]); + domainsLabel = game.i18n.format('DAGGERHEART.GENERAL.thingsAndThing', { + things: firstDomains, + thing: lastDomain + }); + } + } + + const classItems = []; + for (const itemData of this.inventory.choiceB) { + const linkData = [ + undefined, + 'UUID', // type + itemData.uuid // target + ]; + const contentLink = await foundry.applications.ux.TextEditor.implementation._createContentLink(linkData); + classItems.push(contentLink.outerHTML); + } + + const hopeFeatures = await getFeaturesHTMLData(this.hopeFeatures); + const classFeatures = await getFeaturesHTMLData(this.classFeatures); + + const suffix = await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/sheets/items/class/description.hbs', + { + class: this.parent, + domains: domainsLabel, + classItems, + hopeFeatures, + classFeatures + } + ); + + return { prefix: null, value: baseDescription, suffix }; + } } diff --git a/module/data/item/community.mjs b/module/data/item/community.mjs index a8000144..6d054976 100644 --- a/module/data/item/community.mjs +++ b/module/data/item/community.mjs @@ -1,3 +1,4 @@ +import { getFeaturesHTMLData } from '../../helpers/utils.mjs'; import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; import BaseDataItem from './base.mjs'; @@ -24,4 +25,17 @@ export default class DHCommunity extends BaseDataItem { /**@override */ static DEFAULT_ICON = 'systems/daggerheart/assets/icons/documents/items/village.svg'; + /**@inheritdoc */ + async getDescriptionData() { + const baseDescription = this.description; + const features = await getFeaturesHTMLData(this.features); + + if (!features.length) return { prefix: null, value: baseDescription, suffix: null }; + const suffix = await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/sheets/items/description.hbs', + { label: 'DAGGERHEART.ITEMS.Community.featuresLabel', features } + ); + + return { prefix: null, value: baseDescription, suffix }; + } } diff --git a/module/data/item/subclass.mjs b/module/data/item/subclass.mjs index 375588fb..06a80f7b 100644 --- a/module/data/item/subclass.mjs +++ b/module/data/item/subclass.mjs @@ -1,3 +1,4 @@ +import { getFeaturesHTMLData } from '../../helpers/utils.mjs'; import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; import ItemLinkFields from '../fields/itemLinkFields.mjs'; import BaseDataItem from './base.mjs'; @@ -89,4 +90,28 @@ export default class DHSubclass extends BaseDataItem { const allowed = await super._preCreate(data, options, user); if (allowed === false) return; } + + /**@inheritdoc */ + async getDescriptionData() { + const baseDescription = this.description; + + const spellcastTrait = this.spellcastingTrait + ? game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.spellcastingTrait].label) + : null; + const foundationFeatures = await getFeaturesHTMLData(this.foundationFeatures); + const specializationFeatures = await getFeaturesHTMLData(this.specializationFeatures); + const masteryFeatures = await getFeaturesHTMLData(this.masteryFeatures); + + const suffix = await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/sheets/items/subclass/description.hbs', + { + spellcastTrait, + foundationFeatures, + specializationFeatures, + masteryFeatures + } + ); + + return { prefix: null, value: baseDescription, suffix }; + } } diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index f333e5f3..051fd42d 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -38,9 +38,7 @@ export default class DHWeapon extends AttachableItem { weaponFeatures: new fields.ArrayField( new fields.SchemaField({ value: new fields.StringField({ - required: true, - choices: CONFIG.DH.ITEM.allWeaponFeatures, - blank: true + required: true }), effectIds: new fields.ArrayField(new fields.StringField({ required: true })), actionIds: new fields.ArrayField(new fields.StringField({ required: true })) @@ -113,13 +111,26 @@ export default class DHWeapon extends AttachableItem { /**@inheritdoc */ async getDescriptionData() { const baseDescription = this.description; + + const tier = game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${this.tier}`); + const trait = game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.attack.roll.trait].label); + const range = game.i18n.localize(`DAGGERHEART.CONFIG.Range.${this.attack.range}.name`); + const damage = Roll.replaceFormulaData(this.attack.damageFormula, this.parent.parent ?? this.parent); + const burden = game.i18n.localize(CONFIG.DH.GENERAL.burden[this.burden].label); + const allFeatures = CONFIG.DH.ITEM.allWeaponFeatures(); - const features = this.weaponFeatures.map(x => allFeatures[x.value]); - if (!features.length) return { prefix: null, value: baseDescription, suffix: null }; + const features = this.weaponFeatures.map(x => allFeatures[x.value]).filter(x => x); const prefix = await foundry.applications.handlebars.renderTemplate( 'systems/daggerheart/templates/sheets/items/weapon/description.hbs', - { features } + { + features, + tier, + trait, + range, + damage, + burden + } ); return { prefix, value: baseDescription, suffix: null }; diff --git a/module/data/levelData.mjs b/module/data/levelData.mjs index 669077ee..4f55d9ee 100644 --- a/module/data/levelData.mjs +++ b/module/data/levelData.mjs @@ -6,7 +6,12 @@ export default class DhLevelData extends foundry.abstract.DataModel { return { level: new fields.SchemaField({ - current: new fields.NumberField({ required: true, integer: true, initial: 1 }), + current: new fields.NumberField({ + required: true, + integer: true, + initial: 1, + label: 'DAGGERHEART.GENERAL.currentLevel' + }), changed: new fields.NumberField({ required: true, integer: true, initial: 1 }), bonuses: new fields.TypedObjectField(new fields.NumberField({ integer: true, nullable: false })) }), diff --git a/module/data/registeredTriggers.mjs b/module/data/registeredTriggers.mjs index ee4f3b49..ab86351c 100644 --- a/module/data/registeredTriggers.mjs +++ b/module/data/registeredTriggers.mjs @@ -75,7 +75,7 @@ export default class RegisteredTriggers extends Map { unregisterSceneEnvironmentTriggers(flagSystemData) { const sceneData = new game.system.api.data.scenes.DHScene(flagSystemData); for (const environment of sceneData.sceneEnvironments) { - if (environment.pack) continue; + if (!environment || environment.pack) continue; this.unregisterItemTriggers(environment.system.features); } } diff --git a/module/data/settings/Appearance.mjs b/module/data/settings/Appearance.mjs index d7a638d7..cd98d6f9 100644 --- a/module/data/settings/Appearance.mjs +++ b/module/data/settings/Appearance.mjs @@ -37,7 +37,7 @@ export default class DhAppearance extends foundry.abstract.DataModel { extendEnvironmentDescriptions: new BooleanField(), extendItemDescriptions: new BooleanField(), expandRollMessage: new SchemaField({ - desc: new BooleanField(), + desc: new BooleanField({ initial: true }), roll: new BooleanField(), damage: new BooleanField(), target: new BooleanField() diff --git a/module/data/settings/Automation.mjs b/module/data/settings/Automation.mjs index e9952b1c..20fe0baf 100644 --- a/module/data/settings/Automation.mjs +++ b/module/data/settings/Automation.mjs @@ -18,6 +18,10 @@ export default class DhAutomation extends foundry.abstract.DataModel { label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hopeFear.players.label' }) }), + vulnerableAutomation: new fields.BooleanField({ + initial: true, + label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.vulnerableAutomation.label' + }), countdownAutomation: new fields.BooleanField({ required: true, initial: true, diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index cd26eb21..ef5f9434 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -1,4 +1,5 @@ import DamageDialog from '../applications/dialogs/damageDialog.mjs'; +import { parseRallyDice } from '../helpers/utils.mjs'; import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs'; import DHRoll from './dhRoll.mjs'; @@ -33,7 +34,7 @@ export default class DamageRoll extends DHRoll { static async buildPost(roll, config, message) { const chatMessage = config.source?.message ? ui.chat.collection.get(config.source.message) - : getDocumentClass('ChatMessage').applyRollMode({}, config.rollMode); + : getDocumentClass('ChatMessage').applyRollMode({}, config.rollMode ?? CONST.DICE_ROLL_MODES.PUBLIC); if (game.modules.get('dice-so-nice')?.active) { const pool = foundry.dice.terms.PoolTerm.fromRolls( Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll)) @@ -46,9 +47,14 @@ export default class DamageRoll extends DHRoll { chatMessage.whisper?.length > 0 ? chatMessage.whisper : null, chatMessage.blind ); + config.mute = true; } await super.buildPost(roll, config, message); - if (config.source?.message) chatMessage.update({ 'system.damage': config.damage }); + if (config.source?.message) { + chatMessage.update({ 'system.damage': config.damage }); + + if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice }); + } } static unifyDamageRoll(rolls) { @@ -192,7 +198,7 @@ export default class DamageRoll extends DHRoll { // Bardic Rally const rallyChoices = config.data?.parent?.appliedEffects.reduce((a, c) => { const change = c.changes.find(ch => ch.key === 'system.bonuses.rally'); - if (change) a.push({ value: c.id, label: change.value }); + if (change) a.push({ value: c.id, label: parseRallyDice(change.value, c) }); return a; }, []); if (rallyChoices.length) { diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index 1977c7ea..d8e5f6dd 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -96,6 +96,19 @@ export default class DHRoll extends Roll { } static async toMessage(roll, config) { + const item = config.data.parent?.items?.get?.(config.source.item) ?? null; + const action = item ? item.system.actions.get(config.source.action) : null; + let actionDescription = null; + if (action?.chatDisplay) { + actionDescription = action + ? await foundry.applications.ux.TextEditor.implementation.enrichHTML(action.description, { + relativeTo: config.data, + rollData: config.data.getRollData?.() ?? {} + }) + : null; + config.actionChatMessageHandled = true; + } + const cls = getDocumentClass('ChatMessage'), msgData = { type: this.messageType, @@ -103,7 +116,7 @@ export default class DHRoll extends Roll { title: roll.title, speaker: cls.getSpeaker({ actor: roll.data?.parent }), sound: config.mute ? null : CONFIG.sounds.dice, - system: config, + system: { ...config, actionDescription }, rolls: [roll] }; diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index e65d0ff5..9037250a 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -1,6 +1,6 @@ import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; import D20Roll from './d20Roll.mjs'; -import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs'; +import { parseRallyDice, setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs'; import { getDiceSoNicePresets } from '../config/generalConfig.mjs'; import { ResourceUpdateMap } from '../data/action/baseAction.mjs'; @@ -68,7 +68,7 @@ export default class DualityRoll extends D20Roll { setRallyChoices() { return this.data?.parent?.appliedEffects.reduce((a, c) => { const change = c.changes.find(ch => ch.key === 'system.bonuses.rally'); - if (change) a.push({ value: c.id, label: change.value }); + if (change) a.push({ value: c.id, label: parseRallyDice(change.value, c) }); return a; }, []); } @@ -409,7 +409,9 @@ export default class DualityRoll extends D20Roll { difficulty: message.system.roll.difficulty ? Number(message.system.roll.difficulty) : null } }); - newRoll.extra = newRoll.extra.slice(2); + + const extraIndex = newRoll.advantage ? 3 : 2; + newRoll.extra = newRoll.extra.slice(extraIndex); const tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); diff --git a/module/documents/activeEffect.mjs b/module/documents/activeEffect.mjs index 5e9b0c3b..dd5f1b55 100644 --- a/module/documents/activeEffect.mjs +++ b/module/documents/activeEffect.mjs @@ -61,14 +61,15 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { update.img = 'icons/magic/life/heart-cross-blue.webp'; } + const statuses = Object.keys(data.statuses ?? {}); const immuneStatuses = - data.statuses?.filter( + statuses.filter( status => this.parent.system.rules?.conditionImmunities && this.parent.system.rules.conditionImmunities[status] ) ?? []; if (immuneStatuses.length > 0) { - update.statuses = data.statuses.filter(x => !immuneStatuses.includes(x)); + update.statuses = statuses.filter(x => !immuneStatuses.includes(x)); const conditions = CONFIG.DH.GENERAL.conditions(); const scrollingTexts = immuneStatuses.map(status => ({ text: game.i18n.format('DAGGERHEART.ACTIVEEFFECT.immuneStatusText', { @@ -113,6 +114,11 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { super.applyField(model, change, field); } + _applyLegacy(actor, change, changes) { + change.value = DhActiveEffect.getChangeValue(actor, change, change.effect); + super._applyLegacy(actor, change, changes); + } + /** */ static getChangeValue(model, change, effect) { let value = change.value; diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index e8bea0bf..787797ff 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -934,10 +934,23 @@ export default class DhpActor extends Actor { /** Get active effects */ getActiveEffects() { + const conditions = CONFIG.DH.GENERAL.conditions(); const statusMap = new Map(foundry.CONFIG.statusEffects.map(status => [status.id, status])); + const autoVulnerableActive = this.system.isAutoVulnerableActive; return this.effects .filter(x => !x.disabled) .reduce((acc, effect) => { + /* Could be generalized if needed. Currently just related to Vulnerable */ + const isAutoVulnerableEffect = + effect.flags.daggerheart?.autoApplyFlagId === conditions.vulnerable.autoApplyFlagId; + if (isAutoVulnerableEffect) { + if (!autoVulnerableActive) return acc; + + effect.appliedBy = game.i18n.localize('DAGGERHEART.CONFIG.Condition.vulnerable.autoAppliedByLabel'); + effect.isLockedCondition = true; + effect.condition = 'vulnerable'; + } + acc.push(effect); const currentStatusActiveEffects = acc.filter( diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index 1d2c6c41..668ad06b 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -110,6 +110,8 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { } else if (s.classList.contains('damage-section')) s.classList.toggle('expanded', autoExpandRoll.damage); else if (s.classList.contains('target-section')) s.classList.toggle('expanded', autoExpandRoll.target); + else if (s.classList.contains('description-section')) + s.classList.toggle('expanded', autoExpandRoll.desc); }); if (itemDesc && autoExpandRoll.desc) itemDesc.setAttribute('open', ''); } diff --git a/module/documents/token.mjs b/module/documents/token.mjs index b9507c2f..4ee7ce05 100644 --- a/module/documents/token.mjs +++ b/module/documents/token.mjs @@ -1,78 +1,30 @@ export default class DHToken extends CONFIG.Token.documentClass { - /** - * Inspect the Actor data model and identify the set of attributes which could be used for a Token Bar. - * @param {object} attributes The tracked attributes which can be chosen from - * @returns {object} A nested object of attribute choices to display - */ - static getTrackedAttributeChoices(attributes, model) { + /**@inheritdoc */ + static getTrackedAttributeChoices(attributes, typeKey) { attributes = attributes || this.getTrackedAttributes(); const barGroup = game.i18n.localize('TOKEN.BarAttributes'); const valueGroup = game.i18n.localize('TOKEN.BarValues'); + const actorModel = typeKey ? game.system.api.data.actors[`Dh${typeKey.capitalize()}`] : null; + const getLabel = path => { + const label = actorModel?.schema.getField(path)?.label; + return label ? game.i18n.localize(label) : path; + }; const bars = attributes.bar.map(v => { const a = v.join('.'); - const modelLabel = model ? game.i18n.localize(model.schema.getField(`${a}.value`).label) : null; - return { group: barGroup, value: a, label: modelLabel ? modelLabel : a }; + return { group: barGroup, value: a, label: getLabel(a) }; }); - bars.sort((a, b) => a.label.compare(b.label)); + bars.sort((a, b) => a.value.compare(b.value)); - const invalidAttributes = [ - 'gold', - 'levelData', - 'actions', - 'biography', - 'class', - 'multiclass', - 'companion', - 'notes', - 'partner', - 'description', - 'impulses', - 'tier', - 'type' - ]; - const values = attributes.value.reduce((acc, v) => { + const values = attributes.value.map(v => { const a = v.join('.'); - if (invalidAttributes.some(x => a.startsWith(x))) return acc; - - const field = model ? model.schema.getField(a) : null; - const modelLabel = field ? game.i18n.localize(field.label) : null; - const hint = field ? game.i18n.localize(field.hint) : null; - acc.push({ group: valueGroup, value: a, label: modelLabel ? modelLabel : a, hint: hint }); - - return acc; - }, []); - values.sort((a, b) => a.label.compare(b.label)); + return { group: valueGroup, value: a, label: getLabel(a) }; + }); + values.sort((a, b) => a.value.compare(b.value)); return bars.concat(values); } - static _getTrackedAttributesFromSchema(schema, _path = []) { - const attributes = { bar: [], value: [] }; - for (const [name, field] of Object.entries(schema.fields)) { - const p = _path.concat([name]); - if (field instanceof foundry.data.fields.NumberField) attributes.value.push(p); - if (field instanceof foundry.data.fields.BooleanField && field.options.isAttributeChoice) - attributes.value.push(p); - if (field instanceof foundry.data.fields.StringField) attributes.value.push(p); - if (field instanceof foundry.data.fields.ArrayField) attributes.value.push(p); - const isSchema = field instanceof foundry.data.fields.SchemaField; - const isModel = field instanceof foundry.data.fields.EmbeddedDataField; - - if (isSchema || isModel) { - const schema = isModel ? field.model.schema : field; - const isBar = schema.has && schema.has('value') && schema.has('max'); - if (isBar) attributes.bar.push(p); - else { - const inner = this.getTrackedAttributes(schema, p); - attributes.bar.push(...inner.bar); - attributes.value.push(...inner.value); - } - } - } - return attributes; - } - _shouldRecordMovementHistory() { return false; } diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index c0dd45bd..c8b62ff6 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -119,8 +119,8 @@ export const tagifyElement = (element, baseOptions, onChange, tagifyOptions = {} }), maxTags: typeof maxTags === 'function' ? maxTags() : maxTags, dropdown: { + searchKeys: ['value', 'name'], mapValueTo: 'name', - searchKeys: ['value'], enabled: 0, maxItems: 100, closeOnSelect: true, @@ -472,7 +472,7 @@ export function refreshIsAllowed(allowedTypes, typeToCheck) { case CONFIG.DH.GENERAL.refreshTypes.scene.id: case CONFIG.DH.GENERAL.refreshTypes.session.id: case CONFIG.DH.GENERAL.refreshTypes.longRest.id: - return allowedTypes.includes(typeToCheck); + return allowedTypes.includes?.(typeToCheck) ?? allowedTypes.has(typeToCheck); case CONFIG.DH.GENERAL.refreshTypes.shortRest.id: return allowedTypes.some( x => @@ -495,3 +495,183 @@ export function htmlToText(html) { return tempDivElement.textContent || tempDivElement.innerText || ''; } + +export async function getFeaturesHTMLData(features) { + const result = []; + for (const feature of features) { + if (feature) { + const base = feature.item ?? feature; + const item = base.system ? base : await foundry.utils.fromUuid(base.uuid); + const itemDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML( + item.system.description + ); + result.push({ label: item.name, description: itemDescription }); + } + } + + return result; +} + +/** + * Given a simple flavor-less formula with only +/- operators, returns a list of damage partial terms. + * All subtracted terms become negative terms. + * If there are no dice, it returns 0d1 for that term. + */ +export function parseTermsFromSimpleFormula(formula) { + const roll = formula instanceof Roll ? formula : new Roll(formula); + + // Parse from right to left so that when we hit an operator, we already have the term. + return roll.terms.reduceRight((result, term) => { + // Ignore + terms, we assume + by default + if (term.expression === ' + ') return result; + + // - terms modify the last term we parsed + if (term.expression === ' - ') { + const termToModify = result[0]; + if (termToModify) { + if (termToModify.bonus) termToModify.bonus *= -1; + if (termToModify.dice) termToModify.dice *= -1; + } + return result; + } + + result.unshift({ + bonus: term instanceof foundry.dice.terms.NumericTerm ? term.number : 0, + diceQuantity: term instanceof foundry.dice.terms.Die ? term.number : 0, + faces: term.faces ?? 1 + }); + + return result; + }, []); +} + +/** + * Calculates the expectede value from a formula or the results of parseTermsFromSimpleFormula. + * @returns {number} the average result of rolling the given dice + */ +export function calculateExpectedValue(formulaOrTerms) { + const terms = Array.isArray(formulaOrTerms) + ? formulaOrTerms + : typeof formulaOrTerms === 'string' + ? parseTermsFromSimpleFormula(formulaOrTerms) + : [formulaOrTerms]; + return terms.reduce((r, t) => r + (t.bonus ?? 0) + (t.diceQuantity ? (t.diceQuantity * (t.faces + 1)) / 2 : 0), 0); +} + +export function parseRallyDice(value, effect) { + const legacyStartsWithPrefix = value.toLowerCase().startsWith('d'); + const workingValue = legacyStartsWithPrefix ? value.slice(1) : value; + const dataParsedValue = itemAbleRollParse(workingValue, effect.parent); + + return `d${game.system.api.documents.DhActiveEffect.effectSafeEval(dataParsedValue)}`; +} +/** + * Refreshes character and/or adversary resources. + * @param { string[] } refreshTypes Which type of features to refresh using IDs from CONFIG.DH.GENERAL.refreshTypes + * @param { string[] = ['character', 'adversary'] } actorTypes Which actor types should refresh their features. Defaults to character and adversary. + * @param { boolean = true } sendRefreshMessage If a chat message should be created detailing the refresh + * @return { Actor[] } The actors that had their features refreshed + */ +export async function RefreshFeatures( + refreshTypes = [], + actorTypes = ['character', 'adversary'], + sendNotificationMessage = true, + sendRefreshMessage = true +) { + const refreshedActors = {}; + for (let actor of game.actors) { + if (actorTypes.includes(actor.type) && actor.prototypeToken.actorLink) { + const updates = {}; + for (let item of actor.items) { + if ( + item.system.metadata?.hasResource && + refreshIsAllowed(refreshTypes, item.system.resource?.recovery) + ) { + if (!refreshedActors[actor.id]) + refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() }; + refreshedActors[actor.id].refreshed.add( + game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[item.system.resource.recovery].label) + ); + + if (!updates[item.id]?.system) updates[item.id] = { system: {} }; + + const increasing = + item.system.resource.progression === CONFIG.DH.ITEM.itemResourceProgression.increasing.id; + updates[item.id].system = { + ...updates[item.id].system, + 'resource.value': increasing + ? 0 + : game.system.api.documents.DhActiveEffect.effectSafeEval( + Roll.replaceFormulaData(item.system.resource.max, actor.getRollData()) + ) + }; + } + if (item.system.metadata?.hasActions) { + const usedTypes = new Set(); + const actions = item.system.actions.filter(action => { + if (refreshIsAllowed(refreshTypes, action.uses.recovery)) { + usedTypes.add(action.uses.recovery); + return true; + } + + return false; + }); + if (actions.length === 0) continue; + + if (!refreshedActors[actor.id]) + refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() }; + refreshedActors[actor.id].refreshed.add( + ...usedTypes.map(type => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[type].label)) + ); + + if (!updates[item.id]?.system) updates[item.id] = { system: {} }; + + updates[item.id].system = { + ...updates[item.id].system, + ...actions.reduce( + (acc, action) => { + acc.actions[action.id] = { 'uses.value': 0 }; + return acc; + }, + { actions: updates[item.id].system.actions ?? {} } + ) + }; + } + } + + for (let key in updates) { + const update = updates[key]; + await actor.items.get(key).update(update); + } + } + } + + const types = refreshTypes.map(x => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[x].label)).join(', '); + + if (sendNotificationMessage) { + ui.notifications.info( + game.i18n.format('DAGGERHEART.UI.Notifications.gmMenuRefresh', { + types: `[${types}]` + }) + ); + } + + if (sendRefreshMessage) { + const cls = getDocumentClass('ChatMessage'); + const msg = { + user: game.user.id, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/ui/chat/refreshMessage.hbs', + { + types: types + } + ), + title: game.i18n.localize('DAGGERHEART.UI.Chat.refreshMessage.title'), + speaker: cls.getSpeaker() + }; + + cls.create(msg); + } + + return refreshedActors; +} diff --git a/module/systemRegistration/handlebars.mjs b/module/systemRegistration/handlebars.mjs index 97769181..ad8c741a 100644 --- a/module/systemRegistration/handlebars.mjs +++ b/module/systemRegistration/handlebars.mjs @@ -39,6 +39,7 @@ export const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/dialogs/downtime/activities.hbs', 'systems/daggerheart/templates/dialogs/dice-roll/costSelection.hbs', 'systems/daggerheart/templates/ui/chat/parts/roll-part.hbs', + 'systems/daggerheart/templates/ui/chat/parts/description-part.hbs', 'systems/daggerheart/templates/ui/chat/parts/damage-part.hbs', 'systems/daggerheart/templates/ui/chat/parts/target-part.hbs', 'systems/daggerheart/templates/ui/chat/parts/button-part.hbs', diff --git a/module/systemRegistration/settings.mjs b/module/systemRegistration/settings.mjs index 053325a8..c4acf7ed 100644 --- a/module/systemRegistration/settings.mjs +++ b/module/systemRegistration/settings.mjs @@ -7,7 +7,7 @@ import { DhHomebrewSettings, DhVariantRuleSettings } from '../applications/settings/_module.mjs'; -import { DhTagTeamRoll } from '../data/_module.mjs'; +import { CompendiumBrowserSettings, DhTagTeamRoll } from '../data/_module.mjs'; export const registerDHSettings = () => { registerMenuSettings(); @@ -126,7 +126,7 @@ const registerNonConfigSettings = () => { type: Number, default: 0, onChange: () => { - if (ui.resources) ui.resources.render({ force: true }); + if (ui.resources) ui.resources.render(); ui.combat.render({ force: true }); } }); @@ -142,6 +142,12 @@ const registerNonConfigSettings = () => { config: false, type: DhTagTeamRoll }); + + game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings, { + scope: 'client', + config: false, + type: CompendiumBrowserSettings + }); }; /** diff --git a/module/systemRegistration/socket.mjs b/module/systemRegistration/socket.mjs index a9e86917..173ef02b 100644 --- a/module/systemRegistration/socket.mjs +++ b/module/systemRegistration/socket.mjs @@ -38,7 +38,8 @@ export const RefreshType = { Countdown: 'DhCoundownRefresh', TagTeamRoll: 'DhTagTeamRollRefresh', EffectsDisplay: 'DhEffectsDisplayRefresh', - Scene: 'DhSceneRefresh' + Scene: 'DhSceneRefresh', + CompendiumBrowser: 'DhCompendiumBrowserRefresh' }; export const registerSocketHooks = () => { diff --git a/src/packs/adversaries/adversary_Adult_Flickerfly_G7jiltRjgvVhZewm.json b/src/packs/adversaries/adversary_Adult_Flickerfly_G7jiltRjgvVhZewm.json index 266cba24..16fb61d8 100644 --- a/src/packs/adversaries/adversary_Adult_Flickerfly_G7jiltRjgvVhZewm.json +++ b/src/packs/adversaries/adversary_Adult_Flickerfly_G7jiltRjgvVhZewm.json @@ -82,7 +82,7 @@ "enabled": false }, "flatMultiplier": 3, - "dice": "d10", + "dice": "d20", "bonus": null, "multiplier": "flat" }, diff --git a/src/packs/adversaries/adversary_Apprentice_Assassin_vNIbYQ4YSzNf0WPE.json b/src/packs/adversaries/adversary_Apprentice_Assassin_vNIbYQ4YSzNf0WPE.json index 3f31ff76..23f1f339 100644 --- a/src/packs/adversaries/adversary_Apprentice_Assassin_vNIbYQ4YSzNf0WPE.json +++ b/src/packs/adversaries/adversary_Apprentice_Assassin_vNIbYQ4YSzNf0WPE.json @@ -246,7 +246,7 @@ "name": "Group Attack", "type": "feature", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 4 physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", "resource": null, "actions": { "vgguNWz8vG8aoLXR": { diff --git a/src/packs/adversaries/adversary_Archer_Squadron_0ts6CGd93lLqGZI5.json b/src/packs/adversaries/adversary_Archer_Squadron_0ts6CGd93lLqGZI5.json index 55229040..5b15bc09 100644 --- a/src/packs/adversaries/adversary_Archer_Squadron_0ts6CGd93lLqGZI5.json +++ b/src/packs/adversaries/adversary_Archer_Squadron_0ts6CGd93lLqGZI5.json @@ -218,10 +218,10 @@ }, "items": [ { - "name": "Horde (1d6+3)", + "name": "Horde", "type": "feature", "system": { - "description": "

When the @Lookup[@name] has marked half or more of their HP, their standard attack deals 1d6+3 physical damage instead.

", + "description": "

When the @Lookup[@name] has marked half or more of their HP, their standard attack deals @Lookup[@system.attack.altDamageFormula] physical damage instead.

", "resource": null, "actions": {}, "originItemType": null, diff --git a/src/packs/adversaries/adversary_Assassin_Poisoner_h5RuhzGL17dW5FBT.json b/src/packs/adversaries/adversary_Assassin_Poisoner_h5RuhzGL17dW5FBT.json index 6594cbbe..8b553c83 100644 --- a/src/packs/adversaries/adversary_Assassin_Poisoner_h5RuhzGL17dW5FBT.json +++ b/src/packs/adversaries/adversary_Assassin_Poisoner_h5RuhzGL17dW5FBT.json @@ -388,7 +388,7 @@ "name": "Fumigation", "type": "feature", "system": { - "description": "

Drop a smoke bomb that fi lls the air within Close range with smoke, Dizzying all targets in this area. Dizzied targets have disadvantage on their next action roll, then clear the condition.

@Template[type:emanation|range:c]

", + "description": "

Drop a smoke bomb that fills the air within Close range with smoke, Dizzying all targets in this area. Dizzied targets have disadvantage on their next action roll, then clear the condition.

@Template[type:emanation|range:c]

", "resource": null, "actions": { "sp7RfJRQJsEUm09m": { diff --git a/src/packs/adversaries/adversary_Conscript_99TqczuQipBmaB8i.json b/src/packs/adversaries/adversary_Conscript_99TqczuQipBmaB8i.json index c5b4357d..35c43a3b 100644 --- a/src/packs/adversaries/adversary_Conscript_99TqczuQipBmaB8i.json +++ b/src/packs/adversaries/adversary_Conscript_99TqczuQipBmaB8i.json @@ -239,7 +239,7 @@ "name": "Group Attack", "type": "feature", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 6 physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", "resource": null, "actions": { "cbAvPSIhwBMBTI3D": { diff --git a/src/packs/adversaries/adversary_Cult_Initiate_zx99sOGTXicP4SSD.json b/src/packs/adversaries/adversary_Cult_Initiate_zx99sOGTXicP4SSD.json index 0e14a661..a0c0713d 100644 --- a/src/packs/adversaries/adversary_Cult_Initiate_zx99sOGTXicP4SSD.json +++ b/src/packs/adversaries/adversary_Cult_Initiate_zx99sOGTXicP4SSD.json @@ -239,7 +239,7 @@ "name": "Group Attack", "type": "feature", "system": { - "description": "

Spend a Fear to choose a target and spotlight all Cult @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 5 physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all Cult @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", "resource": null, "actions": { "EH1preaTWBD4rOvx": { diff --git a/src/packs/adversaries/adversary_Demonic_Hound_Pack_NoRZ1PqB8N5wcIw0.json b/src/packs/adversaries/adversary_Demonic_Hound_Pack_NoRZ1PqB8N5wcIw0.json index 2947b7a1..7482c734 100644 --- a/src/packs/adversaries/adversary_Demonic_Hound_Pack_NoRZ1PqB8N5wcIw0.json +++ b/src/packs/adversaries/adversary_Demonic_Hound_Pack_NoRZ1PqB8N5wcIw0.json @@ -224,10 +224,10 @@ }, "items": [ { - "name": "Horde (2d4+1)", + "name": "Horde", "type": "feature", "system": { - "description": "

When the @Lookup[@name] has marked half or more of their HP, their standard attack deals 2d4+1 physical damage instead.

", + "description": "

When the @Lookup[@name] has marked half or more of their HP, their standard attack deals @Lookup[@system.attack.altDamageFormula] physical damage instead.

", "resource": null, "actions": {}, "originItemType": null, diff --git a/src/packs/adversaries/adversary_Electric_Eels_TLzY1nDw0Bu9Ud40.json b/src/packs/adversaries/adversary_Electric_Eels_TLzY1nDw0Bu9Ud40.json index 7b41b9e5..9386944f 100644 --- a/src/packs/adversaries/adversary_Electric_Eels_TLzY1nDw0Bu9Ud40.json +++ b/src/packs/adversaries/adversary_Electric_Eels_TLzY1nDw0Bu9Ud40.json @@ -218,10 +218,10 @@ }, "items": [ { - "name": "Horde (2d4+1)", + "name": "Horde", "type": "feature", "system": { - "description": "

When the @Lookup[@name] have marked half or more of their HP, their standard attack deals 2d4+1 physical damage instead.

", + "description": "

When the @Lookup[@name] have marked half or more of their HP, their standard attack deals @Lookup[@system.attack.altDamageFormula] physical damage instead.

", "resource": null, "actions": {}, "originItemType": null, diff --git a/src/packs/adversaries/adversary_Elemental_Spark_P7h54ZePFPHpYwvB.json b/src/packs/adversaries/adversary_Elemental_Spark_P7h54ZePFPHpYwvB.json index b17cae1c..5c25f63e 100644 --- a/src/packs/adversaries/adversary_Elemental_Spark_P7h54ZePFPHpYwvB.json +++ b/src/packs/adversaries/adversary_Elemental_Spark_P7h54ZePFPHpYwvB.json @@ -239,7 +239,7 @@ "name": "Group Attack", "type": "feature", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 5 physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", "resource": null, "actions": { "vXHZVb0Y7Hqu3uso": { diff --git a/src/packs/adversaries/adversary_Fallen_Shock_Troop_OsLG2BjaEdTZUJU9.json b/src/packs/adversaries/adversary_Fallen_Shock_Troop_OsLG2BjaEdTZUJU9.json index 163c61f7..931e4c0a 100644 --- a/src/packs/adversaries/adversary_Fallen_Shock_Troop_OsLG2BjaEdTZUJU9.json +++ b/src/packs/adversaries/adversary_Fallen_Shock_Troop_OsLG2BjaEdTZUJU9.json @@ -317,7 +317,7 @@ "name": "Group Attack", "type": "feature", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 12 physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", "resource": null, "actions": { "QHNRSEQmqOcaoXq4": { diff --git a/src/packs/adversaries/adversary_Giant_Mosquitoes_IIWV4ysJPFPnTP7W.json b/src/packs/adversaries/adversary_Giant_Mosquitoes_IIWV4ysJPFPnTP7W.json index 54f12efa..fbb30d40 100644 --- a/src/packs/adversaries/adversary_Giant_Mosquitoes_IIWV4ysJPFPnTP7W.json +++ b/src/packs/adversaries/adversary_Giant_Mosquitoes_IIWV4ysJPFPnTP7W.json @@ -229,7 +229,7 @@ "_id": "9RduwBLYcBaiouYk", "img": "icons/creatures/magical/humanoid-silhouette-aliens-green.webp", "system": { - "description": "

When the @Lookup[@name] have marked half or more of their HP, their standard attack deals 1d4+1 physical damage instead.

", + "description": "

When the @Lookup[@name] have marked half or more of their HP, their standard attack deals @Lookup[@system.attack.altDamageFormula] physical damage instead.

", "resource": null, "actions": {}, "originItemType": null, diff --git a/src/packs/adversaries/adversary_Giant_Rat_4PfLnaCrOcMdb4dK.json b/src/packs/adversaries/adversary_Giant_Rat_4PfLnaCrOcMdb4dK.json index d4655880..d1df6b57 100644 --- a/src/packs/adversaries/adversary_Giant_Rat_4PfLnaCrOcMdb4dK.json +++ b/src/packs/adversaries/adversary_Giant_Rat_4PfLnaCrOcMdb4dK.json @@ -248,7 +248,7 @@ "_id": "fsaBlCjTdq1jM23G", "img": "icons/creatures/abilities/tail-strike-bone-orange.webp", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 1 physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", "resource": null, "actions": { "q8chow47nQLR9qeF": { diff --git a/src/packs/adversaries/adversary_Giant_Recruit_5s8wSvpyC5rxY5aD.json b/src/packs/adversaries/adversary_Giant_Recruit_5s8wSvpyC5rxY5aD.json index 75da96b2..adcdf015 100644 --- a/src/packs/adversaries/adversary_Giant_Recruit_5s8wSvpyC5rxY5aD.json +++ b/src/packs/adversaries/adversary_Giant_Recruit_5s8wSvpyC5rxY5aD.json @@ -55,7 +55,7 @@ "max": 1 }, "stress": { - "max": 1 + "max": 2 } }, "attack": { @@ -239,7 +239,7 @@ "name": "Group Attack", "type": "feature", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 5 physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", "resource": null, "actions": { "DjbPQowW1OdBD9Zn": { diff --git a/src/packs/adversaries/adversary_Hallowed_Soldier_VENwg7xEFcYObjmT.json b/src/packs/adversaries/adversary_Hallowed_Soldier_VENwg7xEFcYObjmT.json index cceed989..95a2ecd0 100644 --- a/src/packs/adversaries/adversary_Hallowed_Soldier_VENwg7xEFcYObjmT.json +++ b/src/packs/adversaries/adversary_Hallowed_Soldier_VENwg7xEFcYObjmT.json @@ -55,7 +55,7 @@ "max": 1 }, "stress": { - "max": 1 + "max": 2 } }, "attack": { @@ -294,7 +294,7 @@ "name": "Group Attack", "type": "feature", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 10 physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", "resource": null, "actions": { "eo7J0v1B5zPHul1M": { diff --git a/src/packs/adversaries/adversary_Jagged_Knife_Lackey_C0OMQqV7pN6t7ouR.json b/src/packs/adversaries/adversary_Jagged_Knife_Lackey_C0OMQqV7pN6t7ouR.json index 1a95bf87..a52ec1c9 100644 --- a/src/packs/adversaries/adversary_Jagged_Knife_Lackey_C0OMQqV7pN6t7ouR.json +++ b/src/packs/adversaries/adversary_Jagged_Knife_Lackey_C0OMQqV7pN6t7ouR.json @@ -248,7 +248,7 @@ "_id": "1k5TmQIAunM7Bv32", "img": "icons/creatures/abilities/tail-strike-bone-orange.webp", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name] within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 2 physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name] within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", "resource": null, "actions": { "aoQDb2m32NDxE6ZP": { diff --git a/src/packs/adversaries/adversary_Jagged_Knife_Sniper_1zuyof1XuIfi3aMG.json b/src/packs/adversaries/adversary_Jagged_Knife_Sniper_1zuyof1XuIfi3aMG.json index 166c521b..6fd02cb5 100644 --- a/src/packs/adversaries/adversary_Jagged_Knife_Sniper_1zuyof1XuIfi3aMG.json +++ b/src/packs/adversaries/adversary_Jagged_Knife_Sniper_1zuyof1XuIfi3aMG.json @@ -33,7 +33,7 @@ "reduction": 0 } }, - "type": "standard", + "type": "ranged", "notes": "", "hordeHp": 1, "experiences": { diff --git a/src/packs/adversaries/adversary_Minor_Demon_3tqCjDwJAQ7JKqMb.json b/src/packs/adversaries/adversary_Minor_Demon_3tqCjDwJAQ7JKqMb.json index 3a330fdf..0fceeba1 100644 --- a/src/packs/adversaries/adversary_Minor_Demon_3tqCjDwJAQ7JKqMb.json +++ b/src/packs/adversaries/adversary_Minor_Demon_3tqCjDwJAQ7JKqMb.json @@ -104,6 +104,7 @@ ] }, "type": "attack", + "range": "melee", "chatDisplay": false }, "attribution": { diff --git a/src/packs/adversaries/adversary_Minor_Treant_G62k4oSkhkoXEs2D.json b/src/packs/adversaries/adversary_Minor_Treant_G62k4oSkhkoXEs2D.json index 0f1e7ded..f05ba5fc 100644 --- a/src/packs/adversaries/adversary_Minor_Treant_G62k4oSkhkoXEs2D.json +++ b/src/packs/adversaries/adversary_Minor_Treant_G62k4oSkhkoXEs2D.json @@ -242,7 +242,7 @@ "_id": "K08WlZwGqzEo4idT", "img": "icons/creatures/abilities/tail-strike-bone-orange.webp", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 4 physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", "resource": null, "actions": { "xTMNAHcoErKuR6TZ": { diff --git a/src/packs/adversaries/adversary_Oak_Treant_XK78QUfY8c8Go8Uv.json b/src/packs/adversaries/adversary_Oak_Treant_XK78QUfY8c8Go8Uv.json index c0999e70..3c110024 100644 --- a/src/packs/adversaries/adversary_Oak_Treant_XK78QUfY8c8Go8Uv.json +++ b/src/packs/adversaries/adversary_Oak_Treant_XK78QUfY8c8Go8Uv.json @@ -33,7 +33,7 @@ "reduction": 0 } }, - "type": "standard", + "type": "bruiser", "notes": "", "hordeHp": 1, "experiences": {}, @@ -66,12 +66,12 @@ "tier": 3, "description": "

A sturdy animate old-growth tree.

", "attack": { - "name": "Attack", + "name": "Branch", "roll": { "type": "attack", "bonus": 2 }, - "range": "close", + "range": "veryClose", "damage": { "parts": [ { diff --git a/src/packs/adversaries/adversary_Outer_Realms_Thrall_moJhHgKqTKPS2WYS.json b/src/packs/adversaries/adversary_Outer_Realms_Thrall_moJhHgKqTKPS2WYS.json index 370182a5..5347bf49 100644 --- a/src/packs/adversaries/adversary_Outer_Realms_Thrall_moJhHgKqTKPS2WYS.json +++ b/src/packs/adversaries/adversary_Outer_Realms_Thrall_moJhHgKqTKPS2WYS.json @@ -97,7 +97,7 @@ }, "img": "icons/creatures/claws/claw-talons-yellow-red.webp", "type": "attack", - "range": "melee", + "range": "veryClose", "chatDisplay": false }, "attribution": { @@ -239,7 +239,7 @@ "name": "Group Attack", "type": "feature", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 11 physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", "resource": null, "actions": { "tvQetauskZoHDR5y": { diff --git a/src/packs/adversaries/adversary_Pirate_Raiders_5YgEajn0wa4i85kC.json b/src/packs/adversaries/adversary_Pirate_Raiders_5YgEajn0wa4i85kC.json index 7d3733ce..41f79b49 100644 --- a/src/packs/adversaries/adversary_Pirate_Raiders_5YgEajn0wa4i85kC.json +++ b/src/packs/adversaries/adversary_Pirate_Raiders_5YgEajn0wa4i85kC.json @@ -229,7 +229,7 @@ "_id": "Q7DRbWjHl64CNwag", "img": "icons/creatures/magical/humanoid-silhouette-aliens-green.webp", "system": { - "description": "

When the @Lookup[@name] have marked half or more of their HP, their standard attack deals 1d4+1 physical damage instead.

", + "description": "

When the @Lookup[@name] have marked half or more of their HP, their standard attack deals @Lookup[@system.attack.altDamageFormula] physical damage instead.

", "resource": null, "actions": {}, "originItemType": null, diff --git a/src/packs/adversaries/adversary_Rotted_Zombie_gP3fWTLzSFnpA8EJ.json b/src/packs/adversaries/adversary_Rotted_Zombie_gP3fWTLzSFnpA8EJ.json index a9bf3a67..7672961c 100644 --- a/src/packs/adversaries/adversary_Rotted_Zombie_gP3fWTLzSFnpA8EJ.json +++ b/src/packs/adversaries/adversary_Rotted_Zombie_gP3fWTLzSFnpA8EJ.json @@ -242,7 +242,7 @@ "_id": "R9vrwFNl5BD1YXJo", "img": "icons/creatures/abilities/tail-strike-bone-orange.webp", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 2 physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", "resource": null, "actions": { "DJBNtd3hWjwsjPwq": { diff --git a/src/packs/adversaries/adversary_Sellsword_bgreCaQ6ap2DVpCr.json b/src/packs/adversaries/adversary_Sellsword_bgreCaQ6ap2DVpCr.json index e26b48eb..514be8f5 100644 --- a/src/packs/adversaries/adversary_Sellsword_bgreCaQ6ap2DVpCr.json +++ b/src/packs/adversaries/adversary_Sellsword_bgreCaQ6ap2DVpCr.json @@ -242,7 +242,7 @@ "_id": "CQZQiEiRH70Br5Ge", "img": "icons/creatures/abilities/tail-strike-bone-orange.webp", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 3 physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", "resource": null, "actions": { "ghgFZskDiizJDjcn": { diff --git a/src/packs/adversaries/adversary_Skeleton_Dredge_6l1a3Fazq8BoKIcc.json b/src/packs/adversaries/adversary_Skeleton_Dredge_6l1a3Fazq8BoKIcc.json index 5a973b17..4013d7fe 100644 --- a/src/packs/adversaries/adversary_Skeleton_Dredge_6l1a3Fazq8BoKIcc.json +++ b/src/packs/adversaries/adversary_Skeleton_Dredge_6l1a3Fazq8BoKIcc.json @@ -242,7 +242,7 @@ "_id": "wl9KKEpVWDBu62hU", "img": "icons/creatures/abilities/tail-strike-bone-orange.webp", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 1 physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", "resource": null, "actions": { "Sz55uB8xkoNytLwJ": { diff --git a/src/packs/adversaries/adversary_Swarm_of_Rats_qNgs3AbLyJrY19nt.json b/src/packs/adversaries/adversary_Swarm_of_Rats_qNgs3AbLyJrY19nt.json index 33fe06d7..014b3dc6 100644 --- a/src/packs/adversaries/adversary_Swarm_of_Rats_qNgs3AbLyJrY19nt.json +++ b/src/packs/adversaries/adversary_Swarm_of_Rats_qNgs3AbLyJrY19nt.json @@ -223,7 +223,7 @@ "_id": "9Zuu892SO5NmtI4w", "img": "icons/creatures/magical/humanoid-silhouette-aliens-green.webp", "system": { - "description": "

When the @Lookup[@name] has marked half or more of their HP, their standard attack deals 1d4+1 physical damage instead.

", + "description": "

When the @Lookup[@name] has marked half or more of their HP, their standard attack deals @Lookup[@system.attack.altDamageFormula] physical damage instead.

", "resource": null, "actions": {}, "originItemType": null, diff --git a/src/packs/adversaries/adversary_Tangle_Bramble_Swarm_PKSXFuaIHUCoH63A.json b/src/packs/adversaries/adversary_Tangle_Bramble_Swarm_PKSXFuaIHUCoH63A.json index 639fa956..40297eb6 100644 --- a/src/packs/adversaries/adversary_Tangle_Bramble_Swarm_PKSXFuaIHUCoH63A.json +++ b/src/packs/adversaries/adversary_Tangle_Bramble_Swarm_PKSXFuaIHUCoH63A.json @@ -254,12 +254,12 @@ }, "items": [ { - "name": "Horde (1d4+2)", + "name": "Horde", "type": "feature", "_id": "4dSzqtYvH385r9Ng", "img": "icons/creatures/magical/humanoid-silhouette-aliens-green.webp", "system": { - "description": "

When the @Lookup[@name] has marked half or more of their HP, their standard attack deals 1d4+2 physical damage instead.

", + "description": "

When the @Lookup[@name] has marked half or more of their HP, their standard attack deals @Lookup[@system.attack.altDamageFormula] physical damage instead.

", "resource": null, "actions": {}, "originItemType": null, diff --git a/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json b/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json index 0f1ba28f..33afaa3a 100644 --- a/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json +++ b/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json @@ -281,7 +281,7 @@ "_id": "WiobzuyvJ46zfsOv", "img": "icons/creatures/abilities/tail-strike-bone-orange.webp", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 2 physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", "resource": null, "actions": { "ZC5pKIb9N82vgMWu": { diff --git a/src/packs/adversaries/adversary_Treant_Sapling_o63nS0k3wHu6EgKP.json b/src/packs/adversaries/adversary_Treant_Sapling_o63nS0k3wHu6EgKP.json index 8959f78a..ad9d8107 100644 --- a/src/packs/adversaries/adversary_Treant_Sapling_o63nS0k3wHu6EgKP.json +++ b/src/packs/adversaries/adversary_Treant_Sapling_o63nS0k3wHu6EgKP.json @@ -97,8 +97,8 @@ ] }, "type": "attack", - "chatDisplay": false, - "range": "" + "range": "melee", + "chatDisplay": false }, "attribution": { "source": "Daggerheart SRD", @@ -239,7 +239,7 @@ "name": "Group Attack", "type": "feature", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 8 physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", "resource": null, "actions": { "euP8VA4wvfsCpwN1": { diff --git a/src/packs/adversaries/adversary_Young_Ice_Dragon_UGPiPLJsPvMTSKEF.json b/src/packs/adversaries/adversary_Young_Ice_Dragon_UGPiPLJsPvMTSKEF.json index b0a3bded..c55262e4 100644 --- a/src/packs/adversaries/adversary_Young_Ice_Dragon_UGPiPLJsPvMTSKEF.json +++ b/src/packs/adversaries/adversary_Young_Ice_Dragon_UGPiPLJsPvMTSKEF.json @@ -110,7 +110,7 @@ }, "img": "icons/creatures/claws/claw-scaled-red.webp", "type": "attack", - "range": "melee", + "range": "close", "chatDisplay": false }, "attribution": { diff --git a/src/packs/adversaries/adversary_Zombie_Legion_YhJrP7rTBiRdX5Fp.json b/src/packs/adversaries/adversary_Zombie_Legion_YhJrP7rTBiRdX5Fp.json index 1b2cce2a..91bdab81 100644 --- a/src/packs/adversaries/adversary_Zombie_Legion_YhJrP7rTBiRdX5Fp.json +++ b/src/packs/adversaries/adversary_Zombie_Legion_YhJrP7rTBiRdX5Fp.json @@ -97,7 +97,7 @@ } ] }, - "name": "Tentacles", + "name": "Undead Hands", "roll": { "bonus": 2, "type": "attack" @@ -218,10 +218,10 @@ }, "items": [ { - "name": "Horde (2d6+5)", + "name": "Horde", "type": "feature", "system": { - "description": "

When the @Lookup[@name] has marked half or more of their HP, their standard attack deals 2d6+5 physical damage instead.

", + "description": "

When the @Lookup[@name] has marked half or more of their HP, their standard attack deals @Lookup[@system.attack.altDamageFormula] physical damage instead.

", "resource": null, "actions": {}, "originItemType": null, diff --git a/src/packs/adversaries/adversary_Zombie_Pack_Nf0v43rtflV56V2T.json b/src/packs/adversaries/adversary_Zombie_Pack_Nf0v43rtflV56V2T.json index 32519ac6..017537ad 100644 --- a/src/packs/adversaries/adversary_Zombie_Pack_Nf0v43rtflV56V2T.json +++ b/src/packs/adversaries/adversary_Zombie_Pack_Nf0v43rtflV56V2T.json @@ -218,12 +218,12 @@ }, "items": [ { - "name": "Horde (1d4+2)", + "name": "Horde", "type": "feature", "_id": "nNJGAhWu0IuS2ybn", "img": "icons/creatures/magical/humanoid-silhouette-aliens-green.webp", "system": { - "description": "

When the @Lookup[@name] have marked half or more of their HP, their standard attack deals 1d4+2 physical damage instead.

", + "description": "

When the @Lookup[@name] have marked half or more of their HP, their standard attack deals @Lookup[@system.attack.altDamageFormula] physical damage instead.

", "resource": null, "actions": {}, "originItemType": null, diff --git a/src/packs/ancestries/ancestry_Clank_ed8BoLR4SHOpeV00.json b/src/packs/ancestries/ancestry_Clank_ed8BoLR4SHOpeV00.json index 1108fe2e..c975a035 100644 --- a/src/packs/ancestries/ancestry_Clank_ed8BoLR4SHOpeV00.json +++ b/src/packs/ancestries/ancestry_Clank_ed8BoLR4SHOpeV00.json @@ -4,7 +4,7 @@ "type": "ancestry", "folder": null, "system": { - "description": "

Clanks are sentient mechanical beings built from a variety of materials, including metal, wood, and stone. They can resemble humanoids, animals, or even inanimate objects. Like organic beings, their bodies come in a wide array of sizes. Because of their bespoke construction, many clanks have highly specialized physical configurations. Examples include clawed hands for grasping, wheels for movement, or built-in weaponry.

Many clanks embrace body modifications for style as well as function, and members of other ancestries often turn to clank artisans to construct customized mobility aids and physical adornments. Other ancestries can create clanks, even using their own physical characteristics as inspiration, but it’s also common for clanks to build one another. A clank’s lifespan extends as long as they’re able to acquire or craft new parts, making their physical form effectively immortal. That said, their minds are subject to the effects of time, and deteriorate as the magic that powers them loses potency.

ANCESTRY FEATURES

Purposeful Design: Decide who made you and for what purpose. At character creation, choose one of your Experiences that best aligns with this purpose and gain a permanent +1 bonus to it.

Efficient: When you take a short rest, you can choose a long rest move instead of a short rest move.

", + "description": "

Clanks are sentient mechanical beings built from a variety of materials, including metal, wood, and stone. They can resemble humanoids, animals, or even inanimate objects. Like organic beings, their bodies come in a wide array of sizes. Because of their bespoke construction, many clanks have highly specialized physical configurations. Examples include clawed hands for grasping, wheels for movement, or built-in weaponry.

Many clanks embrace body modifications for style as well as function, and members of other ancestries often turn to clank artisans to construct customized mobility aids and physical adornments. Other ancestries can create clanks, even using their own physical characteristics as inspiration, but it’s also common for clanks to build one another. A clank’s lifespan extends as long as they’re able to acquire or craft new parts, making their physical form effectively immortal. That said, their minds are subject to the effects of time, and deteriorate as the magic that powers them loses potency.

", "features": [ { "type": "primary", diff --git a/src/packs/ancestries/ancestry_Drakona_VLeOEqkLS0RbF0tB.json b/src/packs/ancestries/ancestry_Drakona_VLeOEqkLS0RbF0tB.json index 49229d1d..5f4e31f6 100644 --- a/src/packs/ancestries/ancestry_Drakona_VLeOEqkLS0RbF0tB.json +++ b/src/packs/ancestries/ancestry_Drakona_VLeOEqkLS0RbF0tB.json @@ -4,7 +4,7 @@ "type": "ancestry", "folder": null, "system": { - "description": "

Drakona resemble wingless dragons in humanoid form and possess a powerful elemental breath. All drakona have thick scales that provide excellent natural armor against both attacks and the forces of nature. They are large in size, ranging from 5 feet to 7 feet on average, with long sharp teeth. New teeth grow throughout a Drakona’s approximately 350-year lifespan, so they are never in danger of permanently losing an incisor. Unlike their dragon ancestors, drakona don’t have wings and can’t fly without magical aid. Members of this ancestry pass down the element of their breath through generations, though in rare cases, a drakona’s elemental power will differ from the rest of their family’s.

ANCESTRY FEATURES

Scales: Your scales act as natural protection. When you would take Severe damage, you can mark a Stress to mark 1 fewer Hit Points.

Elemental Breath: Choose an element for your breath (such as electricity, fire, or ice). You can use this breath against a target or group of targets within Very Close range, treating it as an Instinct weapon that deals d8 magic damage using your Proficiency.

", + "description": "

Drakona resemble wingless dragons in humanoid form and possess a powerful elemental breath. All drakona have thick scales that provide excellent natural armor against both attacks and the forces of nature. They are large in size, ranging from 5 feet to 7 feet on average, with long sharp teeth. New teeth grow throughout a Drakona’s approximately 350-year lifespan, so they are never in danger of permanently losing an incisor. Unlike their dragon ancestors, drakona don’t have wings and can’t fly without magical aid. Members of this ancestry pass down the element of their breath through generations, though in rare cases, a drakona’s elemental power will differ from the rest of their family’s.

", "features": [ { "type": "primary", diff --git a/src/packs/ancestries/ancestry_Dwarf_pDt6fI6otv2E2odf.json b/src/packs/ancestries/ancestry_Dwarf_pDt6fI6otv2E2odf.json index 640ca729..7ca63d09 100644 --- a/src/packs/ancestries/ancestry_Dwarf_pDt6fI6otv2E2odf.json +++ b/src/packs/ancestries/ancestry_Dwarf_pDt6fI6otv2E2odf.json @@ -4,7 +4,7 @@ "type": "ancestry", "folder": null, "system": { - "description": "

Dwarves are most easily recognized as short humanoids with square frames, dense musculature, and thick hair. Their average height ranges from 4 to 5 ½ feet, and they are often broad in proportion to their stature. Their skin and nails contain a high amount of keratin, making them naturally resilient. This allows dwarves to embed gemstones into their bodies and decorate themselves with tattoos or piercings. Their hair grows thickly—usually on their heads, but some dwarves have thick hair across their bodies as well. Dwarves of all genders can grow facial hair, which they often style in elaborate arrangements. Typically, dwarves live up to 250 years of age, maintaining their muscle mass well into later life.

ANCESTRY FEATURES

Thick Skin: When you take Minor damage, you can mark 2 Stress instead of marking a Hit Point.

Increased Fortitude: Spend 3 Hope to halve incoming physical damage.

", + "description": "

Dwarves are most easily recognized as short humanoids with square frames, dense musculature, and thick hair. Their average height ranges from 4 to 5 ½ feet, and they are often broad in proportion to their stature. Their skin and nails contain a high amount of keratin, making them naturally resilient. This allows dwarves to embed gemstones into their bodies and decorate themselves with tattoos or piercings. Their hair grows thickly—usually on their heads, but some dwarves have thick hair across their bodies as well. Dwarves of all genders can grow facial hair, which they often style in elaborate arrangements. Typically, dwarves live up to 250 years of age, maintaining their muscle mass well into later life.

", "features": [ { "type": "primary", diff --git a/src/packs/ancestries/ancestry_Elf_q2l6g3Ssa04K84GO.json b/src/packs/ancestries/ancestry_Elf_q2l6g3Ssa04K84GO.json index 32868972..10b847f5 100644 --- a/src/packs/ancestries/ancestry_Elf_q2l6g3Ssa04K84GO.json +++ b/src/packs/ancestries/ancestry_Elf_q2l6g3Ssa04K84GO.json @@ -4,7 +4,7 @@ "type": "ancestry", "folder": null, "system": { - "description": "

Elves are typically tall humanoids with pointed ears and acutely attuned senses. Their ears vary in size and pointed shape, and as they age, the tips begin to droop. While elves come in a wide range of body types, they are all fairly tall, with heights ranging from about 6 to 6 ½ feet. All elves have the ability to drop into a celestial trance, rather than sleep. This allows them to rest effectively in a short amount of time.

Some elves possess what is known as a “mystic form,” which occurs when an elf has dedicated themself to the study or protection of the natural world so deeply that their physical form changes. These characteristics can include celestial freckles, the presence of leaves, vines, or flowers in their hair, eyes that flicker like fire, and more. Sometimes these traits are inherited from parents, but if an elf changes their environment or magical focus, their appearance changes over time. Because elves live for about 350 years, these traits can shift more than once throughout their lifespan.

ANCESTRY FEATURES

Quick Reactions: Mark a Stress to gain advantage on a reaction roll.

Celestial Trance: During a rest, you can drop into a trance to choose an additional downtime move.

", + "description": "

Elves are typically tall humanoids with pointed ears and acutely attuned senses. Their ears vary in size and pointed shape, and as they age, the tips begin to droop. While elves come in a wide range of body types, they are all fairly tall, with heights ranging from about 6 to 6 ½ feet. All elves have the ability to drop into a celestial trance, rather than sleep. This allows them to rest effectively in a short amount of time.

Some elves possess what is known as a “mystic form,” which occurs when an elf has dedicated themself to the study or protection of the natural world so deeply that their physical form changes. These characteristics can include celestial freckles, the presence of leaves, vines, or flowers in their hair, eyes that flicker like fire, and more. Sometimes these traits are inherited from parents, but if an elf changes their environment or magical focus, their appearance changes over time. Because elves live for about 350 years, these traits can shift more than once throughout their lifespan.

", "features": [ { "type": "primary", diff --git a/src/packs/ancestries/ancestry_Faerie_XzJVbb5NT9k79ykR.json b/src/packs/ancestries/ancestry_Faerie_XzJVbb5NT9k79ykR.json index 26e58162..c2318491 100644 --- a/src/packs/ancestries/ancestry_Faerie_XzJVbb5NT9k79ykR.json +++ b/src/packs/ancestries/ancestry_Faerie_XzJVbb5NT9k79ykR.json @@ -4,7 +4,7 @@ "type": "ancestry", "folder": null, "system": { - "description": "

Faeries are winged humanoid creatures with insectile features. These characteristics cover a broad spectrum from humanoid to insectoid—some possess additional arms, compound eyes, lantern organs, chitinous exoskeletons, or stingers. Because of their close ties to the natural world, they also frequently possess attributes that allow them to blend in with various plants. The average height of a faerie ranges from about 2 feet to 5 feet, but some faeries grow up to 7 feet tall. All faeries possess membranous wings and they each go through a process of metamorphosis. The process and changes differ from faerie to faerie, but during this transformation each individual manifests the unique appearance they will carry throughout the rest of their approximately 50-year lifespan.

ANCESTRY FEATURE

Luckbender: Once per session, after you or a willing ally within Close range makes an action roll, you can spend 3 Hope to reroll the Duality Dice.

Wings: You can fly. While flying, you can mark a Stress after an adversary makes an attack against you to gain a +2 bonus to your Evasion against that attack.

", + "description": "

Faeries are winged humanoid creatures with insectile features. These characteristics cover a broad spectrum from humanoid to insectoid—some possess additional arms, compound eyes, lantern organs, chitinous exoskeletons, or stingers. Because of their close ties to the natural world, they also frequently possess attributes that allow them to blend in with various plants. The average height of a faerie ranges from about 2 feet to 5 feet, but some faeries grow up to 7 feet tall. All faeries possess membranous wings and they each go through a process of metamorphosis. The process and changes differ from faerie to faerie, but during this transformation each individual manifests the unique appearance they will carry throughout the rest of their approximately 50-year lifespan.

", "features": [ { "type": "primary", diff --git a/src/packs/ancestries/ancestry_Faun_HaYhe6WqoXW5EbRl.json b/src/packs/ancestries/ancestry_Faun_HaYhe6WqoXW5EbRl.json index b4cf3c4a..d3234f0f 100644 --- a/src/packs/ancestries/ancestry_Faun_HaYhe6WqoXW5EbRl.json +++ b/src/packs/ancestries/ancestry_Faun_HaYhe6WqoXW5EbRl.json @@ -4,7 +4,7 @@ "type": "ancestry", "folder": null, "system": { - "description": "

Fauns resemble humanoid goats with curving horns, square pupils, and cloven hooves. Though their appearances may vary, most fauns have a humanoid torso and a goatlike lower body covered in dense fur. Faun faces can be more caprine or more humanlike, and they have a wide variety of ear and horn shapes. Faun horns range from short with minimal curvature to much larger with a distinct curl. The average faun ranges from 4 feet to 6 ½ feet tall, but their height can change dramatically from one moment to the next based on their stance. The majority of fauns have proportionately long limbs, no matter their size or shape, and are known for their ability to deliver powerful blows with their split hooves. Fauns live for roughly 225 years, and as they age, their appearance can become increasingly goatlike.

ANCESTRY FEATURES

Caprine Leap: You can leap anywhere within Close range as though you were using normal movement, allowing you to vault obstacles, jump across gaps, or scale barriers with ease.

Kick: When you succeed on an attack against a target within Melee range, you can mark a Stress to kick yourself off them, dealing an extra 2d6 damage and knocking back either yourself or the target to Very Close range.

", + "description": "

Fauns resemble humanoid goats with curving horns, square pupils, and cloven hooves. Though their appearances may vary, most fauns have a humanoid torso and a goatlike lower body covered in dense fur. Faun faces can be more caprine or more humanlike, and they have a wide variety of ear and horn shapes. Faun horns range from short with minimal curvature to much larger with a distinct curl. The average faun ranges from 4 feet to 6 ½ feet tall, but their height can change dramatically from one moment to the next based on their stance. The majority of fauns have proportionately long limbs, no matter their size or shape, and are known for their ability to deliver powerful blows with their split hooves. Fauns live for roughly 225 years, and as they age, their appearance can become increasingly goatlike.

", "features": [ { "type": "primary", diff --git a/src/packs/ancestries/ancestry_Firbolg_hzKmydI8sR3uk4CO.json b/src/packs/ancestries/ancestry_Firbolg_hzKmydI8sR3uk4CO.json index 0722517e..b98473ac 100644 --- a/src/packs/ancestries/ancestry_Firbolg_hzKmydI8sR3uk4CO.json +++ b/src/packs/ancestries/ancestry_Firbolg_hzKmydI8sR3uk4CO.json @@ -4,7 +4,7 @@ "type": "ancestry", "folder": null, "system": { - "description": "

Firbolgs are bovine humanoids typically recognized by their broad noses and long, drooping ears. Some have faces that are a blend of humanoid and bison, ox, cow, or other bovine creatures. Others, often referred to as minotaurs, have heads that entirely resemble cattle. They are tall and muscular creatures, with heights ranging from around 5 feet to 7 feet, and possess remarkable strength no matter their age. Some firbolgs are known to use this strength to charge their adversaries, an action that is particuarly effective for those who have one of the many varieties of horn styles commonly found in this ancestry. Though their unique characteristics can vary, all firbolgs are covered in fur, which can be muted and earth-toned in color, or come in a variety of pastels, such as soft pinks and blues. On average, firbolgs live for about 150 years.

ANCESTRY FEATURES

Charge: When you succeed on an Agility Roll to move from Far or Very Far range into Melee range with one or more targets, you can mark a Stress to deal 1d12 physical damage to all targets within Melee range.

Unshakable: When you would mark a Stress, roll a d6. On a result of 6, don’t mark it.

", + "description": "

Firbolgs are bovine humanoids typically recognized by their broad noses and long, drooping ears. Some have faces that are a blend of humanoid and bison, ox, cow, or other bovine creatures. Others, often referred to as minotaurs, have heads that entirely resemble cattle. They are tall and muscular creatures, with heights ranging from around 5 feet to 7 feet, and possess remarkable strength no matter their age. Some firbolgs are known to use this strength to charge their adversaries, an action that is particuarly effective for those who have one of the many varieties of horn styles commonly found in this ancestry. Though their unique characteristics can vary, all firbolgs are covered in fur, which can be muted and earth-toned in color, or come in a variety of pastels, such as soft pinks and blues. On average, firbolgs live for about 150 years.

", "features": [ { "type": "primary", diff --git a/src/packs/ancestries/ancestry_Fungril_J1hX7nBBc5jQiHli.json b/src/packs/ancestries/ancestry_Fungril_J1hX7nBBc5jQiHli.json index 75e52c8c..32657c24 100644 --- a/src/packs/ancestries/ancestry_Fungril_J1hX7nBBc5jQiHli.json +++ b/src/packs/ancestries/ancestry_Fungril_J1hX7nBBc5jQiHli.json @@ -4,7 +4,7 @@ "type": "ancestry", "folder": null, "system": { - "description": "

Fungril resemble humanoid mushrooms. They can be either more humanoid or more fungal in appearance, and they come in an assortment of colors, from earth tones to bright reds, yellows, purples, and blues. Fungril display an incredible variety of bodies, faces, and limbs, as there’s no single common shape among them. Even their heights range from a tiny 2 feet tall to a staggering 7 feet tall. While the common lifespan of a fungril is about 300 years, some have been reported to live much longer. They can communicate nonverbally, and many members of this ancestry use a mycelial array to chemically exchange information with other fungril across long distances.

ANCESTRY FEATURES

Fungril Network: Make an Instinct Roll (12) to use your mycelial array to speak with others of your ancestry. On a success, you can communicate across any distance.

Death Connection: While touching a corpse that died recently, you can mark a Stress to extract one memory from the corpse related to a specific emotion or sensation of your choice.

", + "description": "

Fungril resemble humanoid mushrooms. They can be either more humanoid or more fungal in appearance, and they come in an assortment of colors, from earth tones to bright reds, yellows, purples, and blues. Fungril display an incredible variety of bodies, faces, and limbs, as there’s no single common shape among them. Even their heights range from a tiny 2 feet tall to a staggering 7 feet tall. While the common lifespan of a fungril is about 300 years, some have been reported to live much longer. They can communicate nonverbally, and many members of this ancestry use a mycelial array to chemically exchange information with other fungril across long distances.

", "features": [ { "type": "primary", diff --git a/src/packs/ancestries/ancestry_Galapa_eZNG5Iv0yfbHs5CO.json b/src/packs/ancestries/ancestry_Galapa_eZNG5Iv0yfbHs5CO.json index b15fed89..afb0d5e2 100644 --- a/src/packs/ancestries/ancestry_Galapa_eZNG5Iv0yfbHs5CO.json +++ b/src/packs/ancestries/ancestry_Galapa_eZNG5Iv0yfbHs5CO.json @@ -4,7 +4,7 @@ "type": "ancestry", "folder": null, "system": { - "description": "

Galapa resemble anthropomorphic turtles with large, domed shells into which they can retract. On average, they range from 4 feet to 6 feet in height, and their head and body shapes can resemble any type of turtle. Galapa come in a variety of earth tones—most often shades of green and brown— and possess unique patterns on their shells. Members of this ancestry can draw their head, arms, and legs into their shell for protection to use it as a natural shield when defensive measures are needed. Some supplement their shell's strength or appearance by attaching armor or carving unique designs, but the process is exceedingly painful. Most galapa move slowly no matter their age, and they can live approximately 150 years.

ANCESTRY FEATURES

Shell: Gain a bonus to your damage thresholds equal to your Proficiency.

Retract: Mark a Stress to retract into your shell. While in your shell, you have resistance to physical damage, you have disadvantage on action rolls, and you can’t move.

", + "description": "

Galapa resemble anthropomorphic turtles with large, domed shells into which they can retract. On average, they range from 4 feet to 6 feet in height, and their head and body shapes can resemble any type of turtle. Galapa come in a variety of earth tones—most often shades of green and brown— and possess unique patterns on their shells. Members of this ancestry can draw their head, arms, and legs into their shell for protection to use it as a natural shield when defensive measures are needed. Some supplement their shell's strength or appearance by attaching armor or carving unique designs, but the process is exceedingly painful. Most galapa move slowly no matter their age, and they can live approximately 150 years.

", "features": [ { "type": "primary", diff --git a/src/packs/ancestries/ancestry_Giant_3U8CncG92a7ERIJ0.json b/src/packs/ancestries/ancestry_Giant_3U8CncG92a7ERIJ0.json index 2c0031dd..1ce5acfd 100644 --- a/src/packs/ancestries/ancestry_Giant_3U8CncG92a7ERIJ0.json +++ b/src/packs/ancestries/ancestry_Giant_3U8CncG92a7ERIJ0.json @@ -4,7 +4,7 @@ "type": "ancestry", "folder": null, "system": { - "description": "

Giants are towering humanoids with broad shoulders, long arms, and one to three eyes. Adult giants range from 6 ½ to 8 ½ feet tall and are naturally muscular, regardless of body type. They are easily recognized by their wide frames and elongated arms and necks. Though they can have up to three eyes, all giants are born with none and remain sightless for their first year of life. Until a giant reaches the age of 10 and their features fully develop, the formation of their eyes may fluctuate. Those with a single eye are commonly known as cyclops. The average giant lifespan is about 75 years.

ANCESTRY FEATURES

Endurance: Gain an additional Hit Point slot at character creation.

Reach: Treat any weapon, ability, spell, or other feature that has a Melee range as though it has a Very Close range instead.

", + "description": "

Giants are towering humanoids with broad shoulders, long arms, and one to three eyes. Adult giants range from 6 ½ to 8 ½ feet tall and are naturally muscular, regardless of body type. They are easily recognized by their wide frames and elongated arms and necks. Though they can have up to three eyes, all giants are born with none and remain sightless for their first year of life. Until a giant reaches the age of 10 and their features fully develop, the formation of their eyes may fluctuate. Those with a single eye are commonly known as cyclops. The average giant lifespan is about 75 years.

", "features": [ { "type": "primary", diff --git a/src/packs/ancestries/ancestry_Goblin_EKPEdIz9lA9grPqH.json b/src/packs/ancestries/ancestry_Goblin_EKPEdIz9lA9grPqH.json index 7b292343..fcc6eea3 100644 --- a/src/packs/ancestries/ancestry_Goblin_EKPEdIz9lA9grPqH.json +++ b/src/packs/ancestries/ancestry_Goblin_EKPEdIz9lA9grPqH.json @@ -4,7 +4,7 @@ "type": "ancestry", "folder": null, "system": { - "description": "

Goblins are small humanoids easily recognizable by their large eyes and massive membranous ears. With keen hearing and sharp eyesight, they perceive details both at great distances and in darkness, allowing them to move through less-optimal environments with ease. Their skin and eye colors are incredibly varied, with no one hue, either vibrant or subdued, more dominant than another. A typical goblin stands between 3 feet and 4 feet tall, and each of their ears is about the size of their head. Goblins are known to use ear positions to very specific effect when communicating nonverbally. A goblin’s lifespan is roughly 100 years, and many maintain their keen hearing and sight well into advanced age.

ANCESTRY FEATURES

Surefooted: You ignore disadvantage on Agility Rolls.

Danger Sense: Once per rest, mark a Stress to force an adversary to reroll an attack against you or an ally within Very Close range.

", + "description": "

Goblins are small humanoids easily recognizable by their large eyes and massive membranous ears. With keen hearing and sharp eyesight, they perceive details both at great distances and in darkness, allowing them to move through less-optimal environments with ease. Their skin and eye colors are incredibly varied, with no one hue, either vibrant or subdued, more dominant than another. A typical goblin stands between 3 feet and 4 feet tall, and each of their ears is about the size of their head. Goblins are known to use ear positions to very specific effect when communicating nonverbally. A goblin’s lifespan is roughly 100 years, and many maintain their keen hearing and sight well into advanced age.

", "features": [ { "type": "primary", diff --git a/src/packs/ancestries/ancestry_Halfling_CtL2jDjvPOJxNJKm.json b/src/packs/ancestries/ancestry_Halfling_CtL2jDjvPOJxNJKm.json index 4c694f1a..e70184d4 100644 --- a/src/packs/ancestries/ancestry_Halfling_CtL2jDjvPOJxNJKm.json +++ b/src/packs/ancestries/ancestry_Halfling_CtL2jDjvPOJxNJKm.json @@ -4,7 +4,7 @@ "type": "ancestry", "folder": null, "system": { - "description": "

Halflings are small humanoids with large hairy feet and prominent rounded ears. On average, halflings are 3 to 4 feet in height, and their ears, nose, and feet are larger in proportion to the rest of their body. Members of this ancestry live for around 150 years, and a halfling’s appearance is likely to remain youthful even as they progress from adulthood into old age. Halflings are naturally attuned to the magnetic fields of the Mortal Realm, granting them a strong internal compass. They also possess acute senses of hearing and smell, and can often detect those who are familiar to them by the sound of their movements.

ANCESTRY FEATURES

Luckbringer: At the start of each session, everyone in your party gains a Hope.

Internal Compass: When you roll a 1 on your Hope Die, you can reroll it.

", + "description": "

Halflings are small humanoids with large hairy feet and prominent rounded ears. On average, halflings are 3 to 4 feet in height, and their ears, nose, and feet are larger in proportion to the rest of their body. Members of this ancestry live for around 150 years, and a halfling’s appearance is likely to remain youthful even as they progress from adulthood into old age. Halflings are naturally attuned to the magnetic fields of the Mortal Realm, granting them a strong internal compass. They also possess acute senses of hearing and smell, and can often detect those who are familiar to them by the sound of their movements.

", "features": [ { "type": "primary", diff --git a/src/packs/ancestries/ancestry_Human_wtJ5V5qRppLQn61n.json b/src/packs/ancestries/ancestry_Human_wtJ5V5qRppLQn61n.json index 553b2168..d4575e91 100644 --- a/src/packs/ancestries/ancestry_Human_wtJ5V5qRppLQn61n.json +++ b/src/packs/ancestries/ancestry_Human_wtJ5V5qRppLQn61n.json @@ -4,7 +4,7 @@ "type": "ancestry", "folder": null, "system": { - "description": "

Humans are most easily recognized by their dexterous hands, rounded ears, and bodies built for endurance. Their average height ranges from just under 5 feet to about 6 ½ feet. They have a wide variety of builds, with some being quite broad, others lithe, and many inhabiting the spectrum in between. Humans are physically adaptable and adjust to harsh climates with relative ease. In general, humans live to an age of about 100, with their bodies changing dramatically between their youngest and oldest years.

ANCESTRY FEATURES

High Stamina: Gain an additional Stress slot at character creation.

Adaptability: When you fail a roll that utilized one of your Experiences, you can mark a Stress to reroll.

", + "description": "

Humans are most easily recognized by their dexterous hands, rounded ears, and bodies built for endurance. Their average height ranges from just under 5 feet to about 6 ½ feet. They have a wide variety of builds, with some being quite broad, others lithe, and many inhabiting the spectrum in between. Humans are physically adaptable and adjust to harsh climates with relative ease. In general, humans live to an age of about 100, with their bodies changing dramatically between their youngest and oldest years.

", "features": [ { "type": "primary", diff --git a/src/packs/ancestries/ancestry_Infernis_hyxcuF2I0xcZSGkm.json b/src/packs/ancestries/ancestry_Infernis_hyxcuF2I0xcZSGkm.json index d5688721..6dc43b66 100644 --- a/src/packs/ancestries/ancestry_Infernis_hyxcuF2I0xcZSGkm.json +++ b/src/packs/ancestries/ancestry_Infernis_hyxcuF2I0xcZSGkm.json @@ -4,7 +4,7 @@ "type": "ancestry", "folder": null, "system": { - "description": "

Infernis are humanoids who possess sharp canine teeth, pointed ears, and horns. They are the descendants of demons from the Circles Below. On average, infernis range in height from 5 feet to 7 feet and are known to have long fingers and pointed nails. Some have long, thin, and smooth tails that end in points, forks, or arrowheads. It’s common for infernis to have two or four horns—though some have crowns of many horns, or only one. These horns can also grow asymmetrically, forming unique, often curving, shapes that infernis enhance with carving and ornamentation. Their skin, hair, and horns come in an assortment of colors that can include soft pastels, stark tones, or vibrant hues, such as rosy scarlet, deep purple, and pitch black.

Infernis possess a “dread visage” that manifests both involuntarily, such as when they experience fear or other strong emotions, or purposefully, such as when they wish to intimidate an adversary. This visage can briefly modify their appearance in a variety of ways, including lengthening their teeth and nails, changing the colors of their eyes, twisting their horns, or enhancing their height. On average, infernis live up to 350 years, with some attributing this lifespan to their demonic lineage.

ANCESTRY FEATURES

Fearless: When you roll with Fear, you can mark 2 Stress to change it into a roll with Hope instead.

Dread Visage: You have advantage on rolls to intimidate hostile creatures.

", + "description": "

Infernis are humanoids who possess sharp canine teeth, pointed ears, and horns. They are the descendants of demons from the Circles Below. On average, infernis range in height from 5 feet to 7 feet and are known to have long fingers and pointed nails. Some have long, thin, and smooth tails that end in points, forks, or arrowheads. It’s common for infernis to have two or four horns—though some have crowns of many horns, or only one. These horns can also grow asymmetrically, forming unique, often curving, shapes that infernis enhance with carving and ornamentation. Their skin, hair, and horns come in an assortment of colors that can include soft pastels, stark tones, or vibrant hues, such as rosy scarlet, deep purple, and pitch black.

Infernis possess a “dread visage” that manifests both involuntarily, such as when they experience fear or other strong emotions, or purposefully, such as when they wish to intimidate an adversary. This visage can briefly modify their appearance in a variety of ways, including lengthening their teeth and nails, changing the colors of their eyes, twisting their horns, or enhancing their height. On average, infernis live up to 350 years, with some attributing this lifespan to their demonic lineage.

", "features": [ { "type": "primary", diff --git a/src/packs/ancestries/ancestry_Katari_yyW0UM8srD9WuwW7.json b/src/packs/ancestries/ancestry_Katari_yyW0UM8srD9WuwW7.json index 7849f7ec..47d6c1b9 100644 --- a/src/packs/ancestries/ancestry_Katari_yyW0UM8srD9WuwW7.json +++ b/src/packs/ancestries/ancestry_Katari_yyW0UM8srD9WuwW7.json @@ -4,7 +4,7 @@ "type": "ancestry", "folder": null, "system": { - "description": "

Katari are feline humanoids with retractable claws, vertically slit pupils, and high, triangular ears. They can also have small, pointed canine teeth, soft fur, and long whiskers that assist their perception and navigation. Their ears can swivel nearly 180 degrees to detect sound, adding to their heightened senses. Katari may look more or less feline or humanoid, with catlike attributes in the form of hair, whiskers, and a muzzle. About half of the katari population have tails. Their skin and fur come in a wide range of hues and patterns, including solid colors, calico tones, tabby stripes, and an array of spots, patches, marbling, or bands. Their height ranges from about 3 feet to 6 ½ feet, and they live to around 150 years.

ANCESTRY FEATURES

Feline Instincts: When you make an Agility Roll, you can spend 2 Hope to reroll your Hope Die.

Retracting Claws: Make an Agility Roll to scratch a target within Melee range. On a success, they become temporarily Vulnerable.

", + "description": "

Katari are feline humanoids with retractable claws, vertically slit pupils, and high, triangular ears. They can also have small, pointed canine teeth, soft fur, and long whiskers that assist their perception and navigation. Their ears can swivel nearly 180 degrees to detect sound, adding to their heightened senses. Katari may look more or less feline or humanoid, with catlike attributes in the form of hair, whiskers, and a muzzle. About half of the katari population have tails. Their skin and fur come in a wide range of hues and patterns, including solid colors, calico tones, tabby stripes, and an array of spots, patches, marbling, or bands. Their height ranges from about 3 feet to 6 ½ feet, and they live to around 150 years.

", "features": [ { "type": "primary", diff --git a/src/packs/ancestries/ancestry_Orc_D1RbUsRV9HpTrPuF.json b/src/packs/ancestries/ancestry_Orc_D1RbUsRV9HpTrPuF.json index 264d3da3..fdb60eb5 100644 --- a/src/packs/ancestries/ancestry_Orc_D1RbUsRV9HpTrPuF.json +++ b/src/packs/ancestries/ancestry_Orc_D1RbUsRV9HpTrPuF.json @@ -4,7 +4,7 @@ "type": "ancestry", "folder": null, "system": { - "description": "

Orcs are humanoids most easily recognized by their square features and boar-like tusks that protrude from their lower jaw. Tusks come in various sizes, and though they extend from the mouth, they aren’t used for consuming food. Instead, many orcs choose to decorate their tusks with significant ornamentation. Orcs typically live for 125 years, and unless altered, their tusks continue to grow throughout the course of their lives. Their ears are pointed, and their hair and skin typically have green, blue, pink, or gray tones. Orcs tend toward a muscular build, and their average height ranges from 5 feet to 6 ½ feet.

ANCESTRY FEATURES

Sturdy: When you have 1 Hit Point remaining, attacks against you have disadvantage.

Tusks: When you succeed on an attack against a target within Melee range, you can spend a Hope to gore the target with your tusks, dealing an extra 1d6 damage.

", + "description": "

Orcs are humanoids most easily recognized by their square features and boar-like tusks that protrude from their lower jaw. Tusks come in various sizes, and though they extend from the mouth, they aren’t used for consuming food. Instead, many orcs choose to decorate their tusks with significant ornamentation. Orcs typically live for 125 years, and unless altered, their tusks continue to grow throughout the course of their lives. Their ears are pointed, and their hair and skin typically have green, blue, pink, or gray tones. Orcs tend toward a muscular build, and their average height ranges from 5 feet to 6 ½ feet.

", "features": [ { "type": "primary", diff --git a/src/packs/ancestries/ancestry_Ribbet_HwOoBKXOL9Tf5j85.json b/src/packs/ancestries/ancestry_Ribbet_HwOoBKXOL9Tf5j85.json index 48400eaf..3ff1549d 100644 --- a/src/packs/ancestries/ancestry_Ribbet_HwOoBKXOL9Tf5j85.json +++ b/src/packs/ancestries/ancestry_Ribbet_HwOoBKXOL9Tf5j85.json @@ -4,7 +4,7 @@ "type": "ancestry", "folder": null, "system": { - "description": "

Ribbets resemble anthropomorphic frogs with protruding eyes and webbed hands and feet. They have smooth (though sometimes warty) moist skin and eyes positioned on either side of their head. Some ribbets have hind legs more than twice the length of their torso, while others have short limbs. No matter their size (which ranges from about 3 feet to 4 ½ feet), ribbets primarily move by hopping. All ribbets have webbed appendages, allowing them to swim with ease. Some ribbets possess a natural green-and-brown camouflage, while others are vibrantly colored with bold patterns. No matter their appearance, all ribbets are born from eggs laid in the water, hatch into tadpoles, and after about 6 to 7 years, grow into amphibians that can move around on land. Ribbets live for approximately 100 years.

ANCESTRY FEATURES

Amphibious: You can breathe and move naturally underwater.

Long Tongue: You can use your long tongue to grab onto things within Close range. Mark a Stress to use your tongue as a Finesse Close weapon that deals d12 physical damage using your Proficiency.

", + "description": "

Ribbets resemble anthropomorphic frogs with protruding eyes and webbed hands and feet. They have smooth (though sometimes warty) moist skin and eyes positioned on either side of their head. Some ribbets have hind legs more than twice the length of their torso, while others have short limbs. No matter their size (which ranges from about 3 feet to 4 ½ feet), ribbets primarily move by hopping. All ribbets have webbed appendages, allowing them to swim with ease. Some ribbets possess a natural green-and-brown camouflage, while others are vibrantly colored with bold patterns. No matter their appearance, all ribbets are born from eggs laid in the water, hatch into tadpoles, and after about 6 to 7 years, grow into amphibians that can move around on land. Ribbets live for approximately 100 years.

", "features": [ { "type": "primary", diff --git a/src/packs/ancestries/ancestry_Simiah_2yMLxxn7CHEvmShj.json b/src/packs/ancestries/ancestry_Simiah_2yMLxxn7CHEvmShj.json index d3f77f36..995e0c95 100644 --- a/src/packs/ancestries/ancestry_Simiah_2yMLxxn7CHEvmShj.json +++ b/src/packs/ancestries/ancestry_Simiah_2yMLxxn7CHEvmShj.json @@ -4,7 +4,7 @@ "type": "ancestry", "folder": null, "system": { - "description": "

Simiah resemble anthropomorphic monkeys and apes with long limbs and prehensile feet. While their appearance reflects all simian creatures, from the largest gorilla to the smallest marmoset, their size does not align with their animal counterparts, and they can be anywhere from 2 to 6 feet tall. All simiah can use their dexterous feet for nonverbal communication, work, and combat. Additionally, some also have prehensile tails that can grasp objects or help with balance during difficult maneuvers. These traits grant members of this ancestry unique agility that aids them in a variety of physical tasks. In particular, simiah are skilled climbers and can easily transition from bipedal movement to knuckle-walking and climbing, and back again. On average, simiah live for about 100 years.

ANCESTRY FEATURES

Natural Climber: You have advantage on Agility Rolls that involve balancing and climbing.

Nimble: Gain a permanent +1 bonus to your Evasion at character creation.

", + "description": "

Simiah resemble anthropomorphic monkeys and apes with long limbs and prehensile feet. While their appearance reflects all simian creatures, from the largest gorilla to the smallest marmoset, their size does not align with their animal counterparts, and they can be anywhere from 2 to 6 feet tall. All simiah can use their dexterous feet for nonverbal communication, work, and combat. Additionally, some also have prehensile tails that can grasp objects or help with balance during difficult maneuvers. These traits grant members of this ancestry unique agility that aids them in a variety of physical tasks. In particular, simiah are skilled climbers and can easily transition from bipedal movement to knuckle-walking and climbing, and back again. On average, simiah live for about 100 years.

", "features": [ { "type": "primary", diff --git a/src/packs/classes/class_Bard_vegl3bFOq3pcFTWT.json b/src/packs/classes/class_Bard_vegl3bFOq3pcFTWT.json index b596d5c0..c4dd83a7 100644 --- a/src/packs/classes/class_Bard_vegl3bFOq3pcFTWT.json +++ b/src/packs/classes/class_Bard_vegl3bFOq3pcFTWT.json @@ -4,7 +4,7 @@ "type": "class", "img": "icons/tools/instruments/harp-red.webp", "system": { - "description": "

Note: At level 5 use Rally (Level 5) instead of Rally under class feature. (Automation will be implemented in a later release)


Bards are the most charismatic people in all the realms. Members of this class are masters of captivation and specialize in a variety of performance types, including singing, playing musical instruments, weaving tales, or telling jokes. Whether performing for an audience or speaking to an individual, bards thrive in social situations. Members of this profession bond and train at schools or guilds, but a current of egotism runs through those of the bardic persuasion. While they may be the most likely class to bring people together, a bard of ill temper can just as easily tear a party apart.

CLASS ITEMS

A romance novel or a letter never opened

BARD’S HOPE FEATURE

Make a Scene: Spend 3 Hope to temporarily Distract a target within Close range, giving them a -2 penalty to their Difficulty.

CLASS FEATURE

Rally

Once per session, describe how you rally the party and give yourself and each of your allies a Rally Die. At level 1, your Rally Die is a d6. A PC can spend their Rally Die to roll it, adding the result to their action roll, reaction roll, damage roll, or to clear a number of Stress equal to the result. At the end of each session, clear all unspent Rally Dice.

At level 5, your Rally Die increases to a d8.

", + "description": "

Note: At level 5 use Rally (Level 5) instead of Rally under class feature. (Automation will be implemented in a later release)


Bards are the most charismatic people in all the realms. Members of this class are masters of captivation and specialize in a variety of performance types, including singing, playing musical instruments, weaving tales, or telling jokes. Whether performing for an audience or speaking to an individual, bards thrive in social situations. Members of this profession bond and train at schools or guilds, but a current of egotism runs through those of the bardic persuasion. While they may be the most likely class to bring people together, a bard of ill temper can just as easily tear a party apart.

", "domains": [ "grace", "codex" @@ -20,10 +20,6 @@ { "type": "class", "item": "Compendium.daggerheart.classes.Item.PydiMnNCKpd44SGS" - }, - { - "type": "class", - "item": "Compendium.daggerheart.classes.Item.TVeEyqmPPiRa2r3i" } ], "subclasses": [ diff --git a/src/packs/classes/class_Druid_ZNwUTCyGCEcidZFv.json b/src/packs/classes/class_Druid_ZNwUTCyGCEcidZFv.json index 09deb8e6..c6ccaf53 100644 --- a/src/packs/classes/class_Druid_ZNwUTCyGCEcidZFv.json +++ b/src/packs/classes/class_Druid_ZNwUTCyGCEcidZFv.json @@ -4,7 +4,7 @@ "_id": "ZNwUTCyGCEcidZFv", "img": "icons/creatures/mammals/wolf-howl-moon-black.webp", "system": { - "description": "

Becoming a druid is more than an occupation; it’s a calling for those who wish to learn from and protect the magic of the wilderness. While one might underestimate a gentle druid who practices the often-quiet work of cultivating flora, druids who channel the untamed forces of nature are terrifying to behold. Druids cultivate their abilities in small groups, often connected by a specific ethos or locale, but some choose to work alone. Through years of study and dedication, druids can learn to transform into beasts and shape nature itself.

CLASS ITEMS

A small bag of rocks and bones or a strange pendant found in the dirt

DRUID’S HOPE FEATURE

Evolution: Spend 3 Hope to transform into a Beastform without marking a Stress. When you do, choose one trait to raise by +1 until you drop out of that Beastform.

CLASS FEATURES

Beastform: Mark a Stress to magically transform into a creature of your tier or lower from the Beastform list. You can drop out of this form at any time. While transformed, you can’t use weapons or cast spells from domain cards, but you can still use other features or abilities you have access to. Spells you cast before you transform stay active and last for their normal duration, and you can talk and communicate as normal. Additionally, you gain the Beastform’s features, add their Evasion bonus to your Evasion, and use the trait specified in their statistics for your attack. While you’re in a Beastform, your armor becomes part of your body and you mark Armor Slots as usual; when you drop out of a Beastform, those marked Armor Slots remain marked. If you mark your last Hit Point, you automatically drop out of this form.

Wildtouch: You can perform harmless, subtle effects that involve nature—such as causing a flower to rapidly grow, summoning a slight gust of wind, or starting a campfire—at will.

", + "description": "

Becoming a druid is more than an occupation; it’s a calling for those who wish to learn from and protect the magic of the wilderness. While one might underestimate a gentle druid who practices the often-quiet work of cultivating flora, druids who channel the untamed forces of nature are terrifying to behold. Druids cultivate their abilities in small groups, often connected by a specific ethos or locale, but some choose to work alone. Through years of study and dedication, druids can learn to transform into beasts and shape nature itself.

", "domains": [ "sage", "arcana" diff --git a/src/packs/classes/class_Guardian_nRAyoC0fOzXPDa4z.json b/src/packs/classes/class_Guardian_nRAyoC0fOzXPDa4z.json index eec4ba5a..8293ecd5 100644 --- a/src/packs/classes/class_Guardian_nRAyoC0fOzXPDa4z.json +++ b/src/packs/classes/class_Guardian_nRAyoC0fOzXPDa4z.json @@ -4,7 +4,7 @@ "_id": "nRAyoC0fOzXPDa4z", "img": "icons/equipment/shield/heater-wooden-sword-green.webp", "system": { - "description": "

The title of guardian represents an array of martial professions, speaking more to their moral compass and unshakeable fortitude than the means by which they fight. While many guardians join groups of militants for either a country or cause, they’re more likely to follow those few they truly care for, majority be damned. Guardians are known for fighting with remarkable ferocity even against overwhelming odds, defending their cohort above all else. Woe betide those who harm the ally of a guardian, as the guardian will answer this injury in kind.

CLASS ITEMS

A totem from your mentor or a secret key

GUARDIAN’S HOPE FEATURE

Frontline Tank: Spend 3 Hope to clear 2 Armor Slots.

CLASS FEATURE

Unstoppable: Once per long rest, you can become Unstoppable. You gain an Unstoppable Die. At level 1, your Unstoppable Die is a d4. Place it on your character sheet in the space provided, starting with the 1 value facing up. After you make a damage roll that deals 1 or more Hit Points to a target, increase the Unstoppable Die value by one. When the die’s value would exceed its maximum value or when the scene ends, remove the die and drop out of Unstoppable. At level 5, your Unstoppable Die increases to a d6.

While Unstoppable, you gain the following benefits:

• You reduce the severity of physical damage by one threshold (Severe to Major, Major to Minor, Minor to None).

• You add the current value of the Unstoppable Die to your damage roll.

• You can’t be Restrained or Vulnerable.

", + "description": "

The title of guardian represents an array of martial professions, speaking more to their moral compass and unshakeable fortitude than the means by which they fight. While many guardians join groups of militants for either a country or cause, they’re more likely to follow those few they truly care for, majority be damned. Guardians are known for fighting with remarkable ferocity even against overwhelming odds, defending their cohort above all else. Woe betide those who harm the ally of a guardian, as the guardian will answer this injury in kind.

", "domains": [ "valor", "blade" diff --git a/src/packs/classes/class_Ranger_BTyfve69LKqoOi9S.json b/src/packs/classes/class_Ranger_BTyfve69LKqoOi9S.json index a0e16bf9..2d459b7a 100644 --- a/src/packs/classes/class_Ranger_BTyfve69LKqoOi9S.json +++ b/src/packs/classes/class_Ranger_BTyfve69LKqoOi9S.json @@ -4,7 +4,7 @@ "_id": "BTyfve69LKqoOi9S", "img": "icons/weapons/bows/shortbow-recurve-yellow-blue.webp", "system": { - "description": "

Rangers are highly skilled hunters who, despite their martial abilities, rarely lend their skills to an army. Through mastery of the body and a deep understanding of the wilderness, rangers become sly tacticians, pursuing their quarry with cunning and patience. Many rangers track and fight alongside an animal companion with whom they’ve forged a powerful spiritual bond. By honing their skills in the wild, rangers become expert trackers, as likely to ensnare their foes in a trap as they are to assail them head-on.

CLASS ITEMS

A trophy from your first kill or a seemingly broken compass

RANGER’S HOPE FEATURE

Hold Them Off: Spend 3 Hope when you succeed on an attack with a weapon to use that same roll against two additional adversaries within range of the attack.

CLASS FEATURE

Ranger’s Focus: Spend a Hope and make an attack against a target. On a success, deal your attack’s normal damage and temporarily make the attack’s target your Focus. Until this feature ends or you make a different creature your Focus, you gain the following benefits against your Focus:

• You know precisely what direction they are in.

• When you deal damage to them, they must mark a Stress.

• When you fail an attack against them, you can end your Ranger’s Focus feature to reroll your Duality Dice.

", + "description": "

Rangers are highly skilled hunters who, despite their martial abilities, rarely lend their skills to an army. Through mastery of the body and a deep understanding of the wilderness, rangers become sly tacticians, pursuing their quarry with cunning and patience. Many rangers track and fight alongside an animal companion with whom they’ve forged a powerful spiritual bond. By honing their skills in the wild, rangers become expert trackers, as likely to ensnare their foes in a trap as they are to assail them head-on.

", "domains": [ "bone", "sage" diff --git a/src/packs/classes/class_Rogue_CvHlkHZfpMiCz5uT.json b/src/packs/classes/class_Rogue_CvHlkHZfpMiCz5uT.json index 64602794..f4b267ae 100644 --- a/src/packs/classes/class_Rogue_CvHlkHZfpMiCz5uT.json +++ b/src/packs/classes/class_Rogue_CvHlkHZfpMiCz5uT.json @@ -4,7 +4,7 @@ "_id": "CvHlkHZfpMiCz5uT", "img": "icons/magic/defensive/shield-barrier-blades-teal.webp", "system": { - "description": "

Rogues are scoundrels, often in both attitude and practice. Broadly known as liars and thieves, the best among this class move through the world anonymously. Utilizing their sharp wits and blades, rogues trick their foes through social manipulation as easily as breaking locks, climbing through windows, or dealing underhanded blows. These masters of magical craft manipulate shadow and movement, adding an array of useful and deadly tools to their repertoire. Rogues frequently establish guilds to meet future accomplices, hire out jobs, and hone secret skills, proving that there’s honor among thieves for those who know where to look.

CLASS ITEMS

A set of forgery tools or a grappling hook

ROGUE’S HOPE FEATURE

Rogue’s Dodge: Spend 3 Hope to gain a +2 bonus to your Evasion until the next time an attack succeeds against you. Otherwise, this bonus lasts until your next rest.

CLASS FEATURES

Cloaked: Any time you would be Hidden, you are instead Cloaked. In addition to the benefits of the Hidden condition, while Cloaked you remain unseen if you are stationary when an adversary moves to where they would normally see you.

After you make an attack or end a move within line of sight of an adversary, you are no longer Cloaked.

Sneak Attack: When you succeed on an attack while Cloaked or while an ally is within Melee range of your target, add a number of d6s equal to your tier to your damage roll.

", + "description": "

Rogues are scoundrels, often in both attitude and practice. Broadly known as liars and thieves, the best among this class move through the world anonymously. Utilizing their sharp wits and blades, rogues trick their foes through social manipulation as easily as breaking locks, climbing through windows, or dealing underhanded blows. These masters of magical craft manipulate shadow and movement, adding an array of useful and deadly tools to their repertoire. Rogues frequently establish guilds to meet future accomplices, hire out jobs, and hone secret skills, proving that there’s honor among thieves for those who know where to look.

", "domains": [ "midnight", "grace" diff --git a/src/packs/classes/class_Seraph_5ZnlJ5bEoyOTkUJv.json b/src/packs/classes/class_Seraph_5ZnlJ5bEoyOTkUJv.json index 479aa70d..524d2d85 100644 --- a/src/packs/classes/class_Seraph_5ZnlJ5bEoyOTkUJv.json +++ b/src/packs/classes/class_Seraph_5ZnlJ5bEoyOTkUJv.json @@ -4,7 +4,7 @@ "_id": "5ZnlJ5bEoyOTkUJv", "img": "icons/magic/holy/barrier-shield-winged-cross.webp", "system": { - "description": "

Seraphs are divine fighters and healers imbued with sacred purpose. A wide array of deities exist within the realms, and thus numerous kinds of seraphs are appointed by these gods. Their ethos traditionally aligns with the domain or goals of their god, such as defending the weak, exacting vengeance, protecting a land or artifact, or upholding a particular faith. Some seraphs ally themselves with an army or locale, much to the satisfaction of their rulers, but other crusaders fight in opposition to the follies of the Mortal Realm. It is better to be a seraph’s ally than their enemy, as they are terrifying foes to those who defy their purpose.

CLASS ITEMS

A bundle of offerings or a sigil of your god

SERAPH’S HOPE FEATURE

Life Support: Spend 3 Hope to clear a Hit Point on an ally within Close range.

CLASS FEATURE

Prayer Dice: At the beginning of each session, roll a number of d4s equal to your subclass’s Spellcast trait and place them on your character sheet in the space provided. These are your Prayer Dice. You can spend any number of Prayer Dice to aid yourself or an ally within Far range. You can use a spent die’s value to reduce incoming damage, add to a roll’s result after the roll is made, or gain Hope equal to the result. At the end of each session, clear all unspent Prayer Dice.

", + "description": "

Seraphs are divine fighters and healers imbued with sacred purpose. A wide array of deities exist within the realms, and thus numerous kinds of seraphs are appointed by these gods. Their ethos traditionally aligns with the domain or goals of their god, such as defending the weak, exacting vengeance, protecting a land or artifact, or upholding a particular faith. Some seraphs ally themselves with an army or locale, much to the satisfaction of their rulers, but other crusaders fight in opposition to the follies of the Mortal Realm. It is better to be a seraph’s ally than their enemy, as they are terrifying foes to those who defy their purpose.

", "domains": [ "valor", "splendor" diff --git a/src/packs/classes/class_Sorcerer_DchOzHcWIJE9FKcR.json b/src/packs/classes/class_Sorcerer_DchOzHcWIJE9FKcR.json index c2780c92..c80bf31e 100644 --- a/src/packs/classes/class_Sorcerer_DchOzHcWIJE9FKcR.json +++ b/src/packs/classes/class_Sorcerer_DchOzHcWIJE9FKcR.json @@ -4,7 +4,7 @@ "_id": "DchOzHcWIJE9FKcR", "img": "icons/magic/symbols/rune-sigil-horned-white-purple.webp", "system": { - "description": "

Not all innate magic users choose to hone their craft, but those who do can become powerful sorcerers. The gifts of these wielders are passed down through families, even if the family is unaware of or reluctant to practice them. A sorcerer’s abilities can range from the elemental to the illusionary and beyond, and many practitioners band together into collectives based on their talents. The act of becoming a formidable sorcerer is not the practice of acquiring power, but learning to cultivate and control the power one already possesses. The magic of a misguided or undisciplined sorcerer is a dangerous force indeed.

CLASS ITEMS

A whispering orb or a family heirloom

SORCERER’S HOPE FEATURE

Volatile Magic: Spend 3 Hope to reroll any number of your damage dice on an attack that deals magic damage.

CLASS FEATURES

Arcane Sense: You can sense the presence of magical people and objects within Close range.

Minor Illusion: Make a Spellcast Roll (10). On a success, you create a minor visual illusion no larger than yourself

within Close range. This illusion is convincing to anyone at Close range or farther.

Channel Raw Power: Once per long rest, you can place a domain card from your loadout into your vault and choose to either:

• Gain Hope equal to the level of the card.

• Enhance a spell that deals damage, gaining a bonus to your damage roll equal to twice the level of the card.

", + "description": "

Not all innate magic users choose to hone their craft, but those who do can become powerful sorcerers. The gifts of these wielders are passed down through families, even if the family is unaware of or reluctant to practice them. A sorcerer’s abilities can range from the elemental to the illusionary and beyond, and many practitioners band together into collectives based on their talents. The act of becoming a formidable sorcerer is not the practice of acquiring power, but learning to cultivate and control the power one already possesses. The magic of a misguided or undisciplined sorcerer is a dangerous force indeed.

", "domains": [ "arcana", "midnight" diff --git a/src/packs/classes/class_Warrior_xCUWwJz4WSthvLfy.json b/src/packs/classes/class_Warrior_xCUWwJz4WSthvLfy.json index 67ccade8..ce896ff6 100644 --- a/src/packs/classes/class_Warrior_xCUWwJz4WSthvLfy.json +++ b/src/packs/classes/class_Warrior_xCUWwJz4WSthvLfy.json @@ -4,7 +4,7 @@ "_id": "xCUWwJz4WSthvLfy", "img": "icons/weapons/swords/sword-broad-crystal-paired.webp", "system": { - "description": "

Becoming a warrior requires years, often a lifetime, of training and dedication to the mastery of weapons and violence. While many who seek to fight hone only their strength, warriors understand the importance of an agile body and mind, making them some of the most sought-after fighters across the realms. Frequently, warriors find employment within an army, a band of mercenaries, or even a royal guard, but their potential is wasted in any position where they cannot continue to improve and expand their skills. Warriors are known to have a favored weapon; to come between them and their blade would be a grievous mistake.

CLASS ITEMS

The drawing of a lover or a sharpening stone

WARRIOR’S HOPE FEATURE

No Mercy: Spend 3 Hope to gain a +1 bonus to your attack rolls until your next rest.

CLASS FEATURES

Attack of Opportunity: If an adversary within Melee range attempts to leave that range, make a reaction roll using a trait of your choice against their Difficulty. Choose one effect on a success, or two if you critically succeed:

• They can’t move from where they are.

• You deal damage to them equal to your primary weapon’s damage.

• You move with them.

Combat Training: You ignore burden when equipping weapons. When you deal physical damage, you gain a bonus to your damage roll equal to your level.

", + "description": "

Becoming a warrior requires years, often a lifetime, of training and dedication to the mastery of weapons and violence. While many who seek to fight hone only their strength, warriors understand the importance of an agile body and mind, making them some of the most sought-after fighters across the realms. Frequently, warriors find employment within an army, a band of mercenaries, or even a royal guard, but their potential is wasted in any position where they cannot continue to improve and expand their skills. Warriors are known to have a favored weapon; to come between them and their blade would be a grievous mistake.

", "domains": [ "blade", "bone" diff --git a/src/packs/classes/class_Wizard_5LwX4m8ziY3F1ZGC.json b/src/packs/classes/class_Wizard_5LwX4m8ziY3F1ZGC.json index 7257ea38..5d9928db 100644 --- a/src/packs/classes/class_Wizard_5LwX4m8ziY3F1ZGC.json +++ b/src/packs/classes/class_Wizard_5LwX4m8ziY3F1ZGC.json @@ -4,7 +4,7 @@ "_id": "5LwX4m8ziY3F1ZGC", "img": "icons/magic/symbols/circled-gem-pink.webp", "system": { - "description": "

Whether through an institution or individual study, those known as wizards acquire and hone immense magical power over years of learning using a variety of tools, including books, stones, potions, and herbs. Some wizards dedicate their lives to mastering a particular school of magic, while others learn from a wide variety of disciplines. Many wizards become wise and powerful figures in their communities, advising rulers, providing medicines and healing, and even leading war councils. While these mages all work toward the common goal of collecting magical knowledge, wizards often have the most conflict within their own ranks, as the acquisition, keeping, and sharing of powerful secrets is a topic of intense debate that has resulted in innumerable deaths.

CLASS ITEMS

A book you’re trying to translate or a tiny, harmless elemental pet

WIZARD’S HOPE FEATURE

Not This Time: Spend 3 Hope to force an adversary within Far range to reroll an attack or damage roll.

CLASS FEATURES

Prestidigitation: You can perform harmless, subtle magical effects at will. For example, you can change an object’s color, create a smell, light a candle, cause a tiny object to float, illuminate a room, or repair a small object.

Strange Patterns: Choose a number between 1 and 12. When you roll that number on a Duality Die, gain a Hope or clear a Stress.

You can change this number when you take a long rest.

", + "description": "

Whether through an institution or individual study, those known as wizards acquire and hone immense magical power over years of learning using a variety of tools, including books, stones, potions, and herbs. Some wizards dedicate their lives to mastering a particular school of magic, while others learn from a wide variety of disciplines. Many wizards become wise and powerful figures in their communities, advising rulers, providing medicines and healing, and even leading war councils. While these mages all work toward the common goal of collecting magical knowledge, wizards often have the most conflict within their own ranks, as the acquisition, keeping, and sharing of powerful secrets is a topic of intense debate that has resulted in innumerable deaths.

", "domains": [ "codex", "splendor" diff --git a/src/packs/classes/feature_Rally_PydiMnNCKpd44SGS.json b/src/packs/classes/feature_Rally_PydiMnNCKpd44SGS.json index e8d4c3c9..e2a0b5bb 100644 --- a/src/packs/classes/feature_Rally_PydiMnNCKpd44SGS.json +++ b/src/packs/classes/feature_Rally_PydiMnNCKpd44SGS.json @@ -63,7 +63,7 @@ { "key": "system.bonuses.rally", "mode": 2, - "value": "d6", + "value": "6 + min((floor(@system.levelData.level.current / 5)*2), 2)", "priority": null } ], diff --git a/src/packs/classes/feature_Rally__Level_5__TVeEyqmPPiRa2r3i.json b/src/packs/classes/feature_Rally__Level_5__TVeEyqmPPiRa2r3i.json deleted file mode 100644 index 46717fcb..00000000 --- a/src/packs/classes/feature_Rally__Level_5__TVeEyqmPPiRa2r3i.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "folder": "C9y59fIkq50d3SyD", - "name": "Rally (Level 5)", - "type": "feature", - "img": "icons/tools/instruments/drum-hand-tan.webp", - "system": { - "description": "

Once per session, describe how you rally the party and give yourself and each of your allies a Rally Die. At level 1, your Rally Die is a d6. A PC can spend their Rally Die to roll it, adding the result to their action roll, reaction roll, damage roll, or to clear a number of Stress equal to the result. At the end of each session, clear all unspent Rally Dice. At level 5, your Rally Die increases to a d8.

", - "resource": null, - "actions": { - "Z1KWFrpXOqZWuZD1": { - "type": "effect", - "_id": "Z1KWFrpXOqZWuZD1", - "systemPath": "actions", - "description": "", - "chatDisplay": true, - "actionType": "action", - "cost": [], - "uses": { - "value": null, - "max": "1", - "recovery": "session" - }, - "effects": [ - { - "_id": "8CFxYJV8zE6Wabwj", - "onSave": false - } - ], - "target": { - "type": "any", - "amount": null - }, - "name": "Rally your Allies", - "img": "icons/tools/instruments/drum-hand-tan.webp", - "range": "" - } - }, - "originItemType": null, - "originId": null, - "attribution": { - "source": "Daggerheart SRD", - "page": 9, - "artist": "" - } - }, - "effects": [ - { - "name": "Rally (Level 5)", - "img": "icons/tools/instruments/drum-hand-tan.webp", - "origin": "Compendium.daggerheart.classes.Item.oxv0m8AFUQVFKtZ4", - "transfer": false, - "_id": "8CFxYJV8zE6Wabwj", - "type": "base", - "system": { - "rangeDependence": { - "enabled": false, - "type": "withinRange", - "target": "hostile", - "range": "melee" - } - }, - "changes": [ - { - "key": "system.bonuses.rally", - "mode": 2, - "value": "d8", - "priority": null - } - ], - "disabled": false, - "duration": { - "startTime": null, - "combat": null, - "seconds": null, - "rounds": null, - "turns": null, - "startRound": null, - "startTurn": null - }, - "description": "", - "tint": "#ffffff", - "statuses": [], - "sort": 0, - "flags": {}, - "_stats": { - "compendiumSource": null - }, - "_key": "!items.effects!TVeEyqmPPiRa2r3i.8CFxYJV8zE6Wabwj" - } - ], - "flags": {}, - "ownership": { - "default": 0, - "LgnbNMLaxandgMQq": 3 - }, - "_id": "TVeEyqmPPiRa2r3i", - "sort": 300000, - "_key": "!items!TVeEyqmPPiRa2r3i" -} diff --git a/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json b/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json index 95f42c06..953b3a2c 100644 --- a/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json +++ b/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json @@ -85,7 +85,7 @@ { "trigger": "dualityRoll", "triggeringActorType": "self", - "command": "/* Ignore if it's a TagTeam roll */\nconst tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);\nif (tagTeam.members[actor.id]) return;\n\n/* Check if there's a Strange Pattern match */\nconst dice = [roll.dFear.total, roll.dHope.total];\nconst resource = this.parent.resource?.diceStates ? Object.values(this.parent.resource.diceStates).map(x => x.value)[0] : null;\nconst nrMatches = dice.filter(x => x === resource).length;\n\nif (!nrMatches) return;\n\n/* Create a dialog to choose Hope or Stress - or to cancel*/\nconst content = `\n
${game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContentTitle', { nr: nrMatches })}
\n
${game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContentSubTitle', { nr: nrMatches })}
\n
\n \n \n
\n
`;\n\nconst result = await foundry.applications.api.DialogV2.input({\n classes: ['dh-style', 'two-big-buttons'],\n window: { title: this.item.name },\n content: content,\n render: (_, dialog) => {\n const hopeButton = dialog.element.querySelector('#hopeButton');\n const stressButton = dialog.element.querySelector('#stressButton');\ndialog.element.querySelector('button[type=\"submit\"]').disabled = true;\n \n const updateFunc = (event, selector, adding, clamp) => {\n const button = event.target.closest(`#${selector}Button`);\n const parent = event.target.closest('.flexrow');\n const hope = Number.parseInt(parent.querySelector('#hopeButton label').innerHTML);\n const stress = Number.parseInt(parent.querySelector('#stressButton label').innerHTML);\n const currentTotal = (Number.isNumeric(hope) ? hope : 0) + (Number.isNumeric(stress) ? stress : 0);\n if (adding && currentTotal === nrMatches) return;\n \n const current = Number.parseInt(button.querySelector('label').innerHTML);\n if (!adding && current === 0) return;\n \n const value = Number.isNumeric(current) ? adding ? current+1 : current-1 : 1;\n if (!dialog.data) dialog.data = {};\n dialog.data[selector] = clamp(value);\n button.querySelector('label').innerHTML = dialog.data[selector];\n\n event.target.closest('.dialog-form').querySelector('button[type=\"submit\"]').disabled = !adding || currentTotal < (nrMatches-1);\n \n };\n hopeButton.addEventListener('click', event => updateFunc(event, 'hope', true, x => Math.min(x, nrMatches)));\n hopeButton.addEventListener('contextmenu', event => updateFunc(event, 'hope', false, x => Math.max(x, 0)));\n stressButton.addEventListener('click', event => updateFunc(event, 'stress', true, x => Math.min(x, nrMatches)));\n stressButton.addEventListener('contextmenu', event => updateFunc(event, 'stress', false, x => Math.max(x, 0)));\n },\n ok: { callback: (_event, _result, dialog) => {\n const hope = dialog.data.hope ?? 0;\n const stress = dialog.data.stress ?? 0;\n if (!hope && !stress) return;\n\n /* Return resource update according to choices */\n const hopeUpdate = hope ? { key: 'hope', value: hope, total: -hope, enabled: true } : null;\n const stressUpdate = stress ? { key: 'stress', value: -stress, total: stress, enabled: true } : null;\n return { updates: [hopeUpdate, stressUpdate].filter(x => x) };\n }}\n});\n\nreturn result;" + "command": "/* Ignore if it's a TagTeam roll */\nconst tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);\nif (tagTeam.members[actor.id]) return;\n\n/* Check if there's a Strange Pattern match */\nconst dice = [roll.dFear.total, roll.dHope.total];\nconst resource = this.parent.resource?.diceStates ? Object.values(this.parent.resource.diceStates).map(x => x.value)[0] : null;\nconst nrMatches = dice.filter(x => x === resource).length;\n\nif (!nrMatches) return;\n\n/* Create a dialog to choose Hope or Stress - or to cancel*/\nconst content = `\n
${game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContentTitle', { nr: nrMatches })}
\n
${game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContentSubTitle', { nr: nrMatches })}
\n
${game.i18n.localize('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsActionExplanation')}
\n
\n \n \n
\n
`;\n\nconst result = await foundry.applications.api.DialogV2.input({\n classes: ['dh-style', 'two-big-buttons'],\n window: { title: this.item.name },\n content: content,\n render: (_, dialog) => {\n const hopeButton = dialog.element.querySelector('#hopeButton');\n const stressButton = dialog.element.querySelector('#stressButton');\ndialog.element.querySelector('button[type=\"submit\"]').disabled = true;\n \n const updateFunc = (event, selector, adding, clamp) => {\n const button = event.target.closest(`#${selector}Button`);\n const parent = event.target.closest('.flexrow');\n const hope = Number.parseInt(parent.querySelector('#hopeButton label').innerHTML);\n const stress = Number.parseInt(parent.querySelector('#stressButton label').innerHTML);\n const currentTotal = (Number.isNumeric(hope) ? hope : 0) + (Number.isNumeric(stress) ? stress : 0);\n if (adding && currentTotal === nrMatches) return;\n \n const current = Number.parseInt(button.querySelector('label').innerHTML);\n if (!adding && current === 0) return;\n \n const value = Number.isNumeric(current) ? adding ? current+1 : current-1 : 1;\n if (!dialog.data) dialog.data = {};\n dialog.data[selector] = clamp(value);\n button.querySelector('label').innerHTML = dialog.data[selector];\n\n event.target.closest('.dialog-form').querySelector('button[type=\"submit\"]').disabled = !adding || currentTotal < (nrMatches-1);\n \n };\n hopeButton.addEventListener('click', event => updateFunc(event, 'hope', true, x => Math.min(x, nrMatches)));\n hopeButton.addEventListener('contextmenu', event => updateFunc(event, 'hope', false, x => Math.max(x, 0)));\n stressButton.addEventListener('click', event => updateFunc(event, 'stress', true, x => Math.min(x, nrMatches)));\n stressButton.addEventListener('contextmenu', event => updateFunc(event, 'stress', false, x => Math.max(x, 0)));\n },\n ok: { callback: (_event, _result, dialog) => {\n const hope = dialog.data.hope ?? 0;\n const stress = dialog.data.stress ?? 0;\n if (!hope && !stress) return;\n\n /* Return resource update according to choices */\n const hopeUpdate = hope ? { key: 'hope', value: hope, total: -hope, enabled: true } : null;\n const stressUpdate = stress ? { key: 'stress', value: -stress, total: stress, enabled: true } : null;\n return { updates: [hopeUpdate, stressUpdate].filter(x => x) };\n }}\n});\n\nreturn result;" } ] } diff --git a/src/packs/communities/community_Highborne_DVw2mOCHB8i0XeBz.json b/src/packs/communities/community_Highborne_DVw2mOCHB8i0XeBz.json index dbb5ca78..708fc553 100644 --- a/src/packs/communities/community_Highborne_DVw2mOCHB8i0XeBz.json +++ b/src/packs/communities/community_Highborne_DVw2mOCHB8i0XeBz.json @@ -4,7 +4,7 @@ "type": "community", "folder": null, "system": { - "description": "

Being part of a highborne community means you’re accustomed to a life of elegance, opulence, and prestige within the upper echelons of society. Traditionally, members of a highborne community possess incredible material wealth. While this can take a variety of forms depending on the community—including gold and other minerals, land, or controlling the means of production—this status always comes with power and influence. Highborne place great value on titles and possessions, and there is little social mobility within their ranks. Members of a highborne community often control the political and economic status of the areas in which they live due to their ability to influence people and the economy with their substantial wealth. The health and safety of the less affluent people who live in these locations often hinges on the ability of this highborne ruling class to prioritize the well-being of their subjects over profit.

Highborne are often amiable, candid, conniving, enterprising, ostentatious, and unflappable.

COMMUNITY FEATURE

Privilege: You have advantage on rolls to consort with nobles, negotiate prices, or leverage your reputation to get what you want.

", + "description": "

Being part of a highborne community means you’re accustomed to a life of elegance, opulence, and prestige within the upper echelons of society. Traditionally, members of a highborne community possess incredible material wealth. While this can take a variety of forms depending on the community—including gold and other minerals, land, or controlling the means of production—this status always comes with power and influence. Highborne place great value on titles and possessions, and there is little social mobility within their ranks. Members of a highborne community often control the political and economic status of the areas in which they live due to their ability to influence people and the economy with their substantial wealth. The health and safety of the less affluent people who live in these locations often hinges on the ability of this highborne ruling class to prioritize the well-being of their subjects over profit.

Highborne are often amiable, candid, conniving, enterprising, ostentatious, and unflappable.

", "features": [ "Compendium.daggerheart.communities.Item.C7NR6qRatawZusmg" ], diff --git a/src/packs/communities/community_Loreborne_YsvlyqYoi8QQ8kwm.json b/src/packs/communities/community_Loreborne_YsvlyqYoi8QQ8kwm.json index ff7cae4e..ee8e2fc5 100644 --- a/src/packs/communities/community_Loreborne_YsvlyqYoi8QQ8kwm.json +++ b/src/packs/communities/community_Loreborne_YsvlyqYoi8QQ8kwm.json @@ -4,7 +4,7 @@ "type": "community", "folder": null, "system": { - "description": "

Being part of a loreborne community means you’re from a society that favors strong academic or political prowess. Loreborne communities highly value knowledge, frequently in the form of historical preservation, political advancement, scientific study, skill development, or lore and mythology compilation. Most members of these communities research in institutions built in bastions of civilization, while some eclectic few thrive in gathering information from the natural world. Some may be isolationists, operating in smaller enclaves, schools, or guilds and following their own unique ethos. Others still wield their knowledge on a larger scale, making deft political maneuvers across governmental landscapes.

Loreborne are often direct, eloquent, inquisitive, patient, rhapsodic, and witty.

COMMUNITY FEATURE

Well-Read: You have advantage on rolls that involve the history, culture, or politics of a prominent person or place.

", + "description": "

Being part of a loreborne community means you’re from a society that favors strong academic or political prowess. Loreborne communities highly value knowledge, frequently in the form of historical preservation, political advancement, scientific study, skill development, or lore and mythology compilation. Most members of these communities research in institutions built in bastions of civilization, while some eclectic few thrive in gathering information from the natural world. Some may be isolationists, operating in smaller enclaves, schools, or guilds and following their own unique ethos. Others still wield their knowledge on a larger scale, making deft political maneuvers across governmental landscapes.

Loreborne are often direct, eloquent, inquisitive, patient, rhapsodic, and witty.

", "features": [ "Compendium.daggerheart.communities.Item.JBZJmywisJg5X3tH" ], diff --git a/src/packs/communities/community_Orderborne_TY2TejenASXtS484.json b/src/packs/communities/community_Orderborne_TY2TejenASXtS484.json index 869d426a..2af54ca4 100644 --- a/src/packs/communities/community_Orderborne_TY2TejenASXtS484.json +++ b/src/packs/communities/community_Orderborne_TY2TejenASXtS484.json @@ -4,7 +4,7 @@ "type": "community", "folder": null, "system": { - "description": "

Being part of an orderborne community means you’re from a collective that focuses on discipline or faith, and you uphold a set of principles that reflect your experience there. Orderborne are frequently some of the most powerful among the surrounding communities. By aligning the members of their society around a common value or goal, such as a god, doctrine, ethos, or even a shared business or trade, the ruling bodies of these enclaves are able to mobilize larger populations with less effort. While orderborne communities take a variety of forms—some even profoundly pacifistic—perhaps the most feared are those that structure themselves around military prowess. In such a case, it’s not uncommon for orderborne to provide soldiers for hire to other cities or countries.

Orderborne are often ambitious, benevolent, pensive, prudent, sardonic, and stoic.

COMMUNITY FEATURE

Dedicated: Record three sayings or values your upbringing instilled in you. Once per rest, when you describe how you’re embodying one of these principles through your current action, you can roll a d20 as your Hope Die.

", + "description": "

Being part of an orderborne community means you’re from a collective that focuses on discipline or faith, and you uphold a set of principles that reflect your experience there. Orderborne are frequently some of the most powerful among the surrounding communities. By aligning the members of their society around a common value or goal, such as a god, doctrine, ethos, or even a shared business or trade, the ruling bodies of these enclaves are able to mobilize larger populations with less effort. While orderborne communities take a variety of forms—some even profoundly pacifistic—perhaps the most feared are those that structure themselves around military prowess. In such a case, it’s not uncommon for orderborne to provide soldiers for hire to other cities or countries.

Orderborne are often ambitious, benevolent, pensive, prudent, sardonic, and stoic.

", "features": [ "Compendium.daggerheart.communities.Item.7aXWdH3gzaYREK0X" ], diff --git a/src/packs/communities/community_Ridgeborne_WHLA4qrdszXQHOuo.json b/src/packs/communities/community_Ridgeborne_WHLA4qrdszXQHOuo.json index 60f82899..5ebad846 100644 --- a/src/packs/communities/community_Ridgeborne_WHLA4qrdszXQHOuo.json +++ b/src/packs/communities/community_Ridgeborne_WHLA4qrdszXQHOuo.json @@ -4,7 +4,7 @@ "type": "community", "folder": null, "system": { - "description": "

Being part of a ridgeborne community means you’ve called the rocky peaks and sharp cliffs of the mountainside home. Those who’ve lived in the mountains often consider themselves hardier than most because they’ve thrived among the most dangerous terrain many continents have to offer. These groups are adept at adaptation, developing unique technologies and equipment to move both people and products across difficult terrain. As such, ridgeborne grow up scrambling and climbing, making them sturdy and strong-willed. Ridgeborne localities appear in a variety of forms—some cities carve out entire cliff faces, others construct castles of stone, and still more live in small homes on windblown peaks. Outside forces often struggle to attack ridgeborne groups, as the small militias and large military forces of the mountains are adept at utilizing their high-ground advantage.

Ridgeborne are often bold, hardy, indomitable, loyal, reserved, and stubborn.

COMMUNITY FEATURE

Steady: You have advantage on rolls to traverse dangerous cliffs and ledges, navigate harsh environments, and use your survival knowledge.

", + "description": "

Being part of a ridgeborne community means you’ve called the rocky peaks and sharp cliffs of the mountainside home. Those who’ve lived in the mountains often consider themselves hardier than most because they’ve thrived among the most dangerous terrain many continents have to offer. These groups are adept at adaptation, developing unique technologies and equipment to move both people and products across difficult terrain. As such, ridgeborne grow up scrambling and climbing, making them sturdy and strong-willed. Ridgeborne localities appear in a variety of forms—some cities carve out entire cliff faces, others construct castles of stone, and still more live in small homes on windblown peaks. Outside forces often struggle to attack ridgeborne groups, as the small militias and large military forces of the mountains are adept at utilizing their high-ground advantage.

Ridgeborne are often bold, hardy, indomitable, loyal, reserved, and stubborn.

", "features": [ "Compendium.daggerheart.communities.Item.DYmmr5CknLtHnwuj" ], diff --git a/src/packs/communities/community_Seaborne_o5AA5J05N7EvH1rN.json b/src/packs/communities/community_Seaborne_o5AA5J05N7EvH1rN.json index bb430a20..1e2f0970 100644 --- a/src/packs/communities/community_Seaborne_o5AA5J05N7EvH1rN.json +++ b/src/packs/communities/community_Seaborne_o5AA5J05N7EvH1rN.json @@ -4,7 +4,7 @@ "type": "community", "folder": null, "system": { - "description": "

Being part of a seaborne community means you lived on or near a large body of water. Seaborne communities are built, both physically and culturally, around the specific waters they call home. Some of these groups live along the shore, constructing ports for locals and travelers alike. These harbors function as centers of commerce, tourist attractions, or even just a safe place to lay down one’s head after weeks of travel. Other seaborne live on the water in small boats or large ships, with the idea of “home” comprising a ship and its crew, rather than any one landmass. No matter their exact location, seaborne communities are closely tied to the ocean tides and the creatures who inhabit them. Seaborne learn to fish at a young age, and train from birth to hold their breath and swim in even the most tumultuous waters. Individuals from these groups are highly sought after for their sailing skills, and many become captains of vessels, whether within their own community, working for another, or even at the helm of a powerful naval operation.

Seaborne are often candid, cooperative, exuberant, fierce, resolute, and weathered.

COMMUNITY FEATURE

Know the Tide: You can sense the ebb and flow of life. When you roll with Fear, place a token on your community card. You can hold a number of tokens equal to your level. Before you make an action roll, you can spend any number of these tokens to gain a +1 bonus to the roll for each token spent. At the end of each session, clear all unspent tokens.

", + "description": "

Being part of a seaborne community means you lived on or near a large body of water. Seaborne communities are built, both physically and culturally, around the specific waters they call home. Some of these groups live along the shore, constructing ports for locals and travelers alike. These harbors function as centers of commerce, tourist attractions, or even just a safe place to lay down one’s head after weeks of travel. Other seaborne live on the water in small boats or large ships, with the idea of “home” comprising a ship and its crew, rather than any one landmass. No matter their exact location, seaborne communities are closely tied to the ocean tides and the creatures who inhabit them. Seaborne learn to fish at a young age, and train from birth to hold their breath and swim in even the most tumultuous waters. Individuals from these groups are highly sought after for their sailing skills, and many become captains of vessels, whether within their own community, working for another, or even at the helm of a powerful naval operation.

Seaborne are often candid, cooperative, exuberant, fierce, resolute, and weathered.

", "features": [ "Compendium.daggerheart.communities.Item.07x6Qe6qMzDw2xN4" ], diff --git a/src/packs/communities/community_Slyborne_rGwCPMqZtky7SE6d.json b/src/packs/communities/community_Slyborne_rGwCPMqZtky7SE6d.json index fe69a8fe..724d8c36 100644 --- a/src/packs/communities/community_Slyborne_rGwCPMqZtky7SE6d.json +++ b/src/packs/communities/community_Slyborne_rGwCPMqZtky7SE6d.json @@ -4,7 +4,7 @@ "type": "community", "folder": null, "system": { - "description": "

Being part of a slyborne community means you come from a group that operates outside the law, including all manner of criminals, grifters, and con artists. Being part of a slyborne community means you come from a group that operates outside the law, including all manner of criminals, grifters, and con artists. Members of slyborne communities are brought together by their disreputable goals and their clever means of achieving them. Many people in these communities have an array of unscrupulous skills: forging, thievery, smuggling, and violence. People of any social class can be slyborne, from those who have garnered vast wealth and influence to those without a coin to their name. To the outside eye, slyborne might appear to be ruffians with no loyalty, but these communities possess some of the strictest codes of honor which, when broken, can result in a terrifying end for the transgressor.

Slyborne are often calculating, clever, formidable, perceptive, shrewd, and tenacious.

COMMUNITY FEATURE

Scoundrel: You have advantage on rolls to negotiate with criminals, detect lies, or find a safe place to hide.

", + "description": "

Being part of a slyborne community means you come from a group that operates outside the law, including all manner of criminals, grifters, and con artists. Being part of a slyborne community means you come from a group that operates outside the law, including all manner of criminals, grifters, and con artists. Members of slyborne communities are brought together by their disreputable goals and their clever means of achieving them. Many people in these communities have an array of unscrupulous skills: forging, thievery, smuggling, and violence. People of any social class can be slyborne, from those who have garnered vast wealth and influence to those without a coin to their name. To the outside eye, slyborne might appear to be ruffians with no loyalty, but these communities possess some of the strictest codes of honor which, when broken, can result in a terrifying end for the transgressor.

Slyborne are often calculating, clever, formidable, perceptive, shrewd, and tenacious.

", "features": [ "Compendium.daggerheart.communities.Item.ZmEuBdL0JrvuA8le" ], diff --git a/src/packs/communities/community_Underborne_eX0I1ZNMyD3nfaL1.json b/src/packs/communities/community_Underborne_eX0I1ZNMyD3nfaL1.json index 360adc3b..c0b7ba06 100644 --- a/src/packs/communities/community_Underborne_eX0I1ZNMyD3nfaL1.json +++ b/src/packs/communities/community_Underborne_eX0I1ZNMyD3nfaL1.json @@ -4,7 +4,7 @@ "type": "community", "folder": null, "system": { - "description": "

Being part of an underborne community means you’re from a subterranean society. Many underborne live right beneath the cities and villages of other collectives, while some live much deeper. These communities range from small family groups in burrows to massive metropolises in caverns of stone. In many locales, underborne are recognized for their incredible boldness and skill that enable great feats of architecture and engineering. Underborne are regularly hired for their bravery, as even the least daring among them has likely encountered formidable belowground beasts, and learning to dispatch such creatures is common practice amongst these societies. Because of the dangers of their environment, many underborne communities develop unique nonverbal languages that prove equally useful on the surface.

Underborne are often composed, elusive, indomitable, innovative, resourceful, and unpretentious.

COMMUNITY FEATURE

Low-Light Living: When you’re in an area with low light or heavy shadow, you have advantage on rolls to hide, investigate, or perceive details within that area.

", + "description": "

Being part of an underborne community means you’re from a subterranean society. Many underborne live right beneath the cities and villages of other collectives, while some live much deeper. These communities range from small family groups in burrows to massive metropolises in caverns of stone. In many locales, underborne are recognized for their incredible boldness and skill that enable great feats of architecture and engineering. Underborne are regularly hired for their bravery, as even the least daring among them has likely encountered formidable belowground beasts, and learning to dispatch such creatures is common practice amongst these societies. Because of the dangers of their environment, many underborne communities develop unique nonverbal languages that prove equally useful on the surface.

Underborne are often composed, elusive, indomitable, innovative, resourceful, and unpretentious.

", "features": [ "Compendium.daggerheart.communities.Item.aMla3xQuCHEwORGD" ], diff --git a/src/packs/communities/community_Wanderborne_82mDY2EIBfLkNwQj.json b/src/packs/communities/community_Wanderborne_82mDY2EIBfLkNwQj.json index 0adccad8..8f595dc9 100644 --- a/src/packs/communities/community_Wanderborne_82mDY2EIBfLkNwQj.json +++ b/src/packs/communities/community_Wanderborne_82mDY2EIBfLkNwQj.json @@ -4,7 +4,7 @@ "type": "community", "folder": null, "system": { - "description": "

Being part of a wanderborne community means you’ve lived as a nomad, forgoing a permanent home and experiencing a wide variety of cultures. Unlike many communities that are defined by their locale, wanderborne are defined by their traveling lifestyle. Because of their frequent migration, wanderborne put less value on the accumulation of material possessions in favor of acquiring information, skills, and connections. While some wanderborne are allied by a common ethos, such as a religion or a set of political or economic values, others come together after shared tragedy, such as the loss of their home or land. No matter the reason, the dangers posed by life on the road and the choice to continue down that road together mean that wanderborne are known for their unwavering loyalty.

Wanderborne are often inscrutable, magnanimous, mirthful, reliable, savvy, and unorthodox.

COMMUNITY FEATURE

Nomadic Pack: Add a Nomadic Pack to your inventory. Once per session, you can spend a Hope to reach into this pack and pull out a mundane item that’s useful to your situation. Work with the GM to figure out what item you take out.

", + "description": "

Being part of a wanderborne community means you’ve lived as a nomad, forgoing a permanent home and experiencing a wide variety of cultures. Unlike many communities that are defined by their locale, wanderborne are defined by their traveling lifestyle. Because of their frequent migration, wanderborne put less value on the accumulation of material possessions in favor of acquiring information, skills, and connections. While some wanderborne are allied by a common ethos, such as a religion or a set of political or economic values, others come together after shared tragedy, such as the loss of their home or land. No matter the reason, the dangers posed by life on the road and the choice to continue down that road together mean that wanderborne are known for their unwavering loyalty.

Wanderborne are often inscrutable, magnanimous, mirthful, reliable, savvy, and unorthodox.

", "features": [ "Compendium.daggerheart.communities.Item.2RSrQouA2zEJ5Xee" ], diff --git a/src/packs/communities/community_Wildborne_CRJ5pzJj4FjCtIlx.json b/src/packs/communities/community_Wildborne_CRJ5pzJj4FjCtIlx.json index 68787784..7974c147 100644 --- a/src/packs/communities/community_Wildborne_CRJ5pzJj4FjCtIlx.json +++ b/src/packs/communities/community_Wildborne_CRJ5pzJj4FjCtIlx.json @@ -4,7 +4,7 @@ "type": "community", "folder": null, "system": { - "description": "

Being part of a wildborne community means you lived deep within the forest. Wildborne communities are defined by their dedication to the conservation of their homelands, and many have strong religious or cultural ties to the fauna they live among. This results in unique architectural and technological advancements that favor sustainability over short-term, high-yield results. It is a hallmark of wildborne societies to integrate their villages and cities with the natural environment and avoid disturbing the lives of the plants and animals. While some construct their lodgings high in the branches of trees, others establish their homes on the ground beneath the forest canopy. It’s not uncommon for wildborne to remain reclusive and hidden within their woodland homes.

Wildborne are often hardy, loyal, nurturing, reclusive, sagacious, and vibrant.

COMMUNITY FEATURE

Lightfoot: Your movement is naturally silent. You have advantage on rolls to move without being heard.

", + "description": "

Being part of a wildborne community means you lived deep within the forest. Wildborne communities are defined by their dedication to the conservation of their homelands, and many have strong religious or cultural ties to the fauna they live among. This results in unique architectural and technological advancements that favor sustainability over short-term, high-yield results. It is a hallmark of wildborne societies to integrate their villages and cities with the natural environment and avoid disturbing the lives of the plants and animals. While some construct their lodgings high in the branches of trees, others establish their homes on the ground beneath the forest canopy. It’s not uncommon for wildborne to remain reclusive and hidden within their woodland homes.

Wildborne are often hardy, loyal, nurturing, reclusive, sagacious, and vibrant.

", "features": [ "Compendium.daggerheart.communities.Item.TQ1AIQjndC4mYmmU" ], diff --git a/src/packs/domains/domainCard_Bold_Presence_tdsL00yTSLNgZWs6.json b/src/packs/domains/domainCard_Bold_Presence_tdsL00yTSLNgZWs6.json index 08110cca..aaf070fc 100644 --- a/src/packs/domains/domainCard_Bold_Presence_tdsL00yTSLNgZWs6.json +++ b/src/packs/domains/domainCard_Bold_Presence_tdsL00yTSLNgZWs6.json @@ -110,7 +110,7 @@ "startRound": null, "startTurn": null }, - "description": "

Add your Strength to the presence roll roll.

", + "description": "

Add your Strength to the presence roll.

", "tint": "#ffffff", "statuses": [], "sort": 0, diff --git a/src/packs/domains/domainCard_Second_Wind_ffPbSEvLuFrFsMxl.json b/src/packs/domains/domainCard_Second_Wind_ffPbSEvLuFrFsMxl.json index a8a21ec3..8dc535cc 100644 --- a/src/packs/domains/domainCard_Second_Wind_ffPbSEvLuFrFsMxl.json +++ b/src/packs/domains/domainCard_Second_Wind_ffPbSEvLuFrFsMxl.json @@ -170,7 +170,8 @@ "value": 1, "recovery": "shortRest", "max": "1", - "icon": "" + "icon": "", + "progression": "decreasing" }, "attribution": { "source": "Daggerheart SRD", diff --git a/src/packs/domains/domainCard_Wrangle_9DwSxHoUwl8Kxj3n.json b/src/packs/domains/domainCard_Wrangle_9DwSxHoUwl8Kxj3n.json index 8cdb62b0..16753e1e 100644 --- a/src/packs/domains/domainCard_Wrangle_9DwSxHoUwl8Kxj3n.json +++ b/src/packs/domains/domainCard_Wrangle_9DwSxHoUwl8Kxj3n.json @@ -53,7 +53,7 @@ "difficulty": null, "damageMod": "none" }, - "name": "Agility Check", + "name": "Agility Roll", "img": "icons/skills/melee/sword-engraved-glow-purple.webp", "range": "close" } diff --git a/styles/less/dialog/compendiumBrowserPackDialog/sheet.less b/styles/less/dialog/compendiumBrowserPackDialog/sheet.less new file mode 100644 index 00000000..dfe375b5 --- /dev/null +++ b/styles/less/dialog/compendiumBrowserPackDialog/sheet.less @@ -0,0 +1,105 @@ +.daggerheart.dialog.dh-style.views.compendium-brower-settings { + --text-color: light-dark(@dark-blue, @beige); + color: var(--text-color); + + .window-content { + justify-content: space-between; + + > div { + overflow: auto; + display: flex; + flex-direction: column; + max-height: 440px; + } + } + + .types-container { + display: flex; + flex-direction: column; + gap: 8px; + + .type-container { + display: flex; + flex-direction: column; + gap: 8px; + + > label { + display: flex; + align-items: center; + font-size: var(--font-size-16); + font-family: @font-subtitle; + font-weight: bold; + + &::before { + content: ''; + flex: 1; + height: 2px; + background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, light-dark(@dark-blue, @golden) 100%); + margin-right: 8px; + } + &::after { + content: ''; + flex: 1; + height: 2px; + background: linear-gradient(90deg, light-dark(@dark-blue, @golden) 0%, rgba(0, 0, 0, 0) 100%); + margin-left: 8px; + } + } + + .sources-container { + display: flex; + flex-direction: column; + gap: 8px; + + .source-container { + display: flex; + flex-direction: column; + gap: 2px; + + .source-inner-container { + display: flex; + justify-content: space-between; + + .source-inner-label-container { + width: 100%; + display: flex; + gap: 8px; + + i { + font-size: 18px; + // color: light-dark(@dark-blue, @golden); + } + } + } + } + } + } + } + + .checks-container { + padding-left: 24px; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4px; + transition: height 0.4s ease-in-out; + overflow: hidden; + + &.collapsed { + height: 0px; + } + + .check-container { + display: flex; + align-items: center; + } + } + + footer { + margin-top: 8px; + display: flex; + + button { + flex: 1; + } + } +} diff --git a/styles/less/dialog/dice-roll/roll-selection.less b/styles/less/dialog/dice-roll/roll-selection.less index 0f082460..7fdae77a 100644 --- a/styles/less/dialog/dice-roll/roll-selection.less +++ b/styles/less/dialog/dice-roll/roll-selection.less @@ -17,7 +17,9 @@ .dialog-header-inner { display: flex; - justify-content: center; + flex-direction: column; + align-items: center; + gap: 2px; } h1 { @@ -45,6 +47,29 @@ } } + .reaction-chip { + display: flex; + align-items: center; + border-radius: 5px; + width: fit-content; + gap: 5px; + cursor: pointer; + padding: 5px; + background: light-dark(@dark-blue-10, @golden-10); + color: light-dark(@dark-blue, @golden); + + .label { + font-style: normal; + font-weight: 400; + font-size: var(--font-size-14); + line-height: 17px; + } + + &.selected { + background: light-dark(@dark-blue-40, @golden-40); + } + } + .tag-team-controller { display: flex; align-items: center; diff --git a/styles/less/dialog/index.less b/styles/less/dialog/index.less index 733cdd1c..0c70df9f 100644 --- a/styles/less/dialog/index.less +++ b/styles/less/dialog/index.less @@ -43,3 +43,5 @@ @import './risk-it-all/sheet.less'; @import './character-reset/sheet.less'; + +@import './compendiumBrowserPackDialog/sheet.less'; diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index 713a4481..f5e92e2c 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -52,6 +52,14 @@ } } + input[type='checkbox'] { + &:indeterminate { + &::before { + content: '\f0fe'; + } + } + } + input[type='checkbox'], input[type='radio'] { height: 20px; @@ -454,6 +462,12 @@ } } + .three-columns { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 10px; + } + line-div { display: block; height: 1px; diff --git a/styles/less/global/global.less b/styles/less/global/global.less index 6c63fe7a..fb995b8c 100644 --- a/styles/less/global/global.less +++ b/styles/less/global/global.less @@ -50,6 +50,29 @@ transform: rotate(360deg); } } + + .item-description-outer-container { + display: flex; + flex-direction: column; + gap: 8px; + + .item-description-container { + h4 { + margin-bottom: 4px; + } + + .item-description-inner-container { + p { + display: inline; + } + } + + .item-links { + display: flex; + gap: 4px; + } + } + } } /* TODO: Remove me when this issue is resolved https://github.com/foundryvtt/foundryvtt/issues/13734 */ diff --git a/styles/less/global/inventory-item.less b/styles/less/global/inventory-item.less index c8a29795..d703d189 100644 --- a/styles/less/global/inventory-item.less +++ b/styles/less/global/inventory-item.less @@ -103,10 +103,9 @@ display: flex; align-items: center; justify-content: end; - gap: 8px; a { - width: 15px; + width: 20px; text-align: center; } @@ -174,9 +173,12 @@ &.extensible { display: grid; grid-template-rows: 0fr; - transition: grid-template-rows 0.3s ease-in-out; + transition: + grid-template-rows 0.3s ease-in-out, + padding-top 0.3s ease-in-out; &.extended { grid-template-rows: 1fr; + padding-top: 4px; } .invetory-description { overflow: hidden; @@ -272,8 +274,10 @@ grid-area: controls; align-self: start; padding-top: 0.3125rem; - gap: 4px; margin-bottom: -1px; + a { + width: 18px; + } } > .item-labels { align-self: start; @@ -331,6 +335,27 @@ border-radius: 6px; } + .recall-cost { + position: absolute; + right: 4px; + top: 4px; + width: 1.75em; + height: 1.75em; + + align-items: center; + background: @dark-blue; + border-radius: 50%; + border: 1px solid @golden; + color: @golden; + display: flex; + justify-content: center; + padding-top: 0.1em; // compensate for font + + i { + font-size: 0.68em; + } + } + .card-label { display: flex; flex-direction: column; diff --git a/styles/less/sheets/actors/actor-sheet-shared.less b/styles/less/sheets/actors/actor-sheet-shared.less index bf6393f4..23db088a 100644 --- a/styles/less/sheets/actors/actor-sheet-shared.less +++ b/styles/less/sheets/actors/actor-sheet-shared.less @@ -183,6 +183,11 @@ } } + .domain-details { + display: flex; + flex-direction: column; + } + .level-details { align-self: center; } diff --git a/styles/less/ui/chat/action.less b/styles/less/ui/chat/action.less index a3d2f3cc..6eeb7a52 100644 --- a/styles/less/ui/chat/action.less +++ b/styles/less/ui/chat/action.less @@ -38,124 +38,6 @@ flex-direction: column; align-items: center; - details[open] { - .fa-chevron-down { - transform: rotate(180deg); - transition: all 0.3s ease; - } - } - - .action-move { - width: 100%; - - .fa-chevron-down { - transition: all 0.3s ease; - margin-left: auto; - } - - .action-section { - display: flex; - flex-direction: row; - align-items: center; - margin: 8px 8px 0; - padding-bottom: 5px; - width: -webkit-fill-available; - gap: 5px; - border-bottom: 1px solid @golden; - - &:hover { - background: @golden-10; - cursor: pointer; - transition: all 0.3s ease; - } - - .action-img { - width: 40px; - height: 40px; - border-radius: 3px; - object-fit: cover; - } - - .action-header { - display: flex; - flex-direction: column; - gap: 5px; - color: @beige; - - .title { - font-size: var(--font-size-20); - color: @golden; - font-weight: 700; - } - - .label { - font-size: var(--font-size-12); - color: @beige; - margin: 0; - } - } - } - } - - .description { - padding: 8px; - - .summons-header { - font-size: var(--font-size-14); - text-align: center; - display: flex; - align-items: center; - justify-content: center; - - span { - width: 100%; - } - - &:before, - &:after { - content: ' '; - height: 1px; - width: 100%; - } - - &:before { - background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, light-dark(@dark-blue, @golden) 100%); - } - - &:after { - background: linear-gradient(90deg, light-dark(@dark-blue, @golden) 0%, rgba(0, 0, 0, 0) 100%); - } - } - - .summons-container { - display: flex; - flex-direction: column; - gap: 4px; - - .summon-container { - display: flex; - align-items: center; - justify-content: space-between; - - .summon-label-container { - flex: 1; - display: flex; - align-items: center; - gap: 4px; - - img { - height: 32px; - } - - label { - display: flex; - flex-wrap: wrap; - } - } - } - } - } - .ability-card-footer { display: flex; flex-wrap: wrap; diff --git a/styles/less/ui/chat/chat.less b/styles/less/ui/chat/chat.less index 494af5f1..3591fc65 100644 --- a/styles/less/ui/chat/chat.less +++ b/styles/less/ui/chat/chat.less @@ -158,7 +158,7 @@ .daggerheart, #chat-notifications { .chat-message { - --text-color: @golden; + --text-color: light-dark(@dark-blue, @golden); --bg-color: @golden-40; [data-use-perm='false'] { @@ -228,6 +228,15 @@ font-size: var(--font-size-12); padding: 0 20px; + .roll-part-title { + text-align: center; + font-family: @font-subtitle; + font-size: var(--font-size-18); + font-weight: bold; + color: var(--text-color); + margin-bottom: -2px; + } + > .roll-part-header { font-size: var(--font-size-14); } @@ -286,6 +295,7 @@ > :first-child:not(.target-selector) { margin-top: 5px; + text-align: center; } > :last-child { @@ -573,6 +583,30 @@ } } + .chat-roll .description-section { + .roll-part-content { + .dice-tooltip { + .wrapper { + i { + margin: 0; + + :first-child { + margin-top: 0; + } + + :last-child { + margin-bottom: 0; + } + } + + > :first-child:not(.target-selector) { + margin: 0; + } + } + } + } + } + .roll-buttons { display: flex; gap: 5px; @@ -590,5 +624,124 @@ .dice-roll .dice-tooltip fieldset { margin-bottom: 5px; } + + details[open] { + .fa-chevron-down { + transform: rotate(180deg); + transition: all 0.3s ease; + } + } + + .action-move { + width: 100%; + + .fa-chevron-down { + transition: all 0.3s ease; + margin-left: auto; + } + + .action-section { + display: flex; + flex-direction: row; + align-items: center; + margin: 8px 8px 0; + padding-bottom: 5px; + width: -webkit-fill-available; + gap: 5px; + border-bottom: 1px solid @golden; + + &:hover { + background: @golden-10; + cursor: pointer; + transition: all 0.3s ease; + } + + .action-img { + width: 40px; + height: 40px; + border-radius: 3px; + object-fit: cover; + } + + .action-header { + display: flex; + flex-direction: column; + gap: 5px; + color: @beige; + + .title { + font-size: var(--font-size-20); + color: @golden; + font-weight: 700; + margin: 0; + } + + .label { + font-size: var(--font-size-12); + color: @beige; + margin: 0; + } + } + } + } + + .description { + padding: 8px; + + .summons-header { + font-size: var(--font-size-14); + text-align: center; + display: flex; + align-items: center; + justify-content: center; + + span { + width: 100%; + } + + &:before, + &:after { + content: ' '; + height: 1px; + width: 100%; + } + + &:before { + background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, light-dark(@dark-blue, @golden) 100%); + } + + &:after { + background: linear-gradient(90deg, light-dark(@dark-blue, @golden) 0%, rgba(0, 0, 0, 0) 100%); + } + } + + .summons-container { + display: flex; + flex-direction: column; + gap: 4px; + + .summon-container { + display: flex; + align-items: center; + justify-content: space-between; + + .summon-label-container { + flex: 1; + display: flex; + align-items: center; + gap: 4px; + + img { + height: 32px; + } + + label { + display: flex; + flex-wrap: wrap; + } + } + } + } + } } } diff --git a/styles/less/ui/item-browser/item-browser.less b/styles/less/ui/item-browser/item-browser.less index 23844128..b395f8c8 100644 --- a/styles/less/ui/item-browser/item-browser.less +++ b/styles/less/ui/item-browser/item-browser.less @@ -301,7 +301,7 @@ } .item-desc .wrapper { - padding: 0 10px; + padding: 0 0 0 50px; display: flex; flex-direction: column; gap: 5px; diff --git a/styles/less/ui/scene-navigation/scene-navigation.less b/styles/less/ui/scene-navigation/scene-navigation.less index 6b97ddec..38768658 100644 --- a/styles/less/ui/scene-navigation/scene-navigation.less +++ b/styles/less/ui/scene-navigation/scene-navigation.less @@ -1,36 +1,39 @@ #ui-left #ui-left-column-2 { flex: 0 0 230px; - .scene-navigation { - .scene-wrapper { - display: flex; - gap: 2px; - height: var(--control-size); - width: 100%; + .scene-wrapper { + display: flex; + gap: 2px; + height: var(--control-size); + width: 100%; - .scene-environment { - padding: 0; + > ul { + margin: 0; + padding: 0; + } - img { - border-radius: 4px; - } + .scene-environment { + padding: 0; + + img { + border-radius: 4px; } } + } - .scene { - justify-content: center; - align-content: center; - background: var(--control-bg-color); - border: 1px solid var(--control-border-color); - border-radius: 4px; - color: var(--control-icon-color); - pointer-events: all; - transition: - border 0.25s, - color 0.25s; - text-shadow: none; - width: 200px; - max-width: 200px; - } + .scene { + justify-content: center; + align-content: center; + background: var(--control-bg-color); + border: 1px solid var(--control-border-color); + border-radius: 4px; + color: var(--control-icon-color); + pointer-events: all; + transition: + border 0.25s, + color 0.25s; + text-shadow: none; + width: 200px; + max-width: 200px; } } diff --git a/styles/less/ux/autocomplete/autocomplete.less b/styles/less/ux/autocomplete/autocomplete.less index 08854a53..7f799449 100644 --- a/styles/less/ux/autocomplete/autocomplete.less +++ b/styles/less/ux/autocomplete/autocomplete.less @@ -32,7 +32,6 @@ li[role='option'] { display: flex; align-items: center; - gap: 10px; font-size: var(--font-size-14); padding: 0 10px; cursor: pointer; diff --git a/system.json b/system.json index fd0d7e1c..fc5e1615 100644 --- a/system.json +++ b/system.json @@ -2,7 +2,7 @@ "id": "daggerheart", "title": "Daggerheart", "description": "An unofficial implementation of the Daggerheart system", - "version": "1.6.4", + "version": "1.8.0", "compatibility": { "minimum": "13.346", "verified": "13.351", diff --git a/templates/dialogs/compendiumBrowserSettingsDialog/footer.hbs b/templates/dialogs/compendiumBrowserSettingsDialog/footer.hbs new file mode 100644 index 00000000..9dc61cbe --- /dev/null +++ b/templates/dialogs/compendiumBrowserSettingsDialog/footer.hbs @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/templates/dialogs/compendiumBrowserSettingsDialog/packs.hbs b/templates/dialogs/compendiumBrowserSettingsDialog/packs.hbs new file mode 100644 index 00000000..dcda8108 --- /dev/null +++ b/templates/dialogs/compendiumBrowserSettingsDialog/packs.hbs @@ -0,0 +1,36 @@ +
+
+ {{#each typePackCollections as |type|}} +
+ + +
+ {{#each type.sources as |source|}} +
+
+
+ + + + +
+
+ +
+ {{#each source.packs as |pack|}} +
+ + +
+ {{/each}} +
+
+ {{/each}} +
+
+ {{/each}} +
+
\ No newline at end of file diff --git a/templates/dialogs/dice-roll/header.hbs b/templates/dialogs/dice-roll/header.hbs index 21967655..0cb58a01 100644 --- a/templates/dialogs/dice-roll/header.hbs +++ b/templates/dialogs/dice-roll/header.hbs @@ -1,17 +1,12 @@
-

- {{#if reactionOverride}} - {{localize "DAGGERHEART.CONFIG.FeatureForm.reaction"}} - {{else}} - {{ifThen rollConfig.headerTitle rollConfig.headerTitle rollConfig.title}} - {{/if}} - {{#if showReaction}} - - {{/if}} -

+

{{ifThen rollConfig.headerTitle rollConfig.headerTitle rollConfig.title}}

+ {{#if showReaction}} +
+ + {{localize "DAGGERHEART.GENERAL.reactionRoll"}} +
+ {{/if}}
{{#if (and @root.hasRoll @root.activeTagTeamRoll)}}
diff --git a/templates/dialogs/dice-roll/rollSelection.hbs b/templates/dialogs/dice-roll/rollSelection.hbs index f31a4fa8..5139c58a 100644 --- a/templates/dialogs/dice-roll/rollSelection.hbs +++ b/templates/dialogs/dice-roll/rollSelection.hbs @@ -118,7 +118,7 @@ {{#if name}}
- {{name}} +{{value}} + {{name}} {{numberFormat value sign=true}}
{{/if}} {{/each}} diff --git a/templates/settings/automation-settings/general.hbs b/templates/settings/automation-settings/general.hbs index c5f4d871..65bafab8 100644 --- a/templates/settings/automation-settings/general.hbs +++ b/templates/settings/automation-settings/general.hbs @@ -14,6 +14,7 @@ {{formGroup settingFields.schema.fields.summaryMessages.fields.effects value=settingFields._source.summaryMessages.effects localize=true}}
+ {{formGroup settingFields.schema.fields.vulnerableAutomation value=settingFields._source.vulnerableAutomation localize=true}} {{formGroup settingFields.schema.fields.countdownAutomation value=settingFields._source.countdownAutomation localize=true}} {{formGroup settingFields.schema.fields.actionPoints value=settingFields._source.actionPoints localize=true}} {{formGroup settingFields.schema.fields.hordeDamage value=settingFields._source.hordeDamage localize=true}} diff --git a/templates/sheets/actors/character/sidebar.hbs b/templates/sheets/actors/character/sidebar.hbs index 24e68e02..b2757b55 100644 --- a/templates/sheets/actors/character/sidebar.hbs +++ b/templates/sheets/actors/character/sidebar.hbs @@ -145,7 +145,7 @@ {{#each document.system.experiences as |experience id|}}
- +{{experience.value}} + {{numberFormat experience.value sign=true}} {{experience.name}}
diff --git a/templates/sheets/global/partials/domain-card-item.hbs b/templates/sheets/global/partials/domain-card-item.hbs index ae95b7af..54e44e64 100644 --- a/templates/sheets/global/partials/domain-card-item.hbs +++ b/templates/sheets/global/partials/domain-card-item.hbs @@ -1,5 +1,9 @@
  • + + {{item.system.recallCost}} + +
  • +
  • {{!-- Image --}}
    - + {{/if}} {{#if (eq type 'domainCard')}} @@ -113,7 +117,7 @@ Parameters: data-tooltip="DAGGERHEART.UI.Tooltip.{{ifThen item.system.inVault 'sendToLoadout' 'sendToVault' }}"> - {{else if (eq type 'effect')}} + {{else if (and (eq type 'effect') (not (eq item.type 'beastform')))}} @@ -121,7 +125,7 @@ Parameters: {{/if}} {{#if (hasProperty item "toChat")}} - + {{/if}} {{else}} @@ -134,7 +138,7 @@ Parameters: {{/unless}} {{#unless hideContextMenu}} - + {{/unless}} {{/if}} diff --git a/templates/sheets/global/tabs/tab-effects.hbs b/templates/sheets/global/tabs/tab-effects.hbs index c89c6ff4..1f0967b8 100644 --- a/templates/sheets/global/tabs/tab-effects.hbs +++ b/templates/sheets/global/tabs/tab-effects.hbs @@ -2,20 +2,21 @@ data-group='{{tabs.effects.group}}'> {{> 'daggerheart.inventory-items' - title='DAGGERHEART.GENERAL.activeEffects' - type='effect' - isGlassy=true - collection=effects.actives - canCreate=true - hideResources=true + title='DAGGERHEART.GENERAL.activeEffects' + type='effect' + isGlassy=true + collection=effects.actives + canCreate=true + hideResources=true }} {{> 'daggerheart.inventory-items' - title='DAGGERHEART.GENERAL.inactiveEffects' - type='effect' - isGlassy=true - collection=effects.inactives - canCreate=true - hideResources=true + title='DAGGERHEART.GENERAL.inactiveEffects' + type='effect' + disabledEffect=true + isGlassy=true + collection=effects.inactives + canCreate=true + hideResources=true }} \ No newline at end of file diff --git a/templates/sheets/items/armor/description.hbs b/templates/sheets/items/armor/description.hbs index c5a9924e..c234fa10 100644 --- a/templates/sheets/items/armor/description.hbs +++ b/templates/sheets/items/armor/description.hbs @@ -1,5 +1,22 @@ -
    - {{#each features as | feature |}} -
    {{localize feature.label}}: {{{localize feature.description}}}
    - {{/each}} +
    +
    +
    +

    {{localize "DAGGERHEART.ITEMS.Armor.baseThresholds.base"}}

    + {{item.system.baseThresholds.major}}/{{item.system.baseThresholds.severe}} +
    + +
    +

    {{localize "DAGGERHEART.ITEMS.Armor.baseScore"}}

    + {{item.system.baseScore}} +
    +
    + + {{#if features.length}} +
    +

    {{localize "DAGGERHEART.GENERAL.features"}}

    + {{#each features as | feature |}} +
    {{localize feature.label}}: {{{localize feature.description}}}
    + {{/each}} +
    + {{/if}}
    \ No newline at end of file diff --git a/templates/sheets/items/class/description.hbs b/templates/sheets/items/class/description.hbs new file mode 100644 index 00000000..6fe2f6e2 --- /dev/null +++ b/templates/sheets/items/class/description.hbs @@ -0,0 +1,34 @@ +
    +
    +

    {{localize "DAGGERHEART.GENERAL.Domain.plural"}}

    + {{domains}} +
    +
    +

    {{localize "DAGGERHEART.ITEMS.Class.startingEvasionScore"}}

    + {{class.system.evasion}} +
    +
    +

    {{localize "DAGGERHEART.ITEMS.Class.startingHitPoints"}}

    + {{class.system.hitPoints}} +
    +
    +

    {{localize "DAGGERHEART.ITEMS.Class.classItems"}}

    + +
    +
    +

    {{localize "DAGGERHEART.ITEMS.Class.hopeFeatureLabel" class=class.name}}

    + {{#each hopeFeatures as | feature |}} +
    {{localize feature.label}}: {{{localize feature.description}}}
    + {{/each}} +
    +
    +

    {{localize "DAGGERHEART.ITEMS.Class.classFeature"}}

    + {{#each classFeatures as | feature |}} +
    {{localize feature.label}}: {{{localize feature.description}}}
    + {{/each}} +
    +
    \ No newline at end of file diff --git a/templates/sheets/items/description.hbs b/templates/sheets/items/description.hbs new file mode 100644 index 00000000..d253fe11 --- /dev/null +++ b/templates/sheets/items/description.hbs @@ -0,0 +1,8 @@ +
    +
    +

    {{localize label}}

    + {{#each features as | feature |}} +
    {{localize feature.label}}: {{{localize feature.description}}}
    + {{/each}} +
    +
    \ No newline at end of file diff --git a/templates/sheets/items/subclass/description.hbs b/templates/sheets/items/subclass/description.hbs new file mode 100644 index 00000000..4591bd1a --- /dev/null +++ b/templates/sheets/items/subclass/description.hbs @@ -0,0 +1,24 @@ +
    +
    +

    {{localize "DAGGERHEART.ITEMS.Subclass.spellcastTrait"}}

    + {{spellcastTrait}} +
    +
    +

    {{localize "DAGGERHEART.ITEMS.Subclass.foundationFeatures"}}

    + {{#each foundationFeatures as | feature |}} +
    {{localize feature.label}}: {{{localize feature.description}}}
    + {{/each}} +
    +
    +

    {{localize "DAGGERHEART.ITEMS.Subclass.specializationFeature"}}

    + {{#each specializationFeatures as | feature |}} +
    {{localize feature.label}}: {{{localize feature.description}}}
    + {{/each}} +
    +
    +

    {{localize "DAGGERHEART.ITEMS.Subclass.masteryFeature"}}

    + {{#each masteryFeatures as | feature |}} +
    {{localize feature.label}}: {{{localize feature.description}}}
    + {{/each}} +
    +
    \ No newline at end of file diff --git a/templates/sheets/items/weapon/description.hbs b/templates/sheets/items/weapon/description.hbs index c5a9924e..d8e128e7 100644 --- a/templates/sheets/items/weapon/description.hbs +++ b/templates/sheets/items/weapon/description.hbs @@ -1,5 +1,39 @@ -
    - {{#each features as | feature |}} -
    {{localize feature.label}}: {{{localize feature.description}}}
    - {{/each}} +
    +
    +
    +

    {{localize "DAGGERHEART.GENERAL.Tiers.singular"}}

    + {{tier}} +
    + +
    +

    {{localize "DAGGERHEART.GENERAL.Trait.single"}}

    + {{trait}} +
    + +
    +

    {{localize "DAGGERHEART.GENERAL.range"}}

    + {{range}} +
    +
    + +
    +
    +

    {{localize "DAGGERHEART.GENERAL.damage"}}

    + {{damage}} +
    + +
    +

    {{localize "DAGGERHEART.GENERAL.burden"}}

    + {{burden}} +
    +
    + + {{#if features.length}} +
    +

    {{localize "DAGGERHEART.GENERAL.features"}}

    + {{#each features as | feature |}} +
    {{localize feature.label}}: {{{localize feature.description}}}
    + {{/each}} +
    + {{/if}}
    \ No newline at end of file diff --git a/templates/ui/chat/action.hbs b/templates/ui/chat/action.hbs index 65bb0762..2854795c 100644 --- a/templates/ui/chat/action.hbs +++ b/templates/ui/chat/action.hbs @@ -1,5 +1,5 @@
    -
    +
    diff --git a/templates/ui/chat/deathMove.hbs b/templates/ui/chat/deathMove.hbs index 7c677fe3..4df53404 100644 --- a/templates/ui/chat/deathMove.hbs +++ b/templates/ui/chat/deathMove.hbs @@ -7,7 +7,7 @@

    {{this.title}}

    {{localize 'DAGGERHEART.UI.Chat.deathMove.title'}}
    - +
    {{{this.description}}} diff --git a/templates/ui/chat/downtime.hbs b/templates/ui/chat/downtime.hbs index 373724dc..d7152955 100644 --- a/templates/ui/chat/downtime.hbs +++ b/templates/ui/chat/downtime.hbs @@ -1,7 +1,7 @@
      {{#each moves as | move index |}} -
      +
      diff --git a/templates/ui/chat/parts/description-part.hbs b/templates/ui/chat/parts/description-part.hbs new file mode 100644 index 00000000..2e73a753 --- /dev/null +++ b/templates/ui/chat/parts/description-part.hbs @@ -0,0 +1,11 @@ +
      +
      {{localize "DAGGERHEART.GENERAL.description"}}
      + +
      +
      +
      + {{{actionDescription}}} +
      +
      +
      +
      \ No newline at end of file diff --git a/templates/ui/chat/roll.hbs b/templates/ui/chat/roll.hbs index be51969f..c7b17b21 100644 --- a/templates/ui/chat/roll.hbs +++ b/templates/ui/chat/roll.hbs @@ -1,6 +1,10 @@
      -
      {{title}}
      - {{#if hasRoll}}{{> 'systems/daggerheart/templates/ui/chat/parts/roll-part.hbs'}}{{/if}} +
      {{title}}
      + {{#if actionDescription}}{{> 'systems/daggerheart/templates/ui/chat/parts/description-part.hbs'}}{{/if}} + {{#if hasRoll}} +
      {{localize "Result"}}
      + {{> 'systems/daggerheart/templates/ui/chat/parts/roll-part.hbs'}} + {{/if}} {{#if (or hasDamage hasHealing)}}{{> 'systems/daggerheart/templates/ui/chat/parts/damage-part.hbs'}}{{/if}} {{#if hasTarget}}{{> 'systems/daggerheart/templates/ui/chat/parts/target-part.hbs'}}{{/if}}
      diff --git a/templates/ui/itemBrowser/sidebar.hbs b/templates/ui/itemBrowser/sidebar.hbs index 28a34a22..a829f8c5 100644 --- a/templates/ui/itemBrowser/sidebar.hbs +++ b/templates/ui/itemBrowser/sidebar.hbs @@ -1,7 +1,8 @@
      + {{#if isGM}}{{/if}}
      {{#each compendiums}} -
      {{label}}
      +
      {{label}}
      {{#if folders.length}}
      diff --git a/templates/ui/sceneNavigation/scene-navigation.hbs b/templates/ui/sceneNavigation/scene-navigation.hbs index 41e9e3e8..0bd59465 100644 --- a/templates/ui/sceneNavigation/scene-navigation.hbs +++ b/templates/ui/sceneNavigation/scene-navigation.hbs @@ -7,17 +7,19 @@ {{#each scenes.active as |scene|}}
    • -
      - {{scene.name}} - {{#if scene.users}} -
        - {{#each scene.users as |user|}} -
      • {{user.letter}}
      • - {{/each}} -
      - {{/if}} -
      +
        +
      • + {{scene.name}} + {{#if scene.users}} +
          + {{#each scene.users as |user|}} +
        • {{user.letter}}
        • + {{/each}} +
        + {{/if}} +
      • +
      {{#if scene.hasEnvironments}} {{/if}} @@ -27,9 +29,11 @@ {{#each scenes.inactive as |scene|}}
    • -
      +
        +
      • {{scene.name}} -
      +
    • +
  • {{/each}} diff --git a/tools/analyze-damage.mjs b/tools/analyze-damage.mjs new file mode 100644 index 00000000..6d5da3de --- /dev/null +++ b/tools/analyze-damage.mjs @@ -0,0 +1,137 @@ +/** + * Internal script to analyze damage and spit out results. + * There isn't enough entries in the database to make a full analysis, some tiers miss some types. + * This script only checks for "minions" and "everything else". + * Maybe if future book monsters can be part of what we release, we can analyze those too. + */ + +import fs from "fs/promises"; +import path from "path"; + +const allData = []; + +// Read adversary pack data for average damage for attacks +const adversariesDirectory = path.join("src/packs/adversaries"); +for (const basefile of await fs.readdir(adversariesDirectory)) { + if (!basefile.endsWith(".json")) continue; + const filepath = path.join(adversariesDirectory, basefile); + const data = JSON.parse(await fs.readFile(filepath, "utf8")); + if (data?.type !== "adversary" || data.system.type === "social") continue; + + allData.push({ + name: data.name, + tier: data.system.tier, + adversaryType: data.system.type, + damage: parseDamage(data.system.attack.damage), + }); +} + +const adversaryTypes = new Set(allData.map(a => a.adversaryType)); +for (const type of [...adversaryTypes].toSorted()) { + const perTier = Object.groupBy(allData.filter(a => a.adversaryType === type), a => a.tier); + console.log(`${type} per Tier: ${[1, 2, 3, 4].map(t => perTier[t]?.length ?? 0).join(" ")}`) +} + +const result = { + basic: compileData(allData.filter(d => d.adversaryType !== "minion")), + solos_and_bruisers: compileData(allData.filter(d => ["solo", "bruiser"].includes(d.adversaryType))), + leader_and_ranged: compileData(allData.filter(d => ["leader", "ranged"].includes(d.adversaryType))), + minion: compileData(allData.filter(d => d.adversaryType === "minion")), +}; + +console.log(result); + +/** Compiles all data for an adversary type (or all entries) */ +function compileData(entries) { + // Note: sorting numbers sorts by their string version by default + const results = {}; + for (const tier of [1, 2, 3, 4]) { + const tierEntries = entries.filter(e => e.tier === tier); + const allDamage = removeOutliers(tierEntries.map(d => d.damage).sort((a, b) => a - b)); + const mean = getMean(allDamage); + if (tier === 4) console.log(allDamage); + results[tier] = { + mean, + deviation: getStandardDeviation(allDamage, { mean }), + }; + } + + return results; +} + +function removeOutliers(data) { + if (data.length <= 4) return data; + const startIdx = Math.floor(data.length * 0.25); + const endIdx = Math.ceil(data.length * 0.75); + const iqrBound = (data[endIdx] - data[startIdx]) * 1.25; + return data.filter((d) => d >= data[startIdx] - iqrBound && d <= data[endIdx] + iqrBound); +} + +function getMedian(numbers) { + numbers = numbers.toSorted((a, b) => a - b); + const medianIdx = numbers.length / 2; + return medianIdx % 1 ? numbers[Math.floor(medianIdx)] : (numbers[medianIdx] + numbers[medianIdx - 1]) / 2; +} + +function getMean(numbers) { + if (numbers.length === 0) return NaN; + return numbers.reduce((r, a) => r + a, 0) / numbers.length; +} + +function getMedianAverageDeviation(numbers, { median }) { + const residuals = allDamage.map(d => Math.abs(d - median)); + return getMedian(residuals); +} + +function getStandardDeviation(numbers, { mean }) { + const deviations = numbers.map((r) => r - mean); + return Math.sqrt(deviations.reduce((r, d) => r + d * d, 0) / (numbers.length - 1)); +} + +function parseDamage(damage) { + const formula = damage.parts + .filter(p => p.applyTo === 'hitPoints') + .map(p => + p.value.custom.enabled + ? p.value.custom.formula + : [p.value.flatMultiplier ? `${p.value.flatMultiplier}${p.value.dice}` : 0, p.value.bonus ?? 0] + .filter(p => !!p) + .join('+') + ) + .join('+'); + return getExpectedDamage(formula); +} + +/** + * Given a simple flavor-less formula with only +/- operators, returns a list of damage partial terms. + * All subtracted terms become negative terms. + */ +function getExpectedDamage(formula) { + const terms = formula.replace("+", " + ").replace("-", " - ").split(" ").map(t => t.trim()); + let multiplier = 1; + return terms.reduce((total, term) => { + if (term === "-") { + multiplier = -1; + return total; + } else if (term === "+") { + return total; + } + + const currentMultiplier = multiplier; + multiplier = 1; + + const number = Number(term); + if (!Number.isNaN(number)) { + return total + currentMultiplier * number; + } + + const dieMatch = term.match(/(\d+)d(\d+)/); + if (dieMatch) { + const numDice = Number(dieMatch[1]); + const faces = Number(dieMatch[2]); + return total + currentMultiplier * numDice * ((faces + 1) / 2); + } + + throw Error(`Unexpected term ${term} in formula ${formula}`); + }, 0); +} diff --git a/tools/run-start.mjs b/tools/run-start.mjs index e620d13f..3f6b25cb 100644 --- a/tools/run-start.mjs +++ b/tools/run-start.mjs @@ -18,7 +18,7 @@ const foundryPath = process.env.FOUNDRY_MAIN_PATH || '../../../../FoundryDev/mai const dataPath = process.env.FOUNDRY_DATA_PATH || '../../../'; // Run the original command with proper environment -const args = ['rollup -c --watch', `node "${foundryPath}" --dataPath="${dataPath}" --noupnp`, 'gulp']; +const args = ['rollup -c --watch', `node "\"${foundryPath}\"" --dataPath="${dataPath}" --noupnp`, 'gulp']; spawn('npx', ['concurrently', ...args.map(arg => `"${arg}"`)], { stdio: 'inherit',