Merge branch 'main' into bug/103-enrich-htmlfield-content-before-its-used-in-applications

This commit is contained in:
Joaquin Pereyra 2025-07-18 15:24:25 -03:00
commit f56c2482cb
551 changed files with 2831 additions and 14203 deletions

View file

@ -37,7 +37,7 @@ jobs:
download: https://github.com/${{github.repository}}/releases/download/${{github.event.release.tag_name}}/system.zip download: https://github.com/${{github.repository}}/releases/download/${{github.event.release.tag_name}}/system.zip
# Create a zip file with all files required by the module to add to the release # Create a zip file with all files required by the module to add to the release
- run: zip -r ./system.zip system.json README.md LICENSE build/daggerheart.js assets/ templates/ styles/daggerheart.css packs/ lang/ - run: zip -r ./system.zip system.json README.md LICENSE build/daggerheart.js build/tagify.css styles/daggerheart.css assets/ templates/ packs/ lang/
# Create a release for this specific version # Create a release for this specific version
- name: Update Release with Files - name: Update Release with Files

View file

@ -5,7 +5,7 @@ import * as documents from './module/documents/_module.mjs';
import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs'; import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs';
import { DhDualityRollEnricher, DhTemplateEnricher } from './module/enrichers/_module.mjs'; import { DhDualityRollEnricher, DhTemplateEnricher } from './module/enrichers/_module.mjs';
import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.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 { DualityRollColor } from './module/data/settings/Appearance.mjs';
import { DHRoll, DualityRoll, D20Roll, DamageRoll, DualityDie } from './module/dice/_module.mjs'; import { DHRoll, DualityRoll, D20Roll, DamageRoll, DualityDie } from './module/dice/_module.mjs';
import { renderDualityButton } from './module/enrichers/DualityRollEnricher.mjs'; import { renderDualityButton } from './module/enrichers/DualityRollEnricher.mjs';
@ -19,6 +19,7 @@ import {
import { placeables } from './module/canvas/_module.mjs'; import { placeables } from './module/canvas/_module.mjs';
import { registerRollDiceHooks } from './module/dice/dhRoll.mjs'; import { registerRollDiceHooks } from './module/dice/dhRoll.mjs';
import { registerDHActorHooks } from './module/documents/actor.mjs'; import { registerDHActorHooks } from './module/documents/actor.mjs';
import './node_modules/@yaireo/tagify/dist/tagify.css';
Hooks.once('init', () => { Hooks.once('init', () => {
CONFIG.DH = SYSTEM; CONFIG.DH = SYSTEM;
@ -42,7 +43,7 @@ Hooks.once('init', () => {
); );
CONFIG.statusEffects = [ CONFIG.statusEffects = [
...CONFIG.statusEffects, ...CONFIG.statusEffects.filter(x => !['dead', 'unconscious'].includes(x.id)),
...Object.values(SYSTEM.GENERAL.conditions).map(x => ({ ...Object.values(SYSTEM.GENERAL.conditions).map(x => ({
...x, ...x,
name: game.i18n.localize(x.name), name: game.i18n.localize(x.name),
@ -61,6 +62,14 @@ Hooks.once('init', () => {
CONFIG.Dice.rolls = [...CONFIG.Dice.rolls, ...[DHRoll, DualityRoll, D20Roll, DamageRoll]]; CONFIG.Dice.rolls = [...CONFIG.Dice.rolls, ...[DHRoll, DualityRoll, D20Roll, DamageRoll]];
CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate; 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; CONFIG.Item.documentClass = documents.DHItem;
//Registering the Item DataModel //Registering the Item DataModel
@ -98,12 +107,12 @@ Hooks.once('init', () => {
CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect; CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect;
CONFIG.ActiveEffect.dataModels = models.activeEffects.config; CONFIG.ActiveEffect.dataModels = models.activeEffects.config;
foundry.applications.apps.DocumentSheetConfig.unregisterSheet( DocumentSheetConfig.unregisterSheet(
CONFIG.ActiveEffect.documentClass, CONFIG.ActiveEffect.documentClass,
'core', 'core',
foundry.applications.sheets.ActiveEffectConfig foundry.applications.sheets.ActiveEffectConfig
); );
foundry.applications.apps.DocumentSheetConfig.registerSheet( DocumentSheetConfig.registerSheet(
CONFIG.ActiveEffect.documentClass, CONFIG.ActiveEffect.documentClass,
SYSTEM.id, SYSTEM.id,
applications.sheetConfigs.ActiveEffectConfig, applications.sheetConfigs.ActiveEffectConfig,
@ -127,9 +136,11 @@ Hooks.once('init', () => {
CONFIG.Canvas.rulerClass = placeables.DhRuler; CONFIG.Canvas.rulerClass = placeables.DhRuler;
CONFIG.Canvas.layers.templates.layerClass = placeables.DhTemplateLayer; CONFIG.Canvas.layers.templates.layerClass = placeables.DhTemplateLayer;
CONFIG.Token.objectClass = placeables.DhTokenPlaceable;
CONFIG.Combat.documentClass = documents.DhpCombat; CONFIG.Combat.documentClass = documents.DhpCombat;
CONFIG.ui.combat = applications.ui.DhCombatTracker; CONFIG.ui.combat = applications.ui.DhCombatTracker;
CONFIG.ui.chat = applications.ui.DhChatLog; CONFIG.ui.chat = applications.ui.DhChatLog;
CONFIG.ui.hotbar = applications.ui.DhHotbar;
CONFIG.Token.rulerClass = placeables.DhTokenRuler; CONFIG.Token.rulerClass = placeables.DhTokenRuler;
CONFIG.ui.resources = applications.ui.DhFearTracker; CONFIG.ui.resources = applications.ui.DhFearTracker;
@ -160,7 +171,6 @@ Hooks.on('ready', () => {
registerCountdownHooks(); registerCountdownHooks();
socketRegistration.registerSocketHooks(); socketRegistration.registerSocketHooks();
registerCountdownApplicationHooks();
registerRollDiceHooks(); registerRollDiceHooks();
registerDHActorHooks(); registerDHActorHooks();
}); });

View file

@ -99,6 +99,7 @@
"tier": { "label": "Tier" }, "tier": { "label": "Tier" },
"type": { "label": "Type" } "type": { "label": "Type" }
}, },
"hordeDamage": "Horde Damage",
"horderHp": "Horde/HP" "horderHp": "Horde/HP"
}, },
"Character": { "Character": {
@ -249,8 +250,9 @@
"title": "{actor} - Death Move" "title": "{actor} - Death Move"
}, },
"Downtime": { "Downtime": {
"downtimeHeader": "Downtime Moves ({current}/{max})",
"longRest": { "longRest": {
"title": "Long Rest",
"moves": "Long Rest Moves ({current}/{max})",
"clearStress": { "clearStress": {
"description": "Describe how you blow off steam or pull yourself together, and clear all marked Stress.", "description": "Describe how you blow off steam or pull yourself together, and clear all marked Stress.",
"name": "Clear Stress" "name": "Clear Stress"
@ -267,7 +269,6 @@
"description": "Describe how you patch yourself up and remove all marked Hit Points. You may also do this on an ally instead.", "description": "Describe how you patch yourself up and remove all marked Hit Points. You may also do this on an ally instead.",
"name": "Tend to Wounds" "name": "Tend to Wounds"
}, },
"title": "Long Rest",
"workOnAProject": { "workOnAProject": {
"description": "Establish or continue work on a project.", "description": "Establish or continue work on a project.",
"name": "Work on a Project" "name": "Work on a Project"
@ -275,6 +276,7 @@
}, },
"shortRest": { "shortRest": {
"title": "Short Rest", "title": "Short Rest",
"moves": "Short Rest Moves ({current}/{max})",
"tendToWounds": { "tendToWounds": {
"name": "Tend to Wounds", "name": "Tend to Wounds",
"description": "Describe how you hastily patch yourself up, then clear a number of Hit Points equal to 1d4 + your tier. You can do this to an ally instead." "description": "Describe how you hastily patch yourself up, then clear a number of Hit Points equal to 1d4 + your tier. You can do this to an ally instead."
@ -291,7 +293,8 @@
"name": "Prepare", "name": "Prepare",
"description": "Describe how you prepare yourself for the path ahead, then gain a Hope. If you choose to Prepare with one or more members of your party, you each gain 2 Hope." "description": "Describe how you prepare yourself for the path ahead, then gain a Hope. If you choose to Prepare with one or more members of your party, you each gain 2 Hope."
} }
} },
"takeDowntime": "Take Downtime"
}, },
"HUD": { "HUD": {
"tokenHUD": { "tokenHUD": {
@ -407,7 +410,11 @@
"rerollDice": "Reroll Dice" "rerollDice": "Reroll Dice"
} }
}, },
"CLASS": {
"Feature": {
"rallyDice": "Bardic Rally Dice"
}
},
"CONFIG": { "CONFIG": {
"ActionType": { "ActionType": {
"passive": "Passive", "passive": "Passive",
@ -560,9 +567,9 @@
"twoHanded": "Two-Handed" "twoHanded": "Two-Handed"
}, },
"Condition": { "Condition": {
"vulnerable": { "dead": {
"name": "Vulnerable", "name": "Dead",
"description": "While a creature is Vulnerable, all rolls targeting them have advantage.\nA creature who is already Vulnerable cant be made to take the condition again." "description": "The character is dead"
}, },
"hidden": { "hidden": {
"name": "Hidden", "name": "Hidden",
@ -571,6 +578,14 @@
"restrained": { "restrained": {
"name": "Restrained", "name": "Restrained",
"description": "When an effect makes a creature Restrained, it means they cannot move until this condition is cleared.\nThey can still take actions from their current position." "description": "When an effect makes a creature Restrained, it means they cannot move until this condition is cleared.\nThey can still take actions from their current position."
},
"unconcious": {
"name": "Unconcious",
"description": "Your character cant move or act while unconscious, they cant be targeted by an attack."
},
"vulnerable": {
"name": "Vulnerable",
"description": "While a creature is Vulnerable, all rolls targeting them have advantage.\nA creature who is already Vulnerable cant be made to take the condition again."
} }
}, },
"CountdownType": { "CountdownType": {
@ -687,11 +702,11 @@
} }
}, },
"RollTypes": { "RollTypes": {
"ability": { "trait": {
"name": "Ability" "name": "Trait"
}, },
"weapon": { "attack": {
"name": "Weapon" "name": "Attack"
}, },
"spellcast": { "spellcast": {
"name": "SpellCast" "name": "SpellCast"
@ -763,7 +778,7 @@
"WeaponFeature": { "WeaponFeature": {
"barrier": { "barrier": {
"name": "Barrier", "name": "Barrier",
"description": "+{armorScore} to Armor Score; -1 to Evasion" "description": "Gain your character's Tier + 1 to Armor Score; -1 to Evasion"
}, },
"bonded": { "bonded": {
"name": "Bonded", "name": "Bonded",
@ -879,7 +894,7 @@
}, },
"paired": { "paired": {
"name": "Paired", "name": "Paired",
"description": "+{bonusDamage} to primary weapon damage to targets within Melee range" "description": "Add your character's Tier + 1 to primary weapon damage against targets within Melee range"
}, },
"parry": { "parry": {
"name": "Parry", "name": "Parry",
@ -899,7 +914,7 @@
}, },
"protective": { "protective": {
"name": "Protective", "name": "Protective",
"description": "+{tier} to Armor Score" "description": "Add your character's Tier to your Armor Score"
}, },
"quick": { "quick": {
"name": "Quick", "name": "Quick",
@ -948,10 +963,6 @@
"timebending": { "timebending": {
"name": "Timebending", "name": "Timebending",
"description": "You can choose the target of your attack after making your attack roll." "description": "You can choose the target of your attack after making your attack roll."
},
"versatile": {
"name": "Versatile",
"description": "This weapon can also be used with these statistics—{characterTrait}, {range}, {damage}."
} }
} }
}, },
@ -1000,6 +1011,30 @@
"singular": "Adversary", "singular": "Adversary",
"plural": "Adversaries" "plural": "Adversaries"
}, },
"Bonuses": {
"rest": {
"shortRest": {
"shortRestMoves": {
"label": "Short Rest: Bonus Short Rest Moves",
"hint": "The number of extra Short Rest Moves the character can take during a Short Rest."
},
"longRestMoves": {
"label": "Short Rest: Bonus Long Rest Moves",
"hint": "The number of extra Long Rest Moves the character can take during a Short Rest."
}
},
"longRest": {
"shortRestMoves": {
"label": "Long Rest: Bonus Short Rest Moves",
"hint": "The number of extra Short Rest Moves the character can take during a Long Rest."
},
"longRestMoves": {
"label": "Long Rest: Bonus Long Rest Moves",
"hint": "The number of extra Long Rest Moves the character can take during a Long Rest."
}
}
}
},
"Character": { "Character": {
"singular": "Character", "singular": "Character",
"plural": "Characters" "plural": "Characters"
@ -1012,23 +1047,36 @@
"severe": "Severe", "severe": "Severe",
"major": "Major", "major": "Major",
"minor": "Minor", "minor": "Minor",
"none": "None" "none": "None",
"allDamage": "All Damage",
"physicalDamage": "Physical Damage",
"magicalDamage": "Magical Damage",
"primaryWeapon": "Primary Weapon Damage",
"secondaryWeapon": "Secondary Weapon Damage"
}, },
"DamageResistance": { "DamageResistance": {
"none": "None", "none": "None",
"resistance": "Resistance", "resistance": "Resistance",
"immunity": "Immunity" "immunity": "Immunity",
"physicalReduction": "Physical Damage Reduction",
"magicalReduction": "Magical Damage Reduction"
}, },
"DamageThresholds": { "DamageThresholds": {
"title": "Damage Thresholds", "title": "Damage Thresholds",
"minor": "Minor", "minor": "Minor",
"major": "Major", "major": "Major",
"severe": "Severe" "severe": "Severe",
"majorThreshold": "Major Damage Threshold",
"severeThreshold": "Severe Damage Threshold"
}, },
"Dice": { "Dice": {
"single": "Die", "single": "Die",
"plural": "Dice" "plural": "Dice"
}, },
"Difficulty": {
"all": "Difficulty: all",
"reaction": "Difficulty: reaction"
},
"Disadvantage": { "Disadvantage": {
"full": "Disadvantage", "full": "Disadvantage",
"short": "Dis" "short": "Dis"
@ -1038,39 +1086,39 @@
"plural": "Domains", "plural": "Domains",
"arcana": { "arcana": {
"label": "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": { "blade": {
"label": "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": { "bone": {
"label": "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": { "codex": {
"label": "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": { "grace": {
"label": "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": { "midnight": {
"label": "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": { "sage": {
"label": "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": { "splendor": {
"label": "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": { "valor": {
"label": "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": { "Effect": {
@ -1081,10 +1129,18 @@
"single": "Experience", "single": "Experience",
"plural": "Experiences" "plural": "Experiences"
}, },
"Healing": {
"healingAmount": "Healing Amount"
},
"Neutral": { "Neutral": {
"full": "None", "full": "None",
"short": "no" "short": "no"
}, },
"Range": {
"other": "Range Increase: Other",
"spell": "Range Increase: Spell",
"weapon": "Range Increase: Weapon"
},
"RefreshType": { "RefreshType": {
"session": "Session", "session": "Session",
"shortrest": "Short Rest", "shortrest": "Short Rest",
@ -1094,6 +1150,42 @@
"single": "Resource", "single": "Resource",
"plural": "Resources" "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": { "Tabs": {
"details": "Details", "details": "Details",
"attack": "Attack", "attack": "Attack",
@ -1138,7 +1230,9 @@
"single": "Trait", "single": "Trait",
"plural": "Traits" "plural": "Traits"
}, },
"armorScore": "Armor Score",
"activeEffects": "Active Effects", "activeEffects": "Active Effects",
"armorSlots": "Armor Slots",
"attack": "Attack", "attack": "Attack",
"basics": "Basics", "basics": "Basics",
"bonus": "Bonus", "bonus": "Bonus",
@ -1148,14 +1242,24 @@
"damage": "Damage", "damage": "Damage",
"damageType": "Damage Type", "damageType": "Damage Type",
"description": "Description", "description": "Description",
"difficulty": "Difficulty",
"duality": "Duality", "duality": "Duality",
"dualityRoll": "Duality Roll", "dualityRoll": "Duality Roll",
"enabled": "Enabled", "enabled": "Enabled",
"evasion": "Evasion", "evasion": "Evasion",
"experience": {
"single": "Experience",
"plural": "Experiences"
},
"fear": "Fear", "fear": "Fear",
"features": "Features", "features": "Features",
"hitPoints": "Hit Points", "hitPoints": {
"single": "Hit Point",
"plural": "Hit Points",
"short": "HP"
},
"hope": "Hope", "hope": "Hope",
"hordeHp": "Horde HP",
"inactiveEffects": "Inactive Effects", "inactiveEffects": "Inactive Effects",
"inventory": "Inventory", "inventory": "Inventory",
"level": "Level", "level": "Level",
@ -1163,9 +1267,12 @@
"modifier": "Modifier", "modifier": "Modifier",
"multiclass": "Multiclass", "multiclass": "Multiclass",
"none": "None", "none": "None",
"partner": "Partner",
"proficiency": "Proficiency",
"quantity": "Quantity", "quantity": "Quantity",
"range": "Range", "range": "Range",
"recovery": "Recovery", "recovery": "Recovery",
"roll": "Roll",
"scalable": "Scalable", "scalable": "Scalable",
"stress": "Stress", "stress": "Stress",
"take": "Take", "take": "Take",
@ -1177,7 +1284,8 @@
"use": "Use", "use": "Use",
"used": "Used", "used": "Used",
"uses": "Uses", "uses": "Uses",
"value": "Value" "value": "Value",
"withThing": "With {thing}"
}, },
"ITEMS": { "ITEMS": {
"FIELDS": { "FIELDS": {
@ -1240,8 +1348,8 @@
}, },
"DomainCard": { "DomainCard": {
"type": "Type", "type": "Type",
"foundation": "Foundation",
"recallCost": "Recall Cost", "recallCost": "Recall Cost",
"foundationTitle": "Foundation",
"specializationTitle": "Specialization", "specializationTitle": "Specialization",
"masteryTitle": "Mastery" "masteryTitle": "Mastery"
}, },
@ -1272,17 +1380,18 @@
"hint": "Automatically increase the GM's fear pool on a fear duality roll result." "hint": "Automatically increase the GM's fear pool on a fear duality roll result."
}, },
"FIELDS": { "FIELDS": {
"hope": { "hopeFear": {
"label": "Hope", "label": "Hope & Fear",
"hint": "Automatically increase a character's hope on a hope duality roll result." "gm": { "label": "GM" },
"players": { "label": "Players" }
}, },
"actionPoints": { "actionPoints": {
"label": "Action Points", "label": "Action Points",
"hint": "Automatically give and take Action Points as combatants take their turns." "hint": "Automatically give and take Action Points as combatants take their turns."
}, },
"countdowns": { "hordeDamage": {
"label": "Countdowns", "label": "Automatic Horde Damage",
"hint": "Automatically progress non-custom countdowns" "hint": "Automatically active horde effect to lower damage when reaching half or lower HP."
} }
} }
}, },
@ -1451,7 +1560,18 @@
"noAvailableArmorMarks": "You have no more available armor marks", "noAvailableArmorMarks": "You have no more available armor marks",
"notEnoughStress": "You don't have enough stress", "notEnoughStress": "You don't have enough stress",
"damageIgnore": "{character} did not take damage", "damageIgnore": "{character} did not take damage",
"featureIsMissing": "Feature is missing" "featureIsMissing": "Feature is missing",
"actionIsMissing": "Action is missing",
"attackIsMissing": "Attack is missing",
"unownedActionMacro": "Cannot make a Use macro for an Action not on your character",
"unownedAttackMacro": "Cannot make a Use macro for an Attack that doesn't belong to one of your characters",
"featureNotHope": "This feature is used as something else than a Hope feature and cannot be used here.",
"featureNotClass": "This feature is used as something else than a Class feature and cannot be used here.",
"featureNotPrimary": "This feature is used as something else than a Primary feature and cannot be used here.",
"featureNotSecondary": "This feature is used as something else than a Secondary feature and cannot be used here.",
"featureNotFoundation": "This feature is used as something else than a Foundation feature and cannot be used here.",
"featureNotSpecialization": "This feature is used as something else than a Specialization feature and cannot be used here.",
"featureNotMastery": "This feature is used as something else than a Mastery feature and cannot be used here."
}, },
"Tooltip": { "Tooltip": {
"disableEffect": "Disable Effect", "disableEffect": "Disable Effect",

View file

@ -506,9 +506,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
name: this.setup.ancestryName ?? this.setup.primaryAncestry.name, name: this.setup.ancestryName ?? this.setup.primaryAncestry.name,
system: { system: {
...this.setup.primaryAncestry.system, ...this.setup.primaryAncestry.system,
features: [primaryAncestryFeature.uuid, secondaryAncestryFeature.uuid], features: [primaryAncestryFeature.uuid, secondaryAncestryFeature.uuid]
primaryFeature: primaryAncestryFeature.uuid,
secondaryFeature: secondaryAncestryFeature.uuid
} }
}; };

View file

@ -64,6 +64,13 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.rollConfig = this.config; context.rollConfig = this.config;
context.hasRoll = !!this.config.roll; context.hasRoll = !!this.config.roll;
context.canRoll = true; 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) { if (this.config.costs?.length) {
const updatedCosts = this.action.calcCosts(this.config.costs); const updatedCosts = this.action.calcCosts(this.config.costs);
context.costs = updatedCosts.map(x => ({ context.costs = updatedCosts.map(x => ({
@ -82,6 +89,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
if (this.roll) { if (this.roll) {
context.roll = this.roll; context.roll = this.roll;
context.rollType = this.roll?.constructor.name; context.rollType = this.roll?.constructor.name;
context.rallyDie = this.roll.rallyChoices;
context.experiences = Object.keys(this.config.data.experiences).map(id => ({ context.experiences = Object.keys(this.config.data.experiences).map(id => ({
id, id,
...this.config.data.experiences[id] ...this.config.data.experiences[id]
@ -99,6 +107,8 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
static updateRollConfiguration(event, _, formData) { static updateRollConfiguration(event, _, formData) {
const { ...rest } = foundry.utils.expandObject(formData.object); const { ...rest } = foundry.utils.expandObject(formData.object);
this.config.selectedRollMode = rest.selectedRollMode;
if (this.config.costs) { if (this.config.costs) {
this.config.costs = foundry.utils.mergeObject(this.config.costs, rest.costs); this.config.costs = foundry.utils.mergeObject(this.config.costs, rest.costs);
} }
@ -122,11 +132,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
} }
static selectExperience(_, button) { 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 =
this.config.experiences.indexOf(button.dataset.key) > -1 this.config.experiences.indexOf(button.dataset.key) > -1
? this.config.experiences.filter(x => x !== button.dataset.key) ? this.config.experiences.filter(x => x !== button.dataset.key)

View file

@ -48,12 +48,22 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
: game.i18n.localize('DAGGERHEART.EFFECTS.ApplyLocations.damageRoll.name'); : game.i18n.localize('DAGGERHEART.EFFECTS.ApplyLocations.damageRoll.name');
context.extraFormula = this.config.extraFormula; context.extraFormula = this.config.extraFormula;
context.formula = this.roll.constructFormula(this.config); 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; return context;
} }
static updateRollConfiguration(event, _, formData) { static updateRollConfiguration(_event, _, formData) {
const { ...rest } = foundry.utils.expandObject(formData.object); const { ...rest } = foundry.utils.expandObject(formData.object);
this.config.extraFormula = rest.extraFormula; this.config.extraFormula = rest.extraFormula;
this.config.selectedRollMode = rest.selectedRollMode;
this.render(); this.render();
} }

View file

@ -7,8 +7,22 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
this.actor = actor; this.actor = actor;
this.shortrest = shortrest; this.shortrest = shortrest;
const options = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).restMoves; this.moveData = foundry.utils.deepClone(
this.moveData = shortrest ? options.shortRest : options.longRest; game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).restMoves
);
this.nrChoices = {
shortRest: {
max:
(shortrest ? this.moveData.shortRest.nrChoices : 0) +
actor.system.bonuses.rest[`${shortrest ? 'short' : 'long'}Rest`].shortMoves
},
longRest: {
max:
(!shortrest ? this.moveData.longRest.nrChoices : 0) +
actor.system.bonuses.rest[`${shortrest ? 'short' : 'long'}Rest`].longMoves
}
};
this.nrChoices.total = { max: this.nrChoices.shortRest.max + this.nrChoices.longRest.max };
} }
get title() { get title() {
@ -17,8 +31,8 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
classes: ['daggerheart', 'views', 'downtime'], classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'downtime'],
position: { width: 680, height: 'auto' }, position: { width: 'auto', height: 'auto' },
actions: { actions: {
selectMove: this.selectMove, selectMove: this.selectMove,
takeDowntime: this.takeDowntime takeDowntime: this.takeDowntime
@ -29,7 +43,7 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
static PARTS = { static PARTS = {
application: { application: {
id: 'downtime', id: 'downtime',
template: 'systems/daggerheart/templates/dialogs/downtime.hbs' template: 'systems/daggerheart/templates/dialogs/downtime/downtime.hbs'
} }
}; };
@ -37,46 +51,83 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
super._attachPartListeners(partId, htmlElement, options); super._attachPartListeners(partId, htmlElement, options);
htmlElement htmlElement
.querySelectorAll('.activity-image') .querySelectorAll('.activity-container')
.forEach(element => element.addEventListener('contextmenu', this.deselectMove.bind(this))); .forEach(element => element.addEventListener('contextmenu', this.deselectMove.bind(this)));
} }
async _prepareContext(_options) { async _prepareContext(_options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.title = game.i18n.localize(
`DAGGERHEART.APPLICATIONS.Downtime.${this.shortrest ? 'shortRest' : 'longRest'}.title`
);
context.selectedActivity = this.selectedActivity; context.selectedActivity = this.selectedActivity;
context.moveData = this.moveData; context.moveData = this.moveData;
context.nrCurrentChoices = Object.values(this.moveData.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0); context.nrCurrentChoices = Object.values(this.moveData).reduce((acc, category) => {
context.disabledDowntime = context.nrCurrentChoices < context.moveData.nrChoices; acc += Object.values(category.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0);
return acc;
}, 0);
context.nrChoices = {
...this.nrChoices,
shortRest: {
...this.nrChoices.shortRest,
current: Object.values(this.moveData.shortRest.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0)
},
longRest: {
...this.nrChoices.longRest,
current: Object.values(this.moveData.longRest.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0)
}
};
context.nrChoices.total = {
...this.nrChoices.total,
current: context.nrChoices.shortRest.current + context.nrChoices.longRest.current
};
context.shortRestMoves = this.nrChoices.shortRest.max > 0 ? this.moveData.shortRest : null;
context.longRestMoves = this.nrChoices.longRest.max > 0 ? this.moveData.longRest : null;
context.disabledDowntime = context.nrChoices.total.current < context.nrChoices.total.max;
return context; return context;
} }
static selectMove(_, button) { static selectMove(_, target) {
const nrSelected = Object.values(this.moveData.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0); const nrSelected = Object.values(this.moveData[target.dataset.category].moves).reduce(
if (nrSelected === this.moveData.nrChoices) { (acc, x) => acc + (x.selected ?? 0),
0
);
if (nrSelected === this.nrChoices[target.dataset.category].max) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.noMoreMoves')); ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.noMoreMoves'));
return; return;
} }
const move = button.dataset.move; const move = target.dataset.move;
this.moveData.moves[move].selected = this.moveData.moves[move].selected this.moveData[target.dataset.category].moves[move].selected = this.moveData[target.dataset.category].moves[move]
? this.moveData.moves[move].selected + 1 .selected
? this.moveData[target.dataset.category].moves[move].selected + 1
: 1; : 1;
this.render(); this.render();
} }
deselectMove(event) { deselectMove(event) {
const move = event.currentTarget.dataset.move; const button = event.target.closest('.activity-container');
this.moveData.moves[move].selected = this.moveData.moves[move].selected const move = button.dataset.move;
? this.moveData.moves[move].selected - 1 this.moveData[button.dataset.category].moves[move].selected = this.moveData[button.dataset.category].moves[move]
.selected
? this.moveData[button.dataset.category].moves[move].selected - 1
: 0; : 0;
this.render(); this.render();
} }
static async takeDowntime() { static async takeDowntime() {
const moves = Object.values(this.moveData.moves).filter(x => x.selected); const moves = Object.values(this.moveData).flatMap(category => {
return Object.values(category.moves)
.filter(x => x.selected)
.flatMap(move => [...Array(move.selected).keys()].map(_ => move));
});
const cls = getDocumentClass('ChatMessage'); const cls = getDocumentClass('ChatMessage');
const msg = new cls({ const msg = new cls({

View file

@ -67,7 +67,7 @@ export default class ResourceDiceDialog extends HandlebarsApplicationMixin(Appli
static async rerollDice() { static async rerollDice() {
const max = itemAbleRollParse(this.item.system.resource.max, this.actor, this.item); const max = itemAbleRollParse(this.item.system.resource.max, this.actor, this.item);
const diceFormula = `${max}d${this.item.system.resource.dieFaces}`; const diceFormula = `${max}${this.item.system.resource.dieFaces}`;
const roll = await new Roll(diceFormula).evaluate(); const roll = await new Roll(diceFormula).evaluate();
if (game.modules.get('dice-so-nice')?.active) await game.dice3d.showForRoll(roll, game.user, true); if (game.modules.get('dice-so-nice')?.active) await game.dice3d.showForRoll(roll, game.user, true);
this.rollValues = roll.terms[0].results.map(x => ({ value: x.result, used: false })); this.rollValues = roll.terms[0].results.map(x => ({ value: x.result, used: false }));

View file

@ -1 +1 @@
export { default as DHTokenHUD } from './tokenHud.mjs'; export { default as DHTokenHUD } from './tokenHUD.mjs';

View file

@ -1,4 +1,4 @@
export default class DHTokenHUD extends TokenHUD { export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['daggerheart'] classes: ['daggerheart']
}; };

View file

@ -4,13 +4,14 @@ import DHActionConfig from '../../sheets-configs/action-config.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhSettingsActionView extends HandlebarsApplicationMixin(ApplicationV2) { export default class DhSettingsActionView extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(resolve, reject, title, name, img, description, actions) { constructor(resolve, reject, title, name, icon, img, description, actions) {
super({}); super({});
this.resolve = resolve; this.resolve = resolve;
this.reject = reject; this.reject = reject;
this.viewTitle = title; this.viewTitle = title;
this.name = name; this.name = name;
this.icon = icon;
this.img = img; this.img = img;
this.description = description; this.description = description;
this.actions = actions; this.actions = actions;
@ -23,7 +24,7 @@ export default class DhSettingsActionView extends HandlebarsApplicationMixin(App
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
classes: ['daggerheart', 'setting', 'dh-style'], classes: ['daggerheart', 'setting', 'dh-style'],
position: { width: '400', height: 'auto' }, position: { width: 440, height: 'auto' },
actions: { actions: {
editImage: this.onEditImage, editImage: this.onEditImage,
addItem: this.addItem, addItem: this.addItem,
@ -46,6 +47,7 @@ export default class DhSettingsActionView extends HandlebarsApplicationMixin(App
async _prepareContext(_options) { async _prepareContext(_options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.name = this.name; context.name = this.name;
context.icon = this.icon;
context.img = this.img; context.img = this.img;
context.description = this.description; context.description = this.description;
context.enrichedDescription = await foundry.applications.ux.TextEditor.enrichHTML(context.description); context.enrichedDescription = await foundry.applications.ux.TextEditor.enrichHTML(context.description);
@ -55,8 +57,9 @@ export default class DhSettingsActionView extends HandlebarsApplicationMixin(App
} }
static async updateData(event, element, formData) { static async updateData(event, element, formData) {
const { name, img, description } = foundry.utils.expandObject(formData.object); const { name, icon, description } = foundry.utils.expandObject(formData.object);
this.name = name; this.name = name;
this.icon = icon;
this.description = description; this.description = description;
this.render(); this.render();
@ -65,6 +68,7 @@ export default class DhSettingsActionView extends HandlebarsApplicationMixin(App
static async saveForm(event) { static async saveForm(event) {
this.resolve({ this.resolve({
name: this.name, name: this.name,
icon: this.icon,
img: this.img, img: this.img,
description: this.description, description: this.description,
actions: this.actions actions: this.actions

View file

@ -76,6 +76,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
reject, reject,
game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.downtimeMoves'), game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.downtimeMoves'),
move.name, move.name,
move.icon,
move.img, move.img,
move.description, move.description,
move.actions move.actions
@ -87,6 +88,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
await this.settings.updateSource({ await this.settings.updateSource({
[`restMoves.${type}.moves.${id}`]: { [`restMoves.${type}.moves.${id}`]: {
name: data.name, name: data.name,
icon: data.icon,
img: data.img, img: data.img,
description: data.description description: data.description
} }

View file

@ -3,3 +3,5 @@ export { default as AdversarySettings } from './adversary-settings.mjs';
export { default as CompanionSettings } from './companion-settings.mjs'; export { default as CompanionSettings } from './companion-settings.mjs';
export { default as EnvironmentSettings } from './environment-settings.mjs'; export { default as EnvironmentSettings } from './environment-settings.mjs';
export { default as ActiveEffectConfig } from './activeEffectConfig.mjs'; export { default as ActiveEffectConfig } from './activeEffectConfig.mjs';
export { default as DhTokenConfig } from './token-config.mjs';
export { default as DhPrototypeTokenConfig } from './prototype-token-config.mjs';

View file

@ -1,4 +1,24 @@
import autocomplete from 'autocompleter';
export default class DhActiveEffectConfig extends foundry.applications.sheets.ActiveEffectConfig { 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 = { static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'sheet', 'dh-style'] classes: ['daggerheart', 'sheet', 'dh-style']
}; };
@ -27,36 +47,62 @@ 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 ? `<strong>${matchText}</strong>` : ''}${after}`;
if (item.hint) {
element.dataset.tooltip = game.i18n.localize(item.hint);
}
return element;
},
renderGroup: function (label) {
const itemElement = document.createElement('div');
itemElement.textContent = game.i18n.localize(label);
return itemElement;
},
onSelect: function (item) {
element.value = `system.${item.value}`;
},
click: e => e.fetch(),
customize: function (_input, _inputRect, container) {
container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ;
},
minLength: 0
});
});
}
async _preparePartContext(partId, context) { async _preparePartContext(partId, context) {
const partContext = await super._preparePartContext(partId, context); const partContext = await super._preparePartContext(partId, context);
switch (partId) { switch (partId) {
case 'changes': 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; break;
} }
return partContext; 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;
}
} }

View file

@ -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
};
}
}

View file

@ -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
};
}
}

View file

@ -25,6 +25,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
toggleEquipItem: CharacterSheet.#toggleEquipItem, toggleEquipItem: CharacterSheet.#toggleEquipItem,
toggleResourceDice: CharacterSheet.#toggleResourceDice, toggleResourceDice: CharacterSheet.#toggleResourceDice,
handleResourceDice: CharacterSheet.#handleResourceDice, handleResourceDice: CharacterSheet.#handleResourceDice,
useDowntime: this.useDowntime
}, },
window: { window: {
resizable: true resizable: true
@ -113,6 +114,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
htmlElement.querySelectorAll('.inventory-item-quantity').forEach(element => { htmlElement.querySelectorAll('.inventory-item-quantity').forEach(element => {
element.addEventListener('change', this.updateItemQuantity.bind(this)); element.addEventListener('change', this.updateItemQuantity.bind(this));
}); });
// Add listener for armor marks input
htmlElement.querySelectorAll('.armor-marks-input').forEach(element => {
element.addEventListener('change', this.updateArmorMarks.bind(this));
});
} }
/** @inheritDoc */ /** @inheritDoc */
@ -519,6 +525,16 @@ export default class CharacterSheet extends DHBaseActorSheet {
await item.update({ 'system.quantity': event.currentTarget.value }); await item.update({ 'system.quantity': event.currentTarget.value });
} }
async updateArmorMarks(event) {
const armor = this.document.system.armor;
if (!armor) return;
const maxMarks = this.document.system.armorScore;
const value = Math.min(Math.max(Number(event.currentTarget.value), 0), maxMarks);
await armor.update({ 'system.marks.value': value });
this.render();
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Application Clicks Actions */ /* Application Clicks Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -669,7 +685,12 @@ export default class CharacterSheet extends DHBaseActorSheet {
return acc; return acc;
}, {}) }, {})
}); });
//this.render(); }
static useDowntime(_, button) {
new game.system.api.applications.dialogs.Downtime(this.document, button.dataset.type === 'shortRest').render(
true
);
} }
async _onDragStart(event) { async _onDragStart(event) {

View file

@ -272,13 +272,15 @@ export default function DHApplicationMixin(Base) {
const getAction = (target) => { const getAction = (target) => {
const { actionId } = target.closest('[data-action-id]').dataset; const { actionId } = target.closest('[data-action-id]').dataset;
const { actions, attack } = this.document.system; const { actions, attack } = this.document.system;
return attack.id === actionId ? attack : actions?.find(a => a.id === actionId); return attack?.id === actionId ? attack : actions?.find(a => a.id === actionId);
}; };
const options = [ const options = [
{ {
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem', name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem',
icon: 'fa-solid fa-burst', icon: 'fa-solid fa-burst',
condition: this.document instanceof foundry.documents.Actor ||
(this.document instanceof foundry.documents.Item && this.document.parent),
callback: (target, event) => getAction(target).use(event), callback: (target, event) => getAction(target).use(event),
}, },
{ {
@ -297,7 +299,7 @@ export default function DHApplicationMixin(Base) {
condition: (target) => { condition: (target) => {
const { actionId } = target.closest('[data-action-id]').dataset; const { actionId } = target.closest('[data-action-id]').dataset;
const { attack } = this.document.system; const { attack } = this.document.system;
return attack.id !== actionId return attack?.id !== actionId
}, },
callback: async (target) => { callback: async (target) => {
const action = getAction(target) const action = getAction(target)
@ -313,7 +315,7 @@ export default function DHApplicationMixin(Base) {
if (!confirmed) return; if (!confirmed) return;
return this.document.update({ return this.document.update({
'system.actions': this.document.system.actions.do.filter((a) => a.id !== action.id) 'system.actions': this.document.system.actions.filter((a) => a.id !== action.id)
}); });
} }
} }
@ -353,7 +355,11 @@ export default function DHApplicationMixin(Base) {
if (deletable) options.push({ if (deletable) options.push({
name: 'CONTROLS.CommonDelete', name: 'CONTROLS.CommonDelete',
icon: 'fa-solid fa-trash', icon: 'fa-solid fa-trash',
callback: target => getDocFromElement(target).deleteDialog(), callback: (target, event) => {
const doc = getDocFromElement(target);
if (event.shiftKey) return doc.delete();
else return doc.deleteDialog();
}
}) })
return options.map(option => ({ return options.map(option => ({
@ -402,7 +408,7 @@ export default function DHApplicationMixin(Base) {
const isAction = !!actionId; const isAction = !!actionId;
descriptionElement.innerHTML = await foundry.applications.ux.TextEditor.implementation.enrichHTML(description, { descriptionElement.innerHTML = await foundry.applications.ux.TextEditor.implementation.enrichHTML(description, {
relativeTo: isAction ? doc.parent : doc, relativeTo: isAction ? doc.parent : doc,
rollData: doc.getRollData(), rollData: doc.getRollData?.(),
secrets: isAction ? doc.parent.isOwner : doc.isOwner secrets: isAction ? doc.parent.isOwner : doc.isOwner
}); });
} }
@ -432,7 +438,7 @@ export default function DHApplicationMixin(Base) {
type: game.i18n.localize('DAGGERHEART.GENERAL.Action.single') type: game.i18n.localize('DAGGERHEART.GENERAL.Action.single')
}), }),
} }
}); }) ?? {};
if (!actionType) return; if (!actionType) return;
const cls = game.system.api.models.actions.actionsTypes[actionType] const cls = game.system.api.models.actions.actionsTypes[actionType]
const action = new cls({ const action = new cls({
@ -482,7 +488,7 @@ export default function DHApplicationMixin(Base) {
// TODO: REDO this // TODO: REDO this
const { actionId } = target.closest('[data-action-id]').dataset; const { actionId } = target.closest('[data-action-id]').dataset;
const { actions, attack } = this.document.system; const { actions, attack } = this.document.system;
const action = attack.id === actionId ? attack : actions?.find(a => a.id === actionId); const action = attack?.id === actionId ? attack : actions?.find(a => a.id === actionId);
new DHActionConfig(action).render({ force: true }) new DHActionConfig(action).render({ force: true })
} }
@ -490,17 +496,21 @@ export default function DHApplicationMixin(Base) {
* Delete an embedded document. * Delete an embedded document.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #deleteDoc(_event, target) { static async #deleteDoc(event, target) {
const doc = getDocFromElement(target); const doc = getDocFromElement(target);
// TODO: REDO this if (doc) {
if (doc) return await doc.deleteDialog() if (event.shiftKey) return doc.delete()
else return await doc.deleteDialog()
}
// TODO: REDO this
const { actionId } = target.closest('[data-action-id]').dataset; const { actionId } = target.closest('[data-action-id]').dataset;
const { actions, attack } = this.document.system; const { actions, attack } = this.document.system;
if (attack.id === actionId) return; if (attack?.id === actionId) return;
const action = actions.find(a => a.id === actionId); const action = actions.find(a => a.id === actionId);
if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({ const confirmed = await foundry.applications.api.DialogV2.confirm({
window: { window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
@ -511,6 +521,7 @@ export default function DHApplicationMixin(Base) {
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: action.name }) content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: action.name })
}); });
if (!confirmed) return; if (!confirmed) return;
}
return await this.document.update({ return await this.document.update({
'system.actions': actions.filter((a) => a.id !== action.id) 'system.actions': actions.filter((a) => a.id !== action.id)
@ -528,7 +539,7 @@ export default function DHApplicationMixin(Base) {
if (!doc) { if (!doc) {
const { actionId } = target.closest('[data-action-id]').dataset; const { actionId } = target.closest('[data-action-id]').dataset;
const { actions, attack } = this.document.system; const { actions, attack } = this.document.system;
doc = attack.id === actionId ? attack : actions?.find(a => a.id === actionId); doc = attack?.id === actionId ? attack : actions?.find(a => a.id === actionId);
} }
return doc.toChat(this.document.id); return doc.toChat(this.document.id);
} }
@ -544,6 +555,7 @@ export default function DHApplicationMixin(Base) {
const { actionId } = target.closest('[data-action-id]').dataset; const { actionId } = target.closest('[data-action-id]').dataset;
const { actions, attack } = this.document.system; const { actions, attack } = this.document.system;
doc = attack?.id === actionId ? attack : actions?.find(a => a.id === actionId); doc = attack?.id === actionId ? attack : actions?.find(a => a.id === actionId);
if(this.document instanceof foundry.documents.Item && !this.document.parent) return;
} }
await doc.use(event); await doc.use(event);

View file

@ -34,7 +34,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
} }
} }
], ],
dragDrop: [] dragDrop: [{ dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null }]
}; };
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -139,4 +139,27 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
) )
}); });
} }
/* -------------------------------------------- */
/* Application Drag/Drop */
/* -------------------------------------------- */
/**
* On dragStart on the item.
* @param {DragEvent} event - The drag event
*/
async _onDragStart(event) {
const attackItem = event.currentTarget.closest('.inventory-item[data-type="attack"]');
if (attackItem) {
const attackData = {
type: 'Attack',
actorUuid: this.document.uuid,
img: this.document.system.attack.img,
fromInternal: true
};
event.dataTransfer.setData('text/plain', JSON.stringify(attackData));
event.dataTransfer.setDragImage(attackItem.querySelector('img'), 60, 0);
}
}
} }

View file

@ -22,14 +22,14 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
actions: { actions: {
removeAction: DHBaseItemSheet.#removeAction, removeAction: DHBaseItemSheet.#removeAction,
addFeature: DHBaseItemSheet.#addFeature, addFeature: DHBaseItemSheet.#addFeature,
editFeature: DHBaseItemSheet.#editFeature, deleteFeature: DHBaseItemSheet.#deleteFeature,
removeFeature: DHBaseItemSheet.#removeFeature,
addResource: DHBaseItemSheet.#addResource, addResource: DHBaseItemSheet.#addResource,
removeResource: DHBaseItemSheet.#removeResource removeResource: DHBaseItemSheet.#removeResource
}, },
dragDrop: [ dragDrop: [
{ dragSelector: null, dropSelector: '.tab.features .drop-section' }, { dragSelector: null, dropSelector: '.tab.features .drop-section' },
{ dragSelector: '.feature-item', dropSelector: null } { dragSelector: '.feature-item', dropSelector: null },
{ dragSelector: '.action-item', dropSelector: null }
], ],
contextMenus: [ contextMenus: [
{ {
@ -153,6 +153,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
const actionIndex = button.closest('[data-index]').dataset.index; const actionIndex = button.closest('[data-index]').dataset.index;
const action = this.document.system.actions[actionIndex]; const action = this.document.system.actions[actionIndex];
if(!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({ const confirmed = await foundry.applications.api.DialogV2.confirm({
window: { window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
@ -163,6 +164,8 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: action.name }) content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: action.name })
}); });
if (!confirmed) return; if (!confirmed) return;
}
await this.document.update({ await this.document.update({
'system.actions': this.document.system.actions.filter((_, index) => index !== Number.parseInt(actionIndex)) 'system.actions': this.document.system.actions.filter((_, index) => index !== Number.parseInt(actionIndex))
@ -173,58 +176,31 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
* Add a new feature to the item, prompting the user for its type. * Add a new feature to the item, prompting the user for its type.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #addFeature(_event, _button) { static async #addFeature(_, target) {
const { type } = target.dataset;
const cls = foundry.documents.Item.implementation; const cls = foundry.documents.Item.implementation;
const feature = await cls.create({ const feature = await cls.create({
type: 'feature', type: 'feature',
name: cls.defaultName({ type: 'feature' }), name: cls.defaultName({ type: 'feature' }),
"system.subType": CONFIG.DH.ITEM.featureSubTypes[type]
}); });
await this.document.update({ await this.document.update({
'system.features': [...this.document.system.features, feature] 'system.features': [...this.document.system.features, feature].map(f => f.uuid)
}); });
} }
/**
* Edit an existing feature on the item
* @type {ApplicationClickAction}
*/
static async #editFeature(_event, button) {
const target = button.closest('.feature-item');
const feature = this.document.system.features.find(x => x?.id === target.id);
if (!feature) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing'));
return;
}
feature.sheet.render(true);
}
/** /**
* Remove a feature from the item. * Remove a feature from the item.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #removeFeature(event, button) { static async #deleteFeature(_, target) {
event.stopPropagation(); const feature = getDocFromElement(target);
const target = button.closest('.feature-item'); if (!feature) return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing'));
const feature = this.document.system.features.find(x => x && x.id === target.id); await feature.update({ 'system.subType': null });
if (feature) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize(`TYPES.Item.feature`),
name: feature.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: feature.name })
});
if (!confirmed) return;
}
await this.document.update({ await this.document.update({
'system.features': this.document.system.features 'system.features': this.document.system.features
.filter(feature => feature && feature.id !== target.id)
.map(x => x.uuid) .map(x => x.uuid)
.filter(uuid => uuid !== feature.uuid)
}); });
} }
@ -269,6 +245,23 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
const featureData = { type: 'Item', data: { ...feature.toObject(), _id: null }, fromInternal: true }; const featureData = { type: 'Item', data: { ...feature.toObject(), _id: null }, fromInternal: true };
event.dataTransfer.setData('text/plain', JSON.stringify(featureData)); event.dataTransfer.setData('text/plain', JSON.stringify(featureData));
event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0); event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0);
} else {
const actionItem = event.currentTarget.closest('.action-item');
if (actionItem) {
const action = this.document.system.actions[actionItem.dataset.index];
if (!action) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.actionIsMissing'));
return;
}
const actionData = {
type: 'Action',
data: { ...action.toObject(), id: action.id, itemUuid: this.document.uuid },
fromInternal: true
};
event.dataTransfer.setData('text/plain', JSON.stringify(actionData));
event.dataTransfer.setDragImage(actionItem.querySelector('img'), 60, 0);
}
} }
} }

View file

@ -5,7 +5,6 @@ export default class AncestrySheet extends DHHeritageSheet {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['ancestry'], classes: ['ancestry'],
actions: { actions: {
addFeature: AncestrySheet.#addFeature,
editFeature: AncestrySheet.#editFeature, editFeature: AncestrySheet.#editFeature,
removeFeature: AncestrySheet.#removeFeature removeFeature: AncestrySheet.#removeFeature
}, },
@ -23,23 +22,6 @@ export default class AncestrySheet extends DHHeritageSheet {
/* Application Clicks Actions */ /* Application Clicks Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */
/**
* Add a new feature to the item, prompting the user for its type.
* @type {ApplicationClickAction}
*/
static async #addFeature(_event, button) {
const feature = await game.items.documentClass.create({
type: 'feature',
name: game.i18n.format('DOCUMENT.New', { type: game.i18n.localize('TYPES.Item.feature') }),
system: {
subType: button.dataset.type
}
});
await this.document.update({
'system.features': [...this.document.system.features.map(x => x.uuid), feature.uuid]
});
}
/** /**
* Edit an existing feature on the item * Edit an existing feature on the item
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
@ -63,22 +45,8 @@ export default class AncestrySheet extends DHHeritageSheet {
event.stopPropagation(); event.stopPropagation();
const target = button.closest('.feature-item'); const target = button.closest('.feature-item');
const feature = this.document.system[`${target.dataset.type}Feature`]; const feature = this.document.system[`${target.dataset.type}Feature`];
const featureExists = feature && Object.keys(feature).length > 0;
if (featureExists) { if (feature) await feature.update({ 'system.subType': null });
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize(`TYPES.Item.feature`),
name: feature.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: feature.name })
});
if (!confirmed) return;
}
if (featureExists && target.dataset.type === 'primary') await feature.update({ 'system.primary': null });
await this.document.update({ await this.document.update({
'system.features': this.document.system.features.filter(x => x && x.uuid !== feature.uuid).map(x => x.uuid) 'system.features': this.document.system.features.filter(x => x && x.uuid !== feature.uuid).map(x => x.uuid)
}); });
@ -94,15 +62,18 @@ export default class AncestrySheet extends DHHeritageSheet {
*/ */
async _onDrop(event) { async _onDrop(event) {
event.stopPropagation(); event.stopPropagation();
event.preventDefault();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const item = await fromUuid(data.uuid); const item = await fromUuid(data.uuid);
if (item?.type === 'feature') { if (item?.type === 'feature') {
const subType = event.target.closest('.primary-feature') ? 'primary' : 'secondary'; const subType = event.target.closest('.primary-feature') ? 'primary' : 'secondary';
await item.update({ 'system.subType': subType }); if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes[subType]) {
const error = subType === 'primary' ? 'featureNotPrimary' : 'featureNotSecondary';
ui.notifications.warn(game.i18n.localize(`DAGGERHEART.UI.Notifications.${error}`));
return;
}
await item.update({ 'system.subType': subType });
await this.document.update({ await this.document.update({
'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid] 'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
}); });

View file

@ -10,8 +10,6 @@ export default class ClassSheet extends DHBaseItemSheet {
actions: { actions: {
removeItemFromCollection: ClassSheet.#removeItemFromCollection, removeItemFromCollection: ClassSheet.#removeItemFromCollection,
removeSuggestedItem: ClassSheet.#removeSuggestedItem, removeSuggestedItem: ClassSheet.#removeSuggestedItem,
addFeature: ClassSheet.#addFeature,
deleteFeature: ClassSheet.#deleteFeature
}, },
tagifyConfigs: [ tagifyConfigs: [
{ {
@ -80,6 +78,7 @@ export default class ClassSheet extends DHBaseItemSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
async _onDrop(event) { async _onDrop(event) {
event.stopPropagation();
const data = TextEditor.getDragEventData(event); const data = TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid); const item = await fromUuid(data.uuid);
const target = event.target.closest('fieldset.drop-section'); const target = event.target.closest('fieldset.drop-section');
@ -89,12 +88,24 @@ export default class ClassSheet extends DHBaseItemSheet {
}); });
} else if (item.type === 'feature') { } else if (item.type === 'feature') {
if (target.classList.contains('hope-feature')) { if (target.classList.contains('hope-feature')) {
if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.hope) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotHope'));
return;
}
await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.hope });
await this.document.update({ await this.document.update({
'system.hopeFeatures': [...this.document.system.hopeFeatures.map(x => x.uuid), item.uuid] 'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
}); });
} else if (target.classList.contains('class-feature')) { } else if (target.classList.contains('class-feature')) {
if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.class) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotClass'));
return;
}
await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.class });
await this.document.update({ await this.document.update({
'system.classFeatures': [...this.document.system.classFeatures.map(x => x.uuid), item.uuid] 'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
}); });
} }
} else if (item.type === 'weapon') { } else if (item.type === 'weapon') {
@ -168,28 +179,4 @@ export default class ClassSheet extends DHBaseItemSheet {
const { target } = element.dataset; const { target } = element.dataset;
await this.document.update({ [`system.characterGuide.${target}`]: null }); await this.document.update({ [`system.characterGuide.${target}`]: null });
} }
static async #addFeature(_, target) {
const { actionPath } = target.dataset;
const cls = foundry.documents.Item.implementation;
const feature = await cls.create({
type: 'feature',
name: cls.defaultName({ type: 'feature' }),
});
await this.document.update({
[`system.${actionPath}`]: [
...this.document.system[actionPath],
feature.uuid
]
});
}
static async #deleteFeature(_, button) {
const { actionPath, itemUuid } = button.dataset;
await this.document.update({
[`system.${actionPath}`]: this.document.system[actionPath].filter(f => f.uuid !== itemUuid)
});
}
} }

View file

@ -10,7 +10,7 @@ export default class CommunitySheet extends DHHeritageSheet {
static PARTS = { static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/items/community/header.hbs' }, header: { template: 'systems/daggerheart/templates/sheets/items/community/header.hbs' },
...super.PARTS, ...super.PARTS,
feature: { features: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-features.hbs', template: 'systems/daggerheart/templates/sheets/global/tabs/tab-features.hbs',
scrollable: ['.feature'] scrollable: ['.feature']
} }

View file

@ -6,10 +6,7 @@ export default class SubclassSheet extends DHBaseItemSheet {
classes: ['subclass'], classes: ['subclass'],
position: { width: 600 }, position: { width: 600 },
window: { resizable: false }, window: { resizable: false },
actions: { actions: {}
addFeature: this.addFeature,
deleteFeature: this.deleteFeature
}
}; };
/**@override */ /**@override */
@ -40,24 +37,6 @@ export default class SubclassSheet extends DHBaseItemSheet {
} }
}; };
static async addFeature(_, target) {
const cls = foundry.documents.Item.implementation;
const feature = await cls.create({
type: 'feature',
name: cls.defaultName({ type: 'feature' }),
});
await this.document.update({
[`system.${target.dataset.type}`]: feature
});
}
static async deleteFeature(_, button) {
await this.document.update({
[`system.${button.dataset.actionPath}`]: null
});
}
async _onDragStart(event) { async _onDragStart(event) {
const featureItem = event.currentTarget.closest('.drop-section'); const featureItem = event.currentTarget.closest('.drop-section');
@ -75,18 +54,45 @@ export default class SubclassSheet extends DHBaseItemSheet {
} }
async _onDrop(event) { async _onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (data.fromInternal) return; if (data.fromInternal) return;
const item = await fromUuid(data.uuid); const item = await fromUuid(data.uuid);
if (item?.type === 'feature') { const target = event.target.closest('fieldset.drop-section');
const dropSection = event.target.closest('.drop-section'); if (item.type === 'feature') {
if (this.document.system[dropSection.dataset.type]) { if (target.dataset.type === 'foundation') {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.notifications.featureIsFull')); if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.foundation) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotFoundation'));
return; return;
} }
await this.document.update({ [`system.${dropSection.dataset.type}`]: item.uuid }); await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.foundation });
await this.document.update({
'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
});
} else if (target.dataset.type === 'specialization') {
if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.specialization) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotSpecialization'));
return;
}
await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.specialization });
await this.document.update({
'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
});
} else if (target.dataset.type === 'mastery') {
if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.mastery) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotMastery'));
return;
}
await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.mastery });
await this.document.update({
'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
});
}
} }
} }
} }

View file

@ -2,3 +2,4 @@ export { default as DhChatLog } from './chatLog.mjs';
export { default as DhCombatTracker } from './combatTracker.mjs'; export { default as DhCombatTracker } from './combatTracker.mjs';
export * as DhCountdowns from './countdowns.mjs'; export * as DhCountdowns from './countdowns.mjs';
export { default as DhFearTracker } from './fearTracker.mjs'; export { default as DhFearTracker } from './fearTracker.mjs';
export { default as DhHotbar } from './hotbar.mjs';

View file

@ -14,8 +14,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
} }
addChatListeners = async (app, html, data) => { addChatListeners = async (app, html, data) => {
super.addChatListeners(app, html, data);
html.querySelectorAll('.duality-action-damage').forEach(element => html.querySelectorAll('.duality-action-damage').forEach(element =>
element.addEventListener('click', event => this.onRollDamage(event, data.message)) element.addEventListener('click', event => this.onRollDamage(event, data.message))
); );
@ -90,7 +88,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
onRollDamage = async (event, message) => { onRollDamage = async (event, message) => {
event.stopPropagation(); event.stopPropagation();
const actor = await this.getActor(message.system.source.actor); 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) { if (message.system.source.item && message.system.source.action) {
const action = this.getAction(actor, 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; if (!action || !action?.rollDamage) return;

View file

@ -66,6 +66,11 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
} }
async setCombatantSpotlight(combatantId) { async setCombatantSpotlight(combatantId) {
const update = {
system: {
'spotlight.requesting': false
}
};
const combatant = this.viewed.combatants.get(combatantId); const combatant = this.viewed.combatants.get(combatantId);
const toggleTurn = this.viewed.combatants.contents const toggleTurn = this.viewed.combatants.contents
@ -73,10 +78,18 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
.map(x => x.id) .map(x => x.id)
.indexOf(combatantId); .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 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) { static async requestSpotlight(_, target) {

View file

@ -1,4 +1,3 @@
import { countdownTypes } from '../../config/generalConfig.mjs';
import { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import constructHTMLButton from '../../helpers/utils.mjs'; import constructHTMLButton from '../../helpers/utils.mjs';
import OwnershipSelection from '../dialogs/ownershipSelection.mjs'; import OwnershipSelection from '../dialogs/ownershipSelection.mjs';
@ -328,43 +327,29 @@ export class EncounterCountdowns extends Countdowns {
}; };
} }
export const registerCountdownApplicationHooks = () => { export async function updateCountdowns(progressType) {
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); const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
for (let countdownCategoryKey in countdownSetting) { const update = Object.keys(countdownSetting).reduce((update, typeKey) => {
const countdownCategory = countdownSetting[countdownCategoryKey]; return foundry.utils.mergeObject(
for (let countdownKey in countdownCategory.countdowns) { update,
const countdown = countdownCategory.countdowns[countdownKey]; 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;
}
if (shouldProgress(countdown)) { return acc;
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(); }, {});
}
}
}
}
};
Hooks.on(CONFIG.DH.HOOKS.characterAttack, async () => { await countdownSetting.updateSource(update);
updateCountdowns(countdown => { await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, countdownSetting);
return (
countdown.progress.type.value === countdownTypes.characterAttack.id && countdown.progress.current > 0
);
});
});
Hooks.on(CONFIG.DH.HOOKS.spotlight, async () => { const data = { refreshType: RefreshType.Countdown };
updateCountdowns(countdown => { await game.socket.emit(`system.${CONFIG.DH.id}`, {
return countdown.progress.type.value === countdownTypes.spotlight.id && countdown.progress.current > 0; action: socketEvent.Refresh,
data
}); });
}); Hooks.callAll(socketEvent.Refresh, data);
}; }

View file

@ -0,0 +1,129 @@
export default class DhHotbar extends foundry.applications.ui.Hotbar {
constructor(options) {
super(options);
this.setupHooks();
}
static async useItem(uuid) {
const item = await fromUuid(uuid);
if (!item) {
return ui.notifications.warn('WARNING.ObjectDoesNotExist', {
format: {
name: game.i18n.localize('Document'),
identifier: uuid
}
});
}
await item.use({});
}
static async useAction(itemUuid, actionId) {
const item = await foundry.utils.fromUuid(itemUuid);
if (!item) {
return ui.notifications.warn('WARNING.ObjectDoesNotExist', {
format: {
name: game.i18n.localize('Document'),
identifier: itemUuid
}
});
}
const action = item.system.actions.find(x => x.id === actionId);
if (!action) {
return ui.notifications.warn('DAGGERHEART.UI.Notifications.actionIsMissing');
}
await action.use({});
}
static async useAttack(actorUuid) {
const actor = await foundry.utils.fromUuid(actorUuid);
if (!actor) {
return ui.notifications.warn('WARNING.ObjectDoesNotExist', {
format: {
name: game.i18n.localize('Document'),
identifier: actorUuid
}
});
}
const attack = actor.system.attack;
if (!attack) {
return ui.notifications.warn('DAGGERHEART.UI.Notifications.attackIsMissing');
}
await attack.use({});
}
setupHooks() {
Hooks.on('hotbarDrop', (bar, data, slot) => {
if (data.type === 'Item') {
const item = foundry.utils.fromUuidSync(data.uuid);
if (item.uuid.startsWith('Compendium') || !item.isOwned || !item.isOwner) return true;
switch (item.type) {
case 'ancestry':
case 'community':
case 'class':
case 'subclass':
return true;
default:
this.createItemMacro(item, slot);
return false;
}
} else if (data.type === 'Action') {
const item = foundry.utils.fromUuidSync(data.data.itemUuid);
if (item.uuid.startsWith('Compendium')) return true;
if (!item.isOwned || !item.isOwner) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.unownedActionMacro'));
return false;
}
this.createActionMacro(data, slot);
return false;
} else if (data.type === 'Attack') {
const actor = foundry.utils.fromUuidSync(data.actorUuid);
if (actor.uuid.startsWith('Compendium')) return true;
if (!actor.isOwner) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.unownedAttackMacro'));
return false;
}
this.createAttackMacro(data, slot);
return false;
}
});
}
async createItemMacro(data, slot) {
const macro = await Macro.implementation.create({
name: `${game.i18n.localize('Display')} ${name}`,
type: CONST.MACRO_TYPES.SCRIPT,
img: data.img,
command: `await game.system.api.applications.ui.DhHotbar.useItem("${data.uuid}");`
});
await game.user.assignHotbarMacro(macro, slot);
}
async createActionMacro(data, slot) {
const macro = await Macro.implementation.create({
name: `${game.i18n.localize('Display')} ${name}`,
type: CONST.MACRO_TYPES.SCRIPT,
img: data.data.img,
command: `await game.system.api.applications.ui.DhHotbar.useAction("${data.data.itemUuid}", "${data.data.id}");`
});
await game.user.assignHotbarMacro(macro, slot);
}
async createAttackMacro(data, slot) {
const macro = await Macro.implementation.create({
name: `${game.i18n.localize('Display')} ${name}`,
type: CONST.MACRO_TYPES.SCRIPT,
img: data.img,
command: `await game.system.api.applications.ui.DhHotbar.useAttack("${data.actorUuid}");`
});
await game.user.assignHotbarMacro(macro, slot);
}
}

View file

@ -1,4 +1,5 @@
export { default as DhMeasuredTemplate } from './measuredTemplate.mjs'; export { default as DhMeasuredTemplate } from './measuredTemplate.mjs';
export { default as DhRuler } from './ruler.mjs'; export { default as DhRuler } from './ruler.mjs';
export { default as DhTemplateLayer } from './templateLayer.mjs'; export { default as DhTemplateLayer } from './templateLayer.mjs';
export { default as DhTokenPlaceable } from './token.mjs';
export { default as DhTokenRuler } from './tokenRuler.mjs'; export { default as DhTokenRuler } from './tokenRuler.mjs';

View file

@ -0,0 +1,36 @@
export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
/** @inheritDoc */
async _drawEffects() {
this.effects.renderable = false;
// Clear Effects Container
this.effects.removeChildren().forEach(c => c.destroy());
this.effects.bg = this.effects.addChild(new PIXI.Graphics());
this.effects.bg.zIndex = -1;
this.effects.overlay = null;
// Categorize effects
const activeEffects = this.actor ? Array.from(this.actor.effects).filter(x => !x.disabled) : [];
const overlayEffect = activeEffects.findLast(e => e.img && e.getFlag('core', 'overlay'));
// Draw effects
const promises = [];
for (const [i, effect] of activeEffects.entries()) {
if (!effect.img) continue;
const promise =
effect === overlayEffect
? this._drawOverlay(effect.img, effect.tint)
: this._drawEffect(effect.img, effect.tint);
promises.push(
promise.then(e => {
if (e) e.zIndex = i;
})
);
}
await Promise.allSettled(promises);
this.effects.sortChildren();
this.effects.renderable = true;
this.renderFlags.set({ refreshEffects: true });
}
}

View file

@ -4,7 +4,6 @@ export * as domainConfig from './domainConfig.mjs';
export * as effectConfig from './effectConfig.mjs'; export * as effectConfig from './effectConfig.mjs';
export * as flagsConfig from './flagsConfig.mjs'; export * as flagsConfig from './flagsConfig.mjs';
export * as generalConfig from './generalConfig.mjs'; export * as generalConfig from './generalConfig.mjs';
export * as hooksConfig from './hooksConfig.mjs';
export * as itemConfig from './itemConfig.mjs'; export * as itemConfig from './itemConfig.mjs';
export * as settingsConfig from './settingsConfig.mjs'; export * as settingsConfig from './settingsConfig.mjs';
export * as systemConfig from './system.mjs'; export * as systemConfig from './system.mjs';

View file

@ -411,7 +411,7 @@ export const levelupData = {
}; };
export const subclassFeatureLabels = { export const subclassFeatureLabels = {
1: 'DAGGERHEART.ITEMS.DomainCard.foundation', 1: 'DAGGERHEART.ITEMS.DomainCard.foundationTitle',
2: 'DAGGERHEART.ITEMS.DomainCard.specializationTitle', 2: 'DAGGERHEART.ITEMS.DomainCard.specializationTitle',
3: 'DAGGERHEART.ITEMS.DomainCard.masteryTitle' 3: 'DAGGERHEART.ITEMS.DomainCard.masteryTitle'
}; };

View file

@ -3,55 +3,55 @@ export const domains = {
id: 'arcana', id: 'arcana',
label: 'DAGGERHEART.GENERAL.Domain.arcana.label', label: 'DAGGERHEART.GENERAL.Domain.arcana.label',
src: 'systems/daggerheart/assets/icons/domains/arcana.svg', src: 'systems/daggerheart/assets/icons/domains/arcana.svg',
description: 'DAGGERHEART.GENERAL.Domain.Arcana' description: 'DAGGERHEART.GENERAL.Domain.arcana.description'
}, },
blade: { blade: {
id: 'blade', id: 'blade',
label: 'DAGGERHEART.GENERAL.Domain.blade.label', label: 'DAGGERHEART.GENERAL.Domain.blade.label',
src: 'systems/daggerheart/assets/icons/domains/blade.svg', src: 'systems/daggerheart/assets/icons/domains/blade.svg',
description: 'DAGGERHEART.GENERAL.Domain.Blade' description: 'DAGGERHEART.GENERAL.Domain.blade.description'
}, },
bone: { bone: {
id: 'bone', id: 'bone',
label: 'DAGGERHEART.GENERAL.Domain.bone.label', label: 'DAGGERHEART.GENERAL.Domain.bone.label',
src: 'systems/daggerheart/assets/icons/domains/bone.svg', src: 'systems/daggerheart/assets/icons/domains/bone.svg',
description: 'DAGGERHEART.GENERAL.Domain.Bone' description: 'DAGGERHEART.GENERAL.Domain.bone.description'
}, },
codex: { codex: {
id: 'codex', id: 'codex',
label: 'DAGGERHEART.GENERAL.Domain.codex.label', label: 'DAGGERHEART.GENERAL.Domain.codex.label',
src: 'systems/daggerheart/assets/icons/domains/codex.svg', src: 'systems/daggerheart/assets/icons/domains/codex.svg',
description: 'DAGGERHEART.GENERAL.Domain.Codex' description: 'DAGGERHEART.GENERAL.Domain.codex.description'
}, },
grace: { grace: {
id: 'grace', id: 'grace',
label: 'DAGGERHEART.GENERAL.Domain.grace.label', label: 'DAGGERHEART.GENERAL.Domain.grace.label',
src: 'systems/daggerheart/assets/icons/domains/grace.svg', src: 'systems/daggerheart/assets/icons/domains/grace.svg',
description: 'DAGGERHEART.GENERAL.Domain.Grace' description: 'DAGGERHEART.GENERAL.Domain.grace.description'
}, },
midnight: { midnight: {
id: 'midnight', id: 'midnight',
label: 'DAGGERHEART.GENERAL.Domain.midnight.label', label: 'DAGGERHEART.GENERAL.Domain.midnight.label',
src: 'systems/daggerheart/assets/icons/domains/midnight.svg', src: 'systems/daggerheart/assets/icons/domains/midnight.svg',
description: 'DAGGERHEART.GENERAL.Domain.Midnight' description: 'DAGGERHEART.GENERAL.Domain.midnight.description'
}, },
sage: { sage: {
id: 'sage', id: 'sage',
label: 'DAGGERHEART.GENERAL.Domain.sage.label', label: 'DAGGERHEART.GENERAL.Domain.sage.label',
src: 'systems/daggerheart/assets/icons/domains/sage.svg', src: 'systems/daggerheart/assets/icons/domains/sage.svg',
description: 'DAGGERHEART.GENERAL.Domain.Sage' description: 'DAGGERHEART.GENERAL.Domain.sage.description'
}, },
splendor: { splendor: {
id: 'splendor', id: 'splendor',
label: 'DAGGERHEART.GENERAL.Domain.splendor.label', label: 'DAGGERHEART.GENERAL.Domain.splendor.label',
src: 'systems/daggerheart/assets/icons/domains/splendor.svg', src: 'systems/daggerheart/assets/icons/domains/splendor.svg',
description: 'DAGGERHEART.GENERAL.Domain.Splendor' description: 'DAGGERHEART.GENERAL.Domain.splendor.description'
}, },
valor: { valor: {
id: 'valor', id: 'valor',
label: 'DAGGERHEART.GENERAL.Domain.valor.label', label: 'DAGGERHEART.GENERAL.Domain.valor.label',
src: 'systems/daggerheart/assets/icons/domains/valor.svg', src: 'systems/daggerheart/assets/icons/domains/valor.svg',
description: 'DAGGERHEART.GENERAL.Domain.Valor' description: 'DAGGERHEART.GENERAL.Domain.valor.description'
} }
}; };

View file

@ -110,6 +110,18 @@ export const conditions = {
name: 'DAGGERHEART.CONFIG.Condition.restrained.name', name: 'DAGGERHEART.CONFIG.Condition.restrained.name',
icon: 'icons/magic/control/debuff-chains-shackle-movement-red.webp', icon: 'icons/magic/control/debuff-chains-shackle-movement-red.webp',
description: 'DAGGERHEART.CONFIG.Condition.restrained.description' description: 'DAGGERHEART.CONFIG.Condition.restrained.description'
},
unconcious: {
id: 'unconcious',
name: 'DAGGERHEART.CONFIG.Condition.unconcious.name',
icon: 'icons/magic/control/sleep-bubble-purple.webp',
description: 'DAGGERHEART.CONFIG.Condition.unconcious.description'
},
dead: {
id: 'dead',
name: 'DAGGERHEART.CONFIG.Condition.dead.name',
icon: 'icons/magic/death/grave-tombstone-glow-teal.webp',
description: 'DAGGERHEART.CONFIG.Condition.dead.description'
} }
}; };
@ -118,6 +130,7 @@ export const defaultRestOptions = {
tendToWounds: { tendToWounds: {
id: 'tendToWounds', id: 'tendToWounds',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.tendToWounds.name'), name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.tendToWounds.name'),
icon: 'fa-solid fa-bandage',
img: 'icons/magic/life/cross-worn-green.webp', img: 'icons/magic/life/cross-worn-green.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.tendToWounds.description'), description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.tendToWounds.description'),
actions: [ actions: [
@ -141,6 +154,7 @@ export const defaultRestOptions = {
clearStress: { clearStress: {
id: 'clearStress', id: 'clearStress',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.clearStress.name'), name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.clearStress.name'),
icon: 'fa-regular fa-face-surprise',
img: 'icons/magic/perception/eye-ringed-green.webp', img: 'icons/magic/perception/eye-ringed-green.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.clearStress.description'), description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.clearStress.description'),
actions: [ actions: [
@ -164,6 +178,7 @@ export const defaultRestOptions = {
repairArmor: { repairArmor: {
id: 'repairArmor', id: 'repairArmor',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.repairArmor.name'), name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.repairArmor.name'),
icon: 'fa-solid fa-hammer',
img: 'icons/skills/trades/smithing-anvil-silver-red.webp', img: 'icons/skills/trades/smithing-anvil-silver-red.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.repairArmor.description'), description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.repairArmor.description'),
actions: [] actions: []
@ -171,6 +186,7 @@ export const defaultRestOptions = {
prepare: { prepare: {
id: 'prepare', id: 'prepare',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepare.name'), name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepare.name'),
icon: 'fa-solid fa-dumbbell',
img: 'icons/skills/trades/academics-merchant-scribe.webp', img: 'icons/skills/trades/academics-merchant-scribe.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepare.description'), description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepare.description'),
actions: [] actions: []
@ -180,6 +196,7 @@ export const defaultRestOptions = {
tendToWounds: { tendToWounds: {
id: 'tendToWounds', id: 'tendToWounds',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.tendToWounds.name'), name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.tendToWounds.name'),
icon: 'fa-solid fa-bandage',
img: 'icons/magic/life/cross-worn-green.webp', img: 'icons/magic/life/cross-worn-green.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.tendToWounds.description'), description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.tendToWounds.description'),
actions: [] actions: []
@ -187,6 +204,7 @@ export const defaultRestOptions = {
clearStress: { clearStress: {
id: 'clearStress', id: 'clearStress',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.clearStress.name'), name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.clearStress.name'),
icon: 'fa-regular fa-face-surprise',
img: 'icons/magic/perception/eye-ringed-green.webp', img: 'icons/magic/perception/eye-ringed-green.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.clearStress.description'), description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.clearStress.description'),
actions: [] actions: []
@ -194,6 +212,7 @@ export const defaultRestOptions = {
repairArmor: { repairArmor: {
id: 'repairArmor', id: 'repairArmor',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.repairArmor.name'), name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.repairArmor.name'),
icon: 'fa-solid fa-hammer',
img: 'icons/skills/trades/smithing-anvil-silver-red.webp', img: 'icons/skills/trades/smithing-anvil-silver-red.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.repairArmor.description'), description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.repairArmor.description'),
actions: [] actions: []
@ -201,6 +220,7 @@ export const defaultRestOptions = {
prepare: { prepare: {
id: 'prepare', id: 'prepare',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepare.name'), name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepare.name'),
icon: 'fa-solid fa-dumbbell',
img: 'icons/skills/trades/academics-merchant-scribe.webp', img: 'icons/skills/trades/academics-merchant-scribe.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepare.description'), description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepare.description'),
actions: [] actions: []
@ -208,19 +228,12 @@ export const defaultRestOptions = {
workOnAProject: { workOnAProject: {
id: 'workOnAProject', id: 'workOnAProject',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.workOnAProject.name'), name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.workOnAProject.name'),
icon: 'fa-solid fa-diagram-project',
img: 'icons/skills/social/thumbsup-approval-like.webp', img: 'icons/skills/social/thumbsup-approval-like.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.workOnAProject.description'), description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.workOnAProject.description'),
actions: [] actions: []
} }
}), })
custom: {
id: 'customActivity',
name: '',
img: 'icons/skills/trades/academics-investigation-puzzles.webp',
description: '',
namePlaceholder: 'DAGGERHEART.APPLICATIONS.Downtime.custom.namePlaceholder',
placeholder: 'DAGGERHEART.APPLICATIONS.Downtime.custom.placeholder'
}
}; };
export const deathMoves = { export const deathMoves = {
@ -376,29 +389,29 @@ export const abilityCosts = {
export const countdownTypes = { export const countdownTypes = {
spotlight: { spotlight: {
id: 'spotlight', id: 'spotlight',
label: 'DAGGERHEART.CONFIG.CountdownTypes.Spotlight' label: 'DAGGERHEART.CONFIG.CountdownType.spotlight'
}, },
characterAttack: { characterAttack: {
id: 'characterAttack', id: 'characterAttack',
label: 'DAGGERHEART.CONFIG.CountdownTypes.CharacterAttack' label: 'DAGGERHEART.CONFIG.CountdownType.characterAttack'
}, },
custom: { custom: {
id: 'custom', id: 'custom',
label: 'DAGGERHEART.CONFIG.CountdownTypes.Custom' label: 'DAGGERHEART.CONFIG.CountdownType.custom'
} }
}; };
export const rollTypes = { export const rollTypes = {
weapon: { attack: {
id: 'weapon', id: 'attack',
label: 'DAGGERHEART.CONFIG.RollTypes.weapon.name' label: 'DAGGERHEART.CONFIG.RollTypes.attack.name'
}, },
spellcast: { spellcast: {
id: 'spellcast', id: 'spellcast',
label: 'DAGGERHEART.CONFIG.RollTypes.spellcast.name' label: 'DAGGERHEART.CONFIG.RollTypes.spellcast.name'
}, },
ability: { trait: {
id: 'ability', id: 'trait',
label: 'DAGGERHEART.CONFIG.RollTypes.ability.name' label: 'DAGGERHEART.CONFIG.RollTypes.trait.name'
}, },
diceSet: { diceSet: {
id: 'diceSet', id: 'diceSet',

View file

@ -1,4 +0,0 @@
export const hooks = {
characterAttack: 'characterAttackHook',
spotlight: 'spotlightHook'
};

View file

@ -439,7 +439,7 @@ export const weaponFeatures = {
{ {
key: 'system.bonuses.damage.primaryWeapon.bonus', key: 'system.bonuses.damage.primaryWeapon.bonus',
mode: 2, mode: 2,
value: '@system.levelData.levels.current' value: '@system.levelData.level.current'
} }
] ]
} }
@ -1261,15 +1261,6 @@ export const weaponFeatures = {
timebending: { timebending: {
label: 'DAGGERHEART.CONFIG.WeaponFeature.timebending.name', label: 'DAGGERHEART.CONFIG.WeaponFeature.timebending.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.timebending.description' description: 'DAGGERHEART.CONFIG.WeaponFeature.timebending.description'
},
versatile: {
label: 'DAGGERHEART.CONFIG.WeaponFeature.versatile.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.versatile.description'
// versatile: {
// characterTrait: '',
// range: '',
// damage: ''
// }
} }
}; };
@ -1322,7 +1313,12 @@ export const featureTypes = {
export const featureSubTypes = { export const featureSubTypes = {
primary: 'primary', primary: 'primary',
secondary: 'secondary' secondary: 'secondary',
hope: 'hope',
class: 'class',
foundation: 'foundation',
specialization: 'specialization',
mastery: 'mastery'
}; };
export const actionTypes = { export const actionTypes = {

View file

@ -3,7 +3,6 @@ import * as DOMAIN from './domainConfig.mjs';
import * as ACTOR from './actorConfig.mjs'; import * as ACTOR from './actorConfig.mjs';
import * as ITEM from './itemConfig.mjs'; import * as ITEM from './itemConfig.mjs';
import * as SETTINGS from './settingsConfig.mjs'; import * as SETTINGS from './settingsConfig.mjs';
import { hooks as HOOKS } from './hooksConfig.mjs';
import * as EFFECTS from './effectConfig.mjs'; import * as EFFECTS from './effectConfig.mjs';
import * as ACTIONS from './actionConfig.mjs'; import * as ACTIONS from './actionConfig.mjs';
import * as FLAGS from './flagsConfig.mjs'; import * as FLAGS from './flagsConfig.mjs';
@ -17,7 +16,6 @@ export const SYSTEM = {
ACTOR, ACTOR,
ITEM, ITEM,
SETTINGS, SETTINGS,
HOOKS,
EFFECTS, EFFECTS,
ACTIONS, ACTIONS,
FLAGS FLAGS

View file

@ -5,7 +5,7 @@ export default class DHAttackAction extends DHDamageAction {
static extraSchemas = [...super.extraSchemas, ...['roll', 'save']]; static extraSchemas = [...super.extraSchemas, ...['roll', 'save']];
static getRollType(parent) { static getRollType(parent) {
return parent.type === 'weapon' ? 'weapon' : 'spellcast'; return parent.type === 'weapon' ? 'attack' : 'spellcast';
} }
get chatTemplate() { get chatTemplate() {
@ -21,7 +21,7 @@ export default class DHAttackAction extends DHDamageAction {
} }
if (this.roll.useDefault) { if (this.roll.useDefault) {
this.roll.trait = this.item.system.attack.roll.trait; this.roll.trait = this.item.system.attack.roll.trait;
this.roll.type = 'weapon'; this.roll.type = 'attack';
} }
} }
} }
@ -37,4 +37,17 @@ export default class DHAttackAction extends DHDamageAction {
base: true base: true
}; };
} }
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 [];
// }
} }

View file

@ -150,7 +150,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
} }
static getRollType(parent) { static getRollType(parent) {
return 'ability'; return 'trait';
} }
static getSourceConfig(parent) { static getSourceConfig(parent) {
@ -268,7 +268,8 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
hasDamage: !!this.damage?.parts?.length, hasDamage: !!this.damage?.parts?.length,
hasHealing: !!this.healing, hasHealing: !!this.healing,
hasEffect: !!this.effects?.length, hasEffect: !!this.effects?.length,
hasSave: this.hasSave hasSave: this.hasSave,
selectedRollMode: game.settings.get('core', 'rollMode')
}; };
} }
@ -308,7 +309,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
prepareRoll() { prepareRoll() {
const roll = { const roll = {
modifiers: [], modifiers: this.modifiers,
trait: this.roll?.trait, trait: this.roll?.trait,
label: 'Attack', label: 'Attack',
type: this.actionType, type: this.actionType,
@ -362,6 +363,13 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
get hasRoll() { get hasRoll() {
return !!this.roll?.type || !!this.roll?.bonus; return !!this.roll?.type || !!this.roll?.bonus;
} }
get modifiers() {
if (!this.actor) return [];
const modifiers = [];
/** Placeholder for specific bonuses **/
return modifiers;
}
/* ROLL */ /* ROLL */
/* SAVE */ /* SAVE */

View file

@ -6,10 +6,20 @@ export default class DHDamageAction extends DHBaseAction {
getFormulaValue(part, data) { getFormulaValue(part, data) {
let formulaValue = part.value; let formulaValue = part.value;
if (this.hasRoll && part.resultBased && data.system.roll.result.duality === -1) return part.valueAlt; if (this.hasRoll && part.resultBased && data.system.roll.result.duality === -1) return part.valueAlt;
const isAdversary = this.actor.type === 'adversary';
if (isAdversary && this.actor.system.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id) {
const hasHordeDamage = this.actor.effects.find(
x => x.name === game.i18n.localize('DAGGERHEART.CONFIG.AdversaryType.horde.label')
);
if (hasHordeDamage) return part.valueAlt;
}
return formulaValue; return formulaValue;
} }
async rollDamage(event, data) { async rollDamage(event, data) {
const systemData = data.system ?? data;
let formula = this.damage.parts.map(p => this.getFormulaValue(p, data).getFormula(this.actor)).join(' + '), 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]), []))]; damageTypes = [...new Set(this.damage.parts.reduce((a, c) => a.concat([...c.type]), []))];
@ -19,15 +29,16 @@ export default class DHDamageAction extends DHBaseAction {
let roll = { formula: formula, total: formula }, let roll = { formula: formula, total: formula },
bonusDamage = []; 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 = { const config = {
title: game.i18n.format('DAGGERHEART.UI.Chat.damageRoll.title', { damage: this.name }), title: game.i18n.format('DAGGERHEART.UI.Chat.damageRoll.title', { damage: this.name }),
roll: { formula }, roll: { formula },
targets: data.system?.targets.filter(t => t.hit) ?? data.targets, targets: systemData.targets.filter(t => t.hit) ?? data.targets,
hasSave: this.hasSave, hasSave: this.hasSave,
isCritical: data.system?.roll?.isCritical ?? false, isCritical: systemData.roll?.isCritical ?? false,
source: data.system?.source, source: systemData.source,
data: this.getRollData(),
damageTypes, damageTypes,
event event
}; };
@ -35,8 +46,14 @@ export default class DHDamageAction extends DHBaseAction {
if (data.system) { if (data.system) {
config.source.message = data._id; config.source.message = data._id;
config.directDamage = false; config.directDamage = false;
} else {
config.directDamage = true;
} }
roll = CONFIG.Dice.daggerheart.DamageRoll.build(config); roll = CONFIG.Dice.daggerheart.DamageRoll.build(config);
} }
// get modifiers() {
// return [];
// }
} }

View file

@ -39,4 +39,8 @@ export default class DHHealingAction extends DHBaseAction {
get chatTemplate() { get chatTemplate() {
return 'systems/daggerheart/templates/ui/chat/healing-roll.hbs'; return 'systems/daggerheart/templates/ui/chat/healing-roll.hbs';
} }
get modifiers() {
return [];
}
} }

View file

@ -1,13 +1,7 @@
import DHAdversarySettings from '../../applications/sheets-configs/adversary-settings.mjs'; import DHAdversarySettings from '../../applications/sheets-configs/adversary-settings.mjs';
import ActionField from '../fields/actionField.mjs'; import ActionField from '../fields/actionField.mjs';
import BaseDataActor from './base.mjs'; import BaseDataActor from './base.mjs';
import { resourceField, bonusField } from '../fields/actorField.mjs';
const resourceField = () =>
new foundry.data.fields.SchemaField({
value: new foundry.data.fields.NumberField({ initial: 0, integer: true }),
max: new foundry.data.fields.NumberField({ initial: 0, integer: true }),
isReversed: new foundry.data.fields.BooleanField({ initial: true })
});
export default class DhpAdversary extends BaseDataActor { export default class DhpAdversary extends BaseDataActor {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Adversary']; static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Adversary'];
@ -37,14 +31,29 @@ export default class DhpAdversary extends BaseDataActor {
motivesAndTactics: new fields.StringField(), motivesAndTactics: new fields.StringField(),
notes: new fields.HTMLField(), notes: new fields.HTMLField(),
difficulty: new fields.NumberField({ required: true, initial: 1, integer: true }), 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({ damageThresholds: new fields.SchemaField({
major: new fields.NumberField({ required: true, initial: 0, integer: true }), major: new fields.NumberField({
severe: new fields.NumberField({ required: true, initial: 0, integer: true }) 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({ resources: new fields.SchemaField({
hitPoints: resourceField(), hitPoints: resourceField(0, 'DAGGERHEART.GENERAL.hitPoints.plural', true),
stress: resourceField() stress: resourceField(0, 'DAGGERHEART.GENERAL.stress', true)
}), }),
attack: new ActionField({ attack: new ActionField({
initial: { initial: {
@ -59,7 +68,7 @@ export default class DhpAdversary extends BaseDataActor {
amount: 1 amount: 1
}, },
roll: { roll: {
type: 'weapon' type: 'attack'
}, },
damage: { damage: {
parts: [ parts: [
@ -80,9 +89,14 @@ export default class DhpAdversary extends BaseDataActor {
}) })
), ),
bonuses: new fields.SchemaField({ bonuses: new fields.SchemaField({
difficulty: new fields.SchemaField({ roll: new fields.SchemaField({
all: new fields.NumberField({ integer: true, initial: 0 }), attack: bonusField('DAGGERHEART.GENERAL.Roll.attack'),
reaction: new fields.NumberField({ integer: true, initial: 0 }) action: bonusField('DAGGERHEART.GENERAL.Roll.action'),
reaction: bonusField('DAGGERHEART.GENERAL.Roll.reaction')
}),
damage: new fields.SchemaField({
physical: bonusField('DAGGERHEART.GENERAL.Damage.physicalDamage'),
magical: bonusField('DAGGERHEART.GENERAL.Damage.magicalDamage')
}) })
}) })
}; };
@ -95,4 +109,37 @@ export default class DhpAdversary extends BaseDataActor {
get features() { get features() {
return this.parent.items.filter(x => x.type === 'feature'); return this.parent.items.filter(x => x.type === 'feature');
} }
async _preUpdate(changes, options, user) {
const allowed = await super._preUpdate(changes, options, user);
if (allowed === false) return false;
if (this.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id) {
if (changes.system?.resources?.hitPoints?.value) {
const halfHP = Math.ceil(this.resources.hitPoints.max / 2);
const newHitPoints = changes.system.resources.hitPoints.value;
const previouslyAboveHalf = this.resources.hitPoints.value < halfHP;
const loweredBelowHalf = previouslyAboveHalf && newHitPoints >= halfHP;
const raisedAboveHalf = !previouslyAboveHalf && newHitPoints < halfHP;
if (loweredBelowHalf) {
await this.parent.createEmbeddedDocuments('ActiveEffect', [
{
name: game.i18n.localize('DAGGERHEART.CONFIG.AdversaryType.horde.label'),
img: 'icons/magic/movement/chevrons-down-yellow.webp',
disabled: !game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation)
.hordeDamage
}
]);
} else if (raisedAboveHalf) {
const hordeEffects = this.parent.effects.filter(
x => x.name === game.i18n.localize('DAGGERHEART.CONFIG.AdversaryType.horde.label')
);
await this.parent.deleteEmbeddedDocuments(
'ActiveEffect',
hordeEffects.map(x => x.id)
);
}
}
}
}
} }

View file

@ -1,10 +1,10 @@
import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs'; import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs';
const resistanceField = () => const resistanceField = reductionLabel =>
new foundry.data.fields.SchemaField({ new foundry.data.fields.SchemaField({
resistance: new foundry.data.fields.BooleanField({ initial: false }), resistance: new foundry.data.fields.BooleanField({ initial: false }),
immunity: 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.isNPC) schema.description = new fields.HTMLField({ required: true, nullable: true });
if (this.metadata.hasResistances) if (this.metadata.hasResistances)
schema.resistance = new fields.SchemaField({ schema.resistance = new fields.SchemaField({
physical: resistanceField(), physical: resistanceField('DAGGERHEART.GENERAL.DamageResistance.physicalReduction'),
magical: resistanceField() magical: resistanceField('DAGGERHEART.GENERAL.DamageResistance.magicalReduction')
}); });
return schema; return schema;
} }

View file

@ -2,27 +2,11 @@ import { burden } from '../../config/generalConfig.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import DhLevelData from '../levelData.mjs'; import DhLevelData from '../levelData.mjs';
import BaseDataActor from './base.mjs'; import BaseDataActor from './base.mjs';
import { attributeField, resourceField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs';
const attributeField = () =>
new foundry.data.fields.SchemaField({
value: new foundry.data.fields.NumberField({ initial: 0, integer: true }),
tierMarked: new foundry.data.fields.BooleanField({ initial: false })
});
const resourceField = (max, reverse = false) =>
new foundry.data.fields.SchemaField({
value: new foundry.data.fields.NumberField({ initial: 0, integer: true }),
max: new foundry.data.fields.NumberField({ initial: max, integer: true }),
isReversed: new foundry.data.fields.BooleanField({ initial: reverse })
});
const stressDamageReductionRule = () =>
new foundry.data.fields.SchemaField({
enabled: new foundry.data.fields.BooleanField({ required: true, initial: false }),
cost: new foundry.data.fields.NumberField({ integer: true })
});
export default class DhCharacter extends BaseDataActor { export default class DhCharacter extends BaseDataActor {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Character'];
static get metadata() { static get metadata() {
return foundry.utils.mergeObject(super.metadata, { return foundry.utils.mergeObject(super.metadata, {
label: 'TYPES.Actor.character', label: 'TYPES.Actor.character',
@ -37,24 +21,36 @@ export default class DhCharacter extends BaseDataActor {
return { return {
...super.defineSchema(), ...super.defineSchema(),
resources: new fields.SchemaField({ resources: new fields.SchemaField({
hitPoints: resourceField(0, true), hitPoints: resourceField(0, 'DAGGERHEART.GENERAL.hitPoints.plural', true),
stress: resourceField(6, true), stress: resourceField(6, 'DAGGERHEART.GENERAL.stress', true),
hope: resourceField(6) hope: resourceField(6, 'DAGGERHEART.GENERAL.hope')
}), }),
traits: new fields.SchemaField({ traits: new fields.SchemaField({
agility: attributeField(), agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'),
strength: attributeField(), strength: attributeField('DAGGERHEART.CONFIG.Traits.strength.name'),
finesse: attributeField(), finesse: attributeField('DAGGERHEART.CONFIG.Traits.finesse.name'),
instinct: attributeField(), instinct: attributeField('DAGGERHEART.CONFIG.Traits.instinct.name'),
presence: attributeField(), presence: attributeField('DAGGERHEART.CONFIG.Traits.presence.name'),
knowledge: attributeField() knowledge: attributeField('DAGGERHEART.CONFIG.Traits.knowledge.name')
}), }),
proficiency: new fields.NumberField({ initial: 1, integer: true }), proficiency: new fields.NumberField({
evasion: new fields.NumberField({ initial: 0, integer: true }), initial: 1,
armorScore: new fields.NumberField({ integer: true, initial: 0 }), 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({ damageThresholds: new fields.SchemaField({
severe: new fields.NumberField({ integer: true, initial: 0 }), severe: new fields.NumberField({
major: new fields.NumberField({ integer: true, initial: 0 }) 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( experiences: new fields.TypedObjectField(
new fields.SchemaField({ new fields.SchemaField({
@ -91,24 +87,82 @@ export default class DhCharacter extends BaseDataActor {
value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }), value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }),
subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }) subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true })
}), }),
advantageSources: new fields.ArrayField(new fields.StringField()),
disadvantageSources: new fields.ArrayField(new fields.StringField()),
levelData: new fields.EmbeddedDataField(DhLevelData), levelData: new fields.EmbeddedDataField(DhLevelData),
bonuses: new fields.SchemaField({ bonuses: new fields.SchemaField({
roll: new fields.SchemaField({ roll: new fields.SchemaField({
attack: new fields.NumberField({ integer: true, initial: 0 }), attack: bonusField('DAGGERHEART.GENERAL.Roll.attack'),
primaryWeapon: new fields.SchemaField({ spellcast: bonusField('DAGGERHEART.GENERAL.Roll.spellcast'),
attack: new fields.NumberField({ integer: true, initial: 0 }) trait: bonusField('DAGGERHEART.GENERAL.Roll.trait'),
}), action: bonusField('DAGGERHEART.GENERAL.Roll.action'),
spellcast: new fields.NumberField({ integer: true, initial: 0 }), reaction: bonusField('DAGGERHEART.GENERAL.Roll.reaction'),
action: new fields.NumberField({ integer: true, initial: 0 }), primaryWeapon: bonusField('DAGGERHEART.GENERAL.Roll.primaryWeaponAttack'),
hopeOrFear: new fields.NumberField({ integer: true, initial: 0 }) secondaryWeapon: bonusField('DAGGERHEART.GENERAL.Roll.secondaryWeaponAttack')
}), }),
damage: new fields.SchemaField({ damage: new fields.SchemaField({
all: new fields.NumberField({ integer: true, initial: 0 }), physical: bonusField('DAGGERHEART.GENERAL.Damage.physicalDamage'),
physical: new fields.NumberField({ integer: true, initial: 0 }), magical: bonusField('DAGGERHEART.GENERAL.Damage.magicalDamage'),
magic: new fields.NumberField({ integer: true, initial: 0 }), primaryWeapon: bonusField('DAGGERHEART.GENERAL.Damage.primaryWeapon'),
primaryWeapon: new fields.SchemaField({ secondaryWeapon: bonusField('DAGGERHEART.GENERAL.Damage.secondaryWeapon')
bonus: new fields.NumberField({ integer: true }), }),
extraDice: new fields.NumberField({ integer: true }) healing: bonusField('DAGGERHEART.GENERAL.Healing.healingAmount'),
range: new fields.SchemaField({
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'
})
}),
rally: new fields.ArrayField(new fields.StringField(), {
label: 'DAGGERHEART.CLASS.Feature.rallyDice'
}),
rest: new fields.SchemaField({
shortRest: new fields.SchemaField({
shortMoves: new fields.NumberField({
required: true,
integer: true,
min: 0,
initial: 0,
label: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.shortRestMoves.label',
hint: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.shortRestMoves.hint'
}),
longMoves: new fields.NumberField({
required: true,
integer: true,
min: 0,
initial: 0,
label: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.longRestMoves.label',
hint: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.longRestMoves.hint'
})
}),
longRest: new fields.SchemaField({
shortMoves: new fields.NumberField({
required: true,
integer: true,
min: 0,
initial: 0,
label: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.shortRestMoves.label',
hint: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.shortRestMoves.hint'
}),
longMoves: new fields.NumberField({
required: true,
integer: true,
min: 0,
initial: 0,
label: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.longRestMoves.label',
hint: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.longRestMoves.hint'
})
}) })
}) })
}), }),
@ -117,25 +171,34 @@ export default class DhCharacter extends BaseDataActor {
damageReduction: new fields.SchemaField({ damageReduction: new fields.SchemaField({
maxArmorMarked: new fields.SchemaField({ maxArmorMarked: new fields.SchemaField({
value: new fields.NumberField({ required: true, integer: true, initial: 1 }), value: new fields.NumberField({ required: true, integer: true, initial: 1 }),
bonus: new fields.NumberField({ required: true, integer: true, initial: 0 }), bonus: new fields.NumberField({
stressExtra: new fields.NumberField({ required: true, integer: true, initial: 0 }) 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({ stressDamageReduction: new fields.SchemaField({
severe: stressDamageReductionRule(), severe: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.severe'),
major: stressDamageReductionRule(), major: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.major'),
minor: stressDamageReductionRule() 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 }), magical: new fields.BooleanField({ initial: false }),
physical: 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({ weapon: new fields.SchemaField({
/* Unimplemented /* Unimplemented
-> Should remove the lowest damage dice from weapon damage -> Should remove the lowest damage dice from weapon damage
@ -181,6 +244,11 @@ export default class DhCharacter extends BaseDataActor {
return !this.class.value || !this.class.subclass; return !this.class.value || !this.class.subclass;
} }
get spellcastModifier() {
const subClasses = this.parent.items.filter(x => x.type === 'subclass') ?? [];
return Math.max(subClasses?.map(sc => this.traits[sc.system.spellcastingTrait]?.value));
}
get spellcastingModifiers() { get spellcastingModifiers() {
return { return {
main: this.class.subclass?.system?.spellcastingTrait, main: this.class.subclass?.system?.spellcastingTrait,
@ -219,23 +287,23 @@ export default class DhCharacter extends BaseDataActor {
features = []; features = [];
for (let item of this.parent.items) { for (let item of this.parent.items) {
if (item.system.type === CONFIG.DH.ITEM.featureTypes.ancestry.id) { if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.ancestry.id) {
ancestryFeatures.push(item); ancestryFeatures.push(item);
} else if (item.system.type === CONFIG.DH.ITEM.featureTypes.community.id) { } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.community.id) {
communityFeatures.push(item); communityFeatures.push(item);
} else if (item.system.type === CONFIG.DH.ITEM.featureTypes.class.id) { } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.class.id) {
classFeatures.push(item); classFeatures.push(item);
} else if (item.system.type === CONFIG.DH.ITEM.featureTypes.subclass.id) { } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.subclass.id) {
const subclassState = this.class.subclass.system.featureState; const subclassState = this.class.subclass.system.featureState;
const identifier = item.system.identifier; const subType = item.system.subType;
if ( if (
identifier === 'foundationFeature' || subType === CONFIG.DH.ITEM.featureSubTypes.foundation ||
(identifier === 'specializationFeature' && subclassState >= 2) || (subType === CONFIG.DH.ITEM.featureSubTypes.specialization && subclassState >= 2) ||
(identifier === 'masterFeature' && subclassState >= 3) (subType === CONFIG.DH.ITEM.featureSubTypes.mastery && subclassState >= 3)
) { ) {
subclassFeatures.push(item); subclassFeatures.push(item);
} }
} else if (item.system.type === CONFIG.DH.ITEM.featureTypes.companion.id) { } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.companion.id) {
companionFeatures.push(item); companionFeatures.push(item);
} else if (item.type === 'feature' && !item.system.type) { } else if (item.type === 'feature' && !item.system.type) {
features.push(item); features.push(item);
@ -323,6 +391,8 @@ export default class DhCharacter extends BaseDataActor {
} }
prepareBaseData() { prepareBaseData() {
this.evasion = this.class.value?.system?.evasion ?? 0;
const currentLevel = this.levelData.level.current; const currentLevel = this.levelData.level.current;
const currentTier = const currentTier =
currentLevel === 1 currentLevel === 1
@ -380,7 +450,8 @@ export default class DhCharacter extends BaseDataActor {
} }
prepareDerivedData() { prepareDerivedData() {
this.resources.hope.value = Math.min(this.resources.hope.value, this.resources.hope.max); const baseHope = this.resources.hope.value + (this.companion?.system?.resources?.hope ?? 0);
this.resources.hope.value = Math.min(baseHope, this.resources.hope.max);
} }
getRollData() { getRollData() {

View file

@ -4,6 +4,7 @@ import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import ActionField from '../fields/actionField.mjs'; import ActionField from '../fields/actionField.mjs';
import { adjustDice, adjustRange } from '../../helpers/utils.mjs'; import { adjustDice, adjustRange } from '../../helpers/utils.mjs';
import DHCompanionSettings from '../../applications/sheets-configs/companion-settings.mjs'; import DHCompanionSettings from '../../applications/sheets-configs/companion-settings.mjs';
import { resourceField, bonusField } from '../fields/actorField.mjs';
export default class DhCompanion extends BaseDataActor { export default class DhCompanion extends BaseDataActor {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Companion']; static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Companion'];
@ -23,14 +24,16 @@ export default class DhCompanion extends BaseDataActor {
...super.defineSchema(), ...super.defineSchema(),
partner: new ForeignDocumentUUIDField({ type: 'Actor' }), partner: new ForeignDocumentUUIDField({ type: 'Actor' }),
resources: new fields.SchemaField({ resources: new fields.SchemaField({
stress: new fields.SchemaField({ stress: resourceField(3, 'DAGGERHEART.GENERAL.stress', true),
value: new fields.NumberField({ initial: 0, integer: true }), hope: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.hope' })
max: new fields.NumberField({ initial: 3, integer: true }),
isReversed: new foundry.data.fields.BooleanField({ initial: true })
}), }),
hope: new fields.NumberField({ initial: 0, integer: true }) 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( experiences: new fields.TypedObjectField(
new fields.SchemaField({ new fields.SchemaField({
name: new fields.StringField({}), name: new fields.StringField({}),
@ -56,9 +59,8 @@ export default class DhCompanion extends BaseDataActor {
amount: 1 amount: 1
}, },
roll: { roll: {
type: 'weapon', type: 'attack',
bonus: 0, bonus: 0
trait: 'instinct'
}, },
damage: { damage: {
parts: [ parts: [
@ -74,13 +76,13 @@ export default class DhCompanion extends BaseDataActor {
} }
}), }),
actions: new fields.ArrayField(new ActionField()), actions: new fields.ArrayField(new ActionField()),
levelData: new fields.EmbeddedDataField(DhLevelData) levelData: new fields.EmbeddedDataField(DhLevelData),
}; bonuses: new fields.SchemaField({
} damage: new fields.SchemaField({
physical: bonusField('DAGGERHEART.GENERAL.Damage.physicalDamage'),
get traits() { magical: bonusField('DAGGERHEART.GENERAL.Damage.magicalDamage')
return { })
instinct: { value: this.attack.roll.bonus } })
}; };
} }
@ -89,9 +91,7 @@ export default class DhCompanion extends BaseDataActor {
} }
prepareBaseData() { prepareBaseData() {
const partnerSpellcastingModifier = this.partner?.system?.spellcastingModifiers?.main; this.attack.roll.bonus = this.partner?.system?.spellcastModifier ?? 0;
const spellcastingModifier = this.partner?.system?.traits?.[partnerSpellcastingModifier]?.value;
this.attack.roll.bonus = spellcastingModifier ?? 0; // Needs to expand on which modifier it is that should be used because of multiclassing;
for (let levelKey in this.levelData.levelups) { for (let levelKey in this.levelData.levelups) {
const level = this.levelData.levelups[levelKey]; const level = this.levelData.levelups[levelKey];
@ -124,12 +124,6 @@ export default class DhCompanion extends BaseDataActor {
} }
} }
prepareDerivedData() {
if (this.partner) {
this.partner.system.resources.hope.max += this.resources.hope;
}
}
async _preDelete() { async _preDelete() {
if (this.partner) { if (this.partner) {
await this.partner.update({ 'system.companion': null }); await this.partner.update({ 'system.companion': null });

View file

@ -102,7 +102,7 @@ class DhCountdown extends foundry.abstract.DataModel {
value: new fields.StringField({ value: new fields.StringField({
required: true, required: true,
choices: CONFIG.DH.GENERAL.countdownTypes, 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: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.progress.type.value.label'
}), }),
label: new fields.StringField({ label: new fields.StringField({
@ -132,7 +132,13 @@ class DhCountdown extends foundry.abstract.DataModel {
export const registerCountdownHooks = () => { export const registerCountdownHooks = () => {
Hooks.on(socketEvent.Refresh, ({ refreshType, application }) => { Hooks.on(socketEvent.Refresh, ({ refreshType, application }) => {
if (refreshType === RefreshType.Countdown) { if (refreshType === RefreshType.Countdown) {
if (application) {
foundry.applications.instances.get(application)?.render(); foundry.applications.instances.get(application)?.render();
} else {
foundry.applications.instances.get('narrative-countdowns')?.render();
foundry.applications.instances.get('encounter-countdowns')?.render();
}
return false; return false;
} }
}); });

View file

@ -0,0 +1,35 @@
const fields = foundry.data.fields;
const attributeField = label =>
new fields.SchemaField({
value: new fields.NumberField({ initial: 0, integer: true, label }),
tierMarked: new fields.BooleanField({ initial: false })
});
const resourceField = (max = 0, label, reverse = false) =>
new fields.SchemaField({
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 = localizationPath =>
new fields.SchemaField({
enabled: new fields.BooleanField({ required: true, initial: false }),
cost: new fields.NumberField({
integer: true,
label: `${localizationPath}.label`,
hint: `${localizationPath}.hint`
})
});
const bonusField = label =>
new fields.SchemaField({
bonus: new fields.NumberField({ integer: true, initial: 0, label: `${game.i18n.localize(label)} Value` }),
dice: new fields.ArrayField(
new fields.StringField(),
{ label: `${game.i18n.localize(label)} Dice` }
)
});
export { attributeField, resourceField, stressDamageReductionRule, bonusField };

View file

@ -53,11 +53,14 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
}), }),
diceStates: new fields.TypedObjectField( diceStates: new fields.TypedObjectField(
new fields.SchemaField({ new fields.SchemaField({
value: new fields.NumberField({ integer: true, nullable: true, initial: null }), value: new fields.NumberField({ integer: true, initial: 1, min: 1 }),
used: new fields.BooleanField({ initial: false }) used: new fields.BooleanField({ initial: false })
}) })
), ),
dieFaces: new fields.StringField({ initial: '4' }) dieFaces: new fields.StringField({
choices: CONFIG.DH.GENERAL.diceTypes,
initial: CONFIG.DH.GENERAL.diceTypes.d4
})
}, },
{ nullable: true, initial: null } { nullable: true, initial: null }
); );

View file

@ -69,6 +69,7 @@ export default class DHBeastform extends BaseDataItem {
const beastformEffect = this.parent.effects.find(x => x.type === 'beastform'); const beastformEffect = this.parent.effects.find(x => x.type === 'beastform');
await beastformEffect.updateSource({ await beastformEffect.updateSource({
changes: [...beastformEffect.changes, { key: 'system.advantageSources', mode: 2, value: this.advantageOn }],
system: { system: {
characterTokenData: { characterTokenData: {
tokenImg: this.parent.parent.prototypeToken.texture.src, tokenImg: this.parent.parent.prototypeToken.texture.src,

View file

@ -24,11 +24,10 @@ export default class DHClass extends BaseDataItem {
integer: true, integer: true,
min: 1, min: 1,
initial: 5, initial: 5,
label: 'DAGGERHEART.GENERAL.hitPoints' label: 'DAGGERHEART.GENERAL.hitPoints.plural'
}), }),
evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }), evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }),
hopeFeatures: new ForeignDocumentUUIDArrayField({ type: 'Item' }), features: new ForeignDocumentUUIDArrayField({ type: 'Item' }),
classFeatures: new ForeignDocumentUUIDArrayField({ type: 'Item' }),
subclasses: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), subclasses: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
inventory: new fields.SchemaField({ inventory: new fields.SchemaField({
take: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), take: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
@ -52,12 +51,18 @@ export default class DHClass extends BaseDataItem {
}; };
} }
get hopeFeature() { get hopeFeatures() {
return this.hopeFeatures.length > 0 ? this.hopeFeatures[0] : null; return (
this.features.filter(x => x?.system?.subType === CONFIG.DH.ITEM.featureSubTypes.hope) ??
(this.features.filter(x => !x).length > 0 ? {} : null)
);
} }
get features() { get classFeatures() {
return [...this.hopeFeatures.filter(x => x), ...this.classFeatures.filter(x => x)]; return (
this.features.filter(x => x?.system?.subType === CONFIG.DH.ITEM.featureSubTypes.class) ??
(this.features.filter(x => !x).length > 0 ? {} : null)
);
} }
async _preCreate(data, options, user) { async _preCreate(data, options, user) {

View file

@ -29,7 +29,6 @@ export default class DHDomainCard extends BaseDataItem {
required: true, required: true,
initial: CONFIG.DH.DOMAIN.cardTypes.ability.id initial: CONFIG.DH.DOMAIN.cardTypes.ability.id
}), }),
foundation: new fields.BooleanField({ initial: false }),
inVault: new fields.BooleanField({ initial: false }), inVault: new fields.BooleanField({ initial: false }),
actions: new fields.ArrayField(new ActionField()) actions: new fields.ArrayField(new ActionField())
}; };

View file

@ -1,4 +1,4 @@
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
import BaseDataItem from './base.mjs'; import BaseDataItem from './base.mjs';
export default class DHSubclass extends BaseDataItem { export default class DHSubclass extends BaseDataItem {
@ -22,20 +22,22 @@ export default class DHSubclass extends BaseDataItem {
nullable: true, nullable: true,
initial: null initial: null
}), }),
foundationFeature: new ForeignDocumentUUIDField({ type: 'Item' }), features: new ForeignDocumentUUIDArrayField({ type: 'Item' }),
specializationFeature: new ForeignDocumentUUIDField({ type: 'Item' }),
masteryFeature: new ForeignDocumentUUIDField({ type: 'Item' }),
featureState: new fields.NumberField({ required: true, initial: 1, min: 1 }), featureState: new fields.NumberField({ required: true, initial: 1, min: 1 }),
isMulticlass: new fields.BooleanField({ initial: false }) isMulticlass: new fields.BooleanField({ initial: false })
}; };
} }
get features() { get foundationFeatures() {
return [ return this.features.filter(x => x.system.subType === CONFIG.DH.ITEM.featureSubTypes.foundation);
{ ...this.foundationFeature?.toObject(), identifier: 'foundationFeature' }, }
{ ...this.specializationFeature?.toObject(), identifier: 'specializationFeature' },
{ ...this.masteryFeature?.toObject(), identifier: 'masteryFeature' } get specializationFeatures() {
]; return this.features.filter(x => x.system.subType === CONFIG.DH.ITEM.featureSubTypes.specialization);
}
get masteryFeatures() {
return this.features.filter(x => x.system.subType === CONFIG.DH.ITEM.featureSubTypes.mastery);
} }
async _preCreate(data, options, user) { async _preCreate(data, options, user) {

View file

@ -50,7 +50,7 @@ export default class DHWeapon extends AttachableItem {
}, },
roll: { roll: {
trait: 'agility', trait: 'agility',
type: 'weapon' type: 'attack'
}, },
damage: { damage: {
parts: [ parts: [

View file

@ -1,23 +1,28 @@
export default class DhAutomation extends foundry.abstract.DataModel { export default class DhAutomation extends foundry.abstract.DataModel {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.SETTINGS.Automation']; // Doesn't work for some reason
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
hope: new fields.BooleanField({ hopeFear: new fields.SchemaField({
gm: new fields.BooleanField({
required: true, required: true,
initial: false, initial: false,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hope.label' label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hopeFear.gm.label'
}), // Label need to be updated into something like "Duality Roll Auto Gain" + a hint }),
players: new fields.BooleanField({
required: true,
initial: false,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hopeFear.players.label'
})
}),
actionPoints: new fields.BooleanField({ actionPoints: new fields.BooleanField({
required: true, required: true,
initial: false, initial: false,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.actionPoints.label' label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.actionPoints.label'
}), }),
countdowns: new fields.BooleanField({ hordeDamage: new fields.BooleanField({
requireD: true, required: true,
initial: false, initial: true,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.countdowns.label' label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hordeDamage.label'
}) })
}; };
} }

View file

@ -54,6 +54,7 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
moves: new fields.TypedObjectField( moves: new fields.TypedObjectField(
new fields.SchemaField({ new fields.SchemaField({
name: new fields.StringField({ required: true }), name: new fields.StringField({ required: true }),
icon: new fields.StringField({ required: true }),
img: new fields.FilePathField({ img: new fields.FilePathField({
initial: 'icons/magic/life/cross-worn-green.webp', initial: 'icons/magic/life/cross-worn-green.webp',
categories: ['IMAGE'], categories: ['IMAGE'],
@ -70,6 +71,7 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
moves: new fields.TypedObjectField( moves: new fields.TypedObjectField(
new fields.SchemaField({ new fields.SchemaField({
name: new fields.StringField({ required: true }), name: new fields.StringField({ required: true }),
icon: new fields.StringField({ required: true }),
img: new fields.FilePathField({ img: new fields.FilePathField({
initial: 'icons/magic/life/cross-worn-green.webp', initial: 'icons/magic/life/cross-worn-green.webp',
categories: ['IMAGE'], categories: ['IMAGE'],

View file

@ -39,11 +39,13 @@ export default class D20Roll extends DHRoll {
} }
get hasAdvantage() { get hasAdvantage() {
return this.options.roll.advantage === this.constructor.ADV_MODE.ADVANTAGE; const adv = this.options.roll.advantage.type ?? this.options.roll.advantage;
return adv === this.constructor.ADV_MODE.ADVANTAGE;
} }
get hasDisadvantage() { get hasDisadvantage() {
return this.options.roll.advantage === this.constructor.ADV_MODE.DISADVANTAGE; const adv = this.options.roll.advantage.type ?? this.options.roll.advantage;
return adv === this.constructor.ADV_MODE.DISADVANTAGE;
} }
static applyKeybindings(config) { static applyKeybindings(config) {
@ -74,7 +76,6 @@ export default class D20Roll extends DHRoll {
} }
constructFormula(config) { constructFormula(config) {
// this.terms = [];
this.createBaseDice(); this.createBaseDice();
this.configureModifiers(); this.configureModifiers();
this.resetFormula(); this.resetFormula();
@ -91,7 +92,10 @@ export default class D20Roll extends DHRoll {
configureModifiers() { configureModifiers() {
this.applyAdvantage(); this.applyAdvantage();
this.applyBaseBonus();
this.baseTerms = foundry.utils.deepClone(this.dice);
this.options.roll.modifiers = this.applyBaseBonus();
this.options.experiences?.forEach(m => { this.options.experiences?.forEach(m => {
if (this.options.data.experiences?.[m]) if (this.options.data.experiences?.[m])
@ -101,12 +105,7 @@ export default class D20Roll extends DHRoll {
}); });
}); });
this.options.roll.modifiers?.forEach(m => { this.addModifiers();
this.terms.push(...this.formatModifier(m.value));
});
this.baseTerms = foundry.utils.deepClone(this.terms);
if (this.options.extraFormula) { if (this.options.extraFormula) {
this.terms.push( this.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: '+' }), new foundry.dice.terms.OperatorTerm({ operator: '+' }),
@ -125,13 +124,20 @@ export default class D20Roll extends DHRoll {
} }
applyBaseBonus() { applyBaseBonus() {
this.options.roll.modifiers = []; const modifiers = [];
if (!this.options.roll.bonus) return;
this.options.roll.modifiers.push({ if (this.options.roll.bonus)
modifiers.push({
label: 'Bonus to Hit', label: 'Bonus to Hit',
value: this.options.roll.bonus value: this.options.roll.bonus
// value: Roll.replaceFormulaData('@attackBonus', this.data)
}); });
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`)
);
return modifiers;
} }
static async buildEvaluate(roll, config = {}, message = {}) { static async buildEvaluate(roll, config = {}, message = {}) {

View file

@ -24,8 +24,26 @@ export default class DamageRoll extends DHRoll {
} }
} }
applyBaseBonus() {
const modifiers = [],
type = this.options.messageType ?? 'damage';
modifiers.push(...this.getBonus(`${type}`, `${type.capitalize()} Bonus`));
this.options.damageTypes?.forEach(t => {
modifiers.push(...this.getBonus(`${type}.${t}`, `${t.capitalize()} ${type.capitalize()} Bonus`));
});
const weapons = ['primaryWeapon', 'secondaryWeapon'];
weapons.forEach(w => {
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) { constructFormula(config) {
super.constructFormula(config); super.constructFormula(config);
if (config.isCritical) { if (config.isCritical) {
const tmpRoll = new Roll(this._formula)._evaluateSync({ maximize: true }), const tmpRoll = new Roll(this._formula)._evaluateSync({ maximize: true }),
criticalBonus = tmpRoll.total - this.constructor.calculateTotalModifiers(tmpRoll); criticalBonus = tmpRoll.total - this.constructor.calculateTotalModifiers(tmpRoll);

View file

@ -4,6 +4,7 @@ export default class DHRoll extends Roll {
baseTerms = []; baseTerms = [];
constructor(formula, data, options) { constructor(formula, data, options) {
super(formula, data, options); super(formula, data, options);
if (!this.data || !Object.keys(this.data).length) this.data = options.data;
} }
static messageType = 'adversaryRoll'; static messageType = 'adversaryRoll';
@ -86,7 +87,7 @@ export default class DHRoll extends Roll {
system: config, system: config,
rolls: [roll] rolls: [roll]
}; };
return await cls.create(msg); return await cls.create(msg, { rollMode: config.selectedRollMode });
} }
static applyKeybindings(config) { static applyKeybindings(config) {
@ -99,12 +100,45 @@ export default class DHRoll extends Roll {
} }
formatModifier(modifier) { formatModifier(modifier) {
if (Array.isArray(modifier)) {
return [
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
...this.constructor.parse(modifier.join(' + '), this.options.data)
];
} else {
const numTerm = modifier < 0 ? '-' : '+'; const numTerm = modifier < 0 ? '-' : '+';
return [ return [
new foundry.dice.terms.OperatorTerm({ operator: numTerm }), new foundry.dice.terms.OperatorTerm({ operator: numTerm }),
new foundry.dice.terms.NumericTerm({ number: Math.abs(modifier) }) new foundry.dice.terms.NumericTerm({ number: Math.abs(modifier) })
]; ];
} }
}
applyBaseBonus() {
return [];
}
addModifiers() {
this.options.roll.modifiers?.forEach(m => {
this.terms.push(...this.formatModifier(m.value));
});
}
getBonus(path, label) {
const bonus = foundry.utils.getProperty(this.data.bonuses, path),
modifiers = [];
if (bonus?.bonus)
modifiers.push({
label: label,
value: bonus?.bonus
});
if (bonus?.dice?.length)
modifiers.push({
label: label,
value: bonus?.dice
});
return modifiers;
}
getFaces(faces) { getFaces(faces) {
return Number(faces.startsWith('d') ? faces.replace('d', '') : faces); return Number(faces.startsWith('d') ? faces.replace('d', '') : faces);
@ -113,6 +147,9 @@ export default class DHRoll extends Roll {
constructFormula(config) { constructFormula(config) {
this.terms = Roll.parse(this.options.roll.formula, config.data); this.terms = Roll.parse(this.options.roll.formula, config.data);
this.options.roll.modifiers = this.applyBaseBonus();
this.addModifiers();
if (this.options.extraFormula) { if (this.options.extraFormula) {
this.terms.push( this.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: '+' }), new foundry.dice.terms.OperatorTerm({ operator: '+' }),
@ -138,9 +175,10 @@ export default class DHRoll extends Roll {
export const registerRollDiceHooks = () => { export const registerRollDiceHooks = () => {
Hooks.on(`${CONFIG.DH.id}.postRollDuality`, async (config, message) => { Hooks.on(`${CONFIG.DH.id}.postRollDuality`, async (config, message) => {
const hopeFearAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).hopeFear;
if ( if (
!config.source?.actor || !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' config.roll.type === 'reaction'
) )
return; return;
@ -148,11 +186,16 @@ export const registerRollDiceHooks = () => {
const actor = await fromUuid(config.source.actor), const actor = await fromUuid(config.source.actor),
updates = []; updates = [];
if (!actor) return; if (!actor) return;
if (config.roll.isCritical || config.roll.result.duality === 1) updates.push({ type: 'hope', value: 1 }); if (config.roll.isCritical || config.roll.result.duality === 1) updates.push({ key: 'hope', value: 1 });
if (config.roll.isCritical) updates.push({ type: 'stress', value: -1 }); if (config.roll.isCritical) updates.push({ key: 'stress', value: -1 });
if (config.roll.result.duality === -1) updates.push({ type: 'fear', value: 1 }); if (config.roll.result.duality === -1) updates.push({ key: 'fear', value: 1 });
if (updates.length) actor.modifyResource(updates); if (updates.length) {
const target = actor.system.partner ?? actor;
if (!['dead', 'unconcious'].some(x => actor.statuses.has(x))) {
target.modifyResource(updates);
}
}
if (!config.roll.hasOwnProperty('success') && !config.targets?.length) return; if (!config.roll.hasOwnProperty('success') && !config.targets?.length) return;

View file

@ -4,9 +4,12 @@ import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
export default class DualityRoll extends D20Roll { export default class DualityRoll extends D20Roll {
_advantageFaces = 6; _advantageFaces = 6;
_advantageNumber = 1;
_rallyIndex;
constructor(formula, data = {}, options = {}) { constructor(formula, data = {}, options = {}) {
super(formula, data, options); super(formula, data, options);
this.rallyChoices = this.setRallyChoices();
} }
static messageType = 'dualityRoll'; static messageType = 'dualityRoll';
@ -51,6 +54,35 @@ export default class DualityRoll extends D20Roll {
this._advantageFaces = this.getFaces(faces); this._advantageFaces = this.getFaces(faces);
} }
get advantageNumber() {
return this._advantageNumber;
}
set advantageNumber(value) {
this._advantageNumber = Number(value);
}
setRallyChoices() {
return this.data?.parent?.effects.reduce((a,c) => {
const change = c.changes.find(ch => ch.key === 'system.bonuses.rally');
if(change) a.push({ value: c.id, label: change.value });
return a;
}, []);
}
get dRally() {
if(!this.rallyFaces) return null;
if(this.hasDisadvantage || this.hasAdvantage)
return this.dice[3];
else
return this.dice[2];
}
get rallyFaces() {
const rallyChoice = this.rallyChoices?.find(r => r.value === this._rallyIndex)?.label;
return rallyChoice ? this.getFaces(rallyChoice) : null;
}
get isCritical() { get isCritical() {
if (!this.dHope._evaluated || !this.dFear._evaluated) return; if (!this.dHope._evaluated || !this.dFear._evaluated) return;
return this.dHope.total === this.dFear.total; return this.dHope.total === this.dFear.total;
@ -66,10 +98,6 @@ export default class DualityRoll extends D20Roll {
return this.dHope.total < this.dFear.total; return this.dHope.total < this.dFear.total;
} }
get hasBarRally() {
return null;
}
get totalLabel() { get totalLabel() {
const label = this.withHope const label = this.withHope
? 'DAGGERHEART.GENERAL.hope' ? 'DAGGERHEART.GENERAL.hope'
@ -98,37 +126,43 @@ export default class DualityRoll extends D20Roll {
} }
applyAdvantage() { applyAdvantage() {
if (this.hasAdvantage || this.hasDisadvantage) {
const dieFaces = this.advantageFaces, const dieFaces = this.advantageFaces,
bardRallyFaces = this.hasBarRally, advDie = new foundry.dice.terms.Die({ faces: dieFaces, number: this.advantageNumber });
advDie = new foundry.dice.terms.Die({ faces: dieFaces }); if(this.advantageNumber > 1) advDie.modifiers = ['kh'];
if (this.hasAdvantage || this.hasDisadvantage || bardRallyFaces)
this.terms.push(new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }));
if (bardRallyFaces) {
const rallyDie = new foundry.dice.terms.Die({ faces: bardRallyFaces });
if (this.hasAdvantage) {
this.terms.push( this.terms.push(
new foundry.dice.terms.PoolTerm({ new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }),
terms: [advDie.formula, rallyDie.formula], advDie
modifiers: ['kh']
})
); );
} else if (this.hasDisadvantage) {
this.terms.push(advDie, new foundry.dice.terms.OperatorTerm({ operator: '+' }), rallyDie);
} }
} else if (this.hasAdvantage || this.hasDisadvantage) this.terms.push(advDie); if(this.rallyFaces)
this.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }),
new foundry.dice.terms.Die({ faces: this.rallyFaces })
);
} }
applyBaseBonus() { applyBaseBonus() {
this.options.roll.modifiers = []; const modifiers = super.applyBaseBonus();
if (!this.options.roll.trait) return;
this.options.roll.modifiers.push({ if (this.options.roll.trait && this.data.traits[this.options.roll.trait])
modifiers.unshift({
label: `DAGGERHEART.CONFIG.Traits.${this.options.roll.trait}.name`, label: `DAGGERHEART.CONFIG.Traits.${this.options.roll.trait}.name`,
value: Roll.replaceFormulaData(`@traits.${this.options.roll.trait}.value`, this.data) value: this.data.traits[this.options.roll.trait].value
}); });
const weapons = ['primaryWeapon', 'secondaryWeapon'];
weapons.forEach(w => {
if (this.options.source.item && this.options.source.item === this.data[w]?.id)
modifiers.push(...this.getBonus(`roll.${w}`, 'Weapon Bonus'));
});
return modifiers;
} }
static postEvaluate(roll, config = {}) { static postEvaluate(roll, config = {}) {
super.postEvaluate(roll, config); super.postEvaluate(roll, config);
config.roll.hope = { config.roll.hope = {
dice: roll.dHope.denomination, dice: roll.dHope.denomination,
value: roll.dHope.total value: roll.dHope.total
@ -137,12 +171,19 @@ export default class DualityRoll extends D20Roll {
dice: roll.dFear.denomination, dice: roll.dFear.denomination,
value: roll.dFear.total value: roll.dFear.total
}; };
config.roll.rally = {
dice: roll.dRally?.denomination,
value: roll.dRally?.total
};
config.roll.result = { config.roll.result = {
duality: roll.withHope ? 1 : roll.withFear ? -1 : 0, duality: roll.withHope ? 1 : roll.withFear ? -1 : 0,
total: roll.dHope.total + roll.dFear.total, total: roll.dHope.total + roll.dFear.total,
label: roll.totalLabel label: roll.totalLabel
}; };
if(roll._rallyIndex && roll.data?.parent)
roll.data.parent.deleteEmbeddedDocuments('ActiveEffect', [roll._rallyIndex]);
setDiceSoNiceForDualityRoll(roll, config.roll.advantage.type); setDiceSoNiceForDualityRoll(roll, config.roll.advantage.type);
} }
} }

View file

@ -3,4 +3,5 @@ export { default as DHItem } from './item.mjs';
export { default as DhpCombat } from './combat.mjs'; export { default as DhpCombat } from './combat.mjs';
export { default as DhActiveEffect } from './activeEffect.mjs'; export { default as DhActiveEffect } from './activeEffect.mjs';
export { default as DhChatMessage } from './chatMessage.mjs'; export { default as DhChatMessage } from './chatMessage.mjs';
export { default as DhToken } from './token.mjs';
export { default as DhTooltipManager } from './tooltipManager.mjs'; export { default as DhTooltipManager } from './tooltipManager.mjs';

View file

@ -55,10 +55,24 @@ export default class DhActiveEffect extends ActiveEffect {
} }
static applyField(model, change, field) { static applyField(model, change, field) {
change.value = itemAbleRollParse(change.value, model, change.effect.parent); change.value = this.effectSafeEval(itemAbleRollParse(change.value, model, change.effect.parent));
super.applyField(model, change, field); super.applyField(model, change, field);
} }
/* Altered Foundry safeEval to allow non-numeric returns */
static effectSafeEval(expression) {
let result;
try {
// eslint-disable-next-line no-new-func
const evl = new Function('sandbox', `with (sandbox) { return ${expression}}`);
result = evl(Roll.MATH_PROXY);
} catch (err) {
return expression;
}
return result;
}
async toChat(origin) { async toChat(origin) {
const cls = getDocumentClass('ChatMessage'); const cls = getDocumentClass('ChatMessage');
const systemData = { const systemData = {

View file

@ -371,7 +371,7 @@ export default class DhpActor extends Actor {
getRollData() { getRollData() {
const rollData = super.getRollData(); const rollData = super.getRollData();
rollData.prof = this.system.proficiency ?? 1; rollData.prof = this.system.proficiency ?? 1;
rollData.cast = this.system.spellcast ?? 1; rollData.cast = this.system.spellcastModifier ?? 1;
return rollData; return rollData;
} }

View file

@ -37,4 +37,15 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
e.setAttribute('data-use-perm', document.testUserPermission(game.user, 'OWNER')); 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);
}
} }

View file

@ -0,0 +1,57 @@
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', 'actions', '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);
}
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.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;
}
}

View file

@ -21,14 +21,79 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
this.tooltip.innerHTML = html; this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(element); options.direction = this._determineItemTooltipDirection(element);
} }
} else {
const shortRest = element.dataset.tooltip?.startsWith('#shortRest#');
const longRest = element.dataset.tooltip?.startsWith('#longRest#');
if (shortRest || longRest) {
const key = element.dataset.tooltip.slice(shortRest ? 11 : 10);
const downtimeOptions = shortRest
? CONFIG.DH.GENERAL.defaultRestOptions.shortRest()
: CONFIG.DH.GENERAL.defaultRestOptions.longRest();
const move = downtimeOptions[key];
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/downtime.hbs`,
{
move: move
}
);
this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(
element,
this.constructor.TOOLTIP_DIRECTIONS.UP
);
}
const isAdvantage = element.dataset.tooltip?.startsWith('#advantage#');
const isDisadvantage = element.dataset.tooltip?.startsWith('#disadvantage#');
if (isAdvantage || isDisadvantage) {
const actorUuid = element.dataset.tooltip.slice(isAdvantage ? 11 : 14);
const actor = await foundry.utils.fromUuid(actorUuid);
if (actor) {
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/advantage.hbs`,
{
sources: isAdvantage ? actor.system.advantageSources : actor.system.disadvantageSources
}
);
this.tooltip.innerHTML = html;
}
}
} }
super.activate(element, { ...options, html: html }); super.activate(element, { ...options, html: html });
} }
_determineItemTooltipDirection(element) { _determineItemTooltipDirection(element, prefered = this.constructor.TOOLTIP_DIRECTIONS.LEFT) {
const pos = element.getBoundingClientRect(); const pos = element.getBoundingClientRect();
const dirs = this.constructor.TOOLTIP_DIRECTIONS; const dirs = this.constructor.TOOLTIP_DIRECTIONS;
return dirs[pos.x - this.tooltip.offsetWidth < 0 ? 'DOWN' : 'LEFT']; switch (prefered) {
case this.constructor.TOOLTIP_DIRECTIONS.LEFT:
return dirs[
pos.x - this.tooltip.offsetWidth < 0
? this.constructor.TOOLTIP_DIRECTIONS.DOWN
: this.constructor.TOOLTIP_DIRECTIONS.LEFT
];
case this.constructor.TOOLTIP_DIRECTIONS.UP:
return dirs[
pos.y - this.tooltip.offsetHeight < 0
? this.constructor.TOOLTIP_DIRECTIONS.RIGHT
: this.constructor.TOOLTIP_DIRECTIONS.UP
];
case this.constructor.TOOLTIP_DIRECTIONS.RIGHT:
return dirs[
pos.x + this.tooltip.offsetWidth > document.body.clientWidth
? this.constructor.TOOLTIP_DIRECTIONS.DOWN
: this.constructor.TOOLTIP_DIRECTIONS.RIGHT
];
case this.constructor.TOOLTIP_DIRECTIONS.DOWN:
return dirs[
pos.y + this.tooltip.offsetHeight > document.body.clientHeight
? this.constructor.TOOLTIP_DIRECTIONS.LEFT
: this.constructor.TOOLTIP_DIRECTIONS.DOWN
];
}
} }
} }

View file

@ -39,8 +39,9 @@ export default class RegisterHandlebarsHelpers {
} }
static damageSymbols(damageParts) { static damageSymbols(damageParts) {
const symbols = new Set(); const symbols = [...new Set(damageParts.reduce((a, c) => a.concat([...c.type]), []))].map(
damageParts.forEach(part => symbols.add(...CONFIG.DH.GENERAL.damageTypes[part.type].icon)); p => CONFIG.DH.GENERAL.damageTypes[p].icon
);
return new Handlebars.SafeString(Array.from(symbols).map(symbol => `<i class="fa-solid ${symbol}"></i>`)); return new Handlebars.SafeString(Array.from(symbols).map(symbol => `<i class="fa-solid ${symbol}"></i>`));
} }

View file

@ -318,7 +318,7 @@ export const itemAbleRollParse = (value, actor, item) => {
const isItemTarget = value.toLowerCase().startsWith('item.'); const isItemTarget = value.toLowerCase().startsWith('item.');
const slicedValue = isItemTarget ? value.slice(5) : value; const slicedValue = isItemTarget ? value.slice(5) : value;
try { try {
return Roll.safeEval(Roll.replaceFormulaData(slicedValue, isItemTarget ? item : actor)); return Roll.replaceFormulaData(slicedValue, isItemTarget ? item : actor);
} catch (_) { } catch (_) {
return ''; return '';
} }

View file

@ -29,6 +29,7 @@ export const preloadHandlebarsTemplates = async function () {
'systems/daggerheart/templates/settings/components/settings-item-line.hbs', 'systems/daggerheart/templates/settings/components/settings-item-line.hbs',
'systems/daggerheart/templates/ui/chat/parts/damage-chat.hbs', 'systems/daggerheart/templates/ui/chat/parts/damage-chat.hbs',
'systems/daggerheart/templates/ui/chat/parts/target-chat.hbs', 'systems/daggerheart/templates/ui/chat/parts/target-chat.hbs',
'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs' 'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs',
'systems/daggerheart/templates/dialogs/downtime/activities.hbs'
]); ]);
}; };

6
package-lock.json generated
View file

@ -6,6 +6,7 @@
"": { "": {
"dependencies": { "dependencies": {
"@yaireo/tagify": "^4.17.9", "@yaireo/tagify": "^4.17.9",
"autocompleter": "^9.3.2",
"gulp": "^5.0.0", "gulp": "^5.0.0",
"gulp-less": "^5.0.0", "gulp-less": "^5.0.0",
"rollup": "^4.40.0" "rollup": "^4.40.0"
@ -608,6 +609,11 @@
"node": ">= 10.13.0" "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": { "node_modules/available-typed-arrays": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",

View file

@ -1,6 +1,7 @@
{ {
"dependencies": { "dependencies": {
"@yaireo/tagify": "^4.17.9", "@yaireo/tagify": "^4.17.9",
"autocompleter": "^9.3.2",
"gulp": "^5.0.0", "gulp": "^5.0.0",
"gulp-less": "^5.0.0", "gulp-less": "^5.0.0",
"rollup": "^4.40.0" "rollup": "^4.40.0"

View file

@ -15,7 +15,7 @@ export default {
path: './postcss.config.js' path: './postcss.config.js'
}, },
extensions: ['.css'], extensions: ['.css'],
extract: false extract: 'tagify.css'
}), }),
commonjs({ commonjs({
include: /node_modules/, include: /node_modules/,

View file

@ -1,10 +1,10 @@
{ {
"type": "Item", "type": "Item",
"folder": null, "folder": null,
"name": "Rogue", "name": "Tier 1",
"color": null, "color": null,
"sorting": "a", "sorting": "a",
"_id": "AzwvgmcvToIUGYti", "_id": "sxvlEwi25uAoB2C5",
"description": "", "description": "",
"sort": 0, "sort": 0,
"flags": {}, "flags": {},
@ -12,12 +12,12 @@
"compendiumSource": null, "compendiumSource": null,
"duplicateSource": null, "duplicateSource": null,
"exportSource": null, "exportSource": null,
"coreVersion": "13.344", "coreVersion": "13.346",
"systemId": "daggerheart", "systemId": "daggerheart",
"systemVersion": "0.0.1", "systemVersion": "0.0.1",
"createdTime": 1748709700301, "createdTime": 1752684226915,
"modifiedTime": 1748709700301, "modifiedTime": 1752684226915,
"lastModifiedBy": "MxkU9FQYKmOxbdzm" "lastModifiedBy": "k0gmQFlvrPvlTtbh"
}, },
"_key": "!folders!AzwvgmcvToIUGYti" "_key": "!folders!sxvlEwi25uAoB2C5"
} }

View file

@ -1,10 +1,10 @@
{ {
"type": "Item", "type": "Item",
"folder": null, "folder": null,
"name": "Ranger", "name": "Tier 2",
"color": null, "color": null,
"sorting": "a", "sorting": "a",
"_id": "84neMcqoIRAoIvXP", "_id": "OgzrmfH1ZbpljX7k",
"description": "", "description": "",
"sort": 0, "sort": 0,
"flags": {}, "flags": {},
@ -12,12 +12,12 @@
"compendiumSource": null, "compendiumSource": null,
"duplicateSource": null, "duplicateSource": null,
"exportSource": null, "exportSource": null,
"coreVersion": "13.344", "coreVersion": "13.346",
"systemId": "daggerheart", "systemId": "daggerheart",
"systemVersion": "0.0.1", "systemVersion": "0.0.1",
"createdTime": 1748709694559, "createdTime": 1752684230275,
"modifiedTime": 1748709694559, "modifiedTime": 1752684230275,
"lastModifiedBy": "MxkU9FQYKmOxbdzm" "lastModifiedBy": "k0gmQFlvrPvlTtbh"
}, },
"_key": "!folders!84neMcqoIRAoIvXP" "_key": "!folders!OgzrmfH1ZbpljX7k"
} }

View file

@ -1,10 +1,10 @@
{ {
"type": "Item", "type": "Item",
"folder": null, "folder": null,
"name": "Bard", "name": "Tier 3",
"color": null, "color": null,
"sorting": "a", "sorting": "a",
"_id": "3QoIMKARHfRnBvQJ", "_id": "wTI7nZkPhKxl7Wwq",
"description": "", "description": "",
"sort": 0, "sort": 0,
"flags": {}, "flags": {},
@ -12,12 +12,12 @@
"compendiumSource": null, "compendiumSource": null,
"duplicateSource": null, "duplicateSource": null,
"exportSource": null, "exportSource": null,
"coreVersion": "13.344", "coreVersion": "13.346",
"systemId": "daggerheart", "systemId": "daggerheart",
"systemVersion": "0.0.1", "systemVersion": "0.0.1",
"createdTime": 1748709671341, "createdTime": 1752684233122,
"modifiedTime": 1748709671341, "modifiedTime": 1752684233122,
"lastModifiedBy": "MxkU9FQYKmOxbdzm" "lastModifiedBy": "k0gmQFlvrPvlTtbh"
}, },
"_key": "!folders!3QoIMKARHfRnBvQJ" "_key": "!folders!wTI7nZkPhKxl7Wwq"
} }

View file

@ -1,10 +1,10 @@
{ {
"type": "Item", "type": "Item",
"folder": null, "folder": null,
"name": "Druid", "name": "Tier 4",
"color": null, "color": null,
"sorting": "a", "sorting": "a",
"_id": "l94HTIXSvdZG3zIh", "_id": "7XHlANCPz18yvl5L",
"description": "", "description": "",
"sort": 0, "sort": 0,
"flags": {}, "flags": {},
@ -12,12 +12,12 @@
"compendiumSource": null, "compendiumSource": null,
"duplicateSource": null, "duplicateSource": null,
"exportSource": null, "exportSource": null,
"coreVersion": "13.344", "coreVersion": "13.346",
"systemId": "daggerheart", "systemId": "daggerheart",
"systemVersion": "0.0.1", "systemVersion": "0.0.1",
"createdTime": 1748709677349, "createdTime": 1752684235596,
"modifiedTime": 1748709677349, "modifiedTime": 1752684235596,
"lastModifiedBy": "MxkU9FQYKmOxbdzm" "lastModifiedBy": "k0gmQFlvrPvlTtbh"
}, },
"_key": "!folders!l94HTIXSvdZG3zIh" "_key": "!folders!7XHlANCPz18yvl5L"
} }

View file

@ -1,30 +0,0 @@
{
"name": "Clank",
"type": "ancestry",
"_id": "AzKMOIpXnCSLLDEB",
"img": "icons/svg/item-bag.svg",
"system": {
"description": "<p><strong>Purposeful Design</strong>: 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.</p><p><strong>Efficient</strong>: When you take a short rest, you can choose a long rest move instead of a short rest move.</p>",
"abilities": []
},
"effects": [],
"folder": null,
"sort": 100000,
"ownership": {
"default": 0,
"NqO2eQGMjrvUO6v9": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.344",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1747990113503,
"modifiedTime": 1747999445342,
"lastModifiedBy": "NqO2eQGMjrvUO6v9"
},
"_key": "!items!AzKMOIpXnCSLLDEB"
}

View file

@ -0,0 +1,23 @@
{
"type": "Item",
"folder": null,
"name": "Ancestry Features",
"color": null,
"sorting": "a",
"_id": "dSAccOl5ccgXPyje",
"description": "",
"sort": 0,
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.346",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1752680699271,
"modifiedTime": 1752680699271,
"lastModifiedBy": "binNpU8lWev6geDj"
},
"_key": "!folders!dSAccOl5ccgXPyje"
}

View file

@ -1,44 +0,0 @@
{
"name": "Arcane Sense",
"type": "feature",
"_id": "D5HUGwdizhBVZ0RW",
"img": "icons/svg/item-bag.svg",
"system": {
"actionType": "action",
"featureType": {
"type": "normal",
"data": {
"property": "spellcastingTrait",
"max": 1,
"numbers": {}
}
},
"refreshData": null,
"multiclass": null,
"disabled": false,
"description": "<p>You can sense the presence of magical people and objects within Close range.</p>",
"effects": {},
"actions": [],
"type": "class"
},
"effects": [],
"folder": "ncQG0s0ttAPObaeV",
"sort": 100000,
"ownership": {
"default": 0,
"ei8OkswTzyDp4IGC": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.344",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1748033779155,
"modifiedTime": 1748710066261,
"lastModifiedBy": "MxkU9FQYKmOxbdzm"
},
"_key": "!items!D5HUGwdizhBVZ0RW"
}

View file

@ -1,44 +0,0 @@
{
"name": "Attack of Opportunity",
"type": "feature",
"_id": "VfUbJwGU4Cka0xLP",
"img": "icons/svg/item-bag.svg",
"system": {
"actionType": "action",
"featureType": {
"type": "normal",
"data": {
"property": "spellcastingTrait",
"max": 1,
"numbers": {}
}
},
"refreshData": null,
"multiclass": null,
"disabled": false,
"description": "<p>When 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:</p><p>• They cant move from where they are.</p><p>• You deal damage to them equal to your primary weapons damage.</p><p>• You move with them.</p>",
"effects": {},
"actions": [],
"type": "class"
},
"effects": [],
"folder": "3znRxBqsK0lOJhvJ",
"sort": 100000,
"ownership": {
"default": 0,
"ei8OkswTzyDp4IGC": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.344",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1748034185325,
"modifiedTime": 1748710073183,
"lastModifiedBy": "MxkU9FQYKmOxbdzm"
},
"_key": "!items!VfUbJwGU4Cka0xLP"
}

View file

@ -1,44 +0,0 @@
{
"name": "Beastform",
"type": "feature",
"_id": "NkSKDXGNNiOUlFqm",
"img": "icons/svg/item-bag.svg",
"system": {
"actionType": "action",
"featureType": {
"type": "normal",
"data": {
"property": "spellcastingTrait",
"max": 1,
"numbers": {}
}
},
"refreshData": null,
"multiclass": null,
"disabled": false,
"description": "<p>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 cant 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 Beastforms features, add their Evasion bonus to your Evasion, and use the trait specified in their statistics for your attack. While youre 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.</p>",
"effects": {},
"actions": [],
"type": "class"
},
"effects": [],
"folder": "l94HTIXSvdZG3zIh",
"sort": 100000,
"ownership": {
"default": 0,
"ei8OkswTzyDp4IGC": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.344",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1748028817729,
"modifiedTime": 1748710083736,
"lastModifiedBy": "MxkU9FQYKmOxbdzm"
},
"_key": "!items!NkSKDXGNNiOUlFqm"
}

View file

@ -1,46 +0,0 @@
{
"name": "Channel Raw Power",
"type": "feature",
"_id": "ovxuqhl01XZSwx2n",
"img": "icons/svg/item-bag.svg",
"system": {
"actionType": "action",
"featureType": {
"type": "normal",
"data": {
"property": "spellcastingTrait",
"max": 1,
"numbers": {}
}
},
"refreshData": {
"type": "longRest"
},
"multiclass": null,
"disabled": false,
"description": "<p>Once per long rest, you can place a domain card from your loadout into your vault and choose to either:</p><p>• Gain Hope equal to the level of the card.</p><p>• Enhance a spell that deals damage, gaining a bonus to your damage roll equal to twice the level of the card.</p>",
"effects": {},
"actions": [],
"type": "class"
},
"effects": [],
"folder": "ncQG0s0ttAPObaeV",
"sort": 200000,
"ownership": {
"default": 0,
"ei8OkswTzyDp4IGC": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.344",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1748033854379,
"modifiedTime": 1748710088872,
"lastModifiedBy": "MxkU9FQYKmOxbdzm"
},
"_key": "!items!ovxuqhl01XZSwx2n"
}

View file

@ -1,44 +0,0 @@
{
"name": "Cloaked",
"type": "feature",
"_id": "TpaoHSJ3npjWiBOf",
"img": "icons/svg/item-bag.svg",
"system": {
"actionType": "action",
"featureType": {
"type": "normal",
"data": {
"property": "spellcastingTrait",
"max": 1,
"numbers": {}
}
},
"refreshData": null,
"multiclass": null,
"disabled": false,
"description": "<p>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.</p>",
"effects": {},
"actions": [],
"type": "class"
},
"effects": [],
"folder": "AzwvgmcvToIUGYti",
"sort": 100000,
"ownership": {
"default": 0,
"ei8OkswTzyDp4IGC": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.344",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1748032226905,
"modifiedTime": 1748710097141,
"lastModifiedBy": "MxkU9FQYKmOxbdzm"
},
"_key": "!items!TpaoHSJ3npjWiBOf"
}

View file

@ -1,44 +0,0 @@
{
"name": "Combat Training",
"type": "feature",
"_id": "elb6ZVertgu6OdKA",
"img": "icons/svg/item-bag.svg",
"system": {
"actionType": "passive",
"featureType": {
"type": "normal",
"data": {
"property": "spellcastingTrait",
"max": 1,
"numbers": {}
}
},
"refreshData": null,
"multiclass": null,
"disabled": false,
"description": "<p>You ignore burden when equipping weapons. When you deal physical damage, you gain a bonus to your damage roll equal to your level.</p>",
"effects": {},
"actions": [],
"type": "class"
},
"effects": [],
"folder": "3znRxBqsK0lOJhvJ",
"sort": 200000,
"ownership": {
"default": 0,
"ei8OkswTzyDp4IGC": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.344",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1748034225886,
"modifiedTime": 1748710182918,
"lastModifiedBy": "MxkU9FQYKmOxbdzm"
},
"_key": "!items!elb6ZVertgu6OdKA"
}

View file

@ -1,44 +0,0 @@
{
"name": "Evolution",
"type": "feature",
"_id": "bZxfyPTZ6rsakyA2",
"img": "icons/svg/item-bag.svg",
"system": {
"actionType": "action",
"featureType": {
"type": "normal",
"data": {
"property": "spellcastingTrait",
"max": 1,
"numbers": {}
}
},
"refreshData": null,
"multiclass": null,
"disabled": false,
"description": "<p>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.</p>",
"effects": {},
"actions": [],
"type": "class"
},
"effects": [],
"folder": "l94HTIXSvdZG3zIh",
"sort": 200000,
"ownership": {
"default": 0,
"ei8OkswTzyDp4IGC": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.344",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1748028774087,
"modifiedTime": 1748710255976,
"lastModifiedBy": "MxkU9FQYKmOxbdzm"
},
"_key": "!items!bZxfyPTZ6rsakyA2"
}

View file

@ -1,44 +0,0 @@
{
"name": "Frontline Tank",
"type": "feature",
"_id": "ftUZznLFJ5xbcxcu",
"img": "icons/svg/item-bag.svg",
"system": {
"actionType": "action",
"featureType": {
"type": "normal",
"data": {
"property": "spellcastingTrait",
"max": 1,
"numbers": {}
}
},
"refreshData": null,
"multiclass": null,
"disabled": false,
"description": "<p>Spend 3 Hope to clear 2 Armor Slots</p>",
"effects": {},
"actions": [],
"type": "class"
},
"effects": [],
"folder": "imOcur5Zv8WcMHXz",
"sort": 100000,
"ownership": {
"default": 0,
"ei8OkswTzyDp4IGC": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.344",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1748030942761,
"modifiedTime": 1748710261406,
"lastModifiedBy": "MxkU9FQYKmOxbdzm"
},
"_key": "!items!ftUZznLFJ5xbcxcu"
}

View file

@ -1,44 +0,0 @@
{
"name": "Hold Them Off",
"type": "feature",
"_id": "FSx2ojskU0pRE72g",
"img": "icons/svg/item-bag.svg",
"system": {
"actionType": "action",
"featureType": {
"type": "normal",
"data": {
"property": "spellcastingTrait",
"max": 1,
"numbers": {}
}
},
"refreshData": null,
"multiclass": null,
"disabled": false,
"description": "<p>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</p>",
"effects": {},
"actions": [],
"type": "class"
},
"effects": [],
"folder": "84neMcqoIRAoIvXP",
"sort": 100000,
"ownership": {
"default": 0,
"ei8OkswTzyDp4IGC": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.344",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1748030888304,
"modifiedTime": 1748710266972,
"lastModifiedBy": "MxkU9FQYKmOxbdzm"
},
"_key": "!items!FSx2ojskU0pRE72g"
}

View file

@ -1,44 +0,0 @@
{
"name": "Life Support",
"type": "feature",
"_id": "UZ9UjZArSJh6UHXG",
"img": "icons/svg/item-bag.svg",
"system": {
"actionType": "action",
"featureType": {
"type": "normal",
"data": {
"property": "spellcastingTrait",
"max": 1,
"numbers": {}
}
},
"refreshData": null,
"multiclass": null,
"disabled": false,
"description": "<p>Spend 3 Hope to clear a Hit Point on an ally within Close range.</p>",
"effects": {},
"actions": [],
"type": "class"
},
"effects": [],
"folder": "aI54jXjBNrAOm7R8",
"sort": 100000,
"ownership": {
"default": 0,
"ei8OkswTzyDp4IGC": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.344",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1748033044907,
"modifiedTime": 1748710332116,
"lastModifiedBy": "MxkU9FQYKmOxbdzm"
},
"_key": "!items!UZ9UjZArSJh6UHXG"
}

View file

@ -1,44 +0,0 @@
{
"name": "Make a Scene",
"type": "feature",
"_id": "Ddk0PAgwM4VLRbyY",
"img": "icons/svg/item-bag.svg",
"system": {
"actionType": "action",
"featureType": {
"type": "normal",
"data": {
"property": "spellcastingTrait",
"max": 1,
"numbers": {}
}
},
"refreshData": null,
"multiclass": null,
"disabled": false,
"description": "<p>Spend 3 Hope to temporarily Distract a target within Close range, giving them a -2 penalty to their Difficulty.</p>",
"effects": {},
"actions": [],
"type": "class"
},
"effects": [],
"folder": "3QoIMKARHfRnBvQJ",
"sort": 100000,
"ownership": {
"default": 0,
"NqO2eQGMjrvUO6v9": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.344",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1747991384366,
"modifiedTime": 1748710338024,
"lastModifiedBy": "MxkU9FQYKmOxbdzm"
},
"_key": "!items!Ddk0PAgwM4VLRbyY"
}

View file

@ -1,44 +0,0 @@
{
"name": "Minor Illusion",
"type": "feature",
"_id": "qFq7kynAZhbWTbT5",
"img": "icons/svg/item-bag.svg",
"system": {
"actionType": "action",
"featureType": {
"type": "normal",
"data": {
"property": "spellcastingTrait",
"max": 1,
"numbers": {}
}
},
"refreshData": null,
"multiclass": null,
"disabled": false,
"description": "<p>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.</p>",
"effects": {},
"actions": [],
"type": "class"
},
"effects": [],
"folder": "ncQG0s0ttAPObaeV",
"sort": 150000,
"ownership": {
"default": 0,
"ei8OkswTzyDp4IGC": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.344",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1748033817443,
"modifiedTime": 1748710347004,
"lastModifiedBy": "MxkU9FQYKmOxbdzm"
},
"_key": "!items!qFq7kynAZhbWTbT5"
}

View file

@ -1,44 +0,0 @@
{
"name": "No Mercy",
"type": "feature",
"_id": "t3tLoq4h9wgQD7E9",
"img": "icons/svg/item-bag.svg",
"system": {
"actionType": "action",
"featureType": {
"type": "normal",
"data": {
"property": "spellcastingTrait",
"max": 1,
"numbers": {}
}
},
"refreshData": null,
"multiclass": null,
"disabled": false,
"description": "<p>Spend 3 Hope to gain a +1 bonus to your attack rolls until your next rest.</p>",
"effects": {},
"actions": [],
"type": "class"
},
"effects": [],
"folder": "3znRxBqsK0lOJhvJ",
"sort": 150000,
"ownership": {
"default": 0,
"ei8OkswTzyDp4IGC": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.344",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1748034146678,
"modifiedTime": 1748710386929,
"lastModifiedBy": "MxkU9FQYKmOxbdzm"
},
"_key": "!items!t3tLoq4h9wgQD7E9"
}

View file

@ -1,44 +0,0 @@
{
"name": "Not This Time",
"type": "feature",
"_id": "5msGbQyFwdwdFdYs",
"img": "icons/svg/item-bag.svg",
"system": {
"actionType": "reaction",
"featureType": {
"type": "normal",
"data": {
"property": "spellcastingTrait",
"max": 1,
"numbers": {}
}
},
"refreshData": null,
"multiclass": null,
"disabled": false,
"description": "<p>Spend 3 Hope to force an adversary within Far range to reroll an attack or damage roll</p>",
"effects": {},
"actions": [],
"type": "class"
},
"effects": [],
"folder": "8I0S9f458qu36qSW",
"sort": 100000,
"ownership": {
"default": 0,
"ei8OkswTzyDp4IGC": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.344",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1748034883014,
"modifiedTime": 1748710410194,
"lastModifiedBy": "MxkU9FQYKmOxbdzm"
},
"_key": "!items!5msGbQyFwdwdFdYs"
}

View file

@ -1,47 +0,0 @@
{
"name": "Prayer Dice",
"type": "feature",
"_id": "jXfGnLnU8PswJYJd",
"img": "icons/svg/item-bag.svg",
"system": {
"actionType": "action",
"featureType": {
"type": "normal",
"data": {
"property": "spellcastingTrait",
"max": 6,
"numbers": {},
"value": "d4"
}
},
"refreshData": {
"type": "session"
},
"multiclass": null,
"disabled": false,
"description": "<p>At the beginning of each session, roll a number of d4s equal to your subclasss Spellcast trait and place them on this 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 dies value to reduce incoming damage, add to a rolls result after the roll is made, or gain Hope equal to the result. At the end of each session, clear all unspent Prayer Dice.</p>",
"effects": {},
"actions": [],
"type": "class"
},
"effects": [],
"folder": "aI54jXjBNrAOm7R8",
"sort": 200000,
"ownership": {
"default": 0,
"ei8OkswTzyDp4IGC": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.344",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1748033372327,
"modifiedTime": 1748710416279,
"lastModifiedBy": "MxkU9FQYKmOxbdzm"
},
"_key": "!items!jXfGnLnU8PswJYJd"
}

View file

@ -1,44 +0,0 @@
{
"name": "Prestidigitation",
"type": "feature",
"_id": "ofBmJIn6NWxA0wPz",
"img": "icons/svg/item-bag.svg",
"system": {
"actionType": "action",
"featureType": {
"type": "normal",
"data": {
"property": "spellcastingTrait",
"max": 1,
"numbers": {}
}
},
"refreshData": null,
"multiclass": null,
"disabled": false,
"description": "<p>You can perform harmless, subtle magical effects at will. For example, you can change an objects color, create a smell, light a candle, cause a tiny object to float, illuminate a room, or repair a small object.</p>",
"effects": {},
"actions": [],
"type": "class"
},
"effects": [],
"folder": "8I0S9f458qu36qSW",
"sort": 200000,
"ownership": {
"default": 0,
"ei8OkswTzyDp4IGC": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.344",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1748034934147,
"modifiedTime": 1748710421634,
"lastModifiedBy": "MxkU9FQYKmOxbdzm"
},
"_key": "!items!ofBmJIn6NWxA0wPz"
}

View file

@ -1,47 +0,0 @@
{
"name": "Rally",
"type": "feature",
"_id": "8uORDWrXtNFzA00U",
"img": "icons/svg/item-bag.svg",
"system": {
"actionType": "action",
"featureType": {
"type": "normal",
"data": {
"property": "spellcastingTrait",
"max": 1,
"numbers": {},
"value": "d6"
}
},
"refreshData": {
"type": "session"
},
"multiclass": null,
"disabled": false,
"description": "<p>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.</p><p>At level 5, your Rally Die increases to a d8.</p>",
"effects": {},
"actions": [],
"type": "class"
},
"effects": [],
"folder": "3QoIMKARHfRnBvQJ",
"sort": 200000,
"ownership": {
"default": 0,
"NqO2eQGMjrvUO6v9": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.344",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1747991484460,
"modifiedTime": 1748710434139,
"lastModifiedBy": "MxkU9FQYKmOxbdzm"
},
"_key": "!items!8uORDWrXtNFzA00U"
}

View file

@ -1,44 +0,0 @@
{
"name": "Ranger's Focus",
"type": "feature",
"_id": "b4O4r2HPbWU8s59q",
"img": "icons/svg/item-bag.svg",
"system": {
"actionType": "action",
"featureType": {
"type": "normal",
"data": {
"property": "spellcastingTrait",
"max": 1,
"numbers": {}
}
},
"refreshData": null,
"multiclass": null,
"disabled": false,
"description": "<p>Spend a Hope and make an attack against a target. On a success, deal your attacks normal damage and temporarily make the attacks target your Focus. Until this feature ends or you make a different creature yourFocus, you gain the following benefits against your Focus:</p><p>• You know precisely what direction they are in.</p><p>• When you deal damage to them, they must mark a Stress.</p><p>• When you fail an attack against them, you can end your Rangers Focus feature to reroll your Duality Dice.</p>",
"effects": {},
"actions": [],
"type": "class"
},
"effects": [],
"folder": "84neMcqoIRAoIvXP",
"sort": 200000,
"ownership": {
"default": 0,
"ei8OkswTzyDp4IGC": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.344",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1748030780267,
"modifiedTime": 1748710475164,
"lastModifiedBy": "MxkU9FQYKmOxbdzm"
},
"_key": "!items!b4O4r2HPbWU8s59q"
}

View file

@ -1,44 +0,0 @@
{
"name": "Rogues Dodge",
"type": "feature",
"_id": "fPGn9JNV24nt1G9d",
"img": "icons/svg/item-bag.svg",
"system": {
"actionType": "action",
"featureType": {
"type": "normal",
"data": {
"property": "spellcastingTrait",
"max": 1,
"numbers": {}
}
},
"refreshData": null,
"multiclass": null,
"disabled": false,
"description": "<p>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.</p>",
"effects": {},
"actions": [],
"type": "class"
},
"effects": [],
"folder": "AzwvgmcvToIUGYti",
"sort": 200000,
"ownership": {
"default": 0,
"ei8OkswTzyDp4IGC": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.344",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1748032185608,
"modifiedTime": 1748710477647,
"lastModifiedBy": "MxkU9FQYKmOxbdzm"
},
"_key": "!items!fPGn9JNV24nt1G9d"
}

View file

@ -1,44 +0,0 @@
{
"name": "Sneak Attack",
"type": "feature",
"_id": "PhHOmsoYUDC42by6",
"img": "icons/svg/item-bag.svg",
"system": {
"actionType": "action",
"featureType": {
"type": "normal",
"data": {
"property": "spellcastingTrait",
"max": 1,
"numbers": {}
}
},
"refreshData": null,
"multiclass": null,
"disabled": false,
"description": "<p>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.</p><p>Level 1 is Tier 1</p><p>Levels 24 are Tier 2</p><p>Levels 57 are Tier 3</p><p>Levels 810 are Tier 4</p>",
"effects": {},
"actions": [],
"type": "class"
},
"effects": [],
"folder": "AzwvgmcvToIUGYti",
"sort": 150000,
"ownership": {
"default": 0,
"ei8OkswTzyDp4IGC": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.344",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1748032274441,
"modifiedTime": 1748710492033,
"lastModifiedBy": "MxkU9FQYKmOxbdzm"
},
"_key": "!items!PhHOmsoYUDC42by6"
}

View file

@ -1,44 +0,0 @@
{
"name": "Strange Patterns",
"type": "feature",
"_id": "ONtJ7r2g6tN5q6Ga",
"img": "icons/svg/item-bag.svg",
"system": {
"actionType": "action",
"featureType": {
"type": "normal",
"data": {
"property": "spellcastingTrait",
"max": 1,
"numbers": {}
}
},
"refreshData": null,
"multiclass": null,
"disabled": false,
"description": "<p>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.</p>",
"effects": {},
"actions": [],
"type": "class"
},
"effects": [],
"folder": "8I0S9f458qu36qSW",
"sort": 150000,
"ownership": {
"default": 0,
"ei8OkswTzyDp4IGC": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.344",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1748034976746,
"modifiedTime": 1748710516187,
"lastModifiedBy": "MxkU9FQYKmOxbdzm"
},
"_key": "!items!ONtJ7r2g6tN5q6Ga"
}

View file

@ -1,46 +0,0 @@
{
"name": "Unstoppable",
"type": "feature",
"_id": "rlpNYKW18FX4Hw7t",
"img": "icons/svg/item-bag.svg",
"system": {
"actionType": "action",
"featureType": {
"type": "normal",
"data": {
"property": "spellcastingTrait",
"max": 1,
"numbers": {}
}
},
"refreshData": {
"type": "longRest"
},
"multiclass": null,
"disabled": false,
"description": "<p>Once per long rest, you can become Unstoppable.</p><p>You gain an Unstoppable Die. At level 1, your Unstoppable Die is a [[d4]]. Place it on this 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 dies 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.</p><p>While Unstoppable, you gain the following benefits:</p><p>• You reduce the severity of physical damage by one threshold (Severe to Major, Major to Minor, Minor to None).</p><p>• You add the current value of the Unstoppable Die to your damage roll.</p><p>• You cant be Restrained or Vulnerable.</p>",
"effects": {},
"actions": [],
"type": "class"
},
"effects": [],
"folder": "imOcur5Zv8WcMHXz",
"sort": 200000,
"ownership": {
"default": 0,
"ei8OkswTzyDp4IGC": 3
},
"flags": {},
"_stats": {
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
"coreVersion": "13.344",
"systemId": "daggerheart",
"systemVersion": "0.0.1",
"createdTime": 1748030398960,
"modifiedTime": 1748710525532,
"lastModifiedBy": "MxkU9FQYKmOxbdzm"
},
"_key": "!items!rlpNYKW18FX4Hw7t"
}

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