diff --git a/daggerheart.mjs b/daggerheart.mjs index 5a6d8193..aeae0dc6 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -5,7 +5,7 @@ import * as documents from './module/documents/_module.mjs'; import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs'; import { DhDualityRollEnricher, DhTemplateEnricher } from './module/enrichers/_module.mjs'; import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs'; -import { NarrativeCountdowns, registerCountdownApplicationHooks } from './module/applications/ui/countdowns.mjs'; +import { NarrativeCountdowns } from './module/applications/ui/countdowns.mjs'; import { DualityRollColor } from './module/data/settings/Appearance.mjs'; import { DHRoll, DualityRoll, D20Roll, DamageRoll, DualityDie } from './module/dice/_module.mjs'; import { renderDualityButton } from './module/enrichers/DualityRollEnricher.mjs'; @@ -61,6 +61,14 @@ Hooks.once('init', () => { CONFIG.Dice.rolls = [...CONFIG.Dice.rolls, ...[DHRoll, DualityRoll, D20Roll, DamageRoll]]; CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate; + const { DocumentSheetConfig } = foundry.applications.apps; + CONFIG.Token.documentClass = documents.DhToken; + CONFIG.Token.prototypeSheetClass = applications.sheetConfigs.DhPrototypeTokenConfig; + DocumentSheetConfig.unregisterSheet(TokenDocument, 'core', foundry.applications.sheets.TokenConfig); + DocumentSheetConfig.registerSheet(TokenDocument, 'dnd5e', applications.sheetConfigs.DhTokenConfig, { + makeDefault: true + }); + CONFIG.Item.documentClass = documents.DHItem; //Registering the Item DataModel @@ -98,12 +106,12 @@ Hooks.once('init', () => { CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect; CONFIG.ActiveEffect.dataModels = models.activeEffects.config; - foundry.applications.apps.DocumentSheetConfig.unregisterSheet( + DocumentSheetConfig.unregisterSheet( CONFIG.ActiveEffect.documentClass, 'core', foundry.applications.sheets.ActiveEffectConfig ); - foundry.applications.apps.DocumentSheetConfig.registerSheet( + DocumentSheetConfig.registerSheet( CONFIG.ActiveEffect.documentClass, SYSTEM.id, applications.sheetConfigs.ActiveEffectConfig, @@ -160,7 +168,6 @@ Hooks.on('ready', () => { registerCountdownHooks(); socketRegistration.registerSocketHooks(); - registerCountdownApplicationHooks(); registerRollDiceHooks(); registerDHActorHooks(); }); diff --git a/lang/en.json b/lang/en.json index bfaac070..b9e0fefa 100755 --- a/lang/en.json +++ b/lang/en.json @@ -1011,23 +1011,36 @@ "severe": "Severe", "major": "Major", "minor": "Minor", - "none": "None" + "none": "None", + "allDamage": "All Damage", + "physicalDamage": "Physical Damage", + "magicalDamage": "Magical Damage", + "primaryWeapon": "Primary Weapon Damage", + "secondaryWeapon": "Secondary Weapon Damage" }, "DamageResistance": { "none": "None", "resistance": "Resistance", - "immunity": "Immunity" + "immunity": "Immunity", + "physicalReduction": "Physical Damage Reduction", + "magicalReduction": "Magical Damage Reduction" }, "DamageThresholds": { "title": "Damage Thresholds", "minor": "Minor", "major": "Major", - "severe": "Severe" + "severe": "Severe", + "majorThreshold": "Major Damage Threshold", + "severeThreshold": "Severe Damage Threshold" }, "Dice": { "single": "Die", "plural": "Dice" }, + "Difficulty": { + "all": "Difficulty: all", + "reaction": "Difficulty: reaction" + }, "Disadvantage": { "full": "Disadvantage", "short": "Dis" @@ -1037,39 +1050,39 @@ "plural": "Domains", "arcana": { "label": "Arcana", - "Description": "This is the domain of the innate or instinctual use of magic. Those who walk this path tap into the raw, enigmatic forces of the realms to manipulate both the elements and their own energy. Arcana offers wielders a volatile power, but it is incredibly potent when correctly channeled." + "description": "This is the domain of the innate or instinctual use of magic. Those who walk this path tap into the raw, enigmatic forces of the realms to manipulate both the elements and their own energy. Arcana offers wielders a volatile power, but it is incredibly potent when correctly channeled." }, "blade": { "label": "Blade", - "Description": "This is the domain of those who dedicate their lives to the mastery of weapons. Whether by blade, bow, or perhaps a more specialized arm, those who follow this path have the skill to cut short the lives of others. Blade requires study and dedication from its followers, in exchange for inexorable power over death." + "description": "This is the domain of those who dedicate their lives to the mastery of weapons. Whether by blade, bow, or perhaps a more specialized arm, those who follow this path have the skill to cut short the lives of others. Blade requires study and dedication from its followers, in exchange for inexorable power over death." }, "bone": { "label": "Bone", - "Description": "This is the domain of mastery of swiftness and tactical mastery. Practitioners of this domain have an uncanny control over their own physical abilities, and an eye for predicting the behaviors of others in combat. Bone grants its adherents unparalleled understanding of bodies and their movements in exchange for diligent training." + "description": "This is the domain of mastery of swiftness and tactical mastery. Practitioners of this domain have an uncanny control over their own physical abilities, and an eye for predicting the behaviors of others in combat. Bone grants its adherents unparalleled understanding of bodies and their movements in exchange for diligent training." }, "codex": { "label": "Codex", - "Description": "This is the domain of intensive magical study. Those who seek magical knowledge turn to the recipes of power recorded in books, on scrolls, etched into walls, or tattooed on bodies. Codex offers a commanding and versatile understanding of magic to those devotees who are willing to seek beyond the common knowledge." + "description": "This is the domain of intensive magical study. Those who seek magical knowledge turn to the recipes of power recorded in books, on scrolls, etched into walls, or tattooed on bodies. Codex offers a commanding and versatile understanding of magic to those devotees who are willing to seek beyond the common knowledge." }, "grace": { "label": "Grace", - "Description": "This is the domain of charisma. Through rapturous storytelling, clever charm, or a shroud of lies, those who channel this power define the realities of their adversaries, bending perception to their will. Grace offers its wielders raw magnetism and mastery over language." + "description": "This is the domain of charisma. Through rapturous storytelling, clever charm, or a shroud of lies, those who channel this power define the realities of their adversaries, bending perception to their will. Grace offers its wielders raw magnetism and mastery over language." }, "midnight": { "label": "Midnight", - "Description": "This is the domain of shadows and secrecy. Whether by clever tricks, or cloak of night those who channel these forces are practiced in that art of obscurity and there is nothing hidden they cannot reach. Midnight offers practitioners the incredible power to control and create enigmas." + "description": "This is the domain of shadows and secrecy. Whether by clever tricks, or cloak of night those who channel these forces are practiced in that art of obscurity and there is nothing hidden they cannot reach. Midnight offers practitioners the incredible power to control and create enigmas." }, "sage": { "label": "Sage", - "Description": "This is the domain of the natural world. Those who walk this path tap into the unfettered power of the earth and its creatures to unleash raw magic. Sage grants its adherents the vitality of a blooming flower and ferocity of a hungry predator." + "description": "This is the domain of the natural world. Those who walk this path tap into the unfettered power of the earth and its creatures to unleash raw magic. Sage grants its adherents the vitality of a blooming flower and ferocity of a hungry predator." }, "splendor": { "label": "Splendor", - "Description": "This is the domain of life. Through this magic, followers gain the ability to heal, though such power also grants the wielder some control over death. Splendor offers its disciples the magnificent ability to both give and end life." + "description": "This is the domain of life. Through this magic, followers gain the ability to heal, though such power also grants the wielder some control over death. Splendor offers its disciples the magnificent ability to both give and end life." }, "valor": { "label": "Valor", - "Description": "This is the domain of protection. Whether through attack or defense, those who choose this discipline channel formidable strength to protect their allies in battle. Valor offers great power to those who raise their shield in defense of others." + "description": "This is the domain of protection. Whether through attack or defense, those who choose this discipline channel formidable strength to protect their allies in battle. Valor offers great power to those who raise their shield in defense of others." } }, "Effect": { @@ -1080,10 +1093,18 @@ "single": "Experience", "plural": "Experiences" }, + "Healing": { + "healingAmount": "Healing Amount" + }, "Neutral": { "full": "None", "short": "no" }, + "Range": { + "other": "Range Increase: Other", + "spell": "Range Increase: Spell", + "weapon": "Range Increase: Weapon" + }, "RefreshType": { "session": "Session", "shortrest": "Short Rest", @@ -1093,6 +1114,42 @@ "single": "Resource", "plural": "Resources" }, + "Roll": { + "attack": "Attack Roll", + "primaryWeaponAttack": "Primary Weapon Attack Roll", + "secondaryWeaponAttack": "Secondary Weapon Attack Roll", + "spellcast": "Spellcast Roll", + "trait": "Trait Roll", + "action": "Action Roll", + "reaction": "Reaction Roll" + }, + "Rules": { + "damageReduction": { + "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." + }, + "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." + }, + "stress": { + "severe": { + "label": "Stress Damage Reduction: Severe", + "hint": "The cost in stress you can pay to reduce severe damage down to major." + }, + "major": { + "label": "Stress Damage Reduction: Major", + "hint": "The cost in stress you can pay to reduce major damage down to minor." + }, + "minor": { + "label": "Stress Damage Reduction: Minor", + "hint": "The cost in stress you can pay to reduce minor damage to none." + } + } + } + }, "Tabs": { "details": "Details", "attack": "Attack", @@ -1137,6 +1194,7 @@ "single": "Trait", "plural": "Traits" }, + "armorScore": "Armor Score", "activeEffects": "Active Effects", "armorSlots": "Armor Slots", "attack": "Attack", @@ -1152,10 +1210,15 @@ "dualityRoll": "Duality Roll", "enabled": "Enabled", "evasion": "Evasion", + "experience": { + "single": "Experience", + "plural": "Experiences" + }, "fear": "Fear", "features": "Features", "hitPoints": "Hit Points", "hope": "Hope", + "hordeHp": "Horde HP", "inactiveEffects": "Inactive Effects", "inventory": "Inventory", "level": "Level", @@ -1163,9 +1226,12 @@ "modifier": "Modifier", "multiclass": "Multiclass", "none": "None", + "partner": "Partner", + "proficiency": "Proficiency", "quantity": "Quantity", "range": "Range", "recovery": "Recovery", + "roll": "Roll", "scalable": "Scalable", "stress": "Stress", "take": "Take", @@ -1272,9 +1338,10 @@ "hint": "Automatically increase the GM's fear pool on a fear duality roll result." }, "FIELDS": { - "hope": { - "label": "Hope", - "hint": "Automatically increase a character's hope on a hope duality roll result." + "hopeFear": { + "label": "Hope & Fear", + "gm": { "label": "GM" }, + "players": { "label": "Players" } }, "actionPoints": { "label": "Action Points", diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index 6cb0761c..7987dd6b 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -64,6 +64,13 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio context.rollConfig = this.config; context.hasRoll = !!this.config.roll; context.canRoll = true; + context.selectedRollMode = this.config.selectedRollMode; + context.rollModes = Object.entries(CONFIG.Dice.rollModes).map(([action, { label, icon }]) => ({ + action, + label, + icon + })); + if (this.config.costs?.length) { const updatedCosts = this.action.calcCosts(this.config.costs); context.costs = updatedCosts.map(x => ({ @@ -99,6 +106,8 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio static updateRollConfiguration(event, _, formData) { const { ...rest } = foundry.utils.expandObject(formData.object); + this.config.selectedRollMode = rest.selectedRollMode; + if (this.config.costs) { this.config.costs = foundry.utils.mergeObject(this.config.costs, rest.costs); } @@ -122,11 +131,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio } static selectExperience(_, button) { - /* if (this.config.experiences.find(x => x === button.dataset.key)) { - this.config.experiences = this.config.experiences.filter(x => x !== button.dataset.key); - } else { - this.config.experiences = [...this.config.experiences, button.dataset.key]; - } */ this.config.experiences = this.config.experiences.indexOf(button.dataset.key) > -1 ? this.config.experiences.filter(x => x !== button.dataset.key) diff --git a/module/applications/dialogs/damageDialog.mjs b/module/applications/dialogs/damageDialog.mjs index 4030d7a7..70dcace8 100644 --- a/module/applications/dialogs/damageDialog.mjs +++ b/module/applications/dialogs/damageDialog.mjs @@ -48,12 +48,22 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application : game.i18n.localize('DAGGERHEART.EFFECTS.ApplyLocations.damageRoll.name'); context.extraFormula = this.config.extraFormula; context.formula = this.roll.constructFormula(this.config); + context.directDamage = this.config.directDamage; + context.selectedRollMode = this.config.selectedRollMode; + context.rollModes = Object.entries(CONFIG.Dice.rollModes).map(([action, { label, icon }]) => ({ + action, + label, + icon + })); + return context; } - static updateRollConfiguration(event, _, formData) { + static updateRollConfiguration(_event, _, formData) { const { ...rest } = foundry.utils.expandObject(formData.object); this.config.extraFormula = rest.extraFormula; + this.config.selectedRollMode = rest.selectedRollMode; + this.render(); } diff --git a/module/applications/hud/_module.mjs b/module/applications/hud/_module.mjs index 70edaf8f..92abe4e8 100644 --- a/module/applications/hud/_module.mjs +++ b/module/applications/hud/_module.mjs @@ -1 +1 @@ -export { default as DHTokenHUD } from './tokenHud.mjs'; +export { default as DHTokenHUD } from './tokenHUD.mjs'; diff --git a/module/applications/hud/tokenHUD.mjs b/module/applications/hud/tokenHUD.mjs index 9a58bab2..572b03f9 100644 --- a/module/applications/hud/tokenHUD.mjs +++ b/module/applications/hud/tokenHUD.mjs @@ -1,4 +1,4 @@ -export default class DHTokenHUD extends TokenHUD { +export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { static DEFAULT_OPTIONS = { classes: ['daggerheart'] }; diff --git a/module/applications/sheets-configs/_module.mjs b/module/applications/sheets-configs/_module.mjs index e1ae8fe2..fafb1fcf 100644 --- a/module/applications/sheets-configs/_module.mjs +++ b/module/applications/sheets-configs/_module.mjs @@ -3,3 +3,5 @@ export { default as AdversarySettings } from './adversary-settings.mjs'; export { default as CompanionSettings } from './companion-settings.mjs'; export { default as EnvironmentSettings } from './environment-settings.mjs'; export { default as ActiveEffectConfig } from './activeEffectConfig.mjs'; +export { default as DhTokenConfig } from './token-config.mjs'; +export { default as DhPrototypeTokenConfig } from './prototype-token-config.mjs'; diff --git a/module/applications/sheets-configs/activeEffectConfig.mjs b/module/applications/sheets-configs/activeEffectConfig.mjs index 6a629583..8574a5db 100644 --- a/module/applications/sheets-configs/activeEffectConfig.mjs +++ b/module/applications/sheets-configs/activeEffectConfig.mjs @@ -1,4 +1,24 @@ +import autocomplete from 'autocompleter'; + export default class DhActiveEffectConfig extends foundry.applications.sheets.ActiveEffectConfig { + 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); + const group = game.i18n.localize(model.metadata.label); + const choices = CONFIG.Token.documentClass + .getTrackedAttributeChoices(attributes, model) + .map(x => ({ ...x, group: group })); + acc.push(...choices); + } + return acc; + }, []); + } + static DEFAULT_OPTIONS = { classes: ['daggerheart', 'sheet', 'dh-style'] }; @@ -27,36 +47,59 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac } }; + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + const changeChoices = this.changeChoices; + + htmlElement.querySelectorAll('.effect-change-input').forEach(element => { + autocomplete({ + input: element, + fetch: function (text, update) { + if (!text) { + update(changeChoices); + } else { + text = text.toLowerCase(); + var suggestions = changeChoices.filter(n => n.label.toLowerCase().includes(text)); + update(suggestions); + } + }, + render: function (item, search) { + const label = game.i18n.localize(item.label); + const matchIndex = label.toLowerCase().indexOf(search); + + const beforeText = label.slice(0, matchIndex); + const matchText = label.slice(matchIndex, matchIndex + search.length); + const after = label.slice(matchIndex + search.length, label.length); + + const element = document.createElement('li'); + element.innerHTML = `${beforeText}${matchText ? `${matchText}` : ''}${after}`; + if (item.hint) { + element.dataset.tooltip = game.i18n.localize(item.hint); + } + + return element; + }, + renderGroup: function (label) { + const itemElement = document.createElement('div'); + itemElement.textContent = game.i18n.localize(label); + return itemElement; + }, + onSelect: function (item) { + element.value = `system.${item.value}`; + }, + click: e => e.fetch(), + minLength: 0 + }); + }); + } + async _preparePartContext(partId, context) { const partContext = await super._preparePartContext(partId, context); switch (partId) { case 'changes': - const fieldPaths = []; - const validFieldPath = fieldPath => this.validFieldPath(fieldPath, this.#unapplicablePaths); - context.document.parent.system.schema.apply(function () { - if (!(this instanceof foundry.data.fields.SchemaField)) { - if (validFieldPath(this.fieldPath)) { - fieldPaths.push(this.fieldPath); - } - } - }); - - context.fieldPaths = fieldPaths; - break; } return partContext; } - - #unapplicablePaths = ['story', 'pronouns', 'description']; - validFieldPath(fieldPath, unapplicablePaths) { - const splitPath = fieldPath.split('.'); - if (splitPath.length > 1 && unapplicablePaths.includes(splitPath[1])) return false; - - /* The current value of a resource should not be modified */ - if (new RegExp(/resources.*\.value/).exec(fieldPath)) return false; - - return true; - } } diff --git a/module/applications/sheets-configs/prototype-token-config.mjs b/module/applications/sheets-configs/prototype-token-config.mjs new file mode 100644 index 00000000..24c9dabb --- /dev/null +++ b/module/applications/sheets-configs/prototype-token-config.mjs @@ -0,0 +1,20 @@ +export default class DhPrototypeTokenConfig extends foundry.applications.sheets.PrototypeTokenConfig { + /** @inheritDoc */ + async _prepareResourcesTab() { + const token = this.token; + const usesTrackableAttributes = !foundry.utils.isEmpty(CONFIG.Actor.trackableAttributes); + const attributeSource = + this.actor?.system instanceof foundry.abstract.DataModel && usesTrackableAttributes + ? this.actor?.type + : this.actor?.system; + const TokenDocument = foundry.utils.getDocumentClass('Token'); + const attributes = TokenDocument.getTrackedAttributes(attributeSource); + return { + barAttributes: TokenDocument.getTrackedAttributeChoices(attributes, attributeSource), + bar1: token.getBarAttribute?.('bar1'), + bar2: token.getBarAttribute?.('bar2'), + turnMarkerModes: DhPrototypeTokenConfig.TURN_MARKER_MODES, + turnMarkerAnimations: CONFIG.Combat.settings.turnMarkerAnimations + }; + } +} diff --git a/module/applications/sheets-configs/token-config.mjs b/module/applications/sheets-configs/token-config.mjs new file mode 100644 index 00000000..ee573e5d --- /dev/null +++ b/module/applications/sheets-configs/token-config.mjs @@ -0,0 +1,20 @@ +export default class DhTokenConfig extends foundry.applications.sheets.TokenConfig { + /** @inheritDoc */ + async _prepareResourcesTab() { + const token = this.token; + const usesTrackableAttributes = !foundry.utils.isEmpty(CONFIG.Actor.trackableAttributes); + const attributeSource = + this.actor?.system instanceof foundry.abstract.DataModel && usesTrackableAttributes + ? this.actor?.type + : this.actor?.system; + const TokenDocument = foundry.utils.getDocumentClass('Token'); + const attributes = TokenDocument.getTrackedAttributes(attributeSource); + return { + barAttributes: TokenDocument.getTrackedAttributeChoices(attributes, attributeSource), + bar1: token.getBarAttribute?.('bar1'), + bar2: token.getBarAttribute?.('bar2'), + turnMarkerModes: DhTokenConfig.TURN_MARKER_MODES, + turnMarkerAnimations: CONFIG.Combat.settings.turnMarkerAnimations + }; + } +} diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 99545d4e..139a1369 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -103,7 +103,7 @@ export default class CharacterSheet extends DHBaseActorSheet { htmlElement.querySelectorAll('.inventory-item-quantity').forEach(element => { element.addEventListener('change', this.updateItemQuantity.bind(this)); }); - + // Add listener for armor marks input htmlElement.querySelectorAll('.armor-marks-input').forEach(element => { element.addEventListener('change', this.updateArmorMarks.bind(this)); @@ -669,10 +669,7 @@ export default class CharacterSheet extends DHBaseActorSheet { } else if (item instanceof ActiveEffect) { item.toChat(this); } else { - const wasUsed = await item.use(event); - if (wasUsed && item.type === 'weapon') { - Hooks.callAll(CONFIG.DH.HOOKS.characterAttack, {}); - } + item.use(event); } } diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index aa604dda..4570b076 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -88,7 +88,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo onRollDamage = async (event, message) => { event.stopPropagation(); const actor = await this.getActor(message.system.source.actor); - if (!actor || !game.user.isGM) return true; + if (game.user.character?.id !== actor.id && !game.user.isGM) return true; if (message.system.source.item && message.system.source.action) { const action = this.getAction(actor, message.system.source.item, message.system.source.action); if (!action || !action?.rollDamage) return; diff --git a/module/applications/ui/combatTracker.mjs b/module/applications/ui/combatTracker.mjs index f9f49ad1..b3348fe2 100644 --- a/module/applications/ui/combatTracker.mjs +++ b/module/applications/ui/combatTracker.mjs @@ -66,6 +66,11 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C } async setCombatantSpotlight(combatantId) { + const update = { + system: { + 'spotlight.requesting': false + } + }; const combatant = this.viewed.combatants.get(combatantId); const toggleTurn = this.viewed.combatants.contents @@ -73,10 +78,18 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C .map(x => x.id) .indexOf(combatantId); - if (this.viewed.turn !== toggleTurn) Hooks.callAll(CONFIG.DH.HOOKS.spotlight, {}); + if (this.viewed.turn !== toggleTurn) { + const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns; + await updateCountdowns(CONFIG.DH.GENERAL.countdownTypes.spotlight.id); + + const autoPoints = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).actionPoints; + if (autoPoints) { + update.system.actionTokens = Math.max(combatant.system.actionTokens - 1, 0); + } + } await this.viewed.update({ turn: this.viewed.turn === toggleTurn ? null : toggleTurn }); - await combatant.update({ 'system.spotlight.requesting': false }); + await combatant.update(update); } static async requestSpotlight(_, target) { diff --git a/module/applications/ui/countdowns.mjs b/module/applications/ui/countdowns.mjs index c229cda1..5e3ad1ab 100644 --- a/module/applications/ui/countdowns.mjs +++ b/module/applications/ui/countdowns.mjs @@ -1,4 +1,3 @@ -import { countdownTypes } from '../../config/generalConfig.mjs'; import { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import constructHTMLButton from '../../helpers/utils.mjs'; import OwnershipSelection from '../dialogs/ownershipSelection.mjs'; @@ -328,43 +327,29 @@ export class EncounterCountdowns extends Countdowns { }; } -export const registerCountdownApplicationHooks = () => { - const updateCountdowns = async shouldProgress => { - if (game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).countdowns) { - const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); - for (let countdownCategoryKey in countdownSetting) { - const countdownCategory = countdownSetting[countdownCategoryKey]; - for (let countdownKey in countdownCategory.countdowns) { - const countdown = countdownCategory.countdowns[countdownKey]; - - if (shouldProgress(countdown)) { - await countdownSetting.updateSource({ - [`${countdownCategoryKey}.countdowns.${countdownKey}.progress.current`]: - countdown.progress.current - 1 - }); - await game.settings.set( - CONFIG.DH.id, - CONFIG.DH.SETTINGS.gameSettings.Countdowns, - countdownSetting - ); - foundry.applications.instances.get(`${countdownCategoryKey}-countdowns`)?.render(); - } +export async function updateCountdowns(progressType) { + const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); + const update = Object.keys(countdownSetting).reduce((update, typeKey) => { + return foundry.utils.mergeObject( + update, + Object.keys(countdownSetting[typeKey].countdowns).reduce((acc, countdownKey) => { + const countdown = countdownSetting[typeKey].countdowns[countdownKey]; + if (countdown.progress.current > 0 && countdown.progress.type.value === progressType) { + acc[`${typeKey}.countdowns.${countdownKey}.progress.current`] = countdown.progress.current - 1; } - } - } - }; - Hooks.on(CONFIG.DH.HOOKS.characterAttack, async () => { - updateCountdowns(countdown => { - return ( - countdown.progress.type.value === countdownTypes.characterAttack.id && countdown.progress.current > 0 - ); - }); - }); + return acc; + }, {}) + ); + }, {}); - Hooks.on(CONFIG.DH.HOOKS.spotlight, async () => { - updateCountdowns(countdown => { - return countdown.progress.type.value === countdownTypes.spotlight.id && countdown.progress.current > 0; - }); + await countdownSetting.updateSource(update); + await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, countdownSetting); + + const data = { refreshType: RefreshType.Countdown }; + await game.socket.emit(`system.${CONFIG.DH.id}`, { + action: socketEvent.Refresh, + data }); -}; + Hooks.callAll(socketEvent.Refresh, data); +} diff --git a/module/config/_module.mjs b/module/config/_module.mjs index 88003595..99069dda 100644 --- a/module/config/_module.mjs +++ b/module/config/_module.mjs @@ -4,7 +4,6 @@ export * as domainConfig from './domainConfig.mjs'; export * as effectConfig from './effectConfig.mjs'; export * as flagsConfig from './flagsConfig.mjs'; export * as generalConfig from './generalConfig.mjs'; -export * as hooksConfig from './hooksConfig.mjs'; export * as itemConfig from './itemConfig.mjs'; export * as settingsConfig from './settingsConfig.mjs'; export * as systemConfig from './system.mjs'; diff --git a/module/config/domainConfig.mjs b/module/config/domainConfig.mjs index 6c8b8bfd..2387e00f 100644 --- a/module/config/domainConfig.mjs +++ b/module/config/domainConfig.mjs @@ -3,55 +3,55 @@ export const domains = { id: 'arcana', label: 'DAGGERHEART.GENERAL.Domain.arcana.label', src: 'systems/daggerheart/assets/icons/domains/arcana.svg', - description: 'DAGGERHEART.GENERAL.Domain.Arcana' + description: 'DAGGERHEART.GENERAL.Domain.arcana.description' }, blade: { id: 'blade', label: 'DAGGERHEART.GENERAL.Domain.blade.label', src: 'systems/daggerheart/assets/icons/domains/blade.svg', - description: 'DAGGERHEART.GENERAL.Domain.Blade' + description: 'DAGGERHEART.GENERAL.Domain.blade.description' }, bone: { id: 'bone', label: 'DAGGERHEART.GENERAL.Domain.bone.label', src: 'systems/daggerheart/assets/icons/domains/bone.svg', - description: 'DAGGERHEART.GENERAL.Domain.Bone' + description: 'DAGGERHEART.GENERAL.Domain.bone.description' }, codex: { id: 'codex', label: 'DAGGERHEART.GENERAL.Domain.codex.label', src: 'systems/daggerheart/assets/icons/domains/codex.svg', - description: 'DAGGERHEART.GENERAL.Domain.Codex' + description: 'DAGGERHEART.GENERAL.Domain.codex.description' }, grace: { id: 'grace', label: 'DAGGERHEART.GENERAL.Domain.grace.label', src: 'systems/daggerheart/assets/icons/domains/grace.svg', - description: 'DAGGERHEART.GENERAL.Domain.Grace' + description: 'DAGGERHEART.GENERAL.Domain.grace.description' }, midnight: { id: 'midnight', label: 'DAGGERHEART.GENERAL.Domain.midnight.label', src: 'systems/daggerheart/assets/icons/domains/midnight.svg', - description: 'DAGGERHEART.GENERAL.Domain.Midnight' + description: 'DAGGERHEART.GENERAL.Domain.midnight.description' }, sage: { id: 'sage', label: 'DAGGERHEART.GENERAL.Domain.sage.label', src: 'systems/daggerheart/assets/icons/domains/sage.svg', - description: 'DAGGERHEART.GENERAL.Domain.Sage' + description: 'DAGGERHEART.GENERAL.Domain.sage.description' }, splendor: { id: 'splendor', label: 'DAGGERHEART.GENERAL.Domain.splendor.label', src: 'systems/daggerheart/assets/icons/domains/splendor.svg', - description: 'DAGGERHEART.GENERAL.Domain.Splendor' + description: 'DAGGERHEART.GENERAL.Domain.splendor.description' }, valor: { id: 'valor', label: 'DAGGERHEART.GENERAL.Domain.valor.label', src: 'systems/daggerheart/assets/icons/domains/valor.svg', - description: 'DAGGERHEART.GENERAL.Domain.Valor' + description: 'DAGGERHEART.GENERAL.Domain.valor.description' } }; diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 3522b41a..54430860 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -376,15 +376,15 @@ export const abilityCosts = { export const countdownTypes = { spotlight: { id: 'spotlight', - label: 'DAGGERHEART.CONFIG.CountdownTypes.Spotlight' + label: 'DAGGERHEART.CONFIG.CountdownType.spotlight' }, characterAttack: { id: 'characterAttack', - label: 'DAGGERHEART.CONFIG.CountdownTypes.CharacterAttack' + label: 'DAGGERHEART.CONFIG.CountdownType.characterAttack' }, custom: { id: 'custom', - label: 'DAGGERHEART.CONFIG.CountdownTypes.Custom' + label: 'DAGGERHEART.CONFIG.CountdownType.custom' } }; export const rollTypes = { diff --git a/module/config/hooksConfig.mjs b/module/config/hooksConfig.mjs deleted file mode 100644 index 8410c0de..00000000 --- a/module/config/hooksConfig.mjs +++ /dev/null @@ -1,4 +0,0 @@ -export const hooks = { - characterAttack: 'characterAttackHook', - spotlight: 'spotlightHook' -}; diff --git a/module/config/system.mjs b/module/config/system.mjs index 6ad0e689..e72667b1 100644 --- a/module/config/system.mjs +++ b/module/config/system.mjs @@ -3,7 +3,6 @@ import * as DOMAIN from './domainConfig.mjs'; import * as ACTOR from './actorConfig.mjs'; import * as ITEM from './itemConfig.mjs'; import * as SETTINGS from './settingsConfig.mjs'; -import { hooks as HOOKS } from './hooksConfig.mjs'; import * as EFFECTS from './effectConfig.mjs'; import * as ACTIONS from './actionConfig.mjs'; import * as FLAGS from './flagsConfig.mjs'; @@ -17,7 +16,6 @@ export const SYSTEM = { ACTOR, ITEM, SETTINGS, - HOOKS, EFFECTS, ACTIONS, FLAGS diff --git a/module/data/action/attackAction.mjs b/module/data/action/attackAction.mjs index 137879b8..e17c0e9d 100644 --- a/module/data/action/attackAction.mjs +++ b/module/data/action/attackAction.mjs @@ -38,6 +38,15 @@ export default class DHAttackAction extends DHDamageAction { }; } + async use(event, ...args) { + const result = await super.use(event, args); + + const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns; + await updateCountdowns(CONFIG.DH.GENERAL.countdownTypes.characterAttack.id); + + return result; + } + // get modifiers() { // return []; // } diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index 088012e3..a2db300f 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -254,7 +254,8 @@ export default class DHBaseAction extends foundry.abstract.DataModel { hasDamage: !!this.damage?.parts?.length, hasHealing: !!this.healing, hasEffect: !!this.effects?.length, - hasSave: this.hasSave + hasSave: this.hasSave, + selectedRollMode: game.settings.get('core', 'rollMode') }; } @@ -350,7 +351,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel { } get modifiers() { - if(!this.actor) return []; + if (!this.actor) return []; const modifiers = []; /** Placeholder for specific bonuses **/ return modifiers; diff --git a/module/data/action/damageAction.mjs b/module/data/action/damageAction.mjs index 492c4184..388c5eb8 100644 --- a/module/data/action/damageAction.mjs +++ b/module/data/action/damageAction.mjs @@ -10,6 +10,7 @@ export default class DHDamageAction extends DHBaseAction { } async rollDamage(event, data) { + const systemData = data.system ?? data; let formula = this.damage.parts.map(p => this.getFormulaValue(p, data).getFormula(this.actor)).join(' + '), damageTypes = [...new Set(this.damage.parts.reduce((a, c) => a.concat([...c.type]), []))]; @@ -19,15 +20,15 @@ export default class DHDamageAction extends DHBaseAction { let roll = { formula: formula, total: formula }, bonusDamage = []; - if (isNaN(formula)) formula = Roll.replaceFormulaData(formula, this.getRollData(data.system ?? data)); - + if (isNaN(formula)) formula = Roll.replaceFormulaData(formula, this.getRollData(systemData)); + const config = { title: game.i18n.format('DAGGERHEART.UI.Chat.damageRoll.title', { damage: this.name }), roll: { formula }, - targets: data.system?.targets.filter(t => t.hit) ?? data.targets, + targets: systemData.targets.filter(t => t.hit) ?? data.targets, hasSave: this.hasSave, - isCritical: data.system?.roll?.isCritical ?? false, - source: data.system?.source, + isCritical: systemData.roll?.isCritical ?? false, + source: systemData.source, data: this.getRollData(), damageTypes, event @@ -36,6 +37,8 @@ export default class DHDamageAction extends DHBaseAction { if (data.system) { config.source.message = data._id; config.directDamage = false; + } else { + config.directDamage = true; } roll = CONFIG.Dice.daggerheart.DamageRoll.build(config); diff --git a/module/data/actor/adversary.mjs b/module/data/actor/adversary.mjs index 6ecee0a1..e77a4855 100644 --- a/module/data/actor/adversary.mjs +++ b/module/data/actor/adversary.mjs @@ -31,14 +31,29 @@ export default class DhpAdversary extends BaseDataActor { motivesAndTactics: new fields.StringField(), notes: new fields.HTMLField(), difficulty: new fields.NumberField({ required: true, initial: 1, integer: true }), - hordeHp: new fields.NumberField({ required: true, initial: 1, integer: true }), + hordeHp: new fields.NumberField({ + required: true, + initial: 1, + integer: true, + label: 'DAGGERHEART.GENERAL.hordeHp' + }), damageThresholds: new fields.SchemaField({ - major: new fields.NumberField({ required: true, initial: 0, integer: true }), - severe: new fields.NumberField({ required: true, initial: 0, integer: true }) + major: new fields.NumberField({ + required: true, + initial: 0, + integer: true, + label: 'DAGGERHEART.GENERAL.DamageThresholds.majorThreshold' + }), + severe: new fields.NumberField({ + required: true, + initial: 0, + integer: true, + label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold' + }) }), resources: new fields.SchemaField({ - hitPoints: resourceField(0, true), - stress: resourceField(0, true) + hitPoints: resourceField(0, 'DAGGERHEART.GENERAL.hitPoints', true), + stress: resourceField(0, 'DAGGERHEART.GENERAL.stress', true) }), attack: new ActionField({ initial: { @@ -75,13 +90,13 @@ export default class DhpAdversary extends BaseDataActor { ), bonuses: new fields.SchemaField({ roll: new fields.SchemaField({ - attack: bonusField(), - action: bonusField(), - reaction: bonusField() + attack: bonusField('DAGGERHEART.GENERAL.Roll.attack'), + action: bonusField('DAGGERHEART.GENERAL.Roll.action'), + reaction: bonusField('DAGGERHEART.GENERAL.Roll.reaction') }), damage: new fields.SchemaField({ - physical: bonusField(), - magical: bonusField() + physical: bonusField('DAGGERHEART.GENERAL.Damage.physicalDamage'), + magical: bonusField('DAGGERHEART.GENERAL.Damage.magicalDamage') }) }) }; diff --git a/module/data/actor/base.mjs b/module/data/actor/base.mjs index cf1eebb1..19de7b06 100644 --- a/module/data/actor/base.mjs +++ b/module/data/actor/base.mjs @@ -1,10 +1,10 @@ import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs'; -const resistanceField = () => +const resistanceField = reductionLabel => new foundry.data.fields.SchemaField({ resistance: new foundry.data.fields.BooleanField({ initial: false }), immunity: new foundry.data.fields.BooleanField({ initial: false }), - reduction: new foundry.data.fields.NumberField({ integer: true, initial: 0 }) + reduction: new foundry.data.fields.NumberField({ integer: true, initial: 0, label: reductionLabel }) }); /** @@ -40,8 +40,8 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel { if (this.metadata.isNPC) schema.description = new fields.HTMLField({ required: true, nullable: true }); if (this.metadata.hasResistances) schema.resistance = new fields.SchemaField({ - physical: resistanceField(), - magical: resistanceField() + physical: resistanceField('DAGGERHEART.GENERAL.DamageResistance.physicalReduction'), + magical: resistanceField('DAGGERHEART.GENERAL.DamageResistance.magicalReduction') }); return schema; } diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 0df5dd2f..e25dba85 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -5,6 +5,8 @@ import BaseDataActor from './base.mjs'; import { attributeField, resourceField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs'; export default class DhCharacter extends BaseDataActor { + static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Character']; + static get metadata() { return foundry.utils.mergeObject(super.metadata, { label: 'TYPES.Actor.character', @@ -19,24 +21,36 @@ export default class DhCharacter extends BaseDataActor { return { ...super.defineSchema(), resources: new fields.SchemaField({ - hitPoints: resourceField(0, true), - stress: resourceField(6, true), - hope: resourceField(6) + hitPoints: resourceField(0, 'DAGGERHEART.GENERAL.hitPoints', true), + stress: resourceField(6, 'DAGGERHEART.GENERAL.stress', true), + hope: resourceField(6, 'DAGGERHEART.GENERAL.hope') }), traits: new fields.SchemaField({ - agility: attributeField(), - strength: attributeField(), - finesse: attributeField(), - instinct: attributeField(), - presence: attributeField(), - knowledge: attributeField() + agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'), + strength: attributeField('DAGGERHEART.CONFIG.Traits.strength.name'), + finesse: attributeField('DAGGERHEART.CONFIG.Traits.finesse.name'), + instinct: attributeField('DAGGERHEART.CONFIG.Traits.instinct.name'), + presence: attributeField('DAGGERHEART.CONFIG.Traits.presence.name'), + knowledge: attributeField('DAGGERHEART.CONFIG.Traits.knowledge.name') }), - proficiency: new fields.NumberField({ initial: 1, integer: true }), - evasion: new fields.NumberField({ initial: 0, integer: true }), - armorScore: new fields.NumberField({ integer: true, initial: 0 }), + proficiency: new fields.NumberField({ + initial: 1, + integer: true, + label: 'DAGGERHEART.GENERAL.proficiency' + }), + evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }), + armorScore: new fields.NumberField({ integer: true, initial: 0, label: 'DAGGERHEART.GENERAL.armorScore' }), damageThresholds: new fields.SchemaField({ - severe: new fields.NumberField({ integer: true, initial: 0 }), - major: new fields.NumberField({ integer: true, initial: 0 }) + severe: new fields.NumberField({ + integer: true, + initial: 0, + label: 'DAGGERHEART.GENERAL.DamageThresholds.majorThreshold' + }), + major: new fields.NumberField({ + integer: true, + initial: 0, + label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold' + }) }), experiences: new fields.TypedObjectField( new fields.SchemaField({ @@ -76,25 +90,37 @@ export default class DhCharacter extends BaseDataActor { levelData: new fields.EmbeddedDataField(DhLevelData), bonuses: new fields.SchemaField({ roll: new fields.SchemaField({ - attack: bonusField(), - spellcast: bonusField(), - trait: bonusField(), - action: bonusField(), - reaction: bonusField(), - primaryWeapon: bonusField(), - secondaryWeapon: bonusField() + attack: bonusField('DAGGERHEART.GENERAL.Roll.attack'), + spellcast: bonusField('DAGGERHEART.GENERAL.Roll.spellcast'), + trait: bonusField('DAGGERHEART.GENERAL.Roll.trait'), + action: bonusField('DAGGERHEART.GENERAL.Roll.action'), + reaction: bonusField('DAGGERHEART.GENERAL.Roll.reaction'), + primaryWeapon: bonusField('DAGGERHEART.GENERAL.Roll.primaryWeaponAttack'), + secondaryWeapon: bonusField('DAGGERHEART.GENERAL.Roll.secondaryWeaponAttack') }), damage: new fields.SchemaField({ - physical: bonusField(), - magical: bonusField(), - primaryWeapon: bonusField(), - secondaryWeapon: bonusField() + physical: bonusField('DAGGERHEART.GENERAL.Damage.physicalDamage'), + magical: bonusField('DAGGERHEART.GENERAL.Damage.magicalDamage'), + primaryWeapon: bonusField('DAGGERHEART.GENERAL.Damage.primaryWeapon'), + secondaryWeapon: bonusField('DAGGERHEART.GENERAL.Damage.primaryWeapon') }), - healing: bonusField(), + healing: bonusField('DAGGERHEART.GENERAL.Healing.healingAmount'), range: new fields.SchemaField({ - weapon: new fields.NumberField({ integer: true, initial: 0 }), - spell: new fields.NumberField({ integer: true, initial: 0 }), - other: new fields.NumberField({ integer: true, initial: 0 }) + weapon: new fields.NumberField({ + integer: true, + initial: 0, + label: 'DAGGERHEART.GENERAL.Range.weapon' + }), + spell: new fields.NumberField({ + integer: true, + initial: 0, + label: 'DAGGERHEART.GENERAL.Range.spell' + }), + other: new fields.NumberField({ + integer: true, + initial: 0, + label: 'DAGGERHEART.GENERAL.Range.other' + }) }) }), companion: new ForeignDocumentUUIDField({ type: 'Actor', nullable: true, initial: null }), @@ -102,25 +128,34 @@ export default class DhCharacter extends BaseDataActor { damageReduction: new fields.SchemaField({ maxArmorMarked: new fields.SchemaField({ value: new fields.NumberField({ required: true, integer: true, initial: 1 }), - bonus: new fields.NumberField({ required: true, integer: true, initial: 0 }), - stressExtra: new fields.NumberField({ required: true, integer: true, initial: 0 }) + bonus: new fields.NumberField({ + required: true, + integer: true, + initial: 0, + label: 'DAGGERHEART.GENERAL.Rules.damageReduction.maxArmorMarkedBonus' + }), + stressExtra: new fields.NumberField({ + required: true, + integer: true, + initial: 0, + label: 'DAGGERHEART.GENERAL.Rules.damageReduction.maxArmorMarkedStress.label', + hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.maxArmorMarkedStress.hint' + }) }), stressDamageReduction: new fields.SchemaField({ - severe: stressDamageReductionRule(), - major: stressDamageReductionRule(), - minor: stressDamageReductionRule() + severe: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.severe'), + major: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.major'), + minor: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.minor') + }), + increasePerArmorMark: new fields.NumberField({ + integer: true, + initial: 1, + label: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.label', + hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint' }), - increasePerArmorMark: new fields.NumberField({ integer: true, initial: 1 }), magical: new fields.BooleanField({ initial: false }), physical: new fields.BooleanField({ initial: false }) }), - strangePatterns: new fields.NumberField({ - integer: true, - min: 1, - max: 12, - nullable: true, - initial: null - }), weapon: new fields.SchemaField({ /* Unimplemented -> Should remove the lowest damage dice from weapon damage diff --git a/module/data/actor/companion.mjs b/module/data/actor/companion.mjs index f7e94311..005d7a83 100644 --- a/module/data/actor/companion.mjs +++ b/module/data/actor/companion.mjs @@ -24,10 +24,16 @@ export default class DhCompanion extends BaseDataActor { ...super.defineSchema(), partner: new ForeignDocumentUUIDField({ type: 'Actor' }), resources: new fields.SchemaField({ - stress: resourceField(3, true), - hope: new fields.NumberField({ initial: 0, integer: true }) + stress: resourceField(3, 'DAGGERHEART.GENERAL.stress', true), + hope: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.hope' }) + }), + evasion: new fields.NumberField({ + required: true, + min: 1, + initial: 10, + integer: true, + label: 'DAGGERHEART.GENERAL.evasion' }), - evasion: new fields.NumberField({ required: true, min: 1, initial: 10, integer: true }), experiences: new fields.TypedObjectField( new fields.SchemaField({ name: new fields.StringField({}), @@ -74,8 +80,8 @@ export default class DhCompanion extends BaseDataActor { levelData: new fields.EmbeddedDataField(DhLevelData), bonuses: new fields.SchemaField({ damage: new fields.SchemaField({ - physical: bonusField(), - magical: bonusField() + physical: bonusField('DAGGERHEART.GENERAL.Damage.physicalDamage'), + magical: bonusField('DAGGERHEART.GENERAL.Damage.magicalDamage') }) }) }; diff --git a/module/data/countdowns.mjs b/module/data/countdowns.mjs index e9649f6e..881ecf20 100644 --- a/module/data/countdowns.mjs +++ b/module/data/countdowns.mjs @@ -102,7 +102,7 @@ class DhCountdown extends foundry.abstract.DataModel { value: new fields.StringField({ required: true, choices: CONFIG.DH.GENERAL.countdownTypes, - initial: CONFIG.DH.GENERAL.countdownTypes.spotlight.id, + initial: CONFIG.DH.GENERAL.countdownTypes.custom.id, label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.progress.type.value.label' }), label: new fields.StringField({ @@ -132,7 +132,13 @@ class DhCountdown extends foundry.abstract.DataModel { export const registerCountdownHooks = () => { Hooks.on(socketEvent.Refresh, ({ refreshType, application }) => { if (refreshType === RefreshType.Countdown) { - foundry.applications.instances.get(application)?.render(); + if (application) { + foundry.applications.instances.get(application)?.render(); + } else { + foundry.applications.instances.get('narrative-countdowns').render(); + foundry.applications.instances.get('encounter-countdowns').render(); + } + return false; } }); diff --git a/module/data/fields/actorField.mjs b/module/data/fields/actorField.mjs index dc8dcbac..fe00e251 100644 --- a/module/data/fields/actorField.mjs +++ b/module/data/fields/actorField.mjs @@ -1,28 +1,32 @@ const fields = foundry.data.fields; -const attributeField = () => +const attributeField = label => new fields.SchemaField({ - value: new fields.NumberField({ initial: 0, integer: true }), + value: new fields.NumberField({ initial: 0, integer: true, label }), tierMarked: new fields.BooleanField({ initial: false }) }); -const resourceField = (max = 0, reverse = false) => +const resourceField = (max = 0, label, reverse = false) => new fields.SchemaField({ - value: new fields.NumberField({ initial: 0, integer: true }), + value: new fields.NumberField({ initial: 0, integer: true, label }), max: new fields.NumberField({ initial: max, integer: true }), isReversed: new fields.BooleanField({ initial: reverse }) }); -const stressDamageReductionRule = () => +const stressDamageReductionRule = localizationPath => new fields.SchemaField({ enabled: new fields.BooleanField({ required: true, initial: false }), - cost: new fields.NumberField({ integer: true }) + cost: new fields.NumberField({ + integer: true, + label: `${localizationPath}.label`, + hint: `${localizationPath}.hint` + }) }); -const bonusField = () => +const bonusField = label => new fields.SchemaField({ - bonus: new fields.NumberField({ integer: true, initial: 0 }), + bonus: new fields.NumberField({ integer: true, initial: 0, label }), dice: new fields.ArrayField(new fields.StringField()) - }) + }); -export { attributeField, resourceField, stressDamageReductionRule, bonusField }; \ No newline at end of file +export { attributeField, resourceField, stressDamageReductionRule, bonusField }; diff --git a/module/data/settings/Automation.mjs b/module/data/settings/Automation.mjs index 4e375919..4291423b 100644 --- a/module/data/settings/Automation.mjs +++ b/module/data/settings/Automation.mjs @@ -4,20 +4,22 @@ export default class DhAutomation extends foundry.abstract.DataModel { static defineSchema() { const fields = foundry.data.fields; return { - hope: new fields.BooleanField({ - required: true, - initial: false, - label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hope.label' - }), // Label need to be updated into something like "Duality Roll Auto Gain" + a hint + hopeFear: new fields.SchemaField({ + gm: new fields.BooleanField({ + required: true, + initial: false, + label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hopeFear.gm.label' + }), + players: new fields.BooleanField({ + required: true, + initial: false, + label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hopeFear.players.label' + }) + }), actionPoints: new fields.BooleanField({ required: true, initial: false, label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.actionPoints.label' - }), - countdowns: new fields.BooleanField({ - requireD: true, - initial: false, - label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.countdowns.label' }) }; } diff --git a/module/dice/d20Roll.mjs b/module/dice/d20Roll.mjs index 8e13bf46..004e4806 100644 --- a/module/dice/d20Roll.mjs +++ b/module/dice/d20Roll.mjs @@ -102,7 +102,7 @@ export default class D20Roll extends DHRoll { value: this.options.data.experiences[m].value }); }); - + this.addModifiers(); if (this.options.extraFormula) { this.terms.push( @@ -123,15 +123,17 @@ export default class D20Roll extends DHRoll { applyBaseBonus() { const modifiers = []; - - if(this.options.roll.bonus) + + if (this.options.roll.bonus) modifiers.push({ label: 'Bonus to Hit', value: this.options.roll.bonus }); modifiers.push(...this.getBonus(`roll.${this.options.type}`, `${this.options.type.capitalize()} Bonus`)); - modifiers.push(...this.getBonus(`roll.${this.options.roll.type}`, `${this.options.roll.type.capitalize()} Bonus`)); + modifiers.push( + ...this.getBonus(`roll.${this.options.roll.type}`, `${this.options.roll.type.capitalize()} Bonus`) + ); return modifiers; } diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index 8b835583..bfbfc7d5 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -34,16 +34,16 @@ export default class DamageRoll extends DHRoll { }); const weapons = ['primaryWeapon', 'secondaryWeapon']; weapons.forEach(w => { - if(this.options.source.item && this.options.source.item === this.data[w]?.id) + if (this.options.source.item && this.options.source.item === this.data[w]?.id) modifiers.push(...this.getBonus(`${type}.${w}`, 'Weapon Bonus')); }); - + return modifiers; } constructFormula(config) { super.constructFormula(config); - + if (config.isCritical) { const tmpRoll = new Roll(this._formula)._evaluateSync({ maximize: true }), criticalBonus = tmpRoll.total - this.constructor.calculateTotalModifiers(tmpRoll); diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index 22903d6a..27288f15 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -4,7 +4,7 @@ export default class DHRoll extends Roll { baseTerms = []; constructor(formula, data, options) { super(formula, data, options); - if(!this.data || !Object.keys(this.data).length) this.data = options.data; + if (!this.data || !Object.keys(this.data).length) this.data = options.data; } static messageType = 'adversaryRoll'; @@ -87,7 +87,7 @@ export default class DHRoll extends Roll { system: config, rolls: [roll] }; - return await cls.create(msg); + return await cls.create(msg, { rollMode: config.selectedRollMode }); } static applyKeybindings(config) { @@ -100,7 +100,7 @@ export default class DHRoll extends Roll { } formatModifier(modifier) { - if(Array.isArray(modifier)) { + if (Array.isArray(modifier)) { return [ new foundry.dice.terms.OperatorTerm({ operator: '+' }), ...this.constructor.parse(modifier.join(' + '), this.options.data) @@ -127,12 +127,12 @@ export default class DHRoll extends Roll { getBonus(path, label) { const bonus = foundry.utils.getProperty(this.data.bonuses, path), modifiers = []; - if(bonus?.bonus) + if (bonus?.bonus) modifiers.push({ label: label, value: bonus?.bonus }); - if(bonus?.dice?.length) + if (bonus?.dice?.length) modifiers.push({ label: label, value: bonus?.dice @@ -175,9 +175,10 @@ export default class DHRoll extends Roll { export const registerRollDiceHooks = () => { Hooks.on(`${CONFIG.DH.id}.postRollDuality`, async (config, message) => { + const hopeFearAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).hopeFear; if ( !config.source?.actor || - !game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).hope || + (game.user.isGM ? !hopeFearAutomation.gm : !hopeFearAutomation.players) || config.roll.type === 'reaction' ) return; @@ -185,9 +186,9 @@ export const registerRollDiceHooks = () => { const actor = await fromUuid(config.source.actor), updates = []; if (!actor) return; - if (config.roll.isCritical || config.roll.result.duality === 1) updates.push({ type: 'hope', value: 1 }); - if (config.roll.isCritical) updates.push({ type: 'stress', value: -1 }); - if (config.roll.result.duality === -1) updates.push({ type: 'fear', value: 1 }); + if (config.roll.isCritical || config.roll.result.duality === 1) updates.push({ key: 'hope', value: 1 }); + if (config.roll.isCritical) updates.push({ key: 'stress', value: -1 }); + if (config.roll.result.duality === -1) updates.push({ key: 'fear', value: 1 }); if (updates.length) actor.modifyResource(updates); diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index 99e4fa42..d983b2d6 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -121,7 +121,7 @@ export default class DualityRoll extends D20Roll { applyBaseBonus() { const modifiers = super.applyBaseBonus(); - if(this.options.roll.trait && this.data.traits[this.options.roll.trait]) + if (this.options.roll.trait && this.data.traits[this.options.roll.trait]) modifiers.unshift({ label: `DAGGERHEART.CONFIG.Traits.${this.options.roll.trait}.name`, value: this.data.traits[this.options.roll.trait].value @@ -129,7 +129,7 @@ export default class DualityRoll extends D20Roll { const weapons = ['primaryWeapon', 'secondaryWeapon']; weapons.forEach(w => { - if(this.options.source.item && this.options.source.item === this.data[w]?.id) + if (this.options.source.item && this.options.source.item === this.data[w]?.id) modifiers.push(...this.getBonus(`roll.${w}`, 'Weapon Bonus')); }); diff --git a/module/documents/_module.mjs b/module/documents/_module.mjs index 4dfa6264..540b06c1 100644 --- a/module/documents/_module.mjs +++ b/module/documents/_module.mjs @@ -3,4 +3,5 @@ export { default as DHItem } from './item.mjs'; export { default as DhpCombat } from './combat.mjs'; export { default as DhActiveEffect } from './activeEffect.mjs'; export { default as DhChatMessage } from './chatMessage.mjs'; +export { default as DhToken } from './token.mjs'; export { default as DhTooltipManager } from './tooltipManager.mjs'; diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index ef76d18f..409b4dd0 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -37,4 +37,15 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { e.setAttribute('data-use-perm', document.testUserPermission(game.user, 'OWNER')); }); } + + async _preCreate(data, options, user) { + options.speaker = ChatMessage.getSpeaker(); + const rollActorOwner = data.rolls?.[0]?.data?.parent?.owner; + if (rollActorOwner) { + data.author = rollActorOwner ? rollActorOwner.id : data.author; + await this.updateSource({ author: rollActorOwner ?? user }); + } + + return super._preCreate(data, options, rollActorOwner ?? user); + } } diff --git a/module/documents/token.mjs b/module/documents/token.mjs new file mode 100644 index 00000000..4592c843 --- /dev/null +++ b/module/documents/token.mjs @@ -0,0 +1,35 @@ +export default class DHToken extends TokenDocument { + /** + * 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) { + attributes = attributes || this.getTrackedAttributes(); + const barGroup = game.i18n.localize('TOKEN.BarAttributes'); + const valueGroup = game.i18n.localize('TOKEN.BarValues'); + + 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 }; + }); + bars.sort((a, b) => a.label.compare(b.label)); + + const invalidAttributes = ['gold', 'levelData', 'rules.damageReduction.maxArmorMarked.value']; + const values = attributes.value.reduce((acc, 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 bars.concat(values); + } +} diff --git a/package-lock.json b/package-lock.json index 7b1bed60..864d027c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,7 @@ "": { "dependencies": { "@yaireo/tagify": "^4.17.9", + "autocompleter": "^9.3.2", "gulp": "^5.0.0", "gulp-less": "^5.0.0", "rollup": "^4.40.0" @@ -608,6 +609,11 @@ "node": ">= 10.13.0" } }, + "node_modules/autocompleter": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/autocompleter/-/autocompleter-9.3.2.tgz", + "integrity": "sha512-rLbf2TLGOD7y+gOS36ksrZdIsvoHa2KXc2A7503w+NBRPrcF73zzFeYBxEcV/iMPjaBH3jFhNIYObZ7zt1fkCQ==" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", diff --git a/package.json b/package.json index d7b51dfd..a7dd69b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "dependencies": { "@yaireo/tagify": "^4.17.9", + "autocompleter": "^9.3.2", "gulp": "^5.0.0", "gulp-less": "^5.0.0", "rollup": "^4.40.0" diff --git a/styles/less/dialog/damage-selection/sheet.less b/styles/less/dialog/damage-selection/sheet.less index 43e4f4d2..461fb0b5 100644 --- a/styles/less/dialog/damage-selection/sheet.less +++ b/styles/less/dialog/damage-selection/sheet.less @@ -1,20 +1,34 @@ -@import '../../utils/colors.less'; - -.daggerheart.dialog.dh-style.views.damage-selection { - .damage-section-container { - display: flex; - flex-direction: column; - gap: 12px; - - input[type='text'], - input[type='number'] { - color: light-dark(@dark, @beige); - outline: 2px solid transparent; - transition: all 0.3s ease; - - &:hover { - outline: 2px solid light-dark(@dark, @beige); - } - } - } -} +@import '../../utils/colors.less'; + +.daggerheart.dialog.dh-style.views.damage-selection { + .damage-section-container { + display: flex; + flex-direction: column; + gap: 12px; + + input[type='text'], + input[type='number'] { + color: light-dark(@dark, @beige); + outline: 2px solid transparent; + transition: all 0.3s ease; + + &:hover { + outline: 2px solid light-dark(@dark, @beige); + } + } + + .damage-section-controls { + display: flex; + align-items: center; + gap: 16px; + + .roll-mode-select { + width: min-content; + } + + button { + flex: 1; + } + } + } +} diff --git a/styles/less/dialog/dice-roll/roll-selection.less b/styles/less/dialog/dice-roll/roll-selection.less index 575b7ce9..14bccf3d 100644 --- a/styles/less/dialog/dice-roll/roll-selection.less +++ b/styles/less/dialog/dice-roll/roll-selection.less @@ -114,5 +114,19 @@ } } } + + .roll-dialog-controls { + display: flex; + align-items: center; + gap: 16px; + + .roll-mode-select { + width: min-content; + } + + button { + flex: 1; + } + } } } diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index 39f0d1f1..9d38e386 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -38,7 +38,7 @@ input[type='checkbox'], input[type='radio'] { &:checked::after { - color: light-dark(@dark-40, @golden); + color: light-dark(@dark, @golden); } &:checked::before { color: light-dark(@dark-40, @golden-40); @@ -112,22 +112,17 @@ margin: 5px; height: inherit; .tag { - box-shadow: 0 0 0 1.1em @beige inset; - vertical-align: top; - box-sizing: border-box; - max-width: 100%; - padding: 0.3em 0 0.3em 0.5em; - color: black; + padding: 0.3rem 0.5rem; + color: light-dark(@dark-blue, @golden); + background-color: light-dark(@dark-blue-10, @golden-40); + font-family: @font-body; border-radius: 3px; - white-space: nowrap; transition: 0.13s ease-out; - height: 22px; - font-size: 0.9rem; - gap: 0.5em; + gap: 0.5rem; z-index: 1; + .remove { font-size: 10px; - margin-inline: auto 4.6666666667px; } } } @@ -434,6 +429,34 @@ border: 1px solid light-dark(@dark, @beige); height: 34px; + // tagify rule styles + --tags-disabled-bg: none; + --tags-border-color: none; + --tags-hover-border-color: none; + --tags-focus-border-color: none; + --tag-border-radius: 3px; + --tag-bg: light-dark(@dark-blue, @golden); + --tag-remove-btn-color: light-dark(@dark-blue, @golden); + --tag-hover: light-dark(@dark-blue, @golden); + --tag-text-color: light-dark(@beige, @dark); + --tag-text-color--edit: light-dark(@beige, @dark); + --tag-pad: 0.3em 0.5em; + --tag-inset-shadow-size: 1.2em; + --tag-invalid-color: #d39494; + --tag-invalid-bg: rgba(211, 148, 148, 0.5); + --tag--min-width: 1ch; + --tag--max-width: 100%; + --tag-hide-transition: 0.3s; + --tag-remove-bg: light-dark(@dark-blue-40, @golden-40); + --tag-remove-btn-color: light-dark(@beige, @dark); + --tag-remove-btn-bg: none; + --tag-remove-btn-bg--hover: light-dark(@beige, @dark); + --input-color: inherit; + --placeholder-color: light-dark(@beige-15, @dark-15); + --placeholder-color-focus: light-dark(@beige-15, @dark-15); + --loader-size: 0.8em; + --readonly-striped: 1; + border-radius: 3px; margin-right: 1px; @@ -459,30 +482,27 @@ .tagify__dropdown { border: 1px solid light-dark(@dark, @beige) !important; + font-family: @font-body; + color: light-dark(@dark, @beige); .tagify__dropdown__wrapper { background-image: url(../assets/parchments/dh-parchment-dark.png); background-color: transparent; border: 0; + color: light-dark(@dark, @beige); .tagify__dropdown__item--active { background-color: light-dark(@dark, @beige); - color: var(--color-dark-3); + color: light-dark(@beige, @dark); } } } &.theme-light { .tagify__dropdown { - color: black; - .tagify__dropdown__wrapper { background-image: url(../assets/parchments/dh-parchment-light.png); } - - .tagify__dropdown__item--active { - color: @beige; - } } } } diff --git a/styles/less/sheets/actors/companion/details.less b/styles/less/sheets/actors/companion/details.less index 4edf8aa9..2e76cf44 100644 --- a/styles/less/sheets/actors/companion/details.less +++ b/styles/less/sheets/actors/companion/details.less @@ -3,7 +3,8 @@ .application.sheet.daggerheart.actor.dh-style.companion { .partner-section, - .attack-section { + .attack-section, + .experience-list { display: flex; flex-direction: column; align-items: center; @@ -12,6 +13,7 @@ display: flex; gap: 15px; align-items: center; + width: 100%; h3 { font-size: 20px; diff --git a/styles/less/ux/autocomplete/autocomplete.less b/styles/less/ux/autocomplete/autocomplete.less new file mode 100644 index 00000000..06cabf5a --- /dev/null +++ b/styles/less/ux/autocomplete/autocomplete.less @@ -0,0 +1,44 @@ +.theme-light .autocomplete { + background-image: url('../assets/parchments/dh-parchment-light.png'); + color: black; +} + +.autocomplete { + padding: 2px; + border-width: 0 1px 1px 1px; + border-style: solid; + border-color: light-dark(@dark, @beige); + border-radius: 6px; + background-image: url('../assets/parchments/dh-parchment-dark.png'); + z-index: 200; + max-height: 400px !important; + width: fit-content !important; + overflow-y: auto; + font-family: @font-body; + display: flex; + flex-direction: column; + gap: 2px; + + scrollbar-color: light-dark(@dark-blue, @golden) transparent; + + .group { + font-weight: bold; + font-size: 14px; + padding-left: 8px; + } + + li[role='option'] { + font-size: 14px; + padding-left: 10px; + cursor: pointer; + + &:hover { + background-color: light-dark(@dark, @beige); + color: light-dark(@beige, var(--color-dark-3)); + } + + > div { + white-space: nowrap; + } + } +} diff --git a/styles/less/ux/index.less b/styles/less/ux/index.less index ff645288..68cfc7e5 100644 --- a/styles/less/ux/index.less +++ b/styles/less/ux/index.less @@ -1 +1,2 @@ @import './tooltip/tooltip.less'; +@import './autocomplete/autocomplete.less'; diff --git a/system.json b/system.json index 7bd337f0..1af469e0 100644 --- a/system.json +++ b/system.json @@ -264,7 +264,7 @@ "applyEffect": {} } }, - "primaryTokenAttribute": "resources.health", + "primaryTokenAttribute": "resources.hitPoints", "secondaryTokenAttribute": "resources.stress", "url": "https://your/hosted/system/repo/", "manifest": "https://your/hosted/system/repo/system.json", diff --git a/templates/dialogs/dice-roll/damageSelection.hbs b/templates/dialogs/dice-roll/damageSelection.hbs index 0286990e..bd97cfdf 100644 --- a/templates/dialogs/dice-roll/damageSelection.hbs +++ b/templates/dialogs/dice-roll/damageSelection.hbs @@ -6,8 +6,15 @@
- +
+ {{#if directDamage}} + + {{/if}} + +
\ No newline at end of file diff --git a/templates/dialogs/dice-roll/rollSelection.hbs b/templates/dialogs/dice-roll/rollSelection.hbs index 5c0ba41d..11fce27a 100644 --- a/templates/dialogs/dice-roll/rollSelection.hbs +++ b/templates/dialogs/dice-roll/rollSelection.hbs @@ -117,10 +117,15 @@ {{/unless}} Formula: {{@root.formula}} - +
+ + +
{{else}} \ No newline at end of file diff --git a/templates/sheets/activeEffect/changes.hbs b/templates/sheets/activeEffect/changes.hbs index c1047206..9cf137f0 100644 --- a/templates/sheets/activeEffect/changes.hbs +++ b/templates/sheets/activeEffect/changes.hbs @@ -11,12 +11,7 @@ {{#with ../fields.changes.element.fields as |changeFields|}}
  • - - - {{#each @root.fieldPaths}} - - {{/each}} - +
    {{formInput changeFields.mode name=(concat "changes." i ".mode") value=change.mode choices=@root.modes}} diff --git a/templates/sheets/actors/companion/details.hbs b/templates/sheets/actors/companion/details.hbs index eb3665a3..0f5587f3 100644 --- a/templates/sheets/actors/companion/details.hbs +++ b/templates/sheets/actors/companion/details.hbs @@ -6,7 +6,7 @@
    -

    Partner

    +

    {{localize "DAGGERHEART.GENERAL.partner"}}

    {{#if document.system.partner}} @@ -20,7 +20,7 @@
    -

    Attack

    +

    {{localize "DAGGERHEART.GENERAL.attack"}}

      @@ -28,6 +28,11 @@
    +
    + +

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

    + +
    {{#each source.system.experiences as |experience id|}}