Compare commits

..

No commits in common. "2123ae19657ea467f6d1b58170427af94c06e3b1" and "c95520c9d8ebbe74a48fe61501a0270efb84561f" have entirely different histories.

177 changed files with 657 additions and 2677 deletions

View file

@ -1,3 +0,0 @@
[*]
indent_size = 4
indent_style = spaces

View file

@ -242,41 +242,6 @@ Hooks.on('setup', () => {
systemEffect: true
}))
];
const damageThresholds = ['damageThresholds.major', 'damageThresholds.severe'];
const traits = Object.keys(game.system.api.data.actors.DhCharacter.schema.fields.traits.fields).map(
trait => `traits.${trait}.value`
);
const resistance = Object.values(game.system.api.data.actors.DhCharacter.schema.fields.resistance.fields).flatMap(
type => Object.keys(type.fields).map(x => `resistance.${type.name}.${x}`)
);
const actorCommon = {
bar: ['resources.stress'],
value: [...resistance, 'advantageSources', 'disadvantageSources']
};
CONFIG.Actor.trackableAttributes = {
character: {
bar: [...actorCommon.bar, 'resources.hitPoints', 'resources.hope'],
value: [
...actorCommon.value,
...traits,
...damageThresholds,
'proficiency',
'evasion',
'armorScore',
'scars',
'levelData.level.current'
]
},
adversary: {
bar: [...actorCommon.bar, 'resources.hitPoints'],
value: [...actorCommon.value, ...damageThresholds, 'criticalThreshold', 'difficulty']
},
companion: {
bar: [...actorCommon.bar],
value: [...actorCommon.value, 'evasion', 'levelData.level.current']
}
};
});
Hooks.on('ready', async () => {
@ -344,7 +309,7 @@ Hooks.on('chatMessage', (_, message) => {
? CONFIG.DH.ACTIONS.advantageState.disadvantage.value
: undefined;
const difficulty = rollCommand.difficulty;
const grantResources = rollCommand.grantResources;
const grantResources = Boolean(rollCommand.grantResources);
const target = getCommandTarget({ allowNull: true });
const title =
@ -420,7 +385,10 @@ const updateActorsRangeDependentEffects = async token => {
// Get required distance and special case 5 feet to test adjacency
const required = rangeMeasurement[range];
const reverse = type === CONFIG.DH.GENERAL.rangeInclusion.outsideRange.id;
const inRange = userTarget.distanceTo(token.object) <= required;
const inRange =
required === 5
? userTarget.isAdjacentWith(token.object)
: userTarget.distanceTo(token.object) <= required;
if (reverse ? inRange : !inRange) {
enabledEffect = false;
break;

View file

@ -192,9 +192,6 @@
},
"age": "Age",
"backgroundQuestions": "Backgrounds",
"burden": {
"ignore": { "label": "Burden: Ignore", "hint": "Ignore burden rules" }
},
"companionFeatures": "Companion Features",
"connections": "Connections",
"contextMenu": {
@ -217,12 +214,6 @@
"maxEvasionBonus": "Max Evasion Increase",
"maxHPBonus": "Max HP Increase",
"pronouns": "Pronouns",
"roll": {
"guaranteedCritical": {
"label": "Guaranteed Critical",
"hint": "Set to 1 to always roll a critical"
}
},
"story": {
"backgroundTitle": "Background",
"characteristics": "Characteristics",
@ -353,12 +344,6 @@
"requestSpotlight": "Request The Spotlight",
"openCountdowns": "Countdowns"
},
"CompendiumBrowserSettings": {
"title": "Enable Compendiums",
"enableSource": "Enable Source",
"disableSource": "Disable Source",
"worldCompendiums": "World Compendiums"
},
"ContextMenu": {
"disableEffect": "Disable Effect",
"enableEffect": "Enable Effect",
@ -459,13 +444,9 @@
"name": "Clear Stress"
},
"prepare": {
"description": "Describe how you are preparing for the next day's adventure, then gain a Hope.",
"description": "Describe how you are preparing for the next day's adventure, then gain a Hope. If you choose to Prepare with one or more members of your party, you may each take two Hope.",
"name": "Prepare"
},
"prepareWithFriends": {
"description": "You prepare with one or more members of your party, and you each gain 2 Hope.",
"name": "Prepare (together)"
},
"repairArmor": {
"description": "Describe how you spend time repairing your armor and clear all of its Armor Slots. You may also do this to an ally's armor instead.",
"name": "Repair Armor"
@ -496,11 +477,7 @@
},
"prepare": {
"name": "Prepare",
"description": "Describe how you prepare yourself for the path ahead, then gain a Hope."
},
"prepareWithFriends": {
"name": "Prepare (together)",
"description": "You prepare with one or more members of your party, and you each gain 2 Hope."
"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."
}
},
"refreshable": {
@ -1864,16 +1841,6 @@
"singular": "Adversary",
"plural": "Adversaries"
},
"Attack": {
"hpDamageMultiplier": {
"label": "HP Damage Multiplier",
"hint": "Multiply any damage you deal by this number"
},
"hpDamageTakenMultiplier": {
"label": "HP Damage Taken Multiplier",
"hint": "Multiply any damage dealt to you by this number"
}
},
"Bonuses": {
"rest": {
"downtimeAction": "Downtime Action",
@ -2058,40 +2025,16 @@
"reaction": "Reaction Roll"
},
"Rules": {
"conditionImmunities": {
"hidden": "Condition Immunity: Hidden",
"restrained": "Condition Immunity: Restrained",
"vulnerable": "Condition Immunity: Vulnerable"
},
"damageReduction": {
"disabledArmor": { "label": "Disabled Armorslots" },
"increasePerArmorMark": {
"label": "Damage Reduction per Armor Slot",
"hint": "A used armor slot normally reduces damage by one step. This value increases the number of steps damage is reduced by."
},
"magical": {
"label": "Daamge Reduction: Only Magical",
"hint": "Armor can only be used to reduce magical damage"
},
"maxArmorMarkedBonus": "Max Armor Used",
"maxArmorMarkedStress": {
"label": "Max Armor Used With Stress",
"hint": "If this value is set you can use up to that much stress to spend additional Armor Marks beyond your normal maximum."
},
"reduceSeverity": {
"magical": {
"label": "Reduce Damage Severity: Magical",
"hint": "Lowers any magical damage received by the set amount of severity degrees"
},
"physical": {
"label": "Reduce Damage Severity: Physical",
"hint": "Lowers any physical damage received by the set amount of severity degrees"
}
},
"physical": {
"label": "Damage Reduction: Only Physical",
"hint": "Armor can only be used to reduce physical damage"
},
"stress": {
"any": {
"label": "Stress Damage Reduction: Any",
@ -2109,12 +2052,6 @@
"label": "Stress Damage Reduction: Minor",
"hint": "The cost in stress you can pay to reduce minor damage to none."
}
},
"thresholdImmunities": {
"minor": {
"label": "Threshold Immunities: Minor",
"hint": "Automatically ignores minor damage when set to 1"
}
}
},
"attack": {
@ -2175,6 +2112,7 @@
"tier4": "tier 4",
"domains": "Domains",
"downtime": "Downtime",
"itemFeatures": "Item Features",
"roll": "Roll",
"rules": "Rules",
"partyMembers": "Party Members",
@ -2183,10 +2121,7 @@
"questions": "Questions",
"configuration": "Configuration",
"base": "Base",
"triggers": "Triggers",
"deathMoves": "Deathmoves",
"sources": "Sources",
"packs": "Packs"
"triggers": "Triggers"
},
"Tiers": {
"singular": "Tier",
@ -2210,7 +2145,6 @@
"armorSlots": "Armor Slots",
"artistAttribution": "Artwork By: {artist}",
"attack": "Attack",
"automation": "Automation",
"basics": "Basics",
"bonus": "Bonus",
"burden": "Burden",
@ -2218,7 +2152,6 @@
"continue": "Continue",
"criticalSuccess": "Critical Success",
"criticalShort": "Critical",
"currentLevel": "Current Level",
"custom": "Custom",
"d20Roll": "D20 Roll",
"damage": "Damage",
@ -2328,7 +2261,6 @@
"single": "Target",
"plural": "Targets"
},
"thingsAndThing": "{things} and {thing}",
"title": "Title",
"tokenSize": "Token Size",
"total": "Total",
@ -2367,8 +2299,7 @@
},
"Ancestry": {
"primaryFeature": "Primary Feature",
"secondaryFeature": "Secondary Feature",
"featuresLabel": "Ancestry Features"
"secondaryFeature": "Secondary Feature"
},
"Armor": {
"baseScore": "Base Score",
@ -2421,12 +2352,7 @@
"evolvedImagePlaceholder": "The image for the form selected for evolution will be used"
},
"Class": {
"startingEvasionScore": "Starting Evasion Score",
"startingHitPoints": "Starting Hit Points",
"classItems": "Class Items",
"hopeFeatureLabel": "{class}'s Hope Feature",
"hopeFeatures": "Hope Features",
"classFeature": "Class Feature",
"classFeatures": "Class Features",
"guide": {
"suggestedEquipment": "Suggested Equipments",
@ -2439,9 +2365,6 @@
}
}
},
"Community": {
"featuresLabel": "Community Feature"
},
"Consumable": {
"consumeOnUse": "Consume On Use",
"destroyOnEmpty": "Destroy On Empty"
@ -2457,11 +2380,7 @@
"masteryTitle": "Mastery"
},
"Subclass": {
"spellcastingTrait": "Spellcasting Trait",
"spellcastTrait": "Spellcast Trait",
"foundationFeatures": "Foundation Features",
"specializationFeature": "Specialization Feature",
"masteryFeature": "Mastery Feature"
"spellcastingTrait": "Spellcasting Trait"
},
"Weapon": {
"weaponType": "Weapon Type",
@ -2490,14 +2409,6 @@
"hideAttribution": {
"label": "Hide Attribution"
},
"showTokenDistance": {
"label": "Show Token Distance on Hover",
"choices": {
"always": "Always",
"encounters": "Encounters",
"never": "Never"
}
},
"expandedTitle": "Auto-expand Descriptions",
"extendCharacterDescriptions": {
"label": "Characters"
@ -2640,8 +2551,6 @@
"resetMovesTitle": "Reset {type} Downtime Moves",
"resetItemFeaturesTitle": "Reset {type}",
"resetMovesText": "Are you sure you want to reset?",
"deleteItemTitle": "Delete Homebrew Item",
"deleteItemText": "Are you sure you want to delete the item?",
"FIELDS": {
"maxFear": { "label": "Max Fear" },
"maxHope": { "label": "Max Hope" },
@ -2873,7 +2782,6 @@
"ItemBrowser": {
"title": "Daggerheart Compendium Browser",
"hint": "Select a Folder in sidebar to start browsing through the compendium",
"browserSettings": "Browser Settings",
"searchPlaceholder": "Search...",
"columnName": "Name",
"tooltipFilters": "Filters",
@ -2998,9 +2906,7 @@
"tier": "Tier {tier} {type}",
"character": "Level {level} Character",
"companion": "Level {level} - {partner}",
"companionNoPartner": "No Partner",
"duplicateToNewTier": "Duplicate to New Tier",
"pickTierTitle": "Pick a new tier for this adversary"
"companionNoPartner": "No Partner"
},
"daggerheartMenu": {
"title": "Daggerheart Menu",
@ -3032,7 +2938,7 @@
"rulesOn": "Rules On",
"rulesOff": "Rules Off",
"remainingUses": "Uses refresh on {type}",
"rightClickExtend": "Right-Click to extend",
"rightClickExtand": "Right-Click to extand",
"companionPartnerLevelBlock": "The companion needs an assigned partner to level up.",
"configureAttribution": "Configure Attribution",
"deleteItem": "Delete Item",

View file

@ -1,143 +0,0 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class CompendiumBrowserSettings extends HandlebarsApplicationMixin(ApplicationV2) {
constructor() {
super();
this.browserSettings = game.settings
.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings)
.toObject();
}
static DEFAULT_OPTIONS = {
tag: 'div',
classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'compendium-brower-settings'],
window: {
icon: 'fa-solid fa-book',
title: 'DAGGERHEART.APPLICATIONS.CompendiumBrowserSettings.title'
},
position: {
width: 500
},
actions: {
toggleSource: CompendiumBrowserSettings.#toggleSource,
finish: CompendiumBrowserSettings.#finish
}
};
/** @override */
static PARTS = {
packs: {
id: 'packs',
template: 'systems/daggerheart/templates/dialogs/compendiumBrowserSettingsDialog/packs.hbs'
},
footer: { template: 'systems/daggerheart/templates/dialogs/compendiumBrowserSettingsDialog/footer.hbs' }
};
static #browserPackTypes = ['Actor', 'Item'];
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
for (const element of htmlElement.querySelectorAll('.pack-checkbox'))
element.addEventListener('change', this.toggleTypedPack.bind(this));
}
/**@inheritdoc */
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
const excludedSourceData = this.browserSettings.excludedSources;
const excludedPackData = this.browserSettings.excludedPacks;
context.typePackCollections = game.packs.reduce((acc, pack) => {
const { type, label, packageType, packageName: basePackageName, id } = pack.metadata;
if (!CompendiumBrowserSettings.#browserPackTypes.includes(type)) return acc;
const isWorldPack = packageType === 'world';
const packageName = isWorldPack ? 'world' : basePackageName;
const sourceChecked =
!excludedSourceData[packageName] ||
!excludedSourceData[packageName].excludedDocumentTypes.includes(type);
const sourceLabel =
game.modules.get(packageName)?.title ??
(isWorldPack
? game.i18n.localize('DAGGERHEART.APPLICATIONS.CompendiumBrowserSettings.worldCompendiums')
: game.system.title);
if (!acc[type]) acc[type] = { label: game.i18n.localize(`DOCUMENT.${type}s`), sources: {} };
if (!acc[type].sources[packageName])
acc[type].sources[packageName] = { label: sourceLabel, checked: sourceChecked, packs: [] };
const checked = !excludedPackData[id] || !excludedPackData[id].excludedDocumentTypes.includes(type);
acc[type].sources[packageName].packs.push({
pack: id,
type,
label: id === game.system.id ? game.system.title : game.i18n.localize(label),
checked: checked
});
return acc;
}, {});
return context;
}
static #toggleSource(event, button) {
event.stopPropagation();
const { type, source } = button.dataset;
const currentlyExcluded = this.browserSettings.excludedSources[source]
? this.browserSettings.excludedSources[source].excludedDocumentTypes.includes(type)
: false;
if (!this.browserSettings.excludedSources[source])
this.browserSettings.excludedSources[source] = { excludedDocumentTypes: [] };
this.browserSettings.excludedSources[source].excludedDocumentTypes = currentlyExcluded
? this.browserSettings.excludedSources[source].excludedDocumentTypes.filter(x => x !== type)
: [...(this.browserSettings.excludedSources[source]?.excludedDocumentTypes ?? []), type];
const toggleIcon = button.querySelector('a > i');
toggleIcon.classList.toggle('fa-toggle-off');
toggleIcon.classList.toggle('fa-toggle-on');
button.closest('.source-container').querySelector('.checks-container').classList.toggle('collapsed');
}
toggleTypedPack(event) {
event.stopPropagation();
const { type, pack } = event.target.dataset;
const currentlyExcluded = this.browserSettings.excludedPacks[pack]
? this.browserSettings.excludedPacks[pack].excludedDocumentTypes.includes(type)
: false;
if (!this.browserSettings.excludedPacks[pack])
this.browserSettings.excludedPacks[pack] = { excludedDocumentTypes: [] };
this.browserSettings.excludedPacks[pack].excludedDocumentTypes = currentlyExcluded
? this.browserSettings.excludedPacks[pack].excludedDocumentTypes.filter(x => x !== type)
: [...(this.browserSettings.excludedPacks[pack]?.excludedDocumentTypes ?? []), type];
this.render();
}
static async #finish() {
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings);
await settings.updateSource(this.browserSettings);
await game.settings.set(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings,
settings.toObject()
);
this.updated = true;
this.close();
}
static async configure() {
return new Promise(resolve => {
const app = new this();
app.addEventListener('close', () => resolve(app.updated), { once: true });
app.render({ force: true });
});
}
}

View file

@ -16,4 +16,3 @@ export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs';
export { default as GroupRollDialog } from './group-roll-dialog.mjs';
export { default as TagTeamDialog } from './tagTeamDialog.mjs';
export { default as RiskItAllDialog } from './riskItAllDialog.mjs';
export { default as CompendiumBrowserSettingsDialog } from './CompendiumBrowserSettings.mjs';

View file

@ -54,11 +54,7 @@ export default class AttributionDialog extends HandlebarsApplicationMixin(Applic
const after = label.slice(matchIndex + search.length, label.length);
const element = document.createElement('li');
element.innerHTML =
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
' ',
'&nbsp;'
);
element.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
if (item.hint) {
element.dataset.tooltip = game.i18n.localize(item.hint);
}

View file

@ -165,10 +165,9 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
}
if (rest.hasOwnProperty('trait')) {
this.config.roll.trait = rest.trait;
if (!this.config.source.item)
this.config.title = game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(abilities[this.config.roll.trait]?.label)
});
this.config.title = game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(abilities[this.config.roll.trait]?.label)
});
}
this.config.extraFormula = rest.extraFormula;
this.render();

View file

@ -43,7 +43,7 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
return context;
}
async handleAvoidDeath(useAutomation) {
async handleAvoidDeath() {
const target = this.actor.uuid;
const config = await enrichedFateRoll({
target,
@ -53,7 +53,6 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
});
if (!config.roll.fate) return;
if (!useAutomation) return '';
let returnMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.avoidScar');
if (config.roll.fate.value <= this.actor.system.levelData.level.current) {
@ -76,7 +75,7 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
return returnMessage;
}
async handleRiskItAll(useAutomation) {
async handleRiskItAll() {
const config = await enrichedDualityRoll({
reaction: true,
traitValue: null,
@ -91,7 +90,6 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
});
if (!config.roll.result) return;
if (!useAutomation) return '';
const clearAllStressAndHitpointsUpdates = [
{ key: 'hitPoints', clear: true },
@ -130,9 +128,7 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
return chatMessage;
}
async handleBlazeOfGlory(useAutomation) {
if (!useAutomation) return '';
async handleBlazeOfGlory() {
this.actor.createEmbeddedDocuments('ActiveEffect', [
{
name: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.blazeOfGlory.name'),
@ -164,23 +160,19 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
let result = '';
const deathMoveAutomation = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Automation
).deathMoveAutomation;
if (CONFIG.DH.GENERAL.deathMoves.blazeOfGlory === this.selectedMove) {
result = await this.handleBlazeOfGlory(deathMoveAutomation.blazeOfGlory);
result = await this.handleBlazeOfGlory();
}
if (CONFIG.DH.GENERAL.deathMoves.avoidDeath === this.selectedMove) {
result = await this.handleAvoidDeath(deathMoveAutomation.avoidDeath);
result = await this.handleAvoidDeath();
}
if (CONFIG.DH.GENERAL.deathMoves.riskItAll === this.selectedMove) {
result = await this.handleRiskItAll(deathMoveAutomation.riskItAll);
result = await this.handleRiskItAll();
}
if (result === undefined) return;
if (!result) return;
const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
.expandRollMessage?.desc;
@ -200,6 +192,7 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
description: game.i18n.localize(this.selectedMove.description),
result: result,
open: autoExpandDescription ? 'open' : '',
chevron: autoExpandDescription ? 'fa-chevron-up' : 'fa-chevron-down',
showRiskItAllButton: this.showRiskItAllButton,
riskItAllButtonLabel: this.riskItAllButtonLabel,
riskItAllHope: this.riskItAllHope

View file

@ -196,9 +196,6 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
.filter(x => x.testUserPermission(game.user, 'LIMITED'))
.filter(x => x.uuid !== this.actor.uuid);
const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
.expandRollMessage?.desc;
const cls = getDocumentClass('ChatMessage');
const msg = {
user: game.user.id,
@ -219,8 +216,7 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
actor: { name: this.actor.name, img: this.actor.img },
moves: moves,
characters: characters,
selfId: this.actor.uuid,
open: autoExpandDescription ? 'open' : ''
selfId: this.actor.uuid
}
),
flags: {

View file

@ -70,11 +70,7 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
element.appendChild(img);
const label = document.createElement('span');
label.innerHTML =
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
' ',
'&nbsp;'
);
label.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
element.appendChild(label);
return element;
@ -123,11 +119,7 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
element.appendChild(img);
const label = document.createElement('span');
label.innerHTML =
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
' ',
'&nbsp;'
);
label.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
element.appendChild(label);
return element;

View file

@ -103,11 +103,7 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
/** @override */
async _processSubmitData(event, form, submitData, options) {
if (!submitData.flags) submitData.flags = {};
submitData.flags.daggerheart = foundry.utils.mergeObject(
this.daggerheartFlag.toObject(),
submitData.flags.daggerheart
);
submitData.flags.daggerheart = this.daggerheartFlag.toObject();
submitData.flags.daggerheart.sceneEnvironments = submitData.flags.daggerheart.sceneEnvironments.filter(x =>
foundry.utils.fromUuidSync(x)
);

View file

@ -34,7 +34,7 @@ export default class DhAutomationSettings extends HandlebarsApplicationMixin(App
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
header: { template: 'systems/daggerheart/templates/settings/automation-settings/header.hbs' },
general: { template: 'systems/daggerheart/templates/settings/automation-settings/general.hbs' },
rules: { template: 'systems/daggerheart/templates/settings/automation-settings/deathMoves.hbs' },
rules: { template: 'systems/daggerheart/templates/settings/automation-settings/rules.hbs' },
roll: { template: 'systems/daggerheart/templates/settings/automation-settings/roll.hbs' },
footer: { template: 'systems/daggerheart/templates/settings/automation-settings/footer.hbs' }
};
@ -42,7 +42,7 @@ export default class DhAutomationSettings extends HandlebarsApplicationMixin(App
/** @inheritdoc */
static TABS = {
main: {
tabs: [{ id: 'general' }, { id: 'deathMoves' }, { id: 'roll' }],
tabs: [{ id: 'general' }, { id: 'rules' }, { id: 'roll' }],
initial: 'general',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}

View file

@ -103,12 +103,6 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
? { id: this.selected.adversaryType, ...this.settings.adversaryTypes[this.selected.adversaryType] }
: null;
break;
case 'downtime':
context.restOptions = {
shortRest: CONFIG.DH.GENERAL.defaultRestOptions.shortRest(),
longRest: CONFIG.DH.GENERAL.defaultRestOptions.longRest()
};
break;
}
return context;
@ -171,8 +165,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newDowntimeMove'),
img: 'icons/magic/life/cross-worn-green.webp',
description: '',
actions: [],
effects: []
actions: []
}
});
} else if (['armorFeatures', 'weaponFeatures'].includes(type)) {
@ -231,15 +224,6 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
}
static async removeItem(_, target) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize(`DAGGERHEART.SETTINGS.Homebrew.deleteItemTitle`)
},
content: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.deleteItemText')
});
if (!confirmed) return;
const { type, id } = target.dataset;
const isDowntime = ['shortRest', 'longRest'].includes(type);
const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`;

View file

@ -314,7 +314,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
const index = Number.parseInt(button.dataset.index);
const toggle = (element, codeMirror) => {
codeMirror.classList.toggle('revealed');
const button = element.querySelector('.expand-trigger > i');
const button = element.querySelector('a > i');
button.classList.toggle('fa-angle-up');
button.classList.toggle('fa-angle-down');
};

View file

@ -4,60 +4,20 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
constructor(options) {
super(options);
const ignoredActorKeys = ['config', 'DhEnvironment', 'DhParty'];
const getAllLeaves = (root, group, parentPath = '') => {
const leaves = [];
const rootKey = `${parentPath ? `${parentPath}.` : ''}${root.name}`;
for (const field of Object.values(root.fields)) {
if (field instanceof foundry.data.fields.SchemaField)
leaves.push(...getAllLeaves(field, group, rootKey));
else
leaves.push({
value: `${rootKey}.${field.name}`,
label: game.i18n.localize(field.label),
hint: game.i18n.localize(field.hint),
group
});
}
return leaves;
};
const ignoredActorKeys = ['config', 'DhEnvironment'];
this.changeChoices = Object.keys(game.system.api.models.actors).reduce((acc, key) => {
if (ignoredActorKeys.includes(key)) return acc;
const model = game.system.api.models.actors[key];
const group = game.i18n.localize(model.metadata.label);
const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model.metadata.type);
const getTranslations = path => {
if (path === 'resources.hope.max')
return {
label: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxHope.label'),
hint: ''
};
const field = model.schema.getField(path);
return {
label: field ? game.i18n.localize(field.label) : path,
hint: field ? game.i18n.localize(field.hint) : ''
};
};
const bars = attributes.bar.flatMap(x => {
const joined = `${x.join('.')}.max`;
return { value: joined, ...getTranslations(joined), group };
});
const values = attributes.value.flatMap(x => {
const joined = x.join('.');
return { value: joined, ...getTranslations(joined), group };
});
const bonuses = getAllLeaves(model.schema.fields.bonuses, group);
const rules = getAllLeaves(model.schema.fields.rules, group);
acc.push(...bars, ...values, ...rules, ...bonuses);
if (!ignoredActorKeys.includes(key)) {
const model = game.system.api.models.actors[key];
const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model);
// As per DHToken._getTrackedAttributesFromSchema, attributes.bar have a max version as well.
const maxAttributes = attributes.bar.map(x => [...x, 'max']);
attributes.value.push(...maxAttributes);
const group = game.i18n.localize(model.metadata.label);
const choices = CONFIG.Token.documentClass
.getTrackedAttributeChoices(attributes, model)
.map(x => ({ ...x, group: group }));
acc.push(...choices);
}
return acc;
}, []);
}
@ -108,18 +68,14 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
},
render: function (item, search) {
const label = game.i18n.localize(item.label);
const matchIndex = label.toLowerCase().indexOf(search.toLowerCase());
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}`.replaceAll(
' ',
'&nbsp;'
);
element.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
if (item.hint) {
element.dataset.tooltip = game.i18n.localize(item.hint);
}

View file

@ -103,11 +103,7 @@ export default class SettingActiveEffectConfig extends HandlebarsApplicationMixi
const after = label.slice(matchIndex + search.length, label.length);
const element = document.createElement('li');
element.innerHTML =
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
' ',
'&nbsp;'
);
element.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
if (item.hint) {
element.dataset.tooltip = game.i18n.localize(item.hint);
}

View file

@ -431,18 +431,18 @@ export default function DHApplicationMixin(Base) {
{
name: 'disableEffect',
icon: 'fa-solid fa-lightbulb',
condition: element => {
const target = element.closest('[data-item-uuid]');
return !target.dataset.disabled && target.dataset.itemType !== 'beastform';
condition: target => {
const doc = getDocFromElementSync(target);
return doc && !doc.disabled;
},
callback: async target => (await getDocFromElement(target)).update({ disabled: true })
},
{
name: 'enableEffect',
icon: 'fa-regular fa-lightbulb',
condition: element => {
const target = element.closest('[data-item-uuid]');
return target.dataset.disabled && target.dataset.itemType !== 'beastform';
condition: target => {
const doc = getDocFromElementSync(target);
return doc && doc.disabled;
},
callback: async target => (await getDocFromElement(target)).update({ disabled: false })
}
@ -570,10 +570,6 @@ export default function DHApplicationMixin(Base) {
options.push({
name: 'CONTROLS.CommonDelete',
icon: 'fa-solid fa-trash',
condition: element => {
const target = element.closest('[data-item-uuid]');
return target.dataset.itemType !== 'beastform';
},
callback: async (target, event) => {
const doc = await getDocFromElement(target);
if (event.shiftKey) return doc.delete();

View file

@ -1,6 +1,7 @@
export default function ItemAttachmentSheet(Base) {
return class extends Base {
static DEFAULT_OPTIONS = {
...super.DEFAULT_OPTIONS,
dragDrop: [
...(super.DEFAULT_OPTIONS.dragDrop || []),
{ dragSelector: null, dropSelector: '.attachments-section' }

View file

@ -43,54 +43,4 @@ export default class DhActorDirectory extends foundry.applications.sidebar.tabs.
event.dataTransfer.setDragImage(preview, w / 2, h / 2);
}
}
_getEntryContextOptions() {
const options = super._getEntryContextOptions();
options.push({
name: 'DAGGERHEART.UI.Sidebar.actorDirectory.duplicateToNewTier',
icon: `<i class="fa-solid fa-arrow-trend-up" inert></i>`,
condition: li => {
const actor = game.actors.get(li.dataset.entryId);
return actor?.type === 'adversary' && actor.system.type !== 'social';
},
callback: async li => {
const actor = game.actors.get(li.dataset.entryId);
if (!actor) throw new Error('Unexpected missing actor');
const tiers = [1, 2, 3, 4].filter(t => t !== actor.system.tier);
const content = document.createElement('div');
const select = document.createElement('select');
select.name = 'tier';
select.append(
...tiers.map(t => {
const option = document.createElement('option');
option.value = t;
option.textContent = game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${t}`);
return option;
})
);
content.append(select);
const tier = await foundry.applications.api.Dialog.input({
classes: ['dh-style', 'dialog'],
window: { title: 'DAGGERHEART.UI.Sidebar.actorDirectory.pickTierTitle' },
content,
ok: {
label: 'Create Adversary',
callback: (event, button, dialog) => Number(button.form.elements.tier.value)
}
});
if (tier === actor.system.tier) {
ui.notifications.warn('This actor is already at this tier');
} else if (tier) {
const source = actor.system.adjustForTier(tier);
await Actor.create(source);
ui.notifications.info(`Tier ${tier} ${actor.name} created`);
}
}
});
return options;
}
}

View file

@ -34,6 +34,8 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
position: {
width: 222,
height: 222
// top: "200px",
// left: "120px"
}
};
@ -64,7 +66,7 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
max = this.maxFear,
percent = (current / max) * 100,
isGM = game.user.isGM;
// Return the data for rendering
return { display, current, max, percent, isGM };
}

View file

@ -1,5 +1,3 @@
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
/**
@ -19,15 +17,6 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
this.config = CONFIG.DH.ITEMBROWSER.compendiumConfig;
this.presets = {};
this.compendiumBrowserTypeKey = 'compendiumBrowserDefault';
this.setupHooks = Hooks.on(socketEvent.Refresh, ({ refreshType }) => {
if (refreshType === RefreshType.CompendiumBrowser) {
if (this.rendered) {
this.render();
this.loadItems();
}
}
});
}
/** @inheritDoc */
@ -46,8 +35,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
selectFolder: this.selectFolder,
expandContent: this.expandContent,
resetFilters: this.resetFilters,
sortList: this.sortList,
openSettings: this.openSettings
sortList: this.sortList
},
position: {
left: 100,
@ -169,8 +157,6 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
context.formatChoices = this.formatChoices;
context.items = this.items;
context.presets = this.presets;
context.isGM = game.user.isGM;
return context;
}
@ -228,10 +214,6 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
loadItems() {
let loadTimeout = this.toggleLoader(true);
const browserSettings = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings
);
const promises = [];
game.packs.forEach(pack => {
@ -245,7 +227,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
Promise.all(promises).then(async result => {
this.items = ItemBrowser.sortBy(
result.flatMap(r => r).filter(r => !browserSettings.isEntryExcluded.bind(browserSettings)(r)),
result.flatMap(r => r),
'name'
);
@ -530,22 +512,6 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
itemListContainer.replaceChildren(...newOrder);
}
static async openSettings() {
const settingsUpdated = await game.system.api.applications.dialogs.CompendiumBrowserSettingsDialog.configure();
if (settingsUpdated) {
if (this.rendered) {
this.render();
this.loadItems();
}
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.CompendiumBrowser
}
});
}
}
_createDragProcess() {
new foundry.applications.ux.DragDrop.implementation({
dragSelector: '.item-container',
@ -605,9 +571,4 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
headerActions.append(button);
}
}
async close(options = {}) {
Hooks.off(socketEvent.Refresh, this.setupHooks);
await super.close(options);
}
}

View file

@ -63,8 +63,7 @@ export default class DhSceneNavigation extends foundry.applications.ui.SceneNavi
if (scene.flags.daggerheart.sceneEnvironments[0] !== environment.uuid) {
const newEnvironments = scene.flags.daggerheart.sceneEnvironments;
const newFirst = newEnvironments.splice(
newEnvironments.findIndex(x => x === environment.uuid),
1
newEnvironments.findIndex(x => x === environment.uuid)
)[0];
newEnvironments.unshift(newFirst);
emitAsGM(

View file

@ -18,9 +18,8 @@ export default class DhMeasuredTemplate extends foundry.canvas.placeables.Measur
static getRangeLabels(distanceValue, settings) {
let result = { distance: distanceValue, units: '' };
if (!settings.enabled) return result;
const sceneRangeMeasurement = canvas.scene.flags.daggerheart?.rangeMeasurement;
const { disable, custom } = CONFIG.DH.GENERAL.sceneRangeMeasurementSetting;
if (sceneRangeMeasurement?.setting === disable.id) {
result.distance = distanceValue;
@ -28,9 +27,31 @@ export default class DhMeasuredTemplate extends foundry.canvas.placeables.Measur
return result;
}
const ranges = sceneRangeMeasurement?.setting === custom.id ? sceneRangeMeasurement : settings;
const distanceKey = ['melee', 'veryClose', 'close', 'far'].find(r => ranges[r] >= distanceValue);
result.distance = game.i18n.localize(`DAGGERHEART.CONFIG.Range.${distanceKey ?? 'veryFar'}.name`);
const melee = sceneRangeMeasurement?.setting === custom.id ? sceneRangeMeasurement.melee : settings.melee;
const veryClose =
sceneRangeMeasurement?.setting === custom.id ? sceneRangeMeasurement.veryClose : settings.veryClose;
const close = sceneRangeMeasurement?.setting === custom.id ? sceneRangeMeasurement.close : settings.close;
const far = sceneRangeMeasurement?.setting === custom.id ? sceneRangeMeasurement.far : settings.far;
if (distanceValue <= melee) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.melee.name');
return result;
}
if (distanceValue <= veryClose) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.veryClose.name');
return result;
}
if (distanceValue <= close) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.close.name');
return result;
}
if (distanceValue <= far) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.far.name');
return result;
}
if (distanceValue > far) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.veryFar.name');
}
return result;
}
}

View file

@ -1,5 +1,3 @@
import DhMeasuredTemplate from './measuredTemplate.mjs';
export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
/** @inheritdoc */
async _draw(options) {
@ -54,111 +52,30 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
if (this === target) return 0;
const originPoint = this.center;
const targetPoint = target.center;
const thisBounds = this.bounds;
const targetBounds = target.bounds;
const adjacencyBuffer = canvas.grid.distance * 1.75; // handles diagonals with one square elevation difference
// Figure out the elevation difference.
// This intends to return "grid distance" for adjacent ones, so we add that number if not overlapping.
const sizePerUnit = canvas.grid.size / canvas.grid.distance;
const thisHeight = Math.max(thisBounds.width, thisBounds.height) / sizePerUnit;
const targetHeight = Math.max(targetBounds.width, targetBounds.height) / sizePerUnit;
const thisElevation = [this.document.elevation, this.document.elevation + thisHeight];
const targetElevation = [target.document.elevation, target.document.elevation + targetHeight];
const isSameAltitude =
thisElevation[0] < targetElevation[1] && // bottom of this must be at or below the top of target
thisElevation[1] > targetElevation[0]; // top of this must be at or above the bottom of target
const [lower, higher] = [targetElevation, thisElevation].sort((a, b) => a[1] - b[1]);
const elevation = isSameAltitude ? 0 : higher[0] - lower[1] + canvas.grid.distance;
const destinationPoint = target.center;
// Compute for gridless. This version returns circular edge to edge + grid distance,
// so that tokens that are touching return 5.
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
const boundsCorrection = canvas.grid.distance / canvas.grid.size;
const originRadius = (thisBounds.width * boundsCorrection) / 2;
const targetRadius = (targetBounds.width * boundsCorrection) / 2;
const measuredDistance = canvas.grid.measurePath([
{ ...originPoint, elevation: 0 },
{ ...targetPoint, elevation }
]).distance;
const distance = Math.floor(measuredDistance - originRadius - targetRadius + canvas.grid.distance);
return Math.min(distance, distance > adjacencyBuffer ? Infinity : canvas.grid.distance);
const originRadius = (this.bounds.width * boundsCorrection) / 2;
const targetRadius = (target.bounds.width * boundsCorrection) / 2;
const distance = canvas.grid.measurePath([originPoint, destinationPoint]).distance;
return distance - originRadius - targetRadius + canvas.grid.distance;
}
// Compute what the closest grid space of each token is, then compute that distance
const originEdge = this.#getEdgeBoundary(thisBounds, originPoint, targetPoint);
const targetEdge = this.#getEdgeBoundary(targetBounds, originPoint, targetPoint);
const adjustedOriginPoint = originEdge
? canvas.grid.getTopLeftPoint({
x: originEdge.x + Math.sign(originPoint.x - originEdge.x),
y: originEdge.y + Math.sign(originPoint.y - originEdge.y)
})
: originPoint;
const adjustDestinationPoint = targetEdge
? canvas.grid.getTopLeftPoint({
x: targetEdge.x + Math.sign(targetPoint.x - targetEdge.x),
y: targetEdge.y + Math.sign(targetPoint.y - targetEdge.y)
})
: targetPoint;
const distance = canvas.grid.measurePath([
{ ...adjustedOriginPoint, elevation: 0 },
{ ...adjustDestinationPoint, elevation }
]).distance;
return Math.min(distance, distance > adjacencyBuffer ? Infinity : canvas.grid.distance);
}
_onHoverIn(event, options) {
super._onHoverIn(event, options);
// Check if the setting is enabled
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance).showTokenDistance;
if (setting === 'never' || (setting === 'encounters' && !game.combat?.started)) return;
// Check if this token isn't invisible and is actually being hovered
const isTokenValid =
this.visible &&
this.hover &&
!this.isPreview &&
!this.document.isSecret &&
!this.controlled &&
!this.animation;
if (!isTokenValid) return;
// Ensure we have a single controlled token
const originToken = canvas.tokens.controlled[0];
if (!originToken || canvas.tokens.controlled.length > 1) return;
// Determine the actual range
const ranges = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement;
const distanceResult = DhMeasuredTemplate.getRangeLabels(originToken.distanceTo(this), ranges);
const distanceLabel = `${distanceResult.distance} ${distanceResult.units}`.trim();
// Create the element
const element = document.createElement('div');
element.id = 'token-hover-distance';
element.classList.add('waypoint-label', 'last');
const ruler = document.createElement('i');
ruler.classList.add('fa-solid', 'fa-ruler');
element.appendChild(ruler);
const labelEl = document.createElement('span');
labelEl.classList.add('total-measurement');
labelEl.textContent = distanceLabel;
element.appendChild(labelEl);
// Position the element and add to the DOM
const center = this.getCenterPoint();
element.style.setProperty('--transformY', 'calc(-100% - 10px)');
element.style.setProperty('--position-y', `${this.y}px`);
element.style.setProperty('--position-x', `${center.x}px`);
element.style.setProperty('--ui-scale', String(canvas.dimensions.uiScale));
document.querySelector('#token-hover-distance')?.remove();
document.querySelector('#measurement').appendChild(element);
}
_onHoverOut(...args) {
super._onHoverOut(...args);
document.querySelector('#token-hover-distance')?.remove();
const originEdge = this.#getEdgeBoundary(this.bounds, originPoint, destinationPoint);
const targetEdge = this.#getEdgeBoundary(target.bounds, originPoint, destinationPoint);
const adjustedOriginPoint = canvas.grid.getTopLeftPoint({
x: originEdge.x + Math.sign(originPoint.x - originEdge.x),
y: originEdge.y + Math.sign(originPoint.y - originEdge.y)
});
const adjustDestinationPoint = canvas.grid.getTopLeftPoint({
x: targetEdge.x + Math.sign(destinationPoint.x - targetEdge.x),
y: targetEdge.y + Math.sign(destinationPoint.y - targetEdge.y)
});
return canvas.grid.measurePath([adjustedOriginPoint, adjustDestinationPoint]).distance;
}
/** Returns the point at which a line starting at origin and ending at destination intersects the edge of the bounds */
@ -183,6 +100,11 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
return null;
}
/** Tests if the token is at least adjacent with another, with some leeway for diagonals */
isAdjacentWith(token) {
return this.distanceTo(token) <= canvas.grid.distance * 1.5;
}
/** @inheritDoc */
_drawBar(number, bar, data) {
const val = Number(data.value);

View file

@ -494,275 +494,3 @@ export const subclassFeatureLabels = {
2: 'DAGGERHEART.ITEMS.DomainCard.specializationTitle',
3: 'DAGGERHEART.ITEMS.DomainCard.masteryTitle'
};
/**
* @typedef {Object} TierData
* @property {number} difficulty
* @property {number} majorThreshold
* @property {number} severeThreshold
* @property {number} hp
* @property {number} stress
* @property {number} attack
* @property {number[]} damage
*/
/**
* @type {Record<string, Record<2 | 3 | 4, TierData>}
* Scaling data used to change an adversary's tier. Each rank is applied incrementally.
*/
export const adversaryScalingData = {
bruiser: {
2: {
difficulty: 2,
majorThreshold: 5,
severeThreshold: 10,
hp: 1,
stress: 2,
attack: 2
},
3: {
difficulty: 2,
majorThreshold: 7,
severeThreshold: 15,
hp: 1,
stress: 0,
attack: 2
},
4: {
difficulty: 2,
majorThreshold: 12,
severeThreshold: 25,
hp: 1,
stress: 0,
attack: 2
}
},
horde: {
2: {
difficulty: 2,
majorThreshold: 5,
severeThreshold: 8,
hp: 2,
stress: 0,
attack: 0
},
3: {
difficulty: 2,
majorThreshold: 5,
severeThreshold: 12,
hp: 0,
stress: 1,
attack: 1
},
4: {
difficulty: 2,
majorThreshold: 10,
severeThreshold: 15,
hp: 2,
stress: 0,
attack: 0
}
},
leader: {
2: {
difficulty: 2,
majorThreshold: 6,
severeThreshold: 10,
hp: 0,
stress: 0,
attack: 1
},
3: {
difficulty: 2,
majorThreshold: 6,
severeThreshold: 15,
hp: 1,
stress: 0,
attack: 2
},
4: {
difficulty: 2,
majorThreshold: 12,
severeThreshold: 25,
hp: 1,
stress: 1,
attack: 3
}
},
minion: {
2: {
difficulty: 2,
majorThreshold: 0,
severeThreshold: 0,
hp: 0,
stress: 0,
attack: 1
},
3: {
difficulty: 2,
majorThreshold: 0,
severeThreshold: 0,
hp: 0,
stress: 1,
attack: 1
},
4: {
difficulty: 2,
majorThreshold: 0,
severeThreshold: 0,
hp: 0,
stress: 0,
attack: 1
}
},
ranged: {
2: {
difficulty: 2,
majorThreshold: 3,
severeThreshold: 6,
hp: 1,
stress: 0,
attack: 1
},
3: {
difficulty: 2,
majorThreshold: 7,
severeThreshold: 14,
hp: 1,
stress: 1,
attack: 2
},
4: {
difficulty: 2,
majorThreshold: 5,
severeThreshold: 10,
hp: 1,
stress: 1,
attack: 1
}
},
skulk: {
2: {
difficulty: 2,
majorThreshold: 3,
severeThreshold: 8,
hp: 1,
stress: 1,
attack: 1
},
3: {
difficulty: 2,
majorThreshold: 8,
severeThreshold: 12,
hp: 1,
stress: 1,
attack: 1
},
4: {
difficulty: 2,
majorThreshold: 8,
severeThreshold: 10,
hp: 1,
stress: 1,
attack: 1
}
},
solo: {
2: {
difficulty: 2,
majorThreshold: 5,
severeThreshold: 10,
hp: 0,
stress: 1,
attack: 2
},
3: {
difficulty: 2,
majorThreshold: 7,
severeThreshold: 15,
hp: 2,
stress: 1,
attack: 2
},
4: {
difficulty: 2,
majorThreshold: 12,
severeThreshold: 25,
hp: 0,
stress: 1,
attack: 3
}
},
standard: {
2: {
difficulty: 2,
majorThreshold: 3,
severeThreshold: 8,
hp: 0,
stress: 0,
attack: 1
},
3: {
difficulty: 2,
majorThreshold: 7,
severeThreshold: 15,
hp: 1,
stress: 1,
attack: 1
},
4: {
difficulty: 2,
majorThreshold: 10,
severeThreshold: 15,
hp: 0,
stress: 1,
attack: 1
}
},
support: {
2: {
difficulty: 2,
majorThreshold: 3,
severeThreshold: 8,
hp: 1,
stress: 1,
attack: 1
},
3: {
difficulty: 2,
majorThreshold: 7,
severeThreshold: 12,
hp: 0,
stress: 0,
attack: 1
},
4: {
difficulty: 2,
majorThreshold: 8,
severeThreshold: 10,
hp: 1,
stress: 1,
attack: 1
}
}
};
/**
* Scaling data used for an adversary's damage.
* Tier 4 is missing certain adversary types and therefore skews upwards.
* We manually set tier 4 data to hopefully lead to better results
*/
export const adversaryExpectedDamage = {
basic: {
1: { mean: 7.321428571428571, deviation: 1.962519002770912 },
2: { mean: 12.444444444444445, deviation: 2.0631069425529676 },
3: { mean: 15.722222222222221, deviation: 2.486565208464823 },
4: { mean: 26, deviation: 5.2 }
},
minion: {
1: { mean: 2.142857142857143, deviation: 1.0690449676496976 },
2: { mean: 5, deviation: 0.816496580927726 },
3: { mean: 6.5, deviation: 2.1213203435596424 },
4: { mean: 11, deviation: 1 }
}
};

View file

@ -236,7 +236,6 @@ export const defaultRestOptions = {
actionType: 'action',
chatDisplay: false,
target: {
amount: 1,
type: 'friendly'
},
damage: {
@ -253,8 +252,7 @@ export const defaultRestOptions = {
]
}
}
},
effects: []
}
},
clearStress: {
id: 'clearStress',
@ -287,8 +285,7 @@ export const defaultRestOptions = {
]
}
}
},
effects: []
}
},
repairArmor: {
id: 'repairArmor',
@ -305,7 +302,6 @@ export const defaultRestOptions = {
actionType: 'action',
chatDisplay: false,
target: {
amount: 1,
type: 'friendly'
},
damage: {
@ -322,8 +318,7 @@ export const defaultRestOptions = {
]
}
}
},
effects: []
}
},
prepare: {
id: 'prepare',
@ -331,57 +326,7 @@ export const defaultRestOptions = {
icon: 'fa-solid fa-dumbbell',
img: 'icons/skills/trades/academics-merchant-scribe.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepare.description'),
actions: {
prepare: {
type: 'healing',
systemPath: 'restMoves.shortRest.moves.prepare.actions',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepare.name'),
img: 'icons/skills/trades/academics-merchant-scribe.webp',
actionType: 'action',
chatDisplay: false,
target: {
type: 'self'
},
damage: {
parts: [
{
applyTo: healingTypes.hope.id,
value: {
custom: {
enabled: true,
formula: '1'
}
}
}
]
}
},
prepareWithFriends: {
type: 'healing',
systemPath: 'restMoves.shortRest.moves.prepare.actions',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepareWithFriends.name'),
img: 'icons/skills/trades/academics-merchant-scribe.webp',
actionType: 'action',
chatDisplay: false,
target: {
type: 'self'
},
damage: {
parts: [
{
applyTo: healingTypes.hope.id,
value: {
custom: {
enabled: true,
formula: '2'
}
}
}
]
}
}
},
effects: []
actions: {}
}
}),
longRest: () => ({
@ -400,7 +345,6 @@ export const defaultRestOptions = {
actionType: 'action',
chatDisplay: false,
target: {
amount: 1,
type: 'friendly'
},
damage: {
@ -417,8 +361,7 @@ export const defaultRestOptions = {
]
}
}
},
effects: []
}
},
clearStress: {
id: 'clearStress',
@ -451,8 +394,7 @@ export const defaultRestOptions = {
]
}
}
},
effects: []
}
},
repairArmor: {
id: 'repairArmor',
@ -469,7 +411,6 @@ export const defaultRestOptions = {
actionType: 'action',
chatDisplay: false,
target: {
amount: 1,
type: 'friendly'
},
damage: {
@ -486,8 +427,7 @@ export const defaultRestOptions = {
]
}
}
},
effects: []
}
},
prepare: {
id: 'prepare',
@ -495,57 +435,7 @@ export const defaultRestOptions = {
icon: 'fa-solid fa-dumbbell',
img: 'icons/skills/trades/academics-merchant-scribe.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepare.description'),
actions: {
prepare: {
type: 'healing',
systemPath: 'restMoves.longRest.moves.prepare.actions',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepare.name'),
img: 'icons/skills/trades/academics-merchant-scribe.webp',
actionType: 'action',
chatDisplay: false,
target: {
type: 'self'
},
damage: {
parts: [
{
applyTo: healingTypes.hope.id,
value: {
custom: {
enabled: true,
formula: '1'
}
}
}
]
}
},
prepareWithFriends: {
type: 'healing',
systemPath: 'restMoves.longRest.moves.prepare.actions',
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepareWithFriends.name'),
img: 'icons/skills/trades/academics-merchant-scribe.webp',
actionType: 'action',
chatDisplay: false,
target: {
type: 'self'
},
damage: {
parts: [
{
applyTo: healingTypes.hope.id,
value: {
custom: {
enabled: true,
formula: '2'
}
}
}
]
}
}
},
effects: []
actions: {}
},
workOnAProject: {
id: 'workOnAProject',
@ -553,8 +443,7 @@ export const defaultRestOptions = {
icon: 'fa-solid fa-diagram-project',
img: 'icons/skills/social/thumbsup-approval-like.webp',
description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.workOnAProject.description'),
actions: {},
effects: []
actions: {}
}
})
};

View file

@ -30,7 +30,6 @@ export const gameSettings = {
LastMigrationVersion: 'LastMigrationVersion',
TagTeamRoll: 'TagTeamRoll',
SpotlightRequestQueue: 'SpotlightRequestQueue',
CompendiumBrowserSettings: 'CompendiumBrowserSettings'
};
export const actionAutomationChoices = {

View file

@ -3,7 +3,6 @@ export { default as DhCombatant } from './combatant.mjs';
export { default as DhTagTeamRoll } from './tagTeamRoll.mjs';
export { default as DhRollTable } from './rollTable.mjs';
export { default as RegisteredTriggers } from './registeredTriggers.mjs';
export { default as CompendiumBrowserSettings } from './compendiumBrowserSettings.mjs';
export * as countdowns from './countdowns.mjs';
export * as actions from './action/_module.mjs';

View file

@ -34,20 +34,6 @@ export default class DHAttackAction extends DHDamageAction {
};
}
get damageFormula() {
const hitPointsPart = this.damage.parts.find(x => x.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
if (!hitPointsPart) return '0';
return hitPointsPart.value.getFormula();
}
get altDamageFormula() {
const hitPointsPart = this.damage.parts.find(x => x.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
if (!hitPointsPart) return '0';
return hitPointsPart.valueAlt.getFormula();
}
async use(event, options) {
const result = await super.use(event, options);
if (!result.message) return;

View file

@ -114,24 +114,9 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
* Return Item the action is attached too.
*/
get item() {
if (!this.parent.parent && this.systemPath)
return foundry.utils.getProperty(this.parent, this.systemPath).get(this.id);
return this.parent.parent;
}
get applyEffects() {
if (this.item.systemPath) {
const itemEffectIds = this.item.effects.map(x => x._id);
const movePathSplit = this.item.systemPath.split('.');
movePathSplit.pop();
const move = foundry.utils.getProperty(this.parent, movePathSplit.join('.'));
return new Collection(itemEffectIds.map(id => [id, move.effects.find(x => x.id === id)]));
}
return this.item.effects;
}
/**
* Return the first Actor parent found.
*/
@ -140,7 +125,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
? this.item
: this.item?.parent instanceof DhpActor
? this.item.parent
: null;
: this.item?.actor;
}
static getRollType(parent) {
@ -229,7 +214,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return;
if (this.chatDisplay && !config.actionChatMessageHandled) await this.toChat();
if (this.chatDisplay) await this.toChat();
return config;
}
@ -240,13 +225,9 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
* @returns {object}
*/
prepareBaseConfig(event) {
const isActor = this.item instanceof CONFIG.Actor.documentClass;
const actionTitle = game.i18n.localize(this.name);
const itemTitle = isActor || this.item.name === actionTitle ? '' : `${this.item.name} - `;
const config = {
event,
title: `${itemTitle}${actionTitle}`,
title: `${this.item instanceof CONFIG.Actor.documentClass ? '' : `${this.item.name}: `}${game.i18n.localize(this.name)}`,
source: {
item: this.item._id,
originItem: this.originItem,

View file

@ -1,12 +1,9 @@
import DHAdversarySettings from '../../applications/sheets-configs/adversary-settings.mjs';
import { ActionField } from '../fields/actionField.mjs';
import { commonActorRules } from './base.mjs';
import DhCreature from './creature.mjs';
import BaseDataActor, { commonActorRules } from './base.mjs';
import { resourceField, bonusField } from '../fields/actorField.mjs';
import { calculateExpectedValue, parseTermsFromSimpleFormula } from '../../helpers/utils.mjs';
import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs';
export default class DhpAdversary extends DhCreature {
export default class DhpAdversary extends BaseDataActor {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Adversary'];
static get metadata() {
@ -43,14 +40,7 @@ export default class DhpAdversary extends DhCreature {
integer: true,
label: 'DAGGERHEART.GENERAL.hordeHp'
}),
criticalThreshold: new fields.NumberField({
required: true,
integer: true,
min: 1,
max: 20,
initial: 20,
label: 'DAGGERHEART.ACTIONS.Settings.criticalThreshold'
}),
criticalThreshold: new fields.NumberField({ required: true, integer: true, min: 1, max: 20, initial: 20 }),
damageThresholds: new fields.SchemaField({
major: new fields.NumberField({
required: true,
@ -190,10 +180,6 @@ export default class DhpAdversary extends DhCreature {
}
}
prepareDerivedData() {
this.attack.roll.isStandardAttack = true;
}
_getTags() {
const tags = [
game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${this.tier}`),
@ -202,211 +188,4 @@ export default class DhpAdversary extends DhCreature {
];
return tags;
}
/** Returns source data for this actor adjusted to a new tier, which can be used to create a new actor. */
adjustForTier(tier) {
const source = this.parent.toObject(true);
/** @type {(2 | 3 | 4)[]} */
const tiers = new Array(Math.abs(tier - this.tier))
.fill(0)
.map((_, idx) => idx + Math.min(tier, this.tier) + 1);
if (tier < this.tier) tiers.reverse();
const typeData = adversaryScalingData[source.system.type] ?? adversaryScalingData[source.system.standard];
const tierEntries = tiers.map(t => ({ tier: t, ...typeData[t] }));
// Apply simple tier changes
const scale = tier > this.tier ? 1 : -1;
for (const entry of tierEntries) {
source.system.difficulty += scale * entry.difficulty;
source.system.damageThresholds.major += scale * entry.majorThreshold;
source.system.damageThresholds.severe += scale * entry.severeThreshold;
source.system.resources.hitPoints.max += scale * entry.hp;
source.system.resources.stress.max += scale * entry.stress;
source.system.attack.roll.bonus += scale * entry.attack;
}
// Get the mean and standard deviation of expected damage in the previous and new tier
// The data we have is for attack scaling, but we reuse this for action scaling later
const expectedDamageData = adversaryExpectedDamage[source.system.type] ?? adversaryExpectedDamage.basic;
const damageMeta = {
currentDamageRange: { tier: source.system.tier, ...expectedDamageData[source.system.tier] },
newDamageRange: { tier, ...expectedDamageData[tier] },
type: 'attack'
};
// Update damage of base attack
try {
this.#adjustActionDamage(source.system.attack, damageMeta);
} catch (err) {
ui.notifications.warn('Failed to convert attack damage of adversary');
console.error(err);
}
// Update damage of each item action, making sure to also update the description if possible
const damageRegex = /@Damage\[([^\[\]]*)\]({[^}]*})?/g;
for (const item of source.items) {
// Replace damage inlines with new formulas
for (const withDescription of [item.system, ...Object.values(item.system.actions)]) {
withDescription.description = withDescription.description.replace(damageRegex, (match, inner) => {
const { value: formula } = parseInlineParams(inner);
if (!formula || !type) return match;
try {
const adjusted = this.#calculateAdjustedDamage(formula, { ...damageMeta, type: 'action' });
const newFormula = [
adjusted.diceQuantity ? `${adjusted.diceQuantity}d${adjusted.faces}` : null,
adjusted.bonus
]
.filter(p => !!p)
.join('+');
return match.replace(formula, newFormula);
} catch {
return match;
}
});
}
// Update damage in item actions
for (const action of Object.values(item.system.actions)) {
if (!action.damage) continue;
// Parse damage, and convert all formula matches in the descriptions to the new damage
try {
const result = this.#adjustActionDamage(action, { ...damageMeta, type: 'action' });
for (const { previousFormula, formula } of Object.values(result)) {
const oldFormulaRegexp = new RegExp(
previousFormula.replace(' ', '').replace('+', '(?:\\s)?\\+(?:\\s)?')
);
item.system.description = item.system.description.replace(oldFormulaRegexp, formula);
action.description = action.description.replace(oldFormulaRegexp, formula);
}
} catch (err) {
ui.notifications.warn(`Failed to convert action damage for item ${item.name}`);
console.error(err);
}
}
}
// Finally set the tier of the source data, now that everything is complete
source.system.tier = tier;
return source;
}
/**
* Converts a damage object to a new damage range
* @returns {{ diceQuantity: number; faces: number; bonus: number }} the adjusted result as a combined term
* @throws error if the formula is the wrong type
*/
#calculateAdjustedDamage(formula, { currentDamageRange, newDamageRange, type }) {
const terms = parseTermsFromSimpleFormula(formula);
const flatTerms = terms.filter(t => t.diceQuantity === 0);
const diceTerms = terms.filter(t => t.diceQuantity > 0);
if (flatTerms.length > 1 || diceTerms.length > 1) {
throw new Error('invalid formula for conversion');
}
const value = {
...(diceTerms[0] ?? { diceQuantity: 0, faces: 1 }),
bonus: flatTerms[0]?.bonus ?? 0
};
const previousExpected = calculateExpectedValue(value);
if (previousExpected === 0) return value; // nothing to do
const dieSizes = [4, 6, 8, 10, 12, 20];
const steps = newDamageRange.tier - currentDamageRange.tier;
const increasing = steps > 0;
const deviation = (previousExpected - currentDamageRange.mean) / currentDamageRange.deviation;
const expected = Math.max(1, newDamageRange.mean + newDamageRange.deviation * deviation);
// If this was just a flat number, convert to the expected damage and exit
if (value.diceQuantity === 0) {
value.bonus = Math.round(expected);
return value;
}
const getExpectedDie = () => calculateExpectedValue({ diceQuantity: 1, faces: value.faces }) || 1;
const getBaseAverage = () => calculateExpectedValue({ ...value, bonus: 0 });
// Check the number of base overages over the expected die. In the end, if the bonus inflates too much, we add a die
const baseOverages = Math.floor(value.bonus / getExpectedDie());
// Prestep. Change number of dice for attacks, bump up/down for actions
// We never bump up to d20, though we might bump down from it
if (type === 'attack') {
const minimum = increasing ? value.diceQuantity : 0;
value.diceQuantity = Math.max(minimum, newDamageRange.tier);
} else {
const currentIdx = dieSizes.indexOf(value.faces);
value.faces = dieSizes[Math.clamp(currentIdx + steps, 0, 4)];
}
value.bonus = Math.round(expected - getBaseAverage());
// Attempt to handle negative values.
// If we can do it with only step downs, do so. Otherwise remove tier dice, and try again
if (value.bonus < 0) {
let stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity);
const currentIdx = dieSizes.indexOf(value.faces);
// If step downs alone don't suffice, change the flat modifier, then calculate steps required again
// If this isn't sufficient, the result will be slightly off. This is unlikely to happen
if (type !== 'attack' && stepsRequired > currentIdx && value.diceQuantity > 0) {
value.diceQuantity -= increasing ? 1 : Math.abs(steps);
value.bonus = Math.round(expected - getBaseAverage());
if (value.bonus >= 0) return value; // complete
}
stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity);
value.faces = dieSizes[Math.max(0, currentIdx - stepsRequired)];
value.bonus = Math.max(0, Math.round(expected - getBaseAverage()));
}
// If value is really high, we add a number of dice based on the number of overages
// This attempts to preserve a similar amount of variance when increasing an action
const overagesToRemove = Math.floor(value.bonus / getExpectedDie()) - baseOverages;
if (type !== 'attack' && increasing && overagesToRemove > 0) {
value.diceQuantity += overagesToRemove;
value.bonus = Math.round(expected - getBaseAverage());
}
return value;
}
/**
* Updates damage to reflect a specific value.
* @throws if damage structure is invalid for conversion
* @returns the converted formula and value as a simplified term
*/
#adjustActionDamage(action, damageMeta) {
// The current algorithm only returns a value if there is a single damage part
const hpDamageParts = action.damage.parts.filter(d => d.applyTo === 'hitPoints');
if (hpDamageParts.length !== 1) throw new Error('incorrect number of hp parts');
const result = {};
for (const property of ['value', 'valueAlt']) {
const data = hpDamageParts[0][property];
const previousFormula = data.custom.enabled
? data.custom.formula
: [data.flatMultiplier ? `${data.flatMultiplier}${data.dice}` : 0, data.bonus ?? 0]
.filter(p => !!p)
.join('+');
const value = this.#calculateAdjustedDamage(previousFormula, damageMeta);
const formula = [value.diceQuantity ? `${value.diceQuantity}d${value.faces}` : null, value.bonus]
.filter(p => !!p)
.join('+');
if (value.diceQuantity) {
data.custom.enabled = false;
data.bonus = value.bonus;
data.dice = `d${value.faces}`;
data.flatMultiplier = value.diceQuantity;
} else if (!value.diceQuantity) {
data.custom.enabled = true;
data.custom.formula = formula;
}
result[property] = { previousFormula, formula, value };
}
return result;
}
}

View file

@ -29,40 +29,17 @@ const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) =>
/* Common rules applying to Characters and Adversaries */
export const commonActorRules = (extendedData = { damageReduction: {}, attack: { damage: {} } }) => ({
conditionImmunities: new fields.SchemaField({
hidden: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.hidden'
}),
restrained: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.restrained'
}),
vulnerable: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.vulnerable'
})
hidden: new fields.BooleanField({ initial: false }),
restrained: new fields.BooleanField({ initial: false }),
vulnerable: new fields.BooleanField({ initial: false })
}),
damageReduction: new fields.SchemaField({
thresholdImmunities: new fields.SchemaField({
minor: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.thresholdImmunities.minor.label',
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.thresholdImmunities.minor.hint'
})
minor: new fields.BooleanField({ initial: false })
}),
reduceSeverity: new fields.SchemaField({
magical: new fields.NumberField({
initial: 0,
min: 0,
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.reduceSeverity.magical.label',
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.reduceSeverity.magical.hint'
}),
physical: new fields.NumberField({
initial: 0,
min: 0,
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.reduceSeverity.physical.label',
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.reduceSeverity.physical.hint'
})
magical: new fields.NumberField({ initial: 0, min: 0 }),
physical: new fields.NumberField({ initial: 0, min: 0 })
}),
...(extendedData.damageReduction ?? {})
}),
@ -72,16 +49,12 @@ export const commonActorRules = (extendedData = { damageReduction: {}, attack: {
hpDamageMultiplier: new fields.NumberField({
required: true,
nullable: false,
initial: 1,
label: 'DAGGERHEART.GENERAL.Attack.hpDamageMultiplier.label',
hint: 'DAGGERHEART.GENERAL.Attack.hpDamageMultiplier.hint'
initial: 1
}),
hpDamageTakenMultiplier: new fields.NumberField({
required: true,
nullable: false,
initial: 1,
label: 'DAGGERHEART.GENERAL.Attack.hpDamageTakenMultiplier.label',
hint: 'DAGGERHEART.GENERAL.Attack.hpDamageTakenMultiplier.hint'
initial: 1
}),
...(extendedData.attack?.damage ?? {})
})

View file

@ -1,14 +1,13 @@
import { burden } from '../../config/generalConfig.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import DhLevelData from '../levelData.mjs';
import { commonActorRules } from './base.mjs';
import DhCreature from './creature.mjs';
import BaseDataActor, { commonActorRules } from './base.mjs';
import { attributeField, resourceField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs';
import { ActionField } from '../fields/actionField.mjs';
import DHCharacterSettings from '../../applications/sheets-configs/character-settings.mjs';
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
export default class DhCharacter extends DhCreature {
export default class DhCharacter extends BaseDataActor {
/**@override */
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Character'];
@ -37,18 +36,14 @@ export default class DhCharacter extends DhCreature {
'DAGGERHEART.ACTORS.Character.maxHPBonus'
),
stress: resourceField(6, 0, 'DAGGERHEART.GENERAL.stress', true),
hope: new fields.SchemaField(
{
value: new fields.NumberField({
initial: 2,
min: 0,
integer: true,
label: 'DAGGERHEART.GENERAL.hope'
}),
isReversed: new fields.BooleanField({ initial: false })
},
{ label: 'DAGGERHEART.GENERAL.hope' }
)
hope: new fields.SchemaField({
value: new fields.NumberField({
initial: 2,
min: 0,
integer: true,
label: 'DAGGERHEART.GENERAL.hope'
})
})
}),
traits: new fields.SchemaField({
agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'),
@ -133,6 +128,14 @@ export default class DhCharacter extends DhCreature {
}
}
}),
advantageSources: new fields.ArrayField(new fields.StringField(), {
label: 'DAGGERHEART.ACTORS.Character.advantageSources.label',
hint: 'DAGGERHEART.ACTORS.Character.advantageSources.hint'
}),
disadvantageSources: new fields.ArrayField(new fields.StringField(), {
label: 'DAGGERHEART.ACTORS.Character.disadvantageSources.label',
hint: 'DAGGERHEART.ACTORS.Character.disadvantageSources.hint'
}),
levelData: new fields.EmbeddedDataField(DhLevelData),
bonuses: new fields.SchemaField({
roll: new fields.SchemaField({
@ -219,16 +222,8 @@ export default class DhCharacter extends DhCreature {
rules: new fields.SchemaField({
...commonActorRules({
damageReduction: {
magical: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.magical.label',
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.magical.hint'
}),
physical: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.physical.label',
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.physical.hint'
}),
magical: new fields.BooleanField({ initial: false }),
physical: new fields.BooleanField({ initial: false }),
maxArmorMarked: new fields.SchemaField({
value: new fields.NumberField({
required: true,
@ -258,10 +253,7 @@ export default class DhCharacter extends DhCreature {
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.label',
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint'
}),
disabledArmor: new fields.BooleanField({
intial: false,
label: 'DAGGERHEART.GENERAL.Rules.damageReduction.disabledArmor.label'
})
disabledArmor: new fields.BooleanField({ intial: false })
},
attack: {
damage: {
@ -309,14 +301,12 @@ export default class DhCharacter extends DhCreature {
label: 'DAGGERHEART.ACTORS.Character.defaultFearDice'
})
}),
runeWard: new fields.BooleanField({ initial: false }),
burden: new fields.SchemaField({
ignore: new fields.BooleanField({ label: 'DAGGERHEART.ACTORS.Character.burden.ignore.label' })
ignore: new fields.BooleanField()
}),
roll: new fields.SchemaField({
guaranteedCritical: new fields.BooleanField({
label: 'DAGGERHEART.ACTORS.Character.roll.guaranteedCritical.label',
hint: 'DAGGERHEART.ACTORS.Character.roll.guaranteedCritical.hint'
})
guaranteedCritical: new fields.BooleanField()
})
}),
sidebarFavorites: new ForeignDocumentUUIDArrayField({ type: 'Item' })
@ -650,7 +640,7 @@ export default class DhCharacter extends DhCreature {
};
const globalHopeMax = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxHope;
this.resources.hope.max = globalHopeMax;
this.resources.hope.max = globalHopeMax - this.scars;
this.resources.hitPoints.max += this.class.value?.system?.hitPoints ?? 0;
/* Companion Related Data */
@ -674,7 +664,6 @@ export default class DhCharacter extends DhCreature {
}
}
this.resources.hope.max -= this.scars;
this.resources.hope.value = Math.min(baseHope, this.resources.hope.max);
this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait;

View file

@ -1,4 +1,4 @@
import DhCreature from './creature.mjs';
import BaseDataActor from './base.mjs';
import DhLevelData from '../levelData.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import { ActionField } from '../fields/actionField.mjs';
@ -6,7 +6,7 @@ import { adjustDice, adjustRange } from '../../helpers/utils.mjs';
import DHCompanionSettings from '../../applications/sheets-configs/companion-settings.mjs';
import { resourceField, bonusField } from '../fields/actorField.mjs';
export default class DhCompanion extends DhCreature {
export default class DhCompanion extends BaseDataActor {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Companion'];
/**@inheritdoc */
@ -53,18 +53,9 @@ export default class DhCompanion extends DhCreature {
),
rules: new fields.SchemaField({
conditionImmunities: new fields.SchemaField({
hidden: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.hidden'
}),
restrained: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.restrained'
}),
vulnerable: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.vulnerable'
})
hidden: new fields.BooleanField({ initial: false }),
restrained: new fields.BooleanField({ initial: false }),
vulnerable: new fields.BooleanField({ initial: false })
})
}),
attack: new ActionField({

View file

@ -1,20 +0,0 @@
import BaseDataActor from './base.mjs';
export default class DhCreature extends BaseDataActor {
/**@inheritdoc */
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
advantageSources: new fields.ArrayField(new fields.StringField(), {
label: 'DAGGERHEART.ACTORS.Character.advantageSources.label',
hint: 'DAGGERHEART.ACTORS.Character.advantageSources.hint'
}),
disadvantageSources: new fields.ArrayField(new fields.StringField(), {
label: 'DAGGERHEART.ACTORS.Character.disadvantageSources.label',
hint: 'DAGGERHEART.ACTORS.Character.disadvantageSources.hint'
})
};
}
}

View file

@ -31,7 +31,6 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
static defineSchema() {
return {
title: new fields.StringField(),
actionDescription: new fields.HTMLField(),
roll: new fields.ObjectField(),
targets: targetsField(),
hasRoll: new fields.BooleanField({ initial: false }),

View file

@ -1,36 +0,0 @@
export default class CompendiumBrowserSettings extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
excludedSources: new fields.TypedObjectField(
new fields.SchemaField({
excludedDocumentTypes: new fields.ArrayField(
new fields.StringField({ required: true, choices: CONST.SYSTEM_SPECIFIC_COMPENDIUM_TYPES })
)
})
),
excludedPacks: new fields.TypedObjectField(
new fields.SchemaField({
excludedDocumentTypes: new fields.ArrayField(
new fields.StringField({ required: true, choices: CONST.SYSTEM_SPECIFIC_COMPENDIUM_TYPES })
)
})
)
};
}
isEntryExcluded(item) {
const pack = game.packs.get(item.pack);
if (!pack) return false;
const packageName = pack.metadata.packageType === 'world' ? 'world' : pack.metadata.packageName;
const excludedSourceData = this.excludedSources[packageName];
if (excludedSourceData && excludedSourceData.excludedDocumentTypes.includes(pack.metadata.type)) return true;
const excludedPackData = this.excludedPacks[item.pack];
if (excludedPackData && excludedPackData.excludedDocumentTypes.includes(pack.metadata.type)) return true;
return false;
}
}

View file

@ -68,8 +68,6 @@ export default class DamageField extends fields.SchemaField {
const damageResult = await CONFIG.Dice.daggerheart.DamageRoll.build(damageConfig);
if (!damageResult) return false;
if (damageResult.actionChatMessageHandled) config.actionChatMessageHandled = true;
config.damage = damageResult.damage;
config.message ??= damageConfig.message;
}
@ -109,8 +107,8 @@ export default class DamageField extends fields.SchemaField {
);
else {
const configDamage = foundry.utils.deepClone(config.damage);
const hpDamageMultiplier = config.actionActor?.system.rules?.attack?.damage?.hpDamageMultiplier ?? 1;
const hpDamageTakenMultiplier = actor.system.rules?.attack?.damage?.hpDamageTakenMultiplier;
const hpDamageMultiplier = config.actionActor?.system.rules.attack.damage.hpDamageMultiplier ?? 1;
const hpDamageTakenMultiplier = actor.system.rules.attack.damage.hpDamageTakenMultiplier;
if (configDamage.hitPoints) {
for (const part of configDamage.hitPoints.parts) {
part.total = Math.ceil(part.total * hpDamageMultiplier * hpDamageTakenMultiplier);
@ -165,8 +163,7 @@ export default class DamageField extends fields.SchemaField {
if (data.hasRoll && part.resultBased && data.roll.result.duality === -1) return part.valueAlt;
const isAdversary = this.actor.type === 'adversary';
const isHorde = this.actor.system.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id;
if (isAdversary && isHorde && this.roll?.isStandardAttack) {
if (isAdversary && this.actor.system.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id) {
const hasHordeDamage = this.actor.effects.find(x => x.type === 'horde');
if (hasHordeDamage && !hasHordeDamage.disabled) return part.valueAlt;
}

View file

@ -73,7 +73,7 @@ export default class EffectsField extends fields.ArrayField {
});
effects.forEach(async e => {
const effect = (this.item.applyEffects ?? this.item.effects).get(e._id);
const effect = this.item.effects.get(e._id);
if (!token.actor || !effect) return;
await EffectsField.applyEffect(effect, token.actor);
});
@ -96,7 +96,7 @@ export default class EffectsField extends fields.ArrayField {
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/effectSummary.hbs',
{
effects: this.effects.map(e => (this.item.applyEffects ?? this.item.effects).get(e._id)),
effects: this.effects.map(e => this.item.effects.get(e._id)),
targets: messageTargets
}
)
@ -123,7 +123,7 @@ export default class EffectsField extends fields.ArrayField {
// Otherwise, create a new effect on the target
const effectData = foundry.utils.mergeObject({
...(effect.toObject?.() ?? effect),
...effect.toObject(),
disabled: false,
transfer: false,
origin: effect.uuid

View file

@ -152,9 +152,7 @@ export function ActionMixin(Base) {
}
get uuid() {
const isItem = this.item instanceof game.system.api.documents.DHItem;
const isActor = this.item instanceof game.system.api.documents.DhpActor;
return isItem || isActor ? `${this.item.uuid}.${this.documentName}.${this.id}` : null;
return `${this.item.uuid}.${this.documentName}.${this.id}`;
}
get sheet() {
@ -262,9 +260,6 @@ export function ActionMixin(Base) {
}
async toChat(origin) {
const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
.expandRollMessage?.desc;
const cls = getDocumentClass('ChatMessage');
const systemData = {
title: game.i18n.localize('DAGGERHEART.CONFIG.FeatureForm.action'),
@ -293,7 +288,7 @@ export function ActionMixin(Base) {
system: systemData,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/action.hbs',
{ ...systemData, open: autoExpandDescription ? 'open' : '' }
systemData
),
flags: {
daggerheart: {

View file

@ -7,20 +7,16 @@ const attributeField = label =>
});
const resourceField = (max = 0, initial = 0, label, reverse = false, maxLabel) =>
new fields.SchemaField(
{
value: new fields.NumberField({ initial: initial, min: 0, integer: true, label }),
max: new fields.NumberField({
initial: max,
integer: true,
label:
maxLabel ??
game.i18n.format('DAGGERHEART.GENERAL.maxWithThing', { thing: game.i18n.localize(label) })
}),
isReversed: new fields.BooleanField({ initial: reverse })
},
{ label }
);
new fields.SchemaField({
value: new fields.NumberField({ initial: initial, min: 0, integer: true, label }),
max: new fields.NumberField({
initial: max,
integer: true,
label:
maxLabel ?? game.i18n.format('DAGGERHEART.GENERAL.maxWithThing', { thing: game.i18n.localize(label) })
}),
isReversed: new fields.BooleanField({ initial: reverse })
});
const stressDamageReductionRule = localizationPath =>
new fields.SchemaField({

View file

@ -1,6 +1,5 @@
import BaseDataItem from './base.mjs';
import ItemLinkFields from '../../data/fields/itemLinkFields.mjs';
import { getFeaturesHTMLData } from '../../helpers/utils.mjs';
export default class DHAncestry extends BaseDataItem {
/** @inheritDoc */
@ -20,6 +19,7 @@ export default class DHAncestry extends BaseDataItem {
};
}
/* -------------------------------------------- */
/**@override */
@ -42,18 +42,4 @@ export default class DHAncestry extends BaseDataItem {
get secondaryFeature() {
return this.features.find(x => x.type === CONFIG.DH.ITEM.featureSubTypes.secondary)?.item;
}
/**@inheritdoc */
async getDescriptionData() {
const baseDescription = this.description;
const features = await getFeaturesHTMLData(this.features);
if (!features.length) return { prefix: null, value: baseDescription, suffix: null };
const suffix = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/items/description.hbs',
{ label: 'DAGGERHEART.ITEMS.Ancestry.featuresLabel', features }
);
return { prefix: null, value: baseDescription, suffix };
}
}

View file

@ -59,10 +59,11 @@ export default class DHArmor extends AttachableItem {
const baseDescription = this.description;
const allFeatures = CONFIG.DH.ITEM.allArmorFeatures();
const features = this.armorFeatures.map(x => allFeatures[x.value]);
if (!features.length) return { prefix: null, value: baseDescription, suffix: null };
const prefix = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/items/armor/description.hbs',
{ item: this.parent, features }
{ features }
);
return { prefix, value: baseDescription, suffix: null };

View file

@ -253,20 +253,4 @@ export default class DHBeastform extends BaseDataItem {
return false;
}
_onCreate(_data, _options, userId) {
if (!this.actor && game.user.id === userId) {
const hasBeastformEffect = this.parent.effects.some(x => x.type === 'beastform');
if (!hasBeastformEffect)
this.parent.createEmbeddedDocuments('ActiveEffect', [
{
type: 'beastform',
name: game.i18n.localize('DAGGERHEART.ITEMS.Beastform.beastformEffect'),
img: 'icons/creatures/abilities/paw-print-pair-purple.webp'
}
]);
return;
}
}
}

View file

@ -2,7 +2,7 @@ import BaseDataItem from './base.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
import ItemLinkFields from '../fields/itemLinkFields.mjs';
import { addLinkedItemsDiff, getFeaturesHTMLData, updateLinkedItemApps } from '../../helpers/utils.mjs';
import { addLinkedItemsDiff, updateLinkedItemApps } from '../../helpers/utils.mjs';
export default class DHClass extends BaseDataItem {
/** @inheritDoc */
@ -163,56 +163,4 @@ export default class DHClass extends BaseDataItem {
updateLinkedItemApps(options, this.parent.sheet);
}
/**@inheritdoc */
async getDescriptionData() {
const baseDescription = this.description;
const getDomainLabel = domain => {
const data = CONFIG.DH.DOMAIN.allDomains()[domain];
return data ? game.i18n.localize(data.label) : '';
};
let domainsLabel = '';
if (this.domains.length) {
if (this.domains.length === 1) domainsLabel = getDomainLabel(this.domains[0]);
else {
const firstDomains = this.domains
.slice(0, this.domains.length - 1)
.map(getDomainLabel)
.join(', ');
const lastDomain = getDomainLabel(this.domains[this.domains.length - 1]);
domainsLabel = game.i18n.format('DAGGERHEART.GENERAL.thingsAndThing', {
things: firstDomains,
thing: lastDomain
});
}
}
const classItems = [];
for (const itemData of this.inventory.choiceB) {
const linkData = [
undefined,
'UUID', // type
itemData.uuid // target
];
const contentLink = await foundry.applications.ux.TextEditor.implementation._createContentLink(linkData);
classItems.push(contentLink.outerHTML);
}
const hopeFeatures = await getFeaturesHTMLData(this.hopeFeatures);
const classFeatures = await getFeaturesHTMLData(this.classFeatures);
const suffix = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/items/class/description.hbs',
{
class: this.parent,
domains: domainsLabel,
classItems,
hopeFeatures,
classFeatures
}
);
return { prefix: null, value: baseDescription, suffix };
}
}

View file

@ -1,4 +1,3 @@
import { getFeaturesHTMLData } from '../../helpers/utils.mjs';
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
import BaseDataItem from './base.mjs';
@ -25,17 +24,4 @@ export default class DHCommunity extends BaseDataItem {
/**@override */
static DEFAULT_ICON = 'systems/daggerheart/assets/icons/documents/items/village.svg';
/**@inheritdoc */
async getDescriptionData() {
const baseDescription = this.description;
const features = await getFeaturesHTMLData(this.features);
if (!features.length) return { prefix: null, value: baseDescription, suffix: null };
const suffix = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/items/description.hbs',
{ label: 'DAGGERHEART.ITEMS.Community.featuresLabel', features }
);
return { prefix: null, value: baseDescription, suffix };
}
}

View file

@ -94,10 +94,8 @@ export default class DHDomainCard extends BaseDataItem {
return false;
}
if (!this.actor.system.loadoutSlot.available && !this.loadoutIgnore) {
if (!this.actor.system.loadoutSlot.available) {
data.system.inVault = true;
await this.updateSource({ inVault: true });
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.loadoutMaxReached'));
}
}
}

View file

@ -1,4 +1,3 @@
import { getFeaturesHTMLData } from '../../helpers/utils.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import ItemLinkFields from '../fields/itemLinkFields.mjs';
import BaseDataItem from './base.mjs';
@ -90,28 +89,4 @@ export default class DHSubclass extends BaseDataItem {
const allowed = await super._preCreate(data, options, user);
if (allowed === false) return;
}
/**@inheritdoc */
async getDescriptionData() {
const baseDescription = this.description;
const spellcastTrait = this.spellcastingTrait
? game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.spellcastingTrait].label)
: null;
const foundationFeatures = await getFeaturesHTMLData(this.foundationFeatures);
const specializationFeatures = await getFeaturesHTMLData(this.specializationFeatures);
const masteryFeatures = await getFeaturesHTMLData(this.masteryFeatures);
const suffix = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/items/subclass/description.hbs',
{
spellcastTrait,
foundationFeatures,
specializationFeatures,
masteryFeatures
}
);
return { prefix: null, value: baseDescription, suffix };
}
}

View file

@ -113,26 +113,13 @@ export default class DHWeapon extends AttachableItem {
/**@inheritdoc */
async getDescriptionData() {
const baseDescription = this.description;
const tier = game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${this.tier}`);
const trait = game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.attack.roll.trait].label);
const range = game.i18n.localize(`DAGGERHEART.CONFIG.Range.${this.attack.range}.name`);
const damage = Roll.replaceFormulaData(this.attack.damageFormula, this.parent.parent ?? this.parent);
const burden = game.i18n.localize(CONFIG.DH.GENERAL.burden[this.burden].label);
const allFeatures = CONFIG.DH.ITEM.allWeaponFeatures();
const features = this.weaponFeatures.map(x => allFeatures[x.value]);
if (!features.length) return { prefix: null, value: baseDescription, suffix: null };
const prefix = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/items/weapon/description.hbs',
{
features,
tier,
trait,
range,
damage,
burden
}
{ features }
);
return { prefix, value: baseDescription, suffix: null };

View file

@ -6,12 +6,7 @@ export default class DhLevelData extends foundry.abstract.DataModel {
return {
level: new fields.SchemaField({
current: new fields.NumberField({
required: true,
integer: true,
initial: 1,
label: 'DAGGERHEART.GENERAL.currentLevel'
}),
current: new fields.NumberField({ required: true, integer: true, initial: 1 }),
changed: new fields.NumberField({ required: true, integer: true, initial: 1 }),
bonuses: new fields.TypedObjectField(new fields.NumberField({ integer: true, nullable: false }))
}),

View file

@ -37,30 +37,11 @@ export default class DhAppearance extends foundry.abstract.DataModel {
extendEnvironmentDescriptions: new BooleanField(),
extendItemDescriptions: new BooleanField(),
expandRollMessage: new SchemaField({
desc: new BooleanField({ initial: true }),
desc: new BooleanField(),
roll: new BooleanField(),
damage: new BooleanField(),
target: new BooleanField()
}),
showTokenDistance: new StringField({
required: true,
choices: {
always: {
value: 'always',
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.showTokenDistance.choices.always'
},
encounters: {
value: 'encounters',
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.showTokenDistance.choices.encounters'
},
never: {
value: 'never',
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.showTokenDistance.choices.never'
}
},
nullable: false,
initial: 'always'
}),
hideAttribution: new BooleanField(),
showGenericStatusEffects: new BooleanField({ initial: true })
};

View file

@ -55,23 +55,6 @@ export default class DhAutomation extends foundry.abstract.DataModel {
initial: true,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.resourceScrollTexts.label'
}),
deathMoveAutomation: new fields.SchemaField({
avoidDeath: new fields.BooleanField({
required: true,
initial: true,
label: 'DAGGERHEART.CONFIG.DeathMoves.avoidDeath.name'
}),
riskItAll: new fields.BooleanField({
required: true,
initial: true,
label: 'DAGGERHEART.CONFIG.DeathMoves.riskItAll.name'
}),
blazeOfGlory: new fields.BooleanField({
required: true,
initial: true,
label: 'DAGGERHEART.CONFIG.DeathMoves.blazeOfGlory.name'
})
}),
defeated: new fields.SchemaField({
enabled: new fields.BooleanField({
required: true,

View file

@ -12,20 +12,6 @@ const currencyField = (initial, label, icon) =>
icon: new foundry.data.fields.StringField({ required: true, nullable: false, blank: true, initial: icon })
});
const restMoveField = () =>
new foundry.data.fields.SchemaField({
name: new foundry.data.fields.StringField({ required: true }),
icon: new foundry.data.fields.StringField({ required: true }),
img: new foundry.data.fields.FilePathField({
initial: 'icons/magic/life/cross-worn-green.webp',
categories: ['IMAGE'],
base64: false
}),
description: new foundry.data.fields.HTMLField(),
actions: new ActionsField(),
effects: new foundry.data.fields.ArrayField(new foundry.data.fields.ObjectField())
});
export default class DhHomebrew extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
@ -119,11 +105,37 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
restMoves: new fields.SchemaField({
longRest: new fields.SchemaField({
nrChoices: new fields.NumberField({ required: true, integer: true, min: 1, initial: 2 }),
moves: new fields.TypedObjectField(restMoveField(), { initial: defaultRestOptions.longRest() })
moves: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({ required: true }),
icon: new fields.StringField({ required: true }),
img: new fields.FilePathField({
initial: 'icons/magic/life/cross-worn-green.webp',
categories: ['IMAGE'],
base64: false
}),
description: new fields.HTMLField(),
actions: new ActionsField()
}),
{ initial: defaultRestOptions.longRest() }
)
}),
shortRest: new fields.SchemaField({
nrChoices: new fields.NumberField({ required: true, integer: true, min: 1, initial: 2 }),
moves: new fields.TypedObjectField(restMoveField(), { initial: defaultRestOptions.shortRest() })
moves: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({ required: true }),
icon: new fields.StringField({ required: true }),
img: new fields.FilePathField({
initial: 'icons/magic/life/cross-worn-green.webp',
categories: ['IMAGE'],
base64: false
}),
description: new fields.HTMLField(),
actions: new ActionsField()
}),
{ initial: defaultRestOptions.shortRest() }
)
})
}),
domains: new fields.TypedObjectField(

View file

@ -96,19 +96,6 @@ export default class DHRoll extends Roll {
}
static async toMessage(roll, config) {
const item = config.data.parent?.items?.get?.(config.source.item) ?? null;
const action = item ? item.system.actions.get(config.source.action) : null;
let actionDescription = null;
if (action?.chatDisplay) {
actionDescription = action
? await foundry.applications.ux.TextEditor.implementation.enrichHTML(action.description, {
relativeTo: config.data,
rollData: config.data.getRollData?.() ?? {}
})
: null;
config.actionChatMessageHandled = true;
}
const cls = getDocumentClass('ChatMessage'),
msgData = {
type: this.messageType,
@ -116,7 +103,7 @@ export default class DHRoll extends Roll {
title: roll.title,
speaker: cls.getSpeaker({ actor: roll.data?.parent }),
sound: config.mute ? null : CONFIG.sounds.dice,
system: { ...config, actionDescription },
system: config,
rolls: [roll]
};

View file

@ -409,9 +409,7 @@ export default class DualityRoll extends D20Roll {
difficulty: message.system.roll.difficulty ? Number(message.system.roll.difficulty) : null
}
});
const extraIndex = newRoll.advantage ? 3 : 2;
newRoll.extra = newRoll.extra.slice(extraIndex);
newRoll.extra = newRoll.extra.slice(2);
const tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);

View file

@ -61,15 +61,14 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
update.img = 'icons/magic/life/heart-cross-blue.webp';
}
const statuses = Object.keys(data.statuses ?? {});
const immuneStatuses =
statuses.filter(
data.statuses?.filter(
status =>
this.parent.system.rules?.conditionImmunities &&
this.parent.system.rules.conditionImmunities[status]
) ?? [];
if (immuneStatuses.length > 0) {
update.statuses = statuses.filter(x => !immuneStatuses.includes(x));
update.statuses = data.statuses.filter(x => !immuneStatuses.includes(x));
const conditions = CONFIG.DH.GENERAL.conditions();
const scrollingTexts = immuneStatuses.map(status => ({
text: game.i18n.format('DAGGERHEART.ACTIVEEFFECT.immuneStatusText', {
@ -114,11 +113,6 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
super.applyField(model, change, field);
}
_applyLegacy(actor, change, changes) {
change.value = DhActiveEffect.getChangeValue(actor, change, change.effect);
super._applyLegacy(actor, change, changes);
}
/** */
static getChangeValue(model, change, effect) {
let value = change.value;

View file

@ -110,8 +110,6 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
} else if (s.classList.contains('damage-section'))
s.classList.toggle('expanded', autoExpandRoll.damage);
else if (s.classList.contains('target-section')) s.classList.toggle('expanded', autoExpandRoll.target);
else if (s.classList.contains('description-section'))
s.classList.toggle('expanded', autoExpandRoll.desc);
});
if (itemDesc && autoExpandRoll.desc) itemDesc.setAttribute('open', '');
}

View file

@ -185,10 +185,7 @@ export default class DHItem extends foundry.documents.Item {
tags: this._getTags()
},
actions: item.system.actionsList,
description: await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.system.description, {
relativeTo: this.parent,
rollData: this.parent?.getRollData() ?? {}
})
description: this.system.description
};
const msg = {

View file

@ -1,30 +1,78 @@
export default class DHToken extends CONFIG.Token.documentClass {
/**@inheritdoc */
static getTrackedAttributeChoices(attributes, typeKey) {
/**
* 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 actorModel = typeKey ? game.system.api.data.actors[`Dh${typeKey.capitalize()}`] : null;
const getLabel = path => {
const label = actorModel?.schema.getField(path)?.label;
return label ? game.i18n.localize(label) : path;
};
const bars = attributes.bar.map(v => {
const a = v.join('.');
return { group: barGroup, value: a, label: getLabel(a) };
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.value.compare(b.value));
bars.sort((a, b) => a.label.compare(b.label));
const values = attributes.value.map(v => {
const invalidAttributes = [
'gold',
'levelData',
'actions',
'biography',
'class',
'multiclass',
'companion',
'notes',
'partner',
'description',
'impulses',
'tier',
'type'
];
const values = attributes.value.reduce((acc, v) => {
const a = v.join('.');
return { group: valueGroup, value: a, label: getLabel(a) };
});
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));
values.sort((a, b) => a.value.compare(b.value));
return bars.concat(values);
}
static _getTrackedAttributesFromSchema(schema, _path = []) {
const attributes = { bar: [], value: [] };
for (const [name, field] of Object.entries(schema.fields)) {
const p = _path.concat([name]);
if (field instanceof foundry.data.fields.NumberField) attributes.value.push(p);
if (field instanceof foundry.data.fields.BooleanField && field.options.isAttributeChoice)
attributes.value.push(p);
if (field instanceof foundry.data.fields.StringField) attributes.value.push(p);
if (field instanceof foundry.data.fields.ArrayField) attributes.value.push(p);
const isSchema = field instanceof foundry.data.fields.SchemaField;
const isModel = field instanceof foundry.data.fields.EmbeddedDataField;
if (isSchema || isModel) {
const schema = isModel ? field.model.schema : field;
const isBar = schema.has && schema.has('value') && schema.has('max');
if (isBar) attributes.bar.push(p);
else {
const inner = this.getTrackedAttributes(schema, p);
attributes.bar.push(...inner.bar);
attributes.value.push(...inner.value);
}
}
}
return attributes;
}
_shouldRecordMovementHistory() {
return false;
}
@ -221,7 +269,7 @@ export default class DHToken extends CONFIG.Token.documentClass {
// Hexagon symmetry
if (columns) {
const rowData = DHToken.#getHexagonalShape(height, width, shape, false);
const rowData = BaseToken.#getHexagonalShape(height, width, shape, false);
if (!rowData) return null;
// Transpose the offsets/points of the shape in row orientation

View file

@ -86,22 +86,19 @@ export const enrichedDualityRoll = async (
{ reaction, traitValue, target, difficulty, title, label, advantage, grantResources, customConfig },
event
) => {
const shouldGrantResources = grantResources === undefined ? true : grantResources;
const config = {
event: event ?? {},
title: title,
headerTitle: label,
actionType: reaction ? 'reaction' : null,
roll: {
trait: traitValue && target ? traitValue : null,
difficulty: difficulty,
advantage
// type: reaction ? 'reaction' : null //not needed really but keeping it for troubleshooting
advantage,
type: reaction ? 'reaction' : null
},
skips: {
resources: !shouldGrantResources,
triggers: !shouldGrantResources
resources: !grantResources,
triggers: !grantResources
},
type: 'trait',
hasRoll: true,

View file

@ -4,7 +4,6 @@ export default function DhTemplateEnricher(match, _options) {
const params = parseInlineParams(match[1]);
const { type, angle = CONFIG.MeasuredTemplate.defaults.angle, inline = false } = params;
const direction = Number(params.direction) || 0;
params.range = params.range?.toLowerCase();
const range =
params.range && Number.isNaN(Number(params.range))
? Object.values(CONFIG.DH.GENERAL.templateRanges).find(

View file

@ -12,7 +12,7 @@ export function rollCommandToJSON(text) {
const flavor = flavorMatch ? flavorMatch[1] : null;
// Match key="quoted string" OR key=unquotedValue
const PAIR_RE = /(\w+)\s*=\s*("(?:[^"\\]|\\.)*"|[^\]\}\s]+)/g; //updated regex to allow escaped quotes in quoted strings and avoid matching closing brackets/braces
const PAIR_RE = /(\w+)=("(?:[^"\\]|\\.)*"|\S+)/g;
const result = {};
for (const [, key, raw] of text.matchAll(PAIR_RE)) {
let value;
@ -495,65 +495,3 @@ export function htmlToText(html) {
return tempDivElement.textContent || tempDivElement.innerText || '';
}
export async function getFeaturesHTMLData(features) {
const result = [];
for (const feature of features) {
if (feature) {
const base = feature.item ?? feature;
const item = base.system ? base : await foundry.utils.fromUuid(base.uuid);
const itemDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
item.system.description
);
result.push({ label: item.name, description: itemDescription });
}
}
return result;
}
/**
* Given a simple flavor-less formula with only +/- operators, returns a list of damage partial terms.
* All subtracted terms become negative terms.
* If there are no dice, it returns 0d1 for that term.
*/
export function parseTermsFromSimpleFormula(formula) {
const roll = formula instanceof Roll ? formula : new Roll(formula);
// Parse from right to left so that when we hit an operator, we already have the term.
return roll.terms.reduceRight((result, term) => {
// Ignore + terms, we assume + by default
if (term.expression === ' + ') return result;
// - terms modify the last term we parsed
if (term.expression === ' - ') {
const termToModify = result[0];
if (termToModify) {
if (termToModify.bonus) termToModify.bonus *= -1;
if (termToModify.dice) termToModify.dice *= -1;
}
return result;
}
result.unshift({
bonus: term instanceof foundry.dice.terms.NumericTerm ? term.number : 0,
diceQuantity: term instanceof foundry.dice.terms.Die ? term.number : 0,
faces: term.faces ?? 1
});
return result;
}, []);
}
/**
* Calculates the expectede value from a formula or the results of parseTermsFromSimpleFormula.
* @returns {number} the average result of rolling the given dice
*/
export function calculateExpectedValue(formulaOrTerms) {
const terms = Array.isArray(formulaOrTerms)
? formulaOrTerms
: typeof formulaOrTerms === 'string'
? parseTermsFromSimpleFormula(formulaOrTerms)
: [formulaOrTerms];
return terms.reduce((r, t) => r + (t.bonus ?? 0) + (t.diceQuantity ? (t.diceQuantity * (t.faces + 1)) / 2 : 0), 0);
}

View file

@ -39,7 +39,6 @@ export const preloadHandlebarsTemplates = async function () {
'systems/daggerheart/templates/dialogs/downtime/activities.hbs',
'systems/daggerheart/templates/dialogs/dice-roll/costSelection.hbs',
'systems/daggerheart/templates/ui/chat/parts/roll-part.hbs',
'systems/daggerheart/templates/ui/chat/parts/description-part.hbs',
'systems/daggerheart/templates/ui/chat/parts/damage-part.hbs',
'systems/daggerheart/templates/ui/chat/parts/target-part.hbs',
'systems/daggerheart/templates/ui/chat/parts/button-part.hbs',

View file

@ -7,7 +7,7 @@ import {
DhHomebrewSettings,
DhVariantRuleSettings
} from '../applications/settings/_module.mjs';
import { CompendiumBrowserSettings, DhTagTeamRoll } from '../data/_module.mjs';
import { DhTagTeamRoll } from '../data/_module.mjs';
export const registerDHSettings = () => {
registerMenuSettings();
@ -126,7 +126,7 @@ const registerNonConfigSettings = () => {
type: Number,
default: 0,
onChange: () => {
if (ui.resources) ui.resources.render();
if (ui.resources) ui.resources.render({ force: true });
ui.combat.render({ force: true });
}
});
@ -142,12 +142,6 @@ const registerNonConfigSettings = () => {
config: false,
type: DhTagTeamRoll
});
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings, {
scope: 'client',
config: false,
type: CompendiumBrowserSettings
});
};
/**

View file

@ -38,8 +38,7 @@ export const RefreshType = {
Countdown: 'DhCoundownRefresh',
TagTeamRoll: 'DhTagTeamRollRefresh',
EffectsDisplay: 'DhEffectsDisplayRefresh',
Scene: 'DhSceneRefresh',
CompendiumBrowser: 'DhCompendiumBrowserRefresh'
Scene: 'DhSceneRefresh'
};
export const registerSocketHooks = () => {

View file

@ -82,7 +82,7 @@
"enabled": false
},
"flatMultiplier": 3,
"dice": "d20",
"dice": "d10",
"bonus": null,
"multiplier": "flat"
},

View file

@ -246,7 +246,7 @@
"name": "Group Attack",
"type": "feature",
"system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 4 physical damage each. Combine this damage.</p>",
"resource": null,
"actions": {
"vgguNWz8vG8aoLXR": {

View file

@ -218,10 +218,10 @@
},
"items": [
{
"name": "Horde",
"name": "Horde (1d6+3)",
"type": "feature",
"system": {
"description": "<p>When the @Lookup[@name] has marked half or more of their HP, their standard attack deals <strong>@Lookup[@system.attack.altDamageFormula]</strong> physical damage instead.</p>",
"description": "<p>When the @Lookup[@name] has marked half or more of their HP, their standard attack deals <strong>1d6+3</strong> physical damage instead.</p>",
"resource": null,
"actions": {},
"originItemType": null,

View file

@ -388,7 +388,7 @@
"name": "Fumigation",
"type": "feature",
"system": {
"description": "<p>Drop a smoke bomb that fills the air within Close range with smoke, Dizzying all targets in this area. Dizzied targets have disadvantage on their next action roll, then clear the condition.</p><p>@Template[type:emanation|range:c]</p>",
"description": "<p>Drop a smoke bomb that fi lls the air within Close range with smoke, Dizzying all targets in this area. Dizzied targets have disadvantage on their next action roll, then clear the condition.</p><p>@Template[type:emanation|range:c]</p>",
"resource": null,
"actions": {
"sp7RfJRQJsEUm09m": {

View file

@ -239,7 +239,7 @@
"name": "Group Attack",
"type": "feature",
"system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 6 physical damage each. Combine this damage.</p>",
"resource": null,
"actions": {
"cbAvPSIhwBMBTI3D": {

View file

@ -239,7 +239,7 @@
"name": "Group Attack",
"type": "feature",
"system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all Cult @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all Cult @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 5 physical damage each. Combine this damage.</p>",
"resource": null,
"actions": {
"EH1preaTWBD4rOvx": {

View file

@ -224,10 +224,10 @@
},
"items": [
{
"name": "Horde",
"name": "Horde (2d4+1)",
"type": "feature",
"system": {
"description": "<p>When the @Lookup[@name] has marked half or more of their HP, their standard attack deals <strong>@Lookup[@system.attack.altDamageFormula]</strong> physical damage instead.</p>",
"description": "<p>When the @Lookup[@name] has marked half or more of their HP, their standard attack deals <strong>2d4+1</strong> physical damage instead.</p>",
"resource": null,
"actions": {},
"originItemType": null,

View file

@ -218,10 +218,10 @@
},
"items": [
{
"name": "Horde",
"name": "Horde (2d4+1)",
"type": "feature",
"system": {
"description": "<p>When the @Lookup[@name] have marked half or more of their HP, their standard attack deals <strong>@Lookup[@system.attack.altDamageFormula]</strong> physical damage instead.</p>",
"description": "<p>When the @Lookup[@name] have marked half or more of their HP, their standard attack deals <strong>2d4+1</strong> physical damage instead.</p>",
"resource": null,
"actions": {},
"originItemType": null,

View file

@ -239,7 +239,7 @@
"name": "Group Attack",
"type": "feature",
"system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 5 physical damage each. Combine this damage.</p>",
"resource": null,
"actions": {
"vXHZVb0Y7Hqu3uso": {

View file

@ -317,7 +317,7 @@
"name": "Group Attack",
"type": "feature",
"system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 12 physical damage each. Combine this damage.</p>",
"resource": null,
"actions": {
"QHNRSEQmqOcaoXq4": {

View file

@ -229,7 +229,7 @@
"_id": "9RduwBLYcBaiouYk",
"img": "icons/creatures/magical/humanoid-silhouette-aliens-green.webp",
"system": {
"description": "<p>When the @Lookup[@name] have marked half or more of their HP, their standard attack deals <strong>@Lookup[@system.attack.altDamageFormula]</strong> physical damage instead.</p>",
"description": "<p>When the @Lookup[@name] have marked half or more of their HP, their standard attack deals <strong>1d4+1</strong> physical damage instead.</p>",
"resource": null,
"actions": {},
"originItemType": null,

View file

@ -248,7 +248,7 @@
"_id": "fsaBlCjTdq1jM23G",
"img": "icons/creatures/abilities/tail-strike-bone-orange.webp",
"system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 1 physical damage each. Combine this damage.</p>",
"resource": null,
"actions": {
"q8chow47nQLR9qeF": {

View file

@ -55,7 +55,7 @@
"max": 1
},
"stress": {
"max": 2
"max": 1
}
},
"attack": {
@ -239,7 +239,7 @@
"name": "Group Attack",
"type": "feature",
"system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 5 physical damage each. Combine this damage.</p>",
"resource": null,
"actions": {
"DjbPQowW1OdBD9Zn": {

View file

@ -55,7 +55,7 @@
"max": 1
},
"stress": {
"max": 2
"max": 1
}
},
"attack": {
@ -294,7 +294,7 @@
"name": "Group Attack",
"type": "feature",
"system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 10 physical damage each. Combine this damage.</p>",
"resource": null,
"actions": {
"eo7J0v1B5zPHul1M": {

View file

@ -248,7 +248,7 @@
"_id": "1k5TmQIAunM7Bv32",
"img": "icons/creatures/abilities/tail-strike-bone-orange.webp",
"system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name] within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name] within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 2 physical damage each. Combine this damage.</p>",
"resource": null,
"actions": {
"aoQDb2m32NDxE6ZP": {

View file

@ -33,7 +33,7 @@
"reduction": 0
}
},
"type": "ranged",
"type": "standard",
"notes": "",
"hordeHp": 1,
"experiences": {

View file

@ -104,7 +104,6 @@
]
},
"type": "attack",
"range": "melee",
"chatDisplay": false
},
"attribution": {

View file

@ -242,7 +242,7 @@
"_id": "K08WlZwGqzEo4idT",
"img": "icons/creatures/abilities/tail-strike-bone-orange.webp",
"system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 4 physical damage each. Combine this damage.</p>",
"resource": null,
"actions": {
"xTMNAHcoErKuR6TZ": {

View file

@ -33,7 +33,7 @@
"reduction": 0
}
},
"type": "bruiser",
"type": "standard",
"notes": "",
"hordeHp": 1,
"experiences": {},
@ -66,12 +66,12 @@
"tier": 3,
"description": "<p>A sturdy animate old-growth tree.</p>",
"attack": {
"name": "Branch",
"name": "Attack",
"roll": {
"type": "attack",
"bonus": 2
},
"range": "veryClose",
"range": "close",
"damage": {
"parts": [
{

View file

@ -97,7 +97,7 @@
},
"img": "icons/creatures/claws/claw-talons-yellow-red.webp",
"type": "attack",
"range": "veryClose",
"range": "melee",
"chatDisplay": false
},
"attribution": {
@ -239,7 +239,7 @@
"name": "Group Attack",
"type": "feature",
"system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 11 physical damage each. Combine this damage.</p>",
"resource": null,
"actions": {
"tvQetauskZoHDR5y": {

View file

@ -229,7 +229,7 @@
"_id": "Q7DRbWjHl64CNwag",
"img": "icons/creatures/magical/humanoid-silhouette-aliens-green.webp",
"system": {
"description": "<p>When the @Lookup[@name] have marked half or more of their HP, their standard attack deals <strong>@Lookup[@system.attack.altDamageFormula]</strong> physical damage instead.</p>",
"description": "<p>When the @Lookup[@name] have marked half or more of their HP, their standard attack deals <strong>1d4+1</strong> physical damage instead.</p>",
"resource": null,
"actions": {},
"originItemType": null,

View file

@ -242,7 +242,7 @@
"_id": "R9vrwFNl5BD1YXJo",
"img": "icons/creatures/abilities/tail-strike-bone-orange.webp",
"system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 2 physical damage each. Combine this damage.</p>",
"resource": null,
"actions": {
"DJBNtd3hWjwsjPwq": {

View file

@ -242,7 +242,7 @@
"_id": "CQZQiEiRH70Br5Ge",
"img": "icons/creatures/abilities/tail-strike-bone-orange.webp",
"system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 3 physical damage each. Combine this damage.</p>",
"resource": null,
"actions": {
"ghgFZskDiizJDjcn": {

View file

@ -242,7 +242,7 @@
"_id": "wl9KKEpVWDBu62hU",
"img": "icons/creatures/abilities/tail-strike-bone-orange.webp",
"system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 1 physical damage each. Combine this damage.</p>",
"resource": null,
"actions": {
"Sz55uB8xkoNytLwJ": {

View file

@ -223,7 +223,7 @@
"_id": "9Zuu892SO5NmtI4w",
"img": "icons/creatures/magical/humanoid-silhouette-aliens-green.webp",
"system": {
"description": "<p>When the @Lookup[@name] has marked half or more of their HP, their standard attack deals <strong>@Lookup[@system.attack.altDamageFormula]</strong> physical damage instead.</p>",
"description": "<p>When the @Lookup[@name] has marked half or more of their HP, their standard attack deals <strong>1d4+1</strong> physical damage instead.</p>",
"resource": null,
"actions": {},
"originItemType": null,

View file

@ -254,12 +254,12 @@
},
"items": [
{
"name": "Horde",
"name": "Horde (1d4+2)",
"type": "feature",
"_id": "4dSzqtYvH385r9Ng",
"img": "icons/creatures/magical/humanoid-silhouette-aliens-green.webp",
"system": {
"description": "<p>When the @Lookup[@name] has marked half or more of their HP, their standard attack deals <strong>@Lookup[@system.attack.altDamageFormula]</strong> physical damage instead.</p>",
"description": "<p>When the @Lookup[@name] has marked half or more of their HP, their standard attack deals <strong>1d4+2</strong> physical damage instead.</p>",
"resource": null,
"actions": {},
"originItemType": null,

View file

@ -281,7 +281,7 @@
"_id": "WiobzuyvJ46zfsOv",
"img": "icons/creatures/abilities/tail-strike-bone-orange.webp",
"system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 2 physical damage each. Combine this damage.</p>",
"resource": null,
"actions": {
"ZC5pKIb9N82vgMWu": {

View file

@ -97,8 +97,8 @@
]
},
"type": "attack",
"range": "melee",
"chatDisplay": false
"chatDisplay": false,
"range": ""
},
"attribution": {
"source": "Daggerheart SRD",
@ -239,7 +239,7 @@
"name": "Group Attack",
"type": "feature",
"system": {
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.</p>",
"description": "<p><strong>Spend a Fear</strong> to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 8 physical damage each. Combine this damage.</p>",
"resource": null,
"actions": {
"euP8VA4wvfsCpwN1": {

View file

@ -110,7 +110,7 @@
},
"img": "icons/creatures/claws/claw-scaled-red.webp",
"type": "attack",
"range": "close",
"range": "melee",
"chatDisplay": false
},
"attribution": {

View file

@ -97,7 +97,7 @@
}
]
},
"name": "Undead Hands",
"name": "Tentacles",
"roll": {
"bonus": 2,
"type": "attack"
@ -218,10 +218,10 @@
},
"items": [
{
"name": "Horde",
"name": "Horde (2d6+5)",
"type": "feature",
"system": {
"description": "<p>When the @Lookup[@name] has marked half or more of their HP, their standard attack deals <strong>@Lookup[@system.attack.altDamageFormula]</strong> physical damage instead.</p>",
"description": "<p>When the @Lookup[@name] has marked half or more of their HP, their standard attack deals <strong>2d6+5</strong> physical damage instead.</p>",
"resource": null,
"actions": {},
"originItemType": null,

View file

@ -218,12 +218,12 @@
},
"items": [
{
"name": "Horde",
"name": "Horde (1d4+2)",
"type": "feature",
"_id": "nNJGAhWu0IuS2ybn",
"img": "icons/creatures/magical/humanoid-silhouette-aliens-green.webp",
"system": {
"description": "<p>When the @Lookup[@name] have marked half or more of their HP, their standard attack deals <strong>@Lookup[@system.attack.altDamageFormula]</strong> physical damage instead.</p>",
"description": "<p>When the @Lookup[@name] have marked half or more of their HP, their standard attack deals <strong>1d4+2</strong> physical damage instead.</p>",
"resource": null,
"actions": {},
"originItemType": null,

View file

@ -4,7 +4,7 @@
"type": "ancestry",
"folder": null,
"system": {
"description": "<p>Clanks are sentient mechanical beings built from a variety of materials, including metal, wood, and stone. They can resemble humanoids, animals, or even inanimate objects. Like organic beings, their bodies come in a wide array of sizes. Because of their bespoke construction, many clanks have highly specialized physical configurations. Examples include clawed hands for grasping, wheels for movement, or built-in weaponry.</p><p class=\"green Body-Styles_Body\">Many clanks embrace body modifications for style as well as function, and members of other ancestries often turn to clank artisans to construct customized mobility aids and physical adornments. Other ancestries can create clanks, even using their own physical characteristics as inspiration, but its also common for clanks to build one another. A clanks lifespan extends as long as theyre able to acquire or craft new parts, making their physical form effectively immortal. That said, their minds are subject to the effects of time, and deteriorate as the magic that powers them loses potency.</p>",
"description": "<p>Clanks are sentient mechanical beings built from a variety of materials, including metal, wood, and stone. They can resemble humanoids, animals, or even inanimate objects. Like organic beings, their bodies come in a wide array of sizes. Because of their bespoke construction, many clanks have highly specialized physical configurations. Examples include clawed hands for grasping, wheels for movement, or built-in weaponry.</p><p class=\"green Body-Styles_Body\">Many clanks embrace body modifications for style as well as function, and members of other ancestries often turn to clank artisans to construct customized mobility aids and physical adornments. Other ancestries can create clanks, even using their own physical characteristics as inspiration, but its also common for clanks to build one another. A clanks lifespan extends as long as theyre able to acquire or craft new parts, making their physical form effectively immortal. That said, their minds are subject to the effects of time, and deteriorate as the magic that powers them loses potency.</p><p></p><h4>ANCESTRY FEATURES</h4><p><em><strong>Purposeful Design:</strong></em> 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><em><strong>Efficient:</strong></em> When you take a short rest, you can choose a long rest move instead of a short rest move.</p><p></p>",
"features": [
{
"type": "primary",

View file

@ -4,7 +4,7 @@
"type": "ancestry",
"folder": null,
"system": {
"description": "<p>Drakona resemble wingless dragons in humanoid form and possess a powerful elemental breath. All drakona have thick scales that provide excellent natural armor against both attacks and the forces of nature. They are large in size, ranging from 5 feet to 7 feet on average, with long sharp teeth. New teeth grow throughout a Drakonas approximately 350-year lifespan, so they are never in danger of permanently losing an incisor. Unlike their dragon ancestors, drakona dont have wings and cant fly without magical aid. Members of this ancestry pass down the element of their breath through generations, though in rare cases, a drakonas elemental power will differ from the rest of their familys.</p>",
"description": "<p>Drakona resemble wingless dragons in humanoid form and possess a powerful elemental breath. All drakona have thick scales that provide excellent natural armor against both attacks and the forces of nature. They are large in size, ranging from 5 feet to 7 feet on average, with long sharp teeth. New teeth grow throughout a Drakonas approximately 350-year lifespan, so they are never in danger of permanently losing an incisor. Unlike their dragon ancestors, drakona dont have wings and cant fly without magical aid. Members of this ancestry pass down the element of their breath through generations, though in rare cases, a drakonas elemental power will differ from the rest of their familys.</p><p></p><h4>ANCESTRY FEATURES</h4><p><em><strong>Scales:</strong></em> Your scales act as natural protection. When you would take Severe damage, you can <strong>mark a Stress</strong> to mark 1 fewer Hit Points.</p><p><em><strong>Elemental Breath:</strong></em> Choose an element for your breath (such as electricity, fire, or ice). You can use this breath against a target or group of targets within Very Close range, treating it as an Instinct weapon that deals <strong>d8</strong> magic damage using your Proficiency.</p><p></p>",
"features": [
{
"type": "primary",

View file

@ -4,7 +4,7 @@
"type": "ancestry",
"folder": null,
"system": {
"description": "<p>Dwarves are most easily recognized as short humanoids with square frames, dense musculature, and thick hair. Their average height ranges from 4 to 5 ½ feet, and they are often broad in proportion to their stature. Their skin and nails contain a high amount of keratin, making them naturally resilient. This allows dwarves to embed gemstones into their bodies and decorate themselves with tattoos or piercings. Their hair grows thickly—usually on their heads, but some dwarves have thick hair across their bodies as well. Dwarves of all genders can grow facial hair, which they often style in elaborate arrangements. Typically, dwarves live up to 250 years of age, maintaining their muscle mass well into later life.</p>",
"description": "<p>Dwarves are most easily recognized as short humanoids with square frames, dense musculature, and thick hair. Their average height ranges from 4 to 5 ½ feet, and they are often broad in proportion to their stature. Their skin and nails contain a high amount of keratin, making them naturally resilient. This allows dwarves to embed gemstones into their bodies and decorate themselves with tattoos or piercings. Their hair grows thickly—usually on their heads, but some dwarves have thick hair across their bodies as well. Dwarves of all genders can grow facial hair, which they often style in elaborate arrangements. Typically, dwarves live up to 250 years of age, maintaining their muscle mass well into later life.</p><p></p><h4>ANCESTRY FEATURES</h4><p><em><strong>Thick Skin:</strong></em> When you take Minor damage, you can <strong>mark 2 Stress</strong> instead of marking a Hit Point.</p><p><em><strong>Increased Fortitude:</strong></em> <strong>Spend 3 Hope</strong> to halve incoming physical damage.</p>",
"features": [
{
"type": "primary",

View file

@ -4,7 +4,7 @@
"type": "ancestry",
"folder": null,
"system": {
"description": "<p>Elves are typically tall humanoids with pointed ears and acutely attuned senses. Their ears vary in size and pointed shape, and as they age, the tips begin to droop. While elves come in a wide range of body types, they are all fairly tall, with heights ranging from about 6 to 6 ½ feet. All elves have the ability to drop into a celestial trance, rather than sleep. This allows them to rest effectively in a short amount of time.</p><p class=\"green Body-Styles_Body\">Some elves possess what is known as a “mystic form,” which occurs when an elf has dedicated themself to the study or protection of the natural world so deeply that their physical form changes. These characteristics can include celestial freckles, the presence of leaves, vines, or flowers in their hair, eyes that flicker like fire, and more. Sometimes these traits are inherited from parents, but if an elf changes their environment or magical focus, their appearance changes over time. Because elves live for about 350 years, these traits can shift more than once throughout their lifespan.</p>",
"description": "<p>Elves are typically tall humanoids with pointed ears and acutely attuned senses. Their ears vary in size and pointed shape, and as they age, the tips begin to droop. While elves come in a wide range of body types, they are all fairly tall, with heights ranging from about 6 to 6 ½ feet. All elves have the ability to drop into a celestial trance, rather than sleep. This allows them to rest effectively in a short amount of time.</p><p class=\"green Body-Styles_Body\">Some elves possess what is known as a “mystic form,” which occurs when an elf has dedicated themself to the study or protection of the natural world so deeply that their physical form changes. These characteristics can include celestial freckles, the presence of leaves, vines, or flowers in their hair, eyes that flicker like fire, and more. Sometimes these traits are inherited from parents, but if an elf changes their environment or magical focus, their appearance changes over time. Because elves live for about 350 years, these traits can shift more than once throughout their lifespan.</p><p></p><h4>ANCESTRY FEATURES</h4><p><em><strong>Quick Reactions:</strong></em> <strong>Mark a Stress</strong> to gain advantage on a reaction roll.</p><p><em><strong>Celestial Trance:</strong></em> During a rest, you can drop into a trance to choose an additional downtime move.</p><p></p>",
"features": [
{
"type": "primary",

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