Merged with main

This commit is contained in:
WBHarry 2026-01-26 14:36:58 +01:00
commit 286944d2e6
207 changed files with 4909 additions and 1073 deletions

View file

@ -3,36 +3,41 @@ import * as applications from './module/applications/_module.mjs';
import * as data from './module/data/_module.mjs';
import * as models from './module/data/_module.mjs';
import * as documents from './module/documents/_module.mjs';
import * as collections from './module/documents/collections/_module.mjs';
import * as dice from './module/dice/_module.mjs';
import * as fields from './module/data/fields/_module.mjs';
import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs';
import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs';
import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs';
import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll } from './module/dice/_module.mjs';
import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll, FateRoll } from './module/dice/_module.mjs';
import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs';
import { enrichedFateRoll, getFateTypeData } from './module/enrichers/FateRollEnricher.mjs';
import {
handlebarsRegistration,
runMigrations,
settingsRegistration,
socketRegistration
} from './module/systemRegistration/_module.mjs';
import { placeables } from './module/canvas/_module.mjs';
import { placeables, DhTokenLayer } from './module/canvas/_module.mjs';
import './node_modules/@yaireo/tagify/dist/tagify.css';
import TemplateManager from './module/documents/templateManager.mjs';
import TokenManager from './module/documents/tokenManager.mjs';
CONFIG.DH = SYSTEM;
CONFIG.TextEditor.enrichers.push(...enricherConfig);
CONFIG.Dice.rolls = [BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll];
CONFIG.Dice.rolls = [BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll, FateRoll];
CONFIG.Dice.daggerheart = {
DHRoll: DHRoll,
DualityRoll: DualityRoll,
D20Roll: D20Roll,
DamageRoll: DamageRoll
DamageRoll: DamageRoll,
FateRoll: FateRoll
};
CONFIG.Actor.documentClass = documents.DhpActor;
CONFIG.Actor.dataModels = models.actors.config;
CONFIG.Actor.collection = collections.DhActorCollection;
CONFIG.Item.documentClass = documents.DHItem;
CONFIG.Item.dataModels = models.items.config;
@ -51,8 +56,13 @@ CONFIG.ChatMessage.template = 'systems/daggerheart/templates/ui/chat/chat-messag
CONFIG.Canvas.rulerClass = placeables.DhRuler;
CONFIG.Canvas.layers.templates.layerClass = placeables.DhTemplateLayer;
CONFIG.Canvas.layers.tokens.layerClass = DhTokenLayer;
CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate;
CONFIG.RollTable.documentClass = documents.DhRollTable;
CONFIG.RollTable.resultTemplate = 'systems/daggerheart/templates/ui/chat/table-result.hbs';
CONFIG.Scene.documentClass = documents.DhScene;
CONFIG.Token.documentClass = documents.DhToken;
@ -74,6 +84,8 @@ CONFIG.ui.countdowns = applications.ui.DhCountdowns;
CONFIG.ux.ContextMenu = applications.ux.DHContextMenu;
CONFIG.ux.TooltipManager = documents.DhTooltipManager;
CONFIG.ux.TemplateManager = new TemplateManager();
CONFIG.ux.TokenManager = new TokenManager();
CONFIG.debug.triggers = false;
Hooks.once('init', () => {
game.system.api = {
@ -85,7 +97,7 @@ Hooks.once('init', () => {
fields
};
game.system.registeredTriggers = new RegisteredTriggers();
game.system.registeredTriggers = new game.system.api.data.RegisteredTriggers();
const { DocumentSheetConfig } = foundry.applications.apps;
DocumentSheetConfig.unregisterSheet(TokenDocument, 'core', foundry.applications.sheets.TokenConfig);
@ -98,7 +110,7 @@ Hooks.once('init', () => {
type: game.i18n.localize(typePath)
});
const { Items, Actors } = foundry.documents.collections;
const { Items, Actors, RollTables } = foundry.documents.collections;
Items.unregisterSheet('core', foundry.applications.sheets.ItemSheetV2);
Items.registerSheet(SYSTEM.id, applications.sheets.items.Ancestry, {
types: ['ancestry'],
@ -183,6 +195,12 @@ Hooks.once('init', () => {
label: sheetLabel('TYPES.Actor.party')
});
RollTables.unregisterSheet('core', foundry.applications.sheets.RollTableSheet);
RollTables.registerSheet(SYSTEM.id, applications.sheets.rollTables.RollTableSheet, {
types: ['base'],
makeDefault: true
});
DocumentSheetConfig.unregisterSheet(
CONFIG.ActiveEffect.documentClass,
'core',
@ -291,13 +309,15 @@ Hooks.on('chatMessage', (_, message) => {
? CONFIG.DH.ACTIONS.advantageState.disadvantage.value
: undefined;
const difficulty = rollCommand.difficulty;
const grantResources = Boolean(rollCommand.grantResources);
const target = getCommandTarget({ allowNull: true });
const title = traitValue
? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(SYSTEM.ACTOR.abilities[traitValue].label)
})
: game.i18n.localize('DAGGERHEART.GENERAL.duality');
const title =
(flavor ?? traitValue)
? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(SYSTEM.ACTOR.abilities[traitValue].label)
})
: game.i18n.localize('DAGGERHEART.GENERAL.duality');
enrichedDualityRoll({
reaction,
@ -305,9 +325,38 @@ Hooks.on('chatMessage', (_, message) => {
target,
difficulty,
title,
label: 'test',
label: game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll'),
actionType: null,
advantage
advantage,
grantResources
});
return false;
}
if (message.startsWith('/fr')) {
const result =
message.trim().toLowerCase() === '/fr' ? { result: {} } : rollCommandToJSON(message.replace(/\/fr\s?/, ''));
if (!result) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateParsing'));
return false;
}
const { result: rollCommand, flavor } = result;
const fateTypeData = getFateTypeData(rollCommand?.type);
if (!fateTypeData)
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing'));
const { value: fateType, label: fateTypeLabel } = fateTypeData;
const target = getCommandTarget({ allowNull: true });
const title = flavor ?? game.i18n.localize('DAGGERHEART.GENERAL.fateRoll');
enrichedFateRoll({
target,
title,
label: fateTypeLabel,
fateType
});
return false;
}
@ -354,7 +403,9 @@ const updateAllRangeDependentEffects = async () => {
const effectsAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).effects;
if (!effectsAutomation.rangeDependent) return;
const tokens = canvas.scene.tokens;
const tokens = canvas.scene?.tokens;
if (!tokens) return;
if (game.user.character) {
// The character updates their character's token. There can be only one token.
const characterToken = tokens.find(x => x.actor === game.user.character);
@ -374,8 +425,8 @@ Hooks.on('targetToken', () => {
debouncedRangeEffectCall();
});
Hooks.on('refreshToken', (_, options) => {
if (options.refreshPosition) {
Hooks.on('refreshToken', (token, options) => {
if (options.refreshPosition && !token._original) {
debouncedRangeEffectCall();
}
});
@ -383,49 +434,12 @@ Hooks.on('refreshToken', (_, options) => {
Hooks.on('renderCompendiumDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html));
Hooks.on('renderDocumentDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html));
class RegisteredTriggers extends Map {
constructor() {
super();
}
/* Non actor-linked Actors should unregister the triggers of their tokens if a scene's token layer is torn down */
Hooks.on('canvasTearDown', canvas => {
game.system.registeredTriggers.unregisterSceneTriggers(canvas.scene);
});
async registerTriggers(trigger, actor, triggeringActorType, uuid, commands) {
const existingTrigger = this.get(trigger);
if (!existingTrigger) this.set(trigger, new Map());
this.get(trigger).set(uuid, { actor, triggeringActorType, commands });
}
async runTrigger(trigger, currentActor, ...args) {
const updates = [];
const triggerSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).triggers;
if (!triggerSettings.enabled) return updates;
const dualityTrigger = this.get(trigger);
if (dualityTrigger) {
for (let { actor, triggeringActorType, commands } of dualityTrigger.values()) {
const triggerData = CONFIG.DH.TRIGGER.triggers[trigger];
if (triggerData.usesActor && triggeringActorType !== 'any') {
if (triggeringActorType === 'self' && currentActor?.uuid !== actor) continue;
else if (triggeringActorType === 'other' && currentActor?.uuid === actor) continue;
}
for (let command of commands) {
try {
const result = await command(...args);
if (result?.updates?.length) updates.push(...result.updates);
} catch (_) {
const triggerName = game.i18n.localize(triggerData.label);
ui.notifications.error(
game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerError', {
trigger: triggerName,
actor: currentActor?.name
})
);
}
}
}
}
return updates;
}
}
/* Non actor-linked Actors should register the triggers of their tokens on a readied scene */
Hooks.on('canvasReady', canas => {
game.system.registeredTriggers.registerSceneTriggers(canvas.scene);
});

View file

@ -69,7 +69,11 @@
},
"summon": {
"name": "Summon",
"tooltip": "Create tokens in the scene."
"tooltip": "Create tokens in the scene.",
"error": "You do not have permission to summon tokens or there is no active scene.",
"invalidDrop": "You can only drop Actor entities to summon.",
"chatMessageTitle": "Test2",
"chatMessageHeaderTitle": "Summoning"
}
},
"Config": {
@ -122,6 +126,9 @@
},
"cost": {
"stepTooltip": "+{step} per step"
},
"summon": {
"dropSummonsHere": "Drop Summons Here"
}
}
},
@ -196,6 +203,8 @@
"unequip": "Unequip",
"useItem": "Use Item"
},
"defaultHopeDice": "Default Hope Dice",
"defaultFearDice": "Default Fear Dice",
"disadvantageSources": {
"label": "Disadvantage Sources",
"hint": "Add single words or short text as reminders and hints of what a character has disadvantage on."
@ -228,11 +237,14 @@
"confirmText": "Would you like to level up your companion {name} by {levelChange} levels at this time? (You can do it manually later)"
},
"viewLevelups": "View Levelups",
"resetCharacter": "Reset Character",
"viewParty": "View Party",
"InvalidOldCharacterImportTitle": "Old Character Import",
"InvalidOldCharacterImportText": "Character data exported prior to system version 1.1 will not generate a complete character. Do you wish to continue?",
"cancelBeastform": "Cancel Beastform",
"sidebarFavoritesHint": "Drag items, features and domain cards from the sheet to here"
"sidebarFavoritesHint": "Drag items, features and domain cards from the sheet to here",
"resetCharacterConfirmationTitle": "Reset Character",
"resetCharacterConfirmationContent": "You are reseting all character data except name and portrait. Are you sure?"
},
"Companion": {
"FIELDS": {
@ -306,6 +318,8 @@
"selectPrimaryWeapon": "Select Primary Weapon",
"selectSecondaryWeapon": "Select Secondary Weapon",
"selectSubclass": "Select Subclass",
"setupSkipTitle": "Skipping Character Setup",
"setupSkipContent": "You are skipping the Character Setup by adding this manually. The character setup is the blinking arrows in the top-right. Are you sure you want to continue?",
"startingItems": "Starting Items",
"story": "Story",
"storyExplanation": "Select which background and connection prompts you want to copy into your character's background.",
@ -317,6 +331,12 @@
"title": "{actor} - Character Setup",
"traitIncreases": "Trait Increases"
},
"CharacterReset": {
"title": "Reset Character",
"alwaysDeleteSection": "Deleted Data",
"optionalDeleteSection": "Optional Data",
"headerTitle": "Select which data you'd like to keep"
},
"CombatTracker": {
"combatStarted": "Active",
"giveSpotlight": "Give The Spotlight",
@ -469,7 +489,9 @@
"tokenHUD": {
"genericEffects": "Foundry Effects",
"depositPartyTokens": "Deposit Party Tokens",
"retrievePartyTokens": "Retrieve Party Tokens"
"retrievePartyTokens": "Retrieve Party Tokens",
"depositCompanionTokens": "Deposit Companion Token",
"retrieveCompanionTokens": "Retrieve Companion Token"
}
},
"ImageSelect": {
@ -599,6 +621,7 @@
},
"RerollDialog": {
"title": "Reroll",
"damageTitle": "Reroll Damage",
"deselectDiceNotification": "Deselect one of the selected dice first",
"acceptCurrentRolls": "Accept Current Rolls"
},
@ -606,6 +629,13 @@
"title": "{name} Resource",
"rerollDice": "Reroll Dice"
},
"RiskItAllDialog": {
"title": "{name} - Risk It All",
"subtitle": "Clear Stress and Hit Points",
"remainingTitle": "Remaining Points",
"clearResource": "Clear {resource}",
"finalTitle": "Final Character Resources"
},
"TagTeamSelect": {
"title": "Tag Team Roll",
"leaderTitle": "Initiating Character",
@ -953,6 +983,10 @@
"outsideRange": "Outside Range"
},
"Condition": {
"deathMove": {
"name": "Death Move",
"description": "The character is about to make a Death Move"
},
"dead": {
"name": "Dead",
"description": "The character is dead"
@ -1004,15 +1038,15 @@
"DeathMoves": {
"avoidDeath": {
"name": "Avoid Death",
"description": "You drop unconscious temporarily and work with the GM to describe how the situation gets much worse because of it. Then roll your Fear die; if its value is equal to or under your Level, take a Scar."
"description": "Your character avoids death and faces the consequences. They temporarily drop unconscious, and then you work with the GM to describe how the situation worsens. While unconscious, your character can't move or act, and they can't be targeted by an attack. They return to consciousness when an ally clears 1 or more of their marked Hit Points or when the party finishes a long rest. After your character falls unconscious, roll your Hope Die. If its value is equal to or less than your character's level, they gain a scar: permanently cross out a Hope slot and work with the GM to determine its lasting narrative impact and how, if possible, it can be restored. If you ever cross out your last Hope slot, your character's journey ends."
},
"riskItAll": {
"name": "Risk It All",
"description": "Roll your Duality Dice. If Hope is higher, you stay on your feet and clear an amount of Hit Points and/or Stress equal to the value of the Hope die (divide the Hope die value up between these however youd prefer). If your Fear die is higher, you cross through the veil of death. If the Duality Dice are tied, you stay on your feet and clear all Hit Points and Stress."
"description": "Roll your Duality Dice. If the Hope Die is higher, your character stays on their feet and clears a number of Hit Points or Stress equal to the value of the Hope Die (you can divide the Hope Die value between Hit Points and Stress however you'd prefer). If the Fear Die is higher, your character crosses through the veil of death. If the Duality Dice show matching results, your character stays up and clears all Hit Points and Stress."
},
"blazeOfGlory": {
"name": "Blaze Of Glory",
"description": "With Blaze of Glory, the player is accepting death for the character. Take one action (at GM discretion), which becomes an automatic critical success, then cross through the veil of death."
"description": "Your character embraces death and goes out in a blaze of glory. Take one final action. It automatically critically succeeds (with GM approval), and then you cross through the veil of death. NOTE: A Blaze of Glory effect has been added to your character. Any Duality Roll will automatically be a critical."
}
},
"DomainCardTypes": {
@ -2053,6 +2087,7 @@
"description": "Description",
"main": "Data",
"information": "Information",
"itemFeatures": "Item Features",
"notes": "Notes",
"inventory": "Inventory",
"loadout": "Loadout",
@ -2077,6 +2112,7 @@
"tier4": "tier 4",
"domains": "Domains",
"downtime": "Downtime",
"itemFeatures": "Item Features",
"roll": "Roll",
"rules": "Rules",
"partyMembers": "Party Members",
@ -2127,6 +2163,7 @@
"dropActorsHere": "Drop Actors here",
"dropFeaturesHere": "Drop Features here",
"duality": "Duality",
"dualityDice": "Duality Dice",
"dualityRoll": "Duality Roll",
"enabled": "Enabled",
"evasion": "Evasion",
@ -2140,11 +2177,14 @@
"single": "Favorite",
"plural": "Favorites"
},
"fate": "Fate",
"fateRoll": "Fate Roll",
"fear": "Fear",
"features": "Features",
"formula": "Formula",
"general": "General",
"gm": "GM",
"guaranteedCriticalSuccess": "Guaranteed Critical Success",
"healing": "Healing",
"healingRoll": "Healing Roll",
"hit": {
@ -2188,6 +2228,7 @@
"single": "Player",
"plurial": "Players"
},
"portrait": "Portrait",
"proficiency": "Proficiency",
"quantity": "Quantity",
"range": "Range",
@ -2204,12 +2245,17 @@
"rollWith": "{roll} Roll",
"save": "Save",
"scalable": "Scalable",
"scars": "Scars",
"situationalBonus": "Situational Bonus",
"spent": "Spent",
"step": "Step",
"stress": "Stress",
"subclasses": "Subclasses",
"success": "Success",
"summon": {
"single": "Summon",
"plural": "Summons"
},
"take": "Take",
"Target": {
"single": "Target",
@ -2276,7 +2322,8 @@
"placeholder": "Using character dimensions",
"disabledPlaceholder": "Set by character size",
"height": { "label": "Height" },
"width": { "label": "Width" }
"width": { "label": "Width" },
"scale": { "label": "Token Scale" }
},
"evolved": {
"maximumTier": { "label": "Maximum Tier" },
@ -2325,6 +2372,9 @@
"DomainCard": {
"type": "Type",
"recallCost": "Recall Cost",
"vaultActive": "Active In Vault",
"loadoutIgnore": "Ignores Loadout Limits",
"domainTouched": "Domain Touched",
"foundationTitle": "Foundation",
"specializationTitle": "Specialization",
"masteryTitle": "Mastery"
@ -2338,6 +2388,12 @@
"secondaryWeapon": "Secondary Weapon"
}
},
"ROLLTABLES": {
"FIELDS": {
"formulaName": { "label": "Formula Name" }
},
"formula": "Formula"
},
"SETTINGS": {
"Appearance": {
"FIELDS": {
@ -2404,7 +2460,11 @@
"overlay": { "label": "Overlay Effect" },
"characterDefault": { "label": "Character Default Defeated Status" },
"adversaryDefault": { "label": "Adversary Default Defeated Status" },
"companionDefault": { "label": "Companion Default Defeated Status" }
"companionDefault": { "label": "Companion Default Defeated Status" },
"deathMove": { "label": "Death Move" },
"dead": { "label": "Dead" },
"defeated": { "label": "Defeated" },
"unconscious": { "label": "Unconscious" }
},
"hopeFear": {
"label": "Hope & Fear",
@ -2437,10 +2497,6 @@
"label": "Show Resource Change Scrolltexts",
"hint": "When a character is damaged, uses armor etc, a scrolling text will briefly appear by the token to signify this."
},
"playerCanEditSheet": {
"label": "Players Can Manually Edit Character Settings",
"hint": "Players are allowed to access the manual Character Settings and change their statistics beyond the rules."
},
"roll": {
"roll": {
"label": "Roll",
@ -2493,9 +2549,11 @@
"itemFeatures": "Item Features",
"nrChoices": "# Moves Per Rest",
"resetMovesTitle": "Reset {type} Downtime Moves",
"resetItemFeaturesTitle": "Reset {type}",
"resetMovesText": "Are you sure you want to reset?",
"FIELDS": {
"maxFear": { "label": "Max Fear" },
"maxHope": { "label": "Max Hope" },
"traitArray": { "label": "Initial Trait Modifiers" },
"maxLoadout": {
"label": "Max Cards in Loadout",
@ -2643,7 +2701,16 @@
"currentTarget": "Current"
},
"deathMove": {
"title": "Death Move"
"title": "Death Move",
"gainScar": "You gained a scar.",
"avoidScar": "You have avoided a new scar.",
"journeysEnd": "You have {scars} Scars and have crossed out your last Hope slot. Your character's journey ends.",
"riskItAllCritical": "Critical Rolled, clearing all marked Stress and Hit Points.",
"riskItAllFailure": "The fear die rolled higher. You have crossed through the veil of death.",
"blazeOfGlory": "Blaze of Glory Effect Added!",
"riskItAllDialogButton": "Clear Stress And Hit Points.",
"riskItAllSuccessWithEnoughHope": "The Hope value is more than the marked Stress and Hit Points. Both are cleared fully.",
"riskItAllSuccess": "The hope die rolled higher, clear up to {hope} Stress And Hit Points."
},
"dicePool": {
"title": "Dice Pool"
@ -2697,6 +2764,9 @@
"rerollDamage": "Reroll Damage",
"assignTagRoll": "Assign as Tag Roll"
},
"ConsoleLogs": {
"triggerRun": "DH TRIGGER | Item '{item}' on actor '{actor}' ran a '{trigger}' trigger."
},
"Countdowns": {
"title": "Countdowns",
"toggleIconMode": "Toggle Icon Only",
@ -2762,7 +2832,9 @@
"noAssignedPlayerCharacter": "You have no assigned character.",
"noSelectedToken": "You have no selected token",
"onlyUseableByPC": "This can only be used with a PC token",
"dualityParsing": "Duality roll not properly formated",
"dualityParsing": "Duality roll not properly formatted",
"fateParsing": "Fate roll not properly formatted",
"fateTypeParsing": "Fate roll not properly formatted, bad fate type. Valid types are 'Hope' and 'Fear'",
"attributeFaulty": "The supplied Attribute doesn't exist",
"domainCardWrongDomain": "You don't have access to that Domain",
"domainCardToHighLevel": "The Domain Card is too high level to be selected",
@ -2825,7 +2897,9 @@
"noActorOwnership": "You do not have permissions for this character",
"documentIsMissing": "The {documentType} is missing from the world.",
"tokenActorMissing": "{name} is missing an Actor",
"tokenActorsMissing": "[{names}] missing Actors"
"tokenActorsMissing": "[{names}] missing Actors",
"domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used",
"knowTheTide": "Know The Tide gained a token"
},
"Sidebar": {
"actorDirectory": {
@ -2870,7 +2944,8 @@
"deleteItem": "Delete Item",
"immune": "Immune",
"middleClick": "[Middle Click] Keep tooltip view",
"tokenSize": "The token size used on the canvas"
"tokenSize": "The token size used on the canvas",
"previewTokenHelp": "Left-click to place, right-click to cancel"
}
}
}

View file

@ -1,5 +1,6 @@
export { default as AttributionDialog } from './attributionDialog.mjs';
export { default as BeastformDialog } from './beastformDialog.mjs';
export { default as CharacterResetDialog } from './characterResetDialog.mjs';
export { default as d20RollDialog } from './d20RollDialog.mjs';
export { default as DamageDialog } from './damageDialog.mjs';
export { default as DamageReductionDialog } from './damageReductionDialog.mjs';
@ -14,3 +15,4 @@ export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs';
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';

View file

@ -0,0 +1,105 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class CharacterResetDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(actor, options = {}) {
super(options);
this.actor = actor;
this.data = {
delete: {
class: { keep: false, label: 'TYPES.Item.class' },
subclass: { keep: false, label: 'TYPES.Item.subclass' },
ancestry: { keep: false, label: 'TYPES.Item.ancestry' },
community: { keep: false, label: 'TYPES.Item.community' }
},
optional: {
portrait: { keep: true, label: 'DAGGERHEART.GENERAL.portrait' },
name: { keep: true, label: 'Name' },
biography: { keep: true, label: 'DAGGERHEART.GENERAL.Tabs.biography' },
inventory: { keep: true, label: 'DAGGERHEART.GENERAL.inventory' }
}
};
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'character-reset'],
window: {
icon: 'fa-solid fa-arrow-rotate-left',
title: 'DAGGERHEART.APPLICATIONS.CharacterReset.title'
},
actions: {
finishSelection: this.#finishSelection
},
form: {
handler: this.updateData,
submitOnChange: true,
submitOnClose: false
}
};
/** @override */
static PARTS = {
resourceDice: {
id: 'resourceDice',
template: 'systems/daggerheart/templates/dialogs/characterReset.hbs'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.data = this.data;
return context;
}
static async updateData(event, _, formData) {
const { data } = foundry.utils.expandObject(formData.object);
this.data = foundry.utils.mergeObject(this.data, data);
this.render();
}
static getUpdateData() {
const update = {};
if (!this.data.optional.portrait) update.if(!this.data.optional.biography);
if (!this.data.optional.inventory) return update;
}
static async #finishSelection() {
const update = {};
if (!this.data.optional.name.keep) {
const defaultName = game.system.api.documents.DhpActor.defaultName({ type: 'character' });
foundry.utils.setProperty(update, 'name', defaultName);
foundry.utils.setProperty(update, 'prototypeToken.name', defaultName);
}
if (!this.data.optional.portrait.keep) {
foundry.utils.setProperty(update, 'img', this.actor.schema.fields.img.initial(this.actor));
foundry.utils.setProperty(update, 'prototypeToken.==texture', {});
foundry.utils.setProperty(update, 'prototypeToken.==ring', {});
}
if (this.data.optional.biography.keep)
foundry.utils.setProperty(update, 'system.biography', this.actor.system.biography);
if (this.data.optional.inventory.keep) foundry.utils.setProperty(update, 'system.gold', this.actor.system.gold);
const { system, ...rest } = update;
await this.actor.update({
...rest,
'==system': system ?? {}
});
const inventoryItemTypes = ['weapon', 'armor', 'consumable', 'loot'];
await this.actor.deleteEmbeddedDocuments(
'Item',
this.actor.items
.filter(x => !inventoryItemTypes.includes(x.type) || !this.data.optional.inventory.keep)
.map(x => x.id)
);
this.close();
}
}

View file

@ -10,6 +10,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.config = config;
this.config.experiences = [];
this.reactionOverride = config.actionType === 'reaction';
this.selectedEffects = this.config.bonusEffects;
if (config.source?.action) {
this.item = config.data.parent.items.get(config.source.item) ?? config.data.parent;
@ -35,6 +36,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
selectExperience: this.selectExperience,
toggleReaction: this.toggleReaction,
toggleTagTeamRoll: this.toggleTagTeamRoll,
toggleSelectedEffect: this.toggleSelectedEffect,
submitRoll: this.submitRoll
},
form: {
@ -76,6 +78,9 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
icon
}));
context.hasSelectedEffects = Boolean(this.selectedEffects && Object.keys(this.selectedEffects).length);
context.selectedEffects = this.selectedEffects;
this.config.costs ??= [];
if (this.config.costs?.length) {
const updatedCosts = game.system.api.fields.ActionFields.CostField.calcCosts.call(
@ -104,11 +109,17 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.roll = this.roll;
context.rollType = this.roll?.constructor.name;
context.rallyDie = this.roll.rallyChoices;
const experiences = this.config.data?.system?.experiences || {};
const actorExperiences = this.config.data?.system?.experiences || {};
const companionExperiences = this.config.roll.companionRoll
? (this.config.data?.companion?.system.experiences ?? {})
: null;
const experiences = companionExperiences ?? actorExperiences;
context.experiences = Object.keys(experiences).map(id => ({
id,
...experiences[id]
}));
context.selectedExperiences = this.config.experiences;
context.advantage = this.config.roll?.advantage;
context.disadvantage = this.config.roll?.disadvantage;
@ -118,7 +129,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.formula = this.roll.constructFormula(this.config);
if (this.actor?.system?.traits) context.abilities = this.getTraitModifiers();
context.showReaction = !this.config.roll?.type && context.rollType === 'DualityRoll';
context.showReaction = !this.config.skips?.reaction && context.rollType === 'DualityRoll';
context.reactionOverride = this.reactionOverride;
}
@ -208,6 +219,11 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.render();
}
static toggleSelectedEffect(_event, button) {
this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected;
this.render();
}
static async submitRoll() {
await this.close({ submitted: true });
}

View file

@ -6,6 +6,7 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
this.roll = roll;
this.config = config;
this.selectedEffects = this.config.bonusEffects;
}
static DEFAULT_OPTIONS = {
@ -20,6 +21,7 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
icon: 'fa-solid fa-dice'
},
actions: {
toggleSelectedEffect: this.toggleSelectedEffect,
submitRoll: this.submitRoll
},
form: {
@ -57,6 +59,9 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
icon
}));
context.modifiers = this.config.modifiers;
context.hasSelectedEffects = Boolean(Object.keys(this.selectedEffects).length);
context.selectedEffects = this.selectedEffects;
return context;
}
@ -69,6 +74,11 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
this.render();
}
static toggleSelectedEffect(_event, button) {
this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected;
this.render();
}
static async submitRoll() {
await this.close({ submitted: true });
}

View file

@ -1,11 +1,16 @@
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
import { enrichedFateRoll } from '../../enrichers/FateRollEnricher.mjs';
import { enrichedDualityRoll } from '../../enrichers/DualityRollEnricher.mjs';
export default class DhpDeathMove extends HandlebarsApplicationMixin(ApplicationV2) {
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(actor) {
super({});
this.actor = actor;
this.selectedMove = null;
this.showRiskItAllButton = false;
this.riskItAllButtonLabel = '';
this.riskItAllHope = 0;
}
get title() {
@ -38,6 +43,111 @@ export default class DhpDeathMove extends HandlebarsApplicationMixin(Application
return context;
}
async handleAvoidDeath() {
const target = this.actor.uuid;
const config = await enrichedFateRoll({
target,
title: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.avoidDeath.name'),
label: `${game.i18n.localize('DAGGERHEART.GENERAL.hope')} ${game.i18n.localize('DAGGERHEART.GENERAL.fateRoll')}`,
fateType: 'Hope'
});
if (!config.roll.fate) return;
let returnMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.avoidScar');
if (config.roll.fate.value <= this.actor.system.levelData.level.current) {
const newScarAmount = this.actor.system.scars + 1;
await this.actor.update({
system: {
scars: newScarAmount
}
});
if (newScarAmount >= this.actor.system.resources.hope.max) {
await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id);
return game.i18n.format('DAGGERHEART.UI.Chat.deathMove.journeysEnd', { scars: newScarAmount });
}
returnMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.gainScar');
}
await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.unconscious.id);
return returnMessage;
}
async handleRiskItAll() {
const config = await enrichedDualityRoll({
reaction: true,
traitValue: null,
target: this.actor,
difficulty: null,
title: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.riskItAll.name'),
label: game.i18n.localize('DAGGERHEART.GENERAL.dualityDice'),
actionType: null,
advantage: null,
grantResources: false,
customConfig: { skips: { resources: true, reaction: true } }
});
if (!config.roll.result) return;
const clearAllStressAndHitpointsUpdates = [
{ key: 'hitPoints', clear: true },
{ key: 'stress', clear: true }
];
let chatMessage = '';
if (config.roll.isCritical) {
config.resourceUpdates.addResources(clearAllStressAndHitpointsUpdates);
chatMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.riskItAllCritical');
}
if (config.roll.result.duality == 1) {
if (
config.roll.hope.value >=
this.actor.system.resources.hitPoints.value + this.actor.system.resources.stress.value
) {
config.resourceUpdates.addResources(clearAllStressAndHitpointsUpdates);
chatMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.riskItAllSuccessWithEnoughHope');
} else {
chatMessage = game.i18n.format('DAGGERHEART.UI.Chat.deathMove.riskItAllSuccess', {
hope: config.roll.hope.value
});
this.showRiskItAllButton = true;
this.riskItAllHope = config.roll.hope.value;
this.riskItAllButtonLabel = game.i18n.format('DAGGERHEART.UI.Chat.deathMove.riskItAllDialogButton');
}
}
if (config.roll.result.duality == -1) {
await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id);
chatMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.riskItAllFailure');
}
await config.resourceUpdates.updateResources();
return chatMessage;
}
async handleBlazeOfGlory() {
this.actor.createEmbeddedDocuments('ActiveEffect', [
{
name: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.blazeOfGlory.name'),
description: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.blazeOfGlory.description'),
img: CONFIG.DH.GENERAL.deathMoves.blazeOfGlory.img,
changes: [
{
key: 'system.rules.roll.guaranteedCritical',
mode: 2,
value: 'true'
}
]
}
]);
await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id);
return game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.blazeOfGlory');
}
static selectMove(_, button) {
const move = button.dataset.move;
this.selectedMove = CONFIG.DH.GENERAL.deathMoves[move];
@ -46,23 +156,49 @@ export default class DhpDeathMove extends HandlebarsApplicationMixin(Application
}
static async takeMove() {
this.close();
let result = '';
if (CONFIG.DH.GENERAL.deathMoves.blazeOfGlory === this.selectedMove) {
result = await this.handleBlazeOfGlory();
}
if (CONFIG.DH.GENERAL.deathMoves.avoidDeath === this.selectedMove) {
result = await this.handleAvoidDeath();
}
if (CONFIG.DH.GENERAL.deathMoves.riskItAll === this.selectedMove) {
result = await this.handleRiskItAll();
}
if (!result) return;
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,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/deathMove.hbs',
{
player: this.actor.name,
actor: { name: this.actor.name, img: this.actor.img },
actor: this.actor,
actorId: this.actor._id,
author: game.users.get(game.user.id),
title: game.i18n.localize(this.selectedMove.name),
img: this.selectedMove.img,
description: game.i18n.localize(this.selectedMove.description)
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
}
),
title: game.i18n.localize(
'DAGGERHEART.UI.Chat.deathMove.title'
),
title: game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.title'),
speaker: cls.getSpeaker(),
flags: {
daggerheart: {
@ -72,7 +208,5 @@ export default class DhpDeathMove extends HandlebarsApplicationMixin(Application
};
cls.create(msg);
this.close();
}
}

View file

@ -93,27 +93,29 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
}
getRefreshables() {
const actionItems = this.actor.items.filter(x => this.actor.system.isItemAvailable(x)).reduce((acc, x) => {
if (x.system.actions) {
const recoverable = x.system.actions.reduce((acc, action) => {
if (refreshIsAllowed([this.shortrest ? 'shortRest' : 'longRest'], action.uses.recovery)) {
acc.push({
title: x.name,
name: action.name,
uuid: action.uuid
});
const actionItems = this.actor.items
.filter(x => this.actor.system.isItemAvailable(x))
.reduce((acc, x) => {
if (x.system.actions) {
const recoverable = x.system.actions.reduce((acc, action) => {
if (refreshIsAllowed([this.shortrest ? 'shortRest' : 'longRest'], action.uses.recovery)) {
acc.push({
title: x.name,
name: action.name,
uuid: action.uuid
});
}
return acc;
}, []);
if (recoverable) {
acc.push(...recoverable);
}
return acc;
}, []);
if (recoverable) {
acc.push(...recoverable);
}
}
return acc;
}, []);
return acc;
}, []);
const resourceItems = this.actor.items.reduce((acc, x) => {
if (
x.system.resource &&
@ -189,7 +191,8 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
}));
});
});
const characters = game.actors.filter(x => x.type === 'character')
const characters = game.actors
.filter(x => x.type === 'character')
.filter(x => x.testUserPermission(game.user, 'LIMITED'))
.filter(x => x.uuid !== this.actor.uuid);

View file

@ -0,0 +1,94 @@
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class RiskItAllDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(actor, resourceValue) {
super({});
this.actor = actor;
this.resourceValue = resourceValue;
this.choices = {
hitPoints: 0,
stress: 0
};
}
get title() {
return game.i18n.format('DAGGERHEART.APPLICATIONS.RiskItAllDialog.title', { name: this.actor.name });
}
static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'dh-style', 'dialog', 'views', 'risk-it-all'],
position: { width: 280, height: 'auto' },
window: { icon: 'fa-solid fa-dice fa-xl' },
actions: {
finish: RiskItAllDialog.#finish
}
};
static PARTS = {
application: {
id: 'risk-it-all',
template: 'systems/daggerheart/templates/dialogs/riskItAllDialog.hbs'
}
};
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
for (const input of htmlElement.querySelectorAll('.resource-container input'))
input.addEventListener('change', this.updateChoice.bind(this));
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.resourceValue = this.resourceValue;
context.maxHitPointsValue = Math.min(this.resourceValue, this.actor.system.resources.hitPoints.max);
context.maxStressValue = Math.min(this.resourceValue, this.actor.system.resources.stress.max);
context.remainingResource = this.resourceValue - this.choices.hitPoints - this.choices.stress;
context.unfinished = context.remainingResource !== 0;
context.choices = this.choices;
context.final = {
hitPoints: {
value: this.actor.system.resources.hitPoints.value - this.choices.hitPoints,
max: this.actor.system.resources.hitPoints.max
},
stress: {
value: this.actor.system.resources.stress.value - this.choices.stress,
max: this.actor.system.resources.stress.max
}
};
context;
return context;
}
updateChoice(event) {
let value = Number.parseInt(event.target.value);
const choiceKey = event.target.dataset.choice;
const actorValue = this.actor.system.resources[choiceKey].value;
const remaining = this.resourceValue - this.choices.hitPoints - this.choices.stress;
const changeAmount = value - this.choices[choiceKey];
/* If trying to increase beyond remaining resource points, just increase to max available */
if (remaining - changeAmount < 0) value = this.choices[choiceKey] + remaining;
else if (actorValue - value < 0) value = actorValue;
this.choices[choiceKey] = value;
this.render();
}
static async #finish() {
const resourceUpdate = Object.keys(this.choices).reduce((acc, resourceKey) => {
const value = this.actor.system.resources[resourceKey].value - this.choices[resourceKey];
acc[resourceKey] = { value };
return acc;
}, {});
await this.actor.update({
'system.resources': resourceUpdate
});
this.close();
}
}

View file

@ -5,7 +5,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
classes: ['daggerheart'],
actions: {
combat: DHTokenHUD.#onToggleCombat,
togglePartyTokens: DHTokenHUD.#togglePartyTokens
togglePartyTokens: DHTokenHUD.#togglePartyTokens,
toggleCompanions: DHTokenHUD.#toggleCompanions
}
};
@ -26,7 +27,7 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
context.partyOnCanvas =
this.actor.type === 'party' &&
this.actor.system.partyMembers.some(member => member.getActiveTokens().length > 0);
context.icons.toggleParty = 'systems/daggerheart/assets/icons/arrow-dunk.png';
context.icons.toggleClowncar = 'systems/daggerheart/assets/icons/arrow-dunk.png';
context.actorType = this.actor.type;
context.usesEffects = this.actor.type !== 'party';
context.canToggleCombat = DHTokenHUD.#nonCombatTypes.includes(this.actor.type)
@ -56,6 +57,9 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
}, {})
: null;
context.hasCompanion = this.actor.system.companion;
context.companionOnCanvas = context.hasCompanion && this.actor.system.companion.getActiveTokens().length > 0;
return context;
}
@ -101,8 +105,24 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
: 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.depositPartyTokens'
);
await this.toggleClowncar(this.actor.system.partyMembers);
}
static async #toggleCompanions(_, button) {
const icon = button.querySelector('img');
icon.classList.toggle('flipped');
button.dataset.tooltip = game.i18n.localize(
icon.classList.contains('flipped')
? 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.retrieveCompanionTokens'
: 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.depositCompanionTokens'
);
await this.toggleClowncar([this.actor.system.companion]);
}
async toggleClowncar(actors) {
const animationDuration = 500;
const activeTokens = this.actor.system.partyMembers.flatMap(member => member.getActiveTokens());
const activeTokens = actors.flatMap(member => member.getActiveTokens());
const { x: actorX, y: actorY } = this.document;
if (activeTokens.length > 0) {
for (let token of activeTokens) {
@ -114,14 +134,15 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
}
} else {
const activeScene = game.scenes.find(x => x.id === game.user.viewedScene);
const partyTokenData = [];
for (let member of this.actor.system.partyMembers) {
const tokenData = [];
for (let member of actors) {
const data = await member.getTokenDocument();
partyTokenData.push(data.toObject());
tokenData.push(data.toObject());
}
const newTokens = await activeScene.createEmbeddedDocuments(
'Token',
partyTokenData.map(tokenData => ({
tokenData.map(tokenData => ({
...tokenData,
alpha: 0,
x: actorX,

View file

@ -1,6 +1,6 @@
import BaseLevelUp from './levelup.mjs';
import { defaultCompanionTier, LevelOptionType } from '../../data/levelTier.mjs';
import { DhLevelup } from '../../data/levelup.mjs';
import { DhCompanionLevelup as DhLevelup } from '../../data/companionLevelup.mjs';
import { diceTypes, range } from '../../config/generalConfig.mjs';
export default class DhCompanionLevelUp extends BaseLevelUp {
@ -9,7 +9,9 @@ export default class DhCompanionLevelUp extends BaseLevelUp {
this.levelTiers = this.addBonusChoices(defaultCompanionTier);
const playerLevelupData = actor.system.levelData;
this.levelup = new DhLevelup(DhLevelup.initializeData(this.levelTiers, playerLevelupData));
this.levelup = new DhLevelup(
DhLevelup.initializeData(this.levelTiers, playerLevelupData, actor.system.levelupChoicesLeft)
);
}
async _preparePartContext(partId, context) {

View file

@ -70,7 +70,10 @@ export default class DhlevelUpViewMode extends HandlebarsApplicationMixin(Applic
return checkbox;
});
let label = game.i18n.localize(option.label);
let label =
optionKey === 'domainCard'
? game.i18n.format(option.label, { maxLevel: tier.levels.end })
: game.i18n.localize(option.label);
return {
label: label,
checkboxGroups: chunkify(checkboxes, option.minCost, chunkedBoxes => {

View file

@ -5,10 +5,7 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
super(options);
Hooks.on(socketEvent.Refresh, ({ refreshType }) => {
if (refreshType === RefreshType.Scene) {
this.daggerheartFlag = new game.system.api.data.scenes.DHScene(this.document.flags.daggerheart);
this.render();
}
if (refreshType === RefreshType.Scene) this.render();
});
}
@ -42,7 +39,9 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
async _preRender(context, options) {
await super._preFirstRender(context, options);
this.daggerheartFlag = new game.system.api.data.scenes.DHScene(this.document.flags.daggerheart);
if (!options.internalRefresh)
this.daggerheartFlag = new game.system.api.data.scenes.DHScene(this.document.flags.daggerheart);
}
_attachPartListeners(partId, htmlElement, options) {
@ -52,7 +51,7 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
case 'dh':
htmlElement.querySelector('#rangeMeasurementSetting')?.addEventListener('change', async event => {
this.daggerheartFlag.updateSource({ rangeMeasurement: { setting: event.target.value } });
this.render();
this.render({ internalRefresh: true });
});
const dragArea = htmlElement.querySelector('.scene-environments');
@ -66,10 +65,17 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const item = await foundry.utils.fromUuid(data.uuid);
if (item instanceof game.system.api.documents.DhpActor && item.type === 'environment') {
let sceneUuid = data.uuid;
if (item.pack) {
const inWorldActor = await game.system.api.documents.DhpActor.create([item.toObject()]);
if (!inWorldActor.length) return;
sceneUuid = inWorldActor[0].uuid;
}
await this.daggerheartFlag.updateSource({
sceneEnvironments: [...this.daggerheartFlag.sceneEnvironments, data.uuid]
sceneEnvironments: [...this.daggerheartFlag.sceneEnvironments, sceneUuid]
});
this.render();
this.render({ internalRefresh: true });
}
}
@ -92,12 +98,16 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
(_, index) => index !== Number.parseInt(button.dataset.index)
)
});
this.render();
this.render({ internalRefresh: true });
}
/** @override */
async _processSubmitData(event, form, submitData, options) {
submitData.flags.daggerheart = this.daggerheartFlag.toObject();
submitData.flags.daggerheart.sceneEnvironments = submitData.flags.daggerheart.sceneEnvironments.filter(x =>
foundry.utils.fromUuidSync(x)
);
for (const key of Object.keys(this.document._source.flags.daggerheart?.sceneEnvironments ?? {})) {
if (!submitData.flags.daggerheart.sceneEnvironments[key]) {
submitData.flags.daggerheart.sceneEnvironments[`-=${key}`] = null;

View file

@ -36,7 +36,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
addItem: this.addItem,
editItem: this.editItem,
removeItem: this.removeItem,
resetMoves: this.resetMoves,
resetDowntimeMoves: this.resetDowntimeMoves,
resetItemFeatures: this.resetItemFeatures,
addDomain: this.addDomain,
toggleSelectedDomain: this.toggleSelectedDomain,
deleteDomain: this.deleteDomain,
@ -232,7 +233,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
this.render();
}
static async resetMoves(_, target) {
static async resetDowntimeMoves(_, target) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.SETTINGS.Homebrew.resetMovesTitle', {
@ -266,7 +267,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
...move,
name: game.i18n.localize(move.name),
description: game.i18n.localize(move.description),
actions: move.actions.reduce((acc, key) => {
actions: Object.keys(move.actions).reduce((acc, key) => {
const action = move.actions[key];
acc[key] = {
...action,
@ -293,6 +294,31 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
this.render();
}
static async resetItemFeatures(_, target) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.SETTINGS.Homebrew.resetItemFeaturesTitle', {
type: game.i18n.localize(`DAGGERHEART.GENERAL.${target.dataset.type}`)
})
},
content: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.resetMovesText')
});
if (!confirmed) return;
await this.settings.updateSource({
[`itemFeatures.${target.dataset.type}`]: Object.keys(
this.settings.itemFeatures[target.dataset.type]
).reduce((acc, key) => {
acc[`-=${key}`] = null;
return acc;
}, {})
});
this.render();
}
static async addDomain(event) {
event.preventDefault();
const content = new foundry.data.fields.StringField({

View file

@ -31,6 +31,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
editEffect: this.editEffect,
addDamage: this.addDamage,
removeDamage: this.removeDamage,
editDoc: this.editDoc,
addTrigger: this.addTrigger,
removeTrigger: this.removeTrigger,
expandTrigger: this.expandTrigger
@ -39,7 +40,8 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
}
},
dragDrop: [{ dragSelector: null, dropSelector: '#summon-drop-zone', handlers: ['_onDrop'] }]
};
static PARTS = {
@ -101,7 +103,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
}
};
static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects'];
static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects', 'summon'];
_getTabs(tabs) {
for (const v of Object.values(tabs)) {
@ -112,9 +114,25 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
return tabs;
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement.querySelectorAll('.summon-count-wrapper input').forEach(element => {
element.addEventListener('change', this.updateSummonCount.bind(this));
});
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options, 'action');
context.source = this.action.toObject(true);
context.action = this.action;
context.summons = [];
for (const summon of context.source.summon ?? []) {
const actor = await foundry.utils.fromUuid(summon.actorUUID);
context.summons.push({ actor, count: summon.count });
}
context.openSection = this.openSection;
context.tabs = this._getTabs(this.constructor.TABS);
context.config = CONFIG.DH;
@ -207,8 +225,9 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
}
static async updateForm(event, _, formData) {
const submitData = this._prepareSubmitData(event, formData),
data = foundry.utils.mergeObject(this.action.toObject(), submitData);
const submitData = this._prepareSubmitData(event, formData);
const data = foundry.utils.mergeObject(this.action.toObject(), submitData);
this.action = await this.action.update(data);
this.sheetUpdate?.(this.action);
@ -227,12 +246,26 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
static removeElement(event, button) {
event.stopPropagation();
const data = this.action.toObject(),
key = event.target.closest('[data-key]').dataset.key,
index = button.dataset.index;
key = event.target.closest('[data-key]').dataset.key;
// Prefer explicit index, otherwise find by uuid
let index = button?.dataset.index;
if (index === undefined || index === null || index === '') {
const uuid = button?.dataset.uuid ?? button?.dataset.itemUuid;
index = data[key].findIndex(e => (e?.actorUUID ?? e?.uuid) === uuid);
if (index === -1) return;
} else index = Number(index);
data[key].splice(index, 1);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static async editDoc(_event, target) {
const element = target.closest('[data-item-uuid]');
const doc = (await foundry.utils.fromUuid(element.dataset.itemUuid)) ?? null;
if (doc) return doc.sheet.render({ force: true });
}
static addDamage(_event) {
if (!this.action.damage.parts) return;
const data = this.action.toObject(),
@ -304,6 +337,15 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
}
}
updateSummonCount(event) {
event.stopPropagation();
const wrapper = event.target.closest('.summon-count-wrapper');
const index = wrapper.dataset.index;
const data = this.action.toObject();
data.summon[index].count = event.target.value;
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
/** Specific implementation in extending classes **/
static async addEffect(_event) {}
static removeEffect(_event, _button) {}
@ -313,4 +355,29 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
this.tabGroups.primary = 'base';
await super.close(options);
}
async _onDrop(event) {
const data = foundry.applications.ux.TextEditor.getDragEventData(event);
const item = await foundry.utils.fromUuid(data.uuid);
if (!(item instanceof game.system.api.documents.DhpActor)) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.summon.invalidDrop'));
return;
}
const actionData = this.action.toObject();
let countvalue = 1;
for (const entry of actionData.summon) {
if (entry.actorUUID === data.uuid) {
entry.count += 1;
countvalue = entry.count;
await this.constructor.updateForm.bind(this)(null, null, {
object: foundry.utils.flattenObject(actionData)
});
return;
}
}
actionData.summon.push({ actorUUID: data.uuid, count: countvalue });
await this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(actionData) });
}
}

View file

@ -1,3 +1,4 @@
export * as actors from './actors/_module.mjs';
export * as api from './api/_modules.mjs';
export * as items from './items/_module.mjs';
export * as rollTables from './rollTables/_module.mjs';

View file

@ -31,7 +31,7 @@ export default class AdversarySheet extends DHBaseActorSheet {
dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
dropSelector: null
}
],
]
};
static PARTS = {
@ -185,7 +185,6 @@ export default class AdversarySheet extends DHBaseActorSheet {
super._onDragStart(event);
}
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */

View file

@ -1,5 +1,5 @@
import DHBaseActorSheet from '../api/base-actor.mjs';
import DhpDeathMove from '../../dialogs/deathMove.mjs';
import DhDeathMove from '../../dialogs/deathMove.mjs';
import { abilities } from '../../../config/actorConfig.mjs';
import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
@ -27,6 +27,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
makeDeathMove: CharacterSheet.#makeDeathMove,
levelManagement: CharacterSheet.#levelManagement,
viewLevelups: CharacterSheet.#viewLevelups,
resetCharacter: CharacterSheet.#resetCharacter,
toggleEquipItem: CharacterSheet.#toggleEquipItem,
toggleResourceDice: CharacterSheet.#toggleResourceDice,
handleResourceDice: CharacterSheet.#handleResourceDice,
@ -42,6 +43,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
icon: 'fa-solid fa-angles-up',
label: 'DAGGERHEART.ACTORS.Character.viewLevelups',
action: 'viewLevelups'
},
{
icon: 'fa-solid fa-arrow-rotate-left',
label: 'DAGGERHEART.ACTORS.Character.resetCharacter',
action: 'resetCharacter'
}
]
},
@ -224,13 +230,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
switch (partId) {
case 'header':
const { playerCanEditSheet, levelupAuto } = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Automation
);
context.showSettings = game.user.isGM || !levelupAuto || (levelupAuto && playerCanEditSheet);
break;
case 'loadout':
await this._prepareLoadoutContext(context, options);
break;
@ -679,12 +678,19 @@ export default class CharacterSheet extends DHBaseActorSheet {
new LevelupViewMode(this.document).render({ force: true });
}
/**
* Resets the character data and removes all embedded documents.
*/
static async #resetCharacter() {
new game.system.api.applications.dialogs.CharacterResetDialog(this.document).render({ force: true });
}
/**
* Opens the Death Move interface for the character.
* @type {ApplicationClickAction}
*/
static async #makeDeathMove() {
await new DhpDeathMove(this.document).render({ force: true });
await new DhDeathMove(this.document).render({ force: true });
}
/**
@ -725,8 +731,10 @@ export default class CharacterSheet extends DHBaseActorSheet {
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: abilityLabel
}),
effects: await game.system.api.data.actions.actionsTypes.base.getEffects(this.document),
roll: {
trait: button.dataset.attribute
trait: button.dataset.attribute,
type: 'trait'
},
hasRoll: true,
actionType: 'action',
@ -736,11 +744,12 @@ export default class CharacterSheet extends DHBaseActorSheet {
})
};
const result = await this.document.diceRoll(config);
if (!result) return;
/* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */
const costResources = result.costs
.filter(x => x.enabled)
.map(cost => ({ ...cost, value: -cost.value, total: -cost.total }));
const costResources =
result.costs?.filter(x => x.enabled).map(cost => ({ ...cost, value: -cost.value, total: -cost.total })) ||
{};
config.resourceUpdates.addResources(costResources);
await config.resourceUpdates.updateResources();
}
@ -840,7 +849,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
static async #toggleVault(_event, button) {
const doc = await getDocFromElement(button);
const { available } = this.document.system.loadoutSlot;
if (doc.system.inVault && !available) {
if (doc.system.inVault && !available && !doc.system.loadoutIgnore) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.loadoutMaxReached'));
}
@ -971,6 +980,18 @@ export default class CharacterSheet extends DHBaseActorSheet {
return this._onSidebarDrop(event, item);
}
const setupCriticalItemTypes = ['class', 'subclass', 'ancestry', 'community'];
if (this.document.system.needsCharacterSetup && setupCriticalItemTypes.includes(item.type)) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.CharacterCreation.setupSkipTitle')
},
content: game.i18n.localize('DAGGERHEART.APPLICATIONS.CharacterCreation.setupSkipContent')
});
if (!confirmed) return;
}
if (this.document.uuid === item.parent?.uuid) {
return super._onDropItem(event, item);
}

View file

@ -38,15 +38,6 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
}
};
/** @inheritDoc */
async _onRender(context, options) {
await super._onRender(context, options);
this.element
.querySelector('.level-value')
?.addEventListener('change', event => this.document.updateLevel(Number(event.currentTarget.value)));
}
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
@ -71,10 +62,10 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
title: `${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}: ${this.actor.name}`,
headerTitle: `Companion ${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}`,
roll: {
trait: partner.system.spellcastModifierTrait?.key
trait: partner.system.spellcastModifierTrait?.key,
companionRoll: true
},
hasRoll: true,
data: partner.getRollData()
hasRoll: true
};
const result = await partner.diceRoll(config);

View file

@ -505,6 +505,10 @@ export default function DHApplicationMixin(Base) {
const doc = await getDocFromElement(target),
action = doc?.system?.attack ?? doc;
const config = action.prepareConfig(event);
config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(
this.document,
doc
);
config.hasRoll = false;
return action && action.workflow.get('damage').execute(config, null, true);
}
@ -629,7 +633,7 @@ export default function DHApplicationMixin(Base) {
{
relativeTo: isAction ? doc.parent : doc,
rollData: doc.getRollData?.(),
secrets: isAction ? doc.parent.isOwner : doc.isOwner
secrets: isAction ? doc.parent.parent.isOwner : doc.isOwner
}
);
}

View file

@ -0,0 +1 @@
export { default as RollTableSheet } from './rollTable.mjs';

View file

@ -0,0 +1,191 @@
export default class DhRollTableSheet extends foundry.applications.sheets.RollTableSheet {
static DEFAULT_OPTIONS = {
...super.DEFAULT_OPTIONS,
actions: {
changeMode: DhRollTableSheet.#onChangeMode,
drawResult: DhRollTableSheet.#onDrawResult,
resetResults: DhRollTableSheet.#onResetResults,
addFormula: DhRollTableSheet.#addFormula,
removeFormula: DhRollTableSheet.#removeFormula
}
};
static buildParts() {
const { footer, header, sheet, results, ...parts } = super.PARTS;
return {
sheet: {
...sheet,
template: 'systems/daggerheart/templates/sheets/rollTable/sheet.hbs'
},
header: { template: 'systems/daggerheart/templates/sheets/rollTable/header.hbs' },
...parts,
results: {
template: 'systems/daggerheart/templates/sheets/rollTable/results.hbs',
templates: ['templates/sheets/roll-table/result-details.hbs'],
scrollable: ['table[data-results] tbody']
},
summary: { template: 'systems/daggerheart/templates/sheets/rollTable/summary.hbs' },
footer
};
}
static PARTS = DhRollTableSheet.buildParts();
async _preRender(context, options) {
await super._preRender(context, options);
if (!options.internalRefresh)
this.daggerheartFlag = new game.system.api.data.DhRollTable(this.document.flags.daggerheart);
}
/* root PART has a blank element on _attachPartListeners, so it cannot be used to set the eventListeners for the view mode */
async _onRender(context, options) {
super._onRender(context, options);
for (const element of this.element.querySelectorAll('.system-update-field'))
element.addEventListener('change', this.updateSystemField.bind(this));
}
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
switch (partId) {
case 'sheet':
context.altFormula = this.daggerheartFlag.altFormula;
context.usesAltFormula = Object.keys(this.daggerheartFlag.altFormula).length > 0;
context.altFormulaOptions = {
'': { name: this.daggerheartFlag.formulaName },
...this.daggerheartFlag.altFormula
};
context.activeAltFormula = this.daggerheartFlag.activeAltFormula;
context.selectedFormula = this.daggerheartFlag.getActiveFormula(this.document.formula);
context.results = this.getExtendedResults(context.results);
break;
case 'header':
context.altFormula = this.daggerheartFlag.altFormula;
context.usesAltFormula = Object.keys(this.daggerheartFlag.altFormula).length > 0;
context.altFormulaOptions = {
'': { name: this.daggerheartFlag.formulaName },
...this.daggerheartFlag.altFormula
};
context.activeAltFormula = this.daggerheartFlag.activeAltFormula;
break;
case 'summary':
context.systemFields = this.daggerheartFlag.schema.fields;
context.altFormula = this.daggerheartFlag.altFormula;
context.formulaName = this.daggerheartFlag.formulaName;
break;
case 'results':
context.results = this.getExtendedResults(context.results);
break;
}
return context;
}
getExtendedResults(results) {
const bodyDarkMode = document.body.classList.contains('theme-dark');
const elementLightMode = this.element.classList.contains('theme-light');
const elementDarkMode = this.element.classList.contains('theme-dark');
const isDarkMode = elementDarkMode || (!elementLightMode && bodyDarkMode);
return results.map(x => ({
...x,
displayImg: isDarkMode && x.img === 'icons/svg/d20-black.svg' ? 'icons/svg/d20.svg' : x.img
}));
}
/* -------------------------------------------- */
/* Flag SystemData update methods */
/* -------------------------------------------- */
async updateSystemField(event) {
const { dataset, value } = event.target;
await this.daggerheartFlag.updateSource({ [dataset.path]: value });
this.render({ internalRefresh: true });
}
getSystemFlagUpdate() {
const deleteUpdate = Object.keys(this.document._source.flags.daggerheart?.altFormula ?? {}).reduce(
(acc, formulaKey) => {
if (!this.daggerheartFlag.altFormula[formulaKey]) acc.altFormula[`-=${formulaKey}`] = null;
return acc;
},
{ altFormula: {} }
);
return { ['flags.daggerheart']: foundry.utils.mergeObject(this.daggerheartFlag.toObject(), deleteUpdate) };
}
static async #addFormula() {
await this.daggerheartFlag.updateSource({
[`altFormula.${foundry.utils.randomID()}`]: game.system.api.data.DhRollTable.getDefaultFormula()
});
this.render({ internalRefresh: true });
}
static async #removeFormula(_event, target) {
await this.daggerheartFlag.updateSource({
[`altFormula.-=${target.dataset.key}`]: null
});
this.render({ internalRefresh: true });
}
/* -------------------------------------------- */
/* Extended RollTable methods */
/* -------------------------------------------- */
/**
* Alternate between view and edit modes.
* @this {RollTableSheet}
* @type {ApplicationClickAction}
*/
static async #onChangeMode() {
this.mode = this.isEditMode ? 'view' : 'edit';
await this.document.update(this.getSystemFlagUpdate());
await this.render({ internalRefresh: true });
}
/** @inheritdoc */
async _processSubmitData(event, form, submitData, options) {
/* RollTable sends an empty dummy event when swapping from view/edit first time */
if (Object.keys(submitData).length) {
if (!submitData.flags) submitData.flags = { daggerheart: {} };
submitData.flags.daggerheart = this.getSystemFlagUpdate();
}
super._processSubmitData(event, form, submitData, options);
}
/** @inheritdoc */
static async #onResetResults() {
await this.document.update(this.getSystemFlagUpdate());
await this.document.resetResults();
}
/**
* Roll and draw a TableResult.
* @this {RollTableSheet}
* @type {ApplicationClickAction}
*/
static async #onDrawResult(_event, button) {
if (this.form) await this.submit({ operation: { render: false } });
button.disabled = true;
const table = this.document;
await this.document.update(this.getSystemFlagUpdate());
/* Sending in the currently selectd activeFormula to table.roll to use as the formula */
const selectedFormula = this.daggerheartFlag.getActiveFormula(this.document.formula);
const tableRoll = await table.roll({ selectedFormula });
const draws = table.getResultsForRoll(tableRoll.roll.total);
if (draws.length > 0) {
if (game.settings.get('core', 'animateRollTable')) await this._animateRoll(draws);
await table.draw(tableRoll);
}
// Reenable the button if drawing with replacement since the draw won't trigger a sheet re-render
if (table.replacement) button.disabled = false;
}
}

View file

@ -25,7 +25,7 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
/** @override */
static DEFAULT_OPTIONS = {
classes: ['dh-style'],
classes: ['dh-style', 'directory'],
window: {
title: 'SIDEBAR.TabSettings'
},

View file

@ -81,6 +81,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
html.querySelectorAll('.group-roll-header-expand-section').forEach(element =>
element.addEventListener('click', this.groupRollExpandSection)
);
html.querySelectorAll('.risk-it-all-button').forEach(element =>
element.addEventListener('click', event => this.riskItAllClearStressAndHitPoints(event, data))
);
};
setupHooks() {
@ -92,6 +95,21 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
super.close(options);
}
/** Ensure the chat theme inherits the interface theme */
_replaceHTML(result, content, options) {
const themedElement = result.log?.querySelector('.chat-log');
themedElement?.classList.remove('themed', 'theme-light', 'theme-dark');
super._replaceHTML(result, content, options);
}
/** Remove chat log theme from notifications area */
async _onFirstRender(result, content) {
await super._onFirstRender(result, content);
document
.querySelector('#chat-notifications .chat-log')
?.classList.remove('themed', 'theme-light', 'theme-dark');
}
async onRollSimple(event, message) {
const buttonType = event.target.dataset.type ?? 'damage',
total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0),
@ -135,7 +153,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
async actionUseButton(event, message) {
const { moveIndex, actionIndex, movePath } = event.currentTarget.dataset;
const targetUuid = event.currentTarget.closest('.action-use-button-parent').querySelector('select')?.value;
const parent = await foundry.utils.fromUuid(targetUuid || message.system.actor)
const parent = await foundry.utils.fromUuid(targetUuid || message.system.actor);
const actionType = message.system.moves[moveIndex].actions[actionIndex];
const cls = game.system.api.models.actions.actionsTypes[actionType.type];
@ -370,4 +388,10 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
});
event.target.closest('.group-roll-section').querySelector('.group-roll-content').classList.toggle('closed');
}
async riskItAllClearStressAndHitPoints(event, data) {
const resourceValue = event.target.dataset.resourceValue;
const actor = game.actors.get(event.target.dataset.actorId);
new game.system.api.applications.dialogs.RiskItAllDialog(actor, resourceValue).render({ force: true });
}
}

View file

@ -42,8 +42,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
this.combats
.find(x => x.active)
?.system?.extendedBattleToggles?.reduce((acc, toggle) => (acc ?? 0) + toggle.category, null) ?? null;
const maxBP = CONFIG.DH.ENCOUNTER.BaseBPPerEncounter(context.characters.length) + modifierBP;
const currentBP = AdversaryBPPerEncounter(context.adversaries, context.characters);
const maxBP = CONFIG.DH.ENCOUNTER.BaseBPPerEncounter(context.allCharacters.length) + modifierBP;
const currentBP = AdversaryBPPerEncounter(context.adversaries, context.allCharacters);
Object.assign(context, {
fear: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
@ -73,9 +73,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
Object.assign(context, {
actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens,
adversaries,
characters: characters
?.filter(x => !x.isNPC)
.filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0),
allCharacters: characters,
characters: characters.filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0),
spotlightRequests
});
}

View file

@ -76,6 +76,8 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica
};
toggleHidden(token, focused) {
if (!this.element) return;
const effects = DhEffectsDisplay.getTokenEffects(focused ? token : null);
this.element.hidden = effects.length === 0;

View file

@ -230,6 +230,14 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
result.flatMap(r => r),
'name'
);
/* If any noticeable slowdown occurs, consider replacing with enriching description on clicking to expand descriptions */
for (const item of this.items) {
item.system.enrichedDescription =
(await item.system.getEnrichedDescription?.()) ??
(await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.description));
}
this.fieldFilter = this._createFieldFilter();
if (this.presets?.filter) {

View file

@ -31,7 +31,7 @@ export default class DhSceneNavigation extends foundry.applications.ui.SceneNavi
const environments = daggerheartInfo.sceneEnvironments.filter(
x => x && x.testUserPermission(game.user, 'LIMITED')
);
const hasEnvironments = environments.length > 0;
const hasEnvironments = environments.length > 0 && x.isView;
return {
...x,
hasEnvironments,

View file

@ -1 +1,2 @@
export * as placeables from './placeables/_module.mjs';
export { default as DhTokenLayer } from './tokens.mjs';

View file

@ -1,4 +1,12 @@
export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
/** @inheritdoc */
async _draw(options) {
await super._draw(options);
if (this.document.flags.daggerheart?.createPlacement)
this.previewHelp ||= this.addChild(this.#drawPreviewHelp());
}
/** @inheritDoc */
async _drawEffects() {
this.effects.renderable = false;
@ -34,7 +42,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
this.renderFlags.set({ refreshEffects: true });
}
/**
/**
* Returns the distance from this token to another token object.
* This value is corrected to handle alternate token sizes and other grid types
* according to the diagonal rules.
@ -47,11 +55,11 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
const destinationPoint = target.center;
// Compute for gridless. This version returns circular edge to edge + grid distance,
// so that tokens that are touching return 5.
// so that tokens that are touching return 5.
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
const boundsCorrection = canvas.grid.distance / canvas.grid.size;
const originRadius = this.bounds.width * boundsCorrection / 2;
const targetRadius = target.bounds.width * boundsCorrection / 2;
const 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;
}
@ -61,11 +69,11 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
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)
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)
y: targetEdge.y + Math.sign(destinationPoint.y - targetEdge.y)
});
return canvas.grid.measurePath([adjustedOriginPoint, adjustDestinationPoint]).distance;
}
@ -94,7 +102,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
/** 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);
return this.distanceTo(token) <= canvas.grid.distance * 1.5;
}
/** @inheritDoc */
@ -132,4 +140,25 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
bar.position.set(0, posY);
return true;
}
/**
* Draw a helptext for previews as a text object
* @returns {PreciseText} The Text object for the preview helper
*/
#drawPreviewHelp() {
const { uiScale } = canvas.dimensions;
const textStyle = CONFIG.canvasTextStyle.clone();
textStyle.fontSize = 18;
textStyle.wordWrapWidth = this.w * 2.5;
textStyle.fontStyle = 'italic';
const helpText = new foundry.canvas.containers.PreciseText(
`(${game.i18n.localize('DAGGERHEART.UI.Tooltip.previewTokenHelp')})`,
textStyle
);
helpText.anchor.set(helpText.width / 900, 1);
helpText.scale.set(uiScale, uiScale);
return helpText;
}
}

16
module/canvas/tokens.mjs Normal file
View file

@ -0,0 +1,16 @@
export default class DhTokenLayer extends foundry.canvas.layers.TokenLayer {
async _createPreview(createData, options) {
if (options.actor) {
const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes;
if (options.actor?.system.metadata.usesSize) {
const tokenSize = tokenSizes[options.actor.system.size];
if (tokenSize && options.actor.system.size !== CONFIG.DH.ACTOR.tokenSize.custom.id) {
createData.width = tokenSize;
createData.height = tokenSize;
}
}
}
return super._createPreview(createData, options);
}
}

View file

@ -171,7 +171,7 @@ export const defeatedConditions = () => {
acc[key] = {
...choice,
img: defeated[`${choice.id}Icon`],
description: `DAGGERHEART.CONFIG.Condition.${choice.id}.description`
description: game.i18n.localize(`DAGGERHEART.CONFIG.Condition.${choice.id}.description`)
};
return acc;
@ -179,6 +179,10 @@ export const defeatedConditions = () => {
};
export const defeatedConditionChoices = {
deathMove: {
id: 'deathMove',
name: 'DAGGERHEART.CONFIG.Condition.deathMove.name'
},
defeated: {
id: 'defeated',
name: 'DAGGERHEART.CONFIG.Condition.defeated.name'
@ -496,6 +500,8 @@ export const diceTypes = {
d20: 'd20'
};
export const dieFaces = [4, 6, 8, 10, 12, 20];
export const multiplierTypes = {
prof: 'Proficiency',
cast: 'Spellcast',

View file

@ -1,6 +1,8 @@
export { default as DhCombat } from './combat.mjs';
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 * as countdowns from './countdowns.mjs';
export * as actions from './action/_module.mjs';

View file

@ -36,6 +36,7 @@ export default class DHAttackAction extends DHDamageAction {
async use(event, options) {
const result = await super.use(event, options);
if (!result.message) return;
if (result.message.system.action.roll?.type === 'attack') {
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;

View file

@ -166,7 +166,6 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
*/
getRollData(data = {}) {
const actorData = this.actor ? this.actor.getRollData(false) : {};
actorData.result = data.roll?.total ?? 1;
actorData.scale = data.costs?.length // Right now only return the first scalable cost.
? (data.costs.find(c => c.scalable)?.total ?? 1)
@ -199,6 +198,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
let config = this.prepareConfig(event);
if (!config) return;
config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(this.actor, this.item);
if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return;
// Display configuration window if necessary
@ -240,6 +241,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
hasHealing: this.hasHealing,
hasEffect: this.hasEffect,
hasSave: this.hasSave,
onSave: this.save?.damageMod,
isDirect: !!this.damage?.direct,
selectedRollMode: game.settings.get('core', 'rollMode'),
data: this.getRollData(),
@ -265,6 +267,28 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return config;
}
/**
* Get the all potentially applicable effects on the actor
* @param {DHActor} actor The actor performing the action
* @param {DHItem|DhActor} effectParent The parent of the effect
* @returns {DhActiveEffect[]}
*/
static async getEffects(actor, effectParent) {
if (!actor) return [];
return Array.from(await actor.allApplicableEffects()).filter(effect => {
/* Effects on weapons only ever apply for the weapon itself */
if (effect.parent.type === 'weapon') {
/* Unless they're secondary - then they apply only to other primary weapons */
if (effect.parent.system.secondary) {
if (effectParent?.type !== 'weapon' || effectParent?.system.secondary) return false;
} else if (effectParent?.id !== effect.parent.id) return false;
}
return !effect.isSuppressed;
});
}
/**
* Method used to know if a configuration dialog must be shown or not when there is no roll.
* @param {*} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
@ -353,14 +377,14 @@ export class ResourceUpdateMap extends Map {
if (!resource.key) continue;
const existing = this.get(resource.key);
if (existing) {
if (!existing || resource.clear) {
this.set(resource.key, resource);
} else if (!existing?.clear) {
this.set(resource.key, {
...existing,
value: existing.value + (resource.value ?? 0),
total: existing.total + (resource.total ?? 0)
});
} else {
this.set(resource.key, resource);
}
}
}

View file

@ -1,19 +1,5 @@
import DHBaseAction from './baseAction.mjs';
export default class DHSummonAction extends DHBaseAction {
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
documentUUID: new fields.DocumentUUIDField({ type: 'Actor' })
};
}
async trigger(event, ...args) {
if (!this.canSummon || !canvas.scene) return;
}
get canSummon() {
return game.user.can('TOKEN_CREATE');
}
static extraSchemas = [...super.extraSchemas, 'summon'];
}

View file

@ -19,6 +19,7 @@ export default class BeastformEffect extends BaseEffect {
base64: false
}),
tokenSize: new fields.SchemaField({
scale: new fields.NumberField({ nullable: false, initial: 1 }),
height: new fields.NumberField({ integer: false, nullable: true }),
width: new fields.NumberField({ integer: false, nullable: true })
})
@ -55,7 +56,9 @@ export default class BeastformEffect extends BaseEffect {
const update = {
...baseUpdate,
texture: {
src: this.characterTokenData.tokenImg
src: this.characterTokenData.tokenImg,
scaleX: this.characterTokenData.tokenSize.scale,
scaleY: this.characterTokenData.tokenSize.scale
},
ring: {
enabled: this.characterTokenData.usesDynamicToken,
@ -86,7 +89,9 @@ export default class BeastformEffect extends BaseEffect {
y,
'texture': {
enabled: this.characterTokenData.usesDynamicToken,
src: token.flags.daggerheart?.beastformTokenImg ?? this.characterTokenData.tokenImg
src: token.flags.daggerheart?.beastformTokenImg ?? this.characterTokenData.tokenImg,
scaleX: this.characterTokenData.tokenSize.scale,
scaleY: this.characterTokenData.tokenSize.scale
},
'ring': {
subject: {

View file

@ -27,7 +27,7 @@ const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) =>
});
/* Common rules applying to Characters and Adversaries */
export const commonActorRules = (extendedData = { damageReduction: {} }) => ({
export const commonActorRules = (extendedData = { damageReduction: {}, attack: { damage: {} } }) => ({
conditionImmunities: new fields.SchemaField({
hidden: new fields.BooleanField({ initial: false }),
restrained: new fields.BooleanField({ initial: false }),
@ -41,7 +41,23 @@ export const commonActorRules = (extendedData = { damageReduction: {} }) => ({
magical: new fields.NumberField({ initial: 0, min: 0 }),
physical: new fields.NumberField({ initial: 0, min: 0 })
}),
...extendedData.damageReduction
...(extendedData.damageReduction ?? {})
}),
attack: new fields.SchemaField({
...extendedData.attack,
damage: new fields.SchemaField({
hpDamageMultiplier: new fields.NumberField({
required: true,
nullable: false,
initial: 1
}),
hpDamageTakenMultiplier: new fields.NumberField({
required: true,
nullable: false,
initial: 1
}),
...(extendedData.attack?.damage ?? {})
})
})
});

View file

@ -36,7 +36,14 @@ export default class DhCharacter extends BaseDataActor {
'DAGGERHEART.ACTORS.Character.maxHPBonus'
),
stress: resourceField(6, 0, 'DAGGERHEART.GENERAL.stress', true),
hope: resourceField(6, 2, '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'),
@ -79,12 +86,7 @@ export default class DhCharacter extends BaseDataActor {
bags: new fields.NumberField({ initial: 0, integer: true }),
chests: new fields.NumberField({ initial: 0, integer: true })
}),
scars: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({}),
description: new fields.StringField()
})
),
scars: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.scars' }),
biography: new fields.SchemaField({
background: new fields.HTMLField(),
connections: new fields.HTMLField(),
@ -252,38 +254,59 @@ export default class DhCharacter extends BaseDataActor {
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint'
}),
disabledArmor: new fields.BooleanField({ intial: false })
},
attack: {
damage: {
diceIndex: new fields.NumberField({
integer: true,
min: 0,
max: 5,
initial: 0,
label: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.label',
hint: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.hint'
}),
bonus: new fields.NumberField({
required: true,
initial: 0,
min: 0,
label: 'DAGGERHEART.GENERAL.Rules.attack.damage.bonus.label'
})
},
roll: new fields.SchemaField({
trait: new fields.StringField({
required: true,
choices: CONFIG.DH.ACTOR.abilities,
nullable: true,
initial: null,
label: 'DAGGERHEART.GENERAL.Rules.attack.roll.trait.label'
})
})
}
}),
attack: new fields.SchemaField({
damage: new fields.SchemaField({
diceIndex: new fields.NumberField({
integer: true,
min: 0,
max: 5,
initial: 0,
label: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.label',
hint: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.hint'
}),
bonus: new fields.NumberField({
required: true,
initial: 0,
min: 0,
label: 'DAGGERHEART.GENERAL.Rules.attack.damage.bonus.label'
})
dualityRoll: new fields.SchemaField({
defaultHopeDice: new fields.NumberField({
nullable: false,
required: true,
integer: true,
choices: CONFIG.DH.GENERAL.dieFaces,
initial: 12,
label: 'DAGGERHEART.ACTORS.Character.defaultHopeDice'
}),
roll: new fields.SchemaField({
trait: new fields.StringField({
required: true,
choices: CONFIG.DH.ACTOR.abilities,
nullable: true,
initial: null,
label: 'DAGGERHEART.GENERAL.Rules.attack.roll.trait.label'
})
defaultFearDice: new fields.NumberField({
nullable: false,
required: true,
integer: true,
choices: CONFIG.DH.GENERAL.dieFaces,
initial: 12,
label: 'DAGGERHEART.ACTORS.Character.defaultFearDice'
})
}),
runeWard: new fields.BooleanField({ initial: false }),
burden: new fields.SchemaField({
ignore: new fields.BooleanField()
}),
roll: new fields.SchemaField({
guaranteedCritical: new fields.BooleanField()
})
}),
sidebarFavorites: new ForeignDocumentUUIDArrayField({ type: 'Item' })
@ -347,7 +370,7 @@ export default class DhCharacter extends BaseDataActor {
const modifiers = subClasses
?.map(sc => ({ ...this.traits[sc.system.spellcastingTrait], key: sc.system.spellcastingTrait }))
.filter(x => x);
return modifiers.sort((a, b) => a.value - b.value)[0];
return modifiers.sort((a, b) => (b.value ?? 0) - (a.value ?? 0))[0];
}
get spellcastModifier() {
@ -528,7 +551,18 @@ export default class DhCharacter extends BaseDataActor {
}
get deathMoveViable() {
return this.resources.hitPoints.max > 0 && this.resources.hitPoints.value >= this.resources.hitPoints.max;
const { characterDefault } = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Automation
).defeated;
const deathMoveOutcomeStatuses = Object.keys(CONFIG.DH.GENERAL.defeatedConditionChoices).filter(
key => key !== characterDefault
);
const deathMoveNotResolved = this.parent.statuses.every(status => !deathMoveOutcomeStatuses.includes(status));
const allHitPointsMarked =
this.resources.hitPoints.max > 0 && this.resources.hitPoints.value >= this.resources.hitPoints.max;
return deathMoveNotResolved && allHitPointsMarked;
}
get armorApplicableDamageTypes() {
@ -626,8 +660,15 @@ export default class DhCharacter extends BaseDataActor {
? armor.system.baseThresholds.severe + this.levelData.level.current
: this.levelData.level.current * 2
};
this.resources.hope.max -= Object.keys(this.scars).length;
const globalHopeMax = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxHope;
this.resources.hope.max = globalHopeMax - this.scars;
this.resources.hitPoints.max += this.class.value?.system?.hitPoints ?? 0;
/* Companion Related Data */
this.companionData = {
levelupChoices: this.levelData.level.current - 1
};
}
prepareDerivedData() {
@ -683,6 +724,30 @@ export default class DhCharacter extends BaseDataActor {
changes.system.experiences[experience].core = true;
}
}
/* Scars can alter the amount of current hope */
if (changes.system?.scars) {
const diff = this.system.scars - changes.system.scars;
const newHopeMax = this.system.resources.hope.max + diff;
const newHopeValue = Math.min(newHopeMax, this.system.resources.hope.value);
if (newHopeValue != this.system.resources.hope.value) {
if (!changes.system.resources) changes.system.resources = { hope: { value: 0 } };
changes.system.resources.hope = {
...changes.system.resources.hope,
value: changes.system.resources.hope.value + newHopeValue
};
}
}
/* Force companion data prep */
if (this.companion) {
if (
changes.system?.levelData?.level?.current !== undefined &&
changes.system.levelData.level.current !== this._source.levelData.level.current
) {
this.companion.update(this.companion.toObject(), { diff: false, recursive: false });
}
}
}
async _preDelete() {
@ -698,4 +763,11 @@ export default class DhCharacter extends BaseDataActor {
t => !!t
);
}
static migrateData(source) {
if (typeof source.scars === 'object') source.scars = 0;
if (source.resources?.hope?.max) source.scars = Math.max(6 - source.resources.hope.max, 0);
return super.migrateData(source);
}
}

View file

@ -108,7 +108,11 @@ export default class DhCompanion extends BaseDataActor {
get proficiency() {
return this.partner?.system?.proficiency ?? 1;
}
get canLevelUp() {
return this.levelupChoicesLeft > 0;
}
isItemValid() {
return false;
}
@ -127,7 +131,7 @@ export default class DhCompanion extends BaseDataActor {
if (selection.data[0] === 'damage') {
this.attack.damage.parts[0].value.dice = adjustDice(this.attack.damage.parts[0].value.dice);
} else {
this.attack.range = adjustRange(this.attack.range);
this.attack.range = adjustRange(this.attack.range).id;
}
break;
case 'stress':
@ -147,6 +151,17 @@ export default class DhCompanion extends BaseDataActor {
}
}
prepareDerivedData() {
/* Partner Related Setup */
if (this.partner) {
this.levelData.level.changed = this.partner.system.levelData.level.current;
this.levelupChoicesLeft = Object.values(this.levelData.levelups).reduce((acc, curr) => {
acc = Math.max(acc - curr.selections.length, 0);
return acc;
}, this.partner.system.companionData.levelupChoices);
}
}
async _preUpdate(changes, options, userId) {
const allowed = await super._preUpdate(changes, options, userId);
if (allowed === false) return;
@ -162,6 +177,16 @@ export default class DhCompanion extends BaseDataActor {
changes.system.experiences[experience].core = true;
}
}
/* Force partner data prep */
if (this.partner) {
if (
changes.system?.levelData?.level?.current !== undefined &&
changes.system.levelData.level.current !== this._source.levelData.level.current
) {
this.partner.update(this.partner.toObject(), { diff: false, recursive: false });
}
}
}
async _preDelete() {

View file

@ -8,6 +8,7 @@ export const config = {
adversaryRoll: DHActorRoll,
damageRoll: DHActorRoll,
dualityRoll: DHActorRoll,
fateRoll: DHActorRoll,
groupRoll: DHGroupRoll,
systemMessage: DHSystemMessage
};

View file

@ -0,0 +1,370 @@
import { abilities } from '../config/actorConfig.mjs';
import { chunkify } from '../helpers/utils.mjs';
import { LevelOptionType } from './levelTier.mjs';
export class DhCompanionLevelup extends foundry.abstract.DataModel {
static initializeData(levelTierData, pcLevelData, origChoicesLeft) {
let choicesLeft = origChoicesLeft;
const { current, changed } = pcLevelData.level;
const bonusChoicesOnly = current === changed;
const startLevel = bonusChoicesOnly ? current : current + 1;
const endLevel = bonusChoicesOnly ? startLevel : changed;
const tiers = {};
const levels = {};
const tierKeys = Object.keys(levelTierData.tiers);
tierKeys.forEach(key => {
const tier = levelTierData.tiers[key];
const belongingLevels = [];
for (var i = tier.levels.start; i <= tier.levels.end; i++) {
if (i <= endLevel) {
const initialAchievements = i === tier.levels.start ? tier.initialAchievements : {};
const experiences = initialAchievements.experience
? [...Array(initialAchievements.experience.nr).keys()].reduce((acc, _) => {
acc[foundry.utils.randomID()] = {
name: '',
modifier: initialAchievements.experience.modifier
};
return acc;
}, {})
: {};
const currentChoices = pcLevelData.levelups[i]?.selections?.length;
const maxSelections =
i === endLevel
? choicesLeft + (currentChoices ?? 0)
: (currentChoices ?? tier.maxSelections[i]);
if (!pcLevelData.levelups[i]) choicesLeft -= maxSelections;
levels[i] = DhLevelupLevel.initializeData(pcLevelData.levelups[i], maxSelections, {
...initialAchievements,
experiences,
domainCards: {}
});
}
belongingLevels.push(i);
}
/* Improve. Temporary handling for Companion new experiences */
Object.keys(tier.extraAchievements ?? {}).forEach(key => {
const level = Number(key);
if (level >= startLevel && level <= endLevel) {
const levelExtras = tier.extraAchievements[level];
if (levelExtras.experience) {
levels[level].achievements.experiences[foundry.utils.randomID()] = {
name: '',
modifier: levelExtras.experience.modifier
};
}
}
});
tiers[key] = {
name: tier.name,
belongingLevels: belongingLevels,
options: Object.keys(tier.options).reduce((acc, key) => {
acc[key] = tier.options[key].toObject?.() ?? tier.options[key];
return acc;
}, {})
};
});
return {
tiers,
levels,
startLevel,
currentLevel: startLevel,
endLevel
};
}
static defineSchema() {
const fields = foundry.data.fields;
return {
tiers: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({ required: true }),
belongingLevels: new fields.ArrayField(new fields.NumberField({ required: true, integer: true })),
options: new fields.TypedObjectField(
new fields.SchemaField({
label: new fields.StringField({ required: true }),
checkboxSelections: new fields.NumberField({ required: true, integer: true }),
minCost: new fields.NumberField({ required: true, integer: true }),
type: new fields.StringField({ required: true, choices: LevelOptionType }),
value: new fields.NumberField({ integer: true }),
amount: new fields.NumberField({ integer: true })
})
)
})
),
levels: new fields.TypedObjectField(new fields.EmbeddedDataField(DhLevelupLevel)),
startLevel: new fields.NumberField({ required: true, integer: true }),
currentLevel: new fields.NumberField({ required: true, integer: true }),
endLevel: new fields.NumberField({ required: true, integer: true })
};
}
#levelFinished(levelKey) {
const allSelectionsMade = this.levels[levelKey].nrSelections.available === 0;
const allChoicesMade = Object.keys(this.levels[levelKey].choices).every(choiceKey => {
const choice = this.levels[levelKey].choices[choiceKey];
return Object.values(choice).every(checkbox => {
switch (choiceKey) {
case 'trait':
case 'experience':
case 'domainCard':
case 'subclass':
case 'vicious':
return checkbox.data.length === (checkbox.amount ?? 1);
case 'multiclass':
const classSelected = checkbox.data.length === 1;
const domainSelected = checkbox.secondaryData.domain;
const subclassSelected = checkbox.secondaryData.subclass;
return classSelected && domainSelected && subclassSelected;
default:
return true;
}
});
});
const experiencesSelected = !this.levels[levelKey].achievements.experiences
? true
: Object.values(this.levels[levelKey].achievements.experiences).every(exp => exp.name);
const domainCardsSelected = Object.values(this.levels[levelKey].achievements.domainCards)
.filter(x => x.level <= this.endLevel)
.every(card => card.uuid);
const allAchievementsSelected = experiencesSelected && domainCardsSelected;
return allSelectionsMade && allChoicesMade && allAchievementsSelected;
}
get currentLevelFinished() {
return this.#levelFinished(this.currentLevel);
}
get allLevelsFinished() {
return Object.keys(this.levels)
.filter(level => Number(level) >= this.startLevel)
.every(this.#levelFinished.bind(this));
}
get unmarkedTraits() {
const possibleLevels = Object.values(this.tiers).reduce((acc, tier) => {
if (tier.belongingLevels.includes(this.currentLevel)) acc = tier.belongingLevels;
return acc;
}, []);
return Object.keys(this.levels)
.filter(key => possibleLevels.some(x => x === Number(key)))
.reduce(
(acc, levelKey) => {
const level = this.levels[levelKey];
Object.values(level.choices).forEach(choice =>
Object.values(choice).forEach(checkbox => {
if (
checkbox.type === 'trait' &&
checkbox.data.length > 0 &&
Number(levelKey) !== this.currentLevel
) {
checkbox.data.forEach(data => delete acc[data]);
}
})
);
return acc;
},
{ ...abilities }
);
}
get classUpgradeChoices() {
let subclasses = [];
let multiclass = null;
Object.keys(this.levels).forEach(levelKey => {
const level = this.levels[levelKey];
Object.values(level.choices).forEach(choice => {
Object.values(choice).forEach(checkbox => {
if (checkbox.type === 'multiclass') {
multiclass = {
class: checkbox.data.length > 0 ? checkbox.data[0] : null,
domain: checkbox.secondaryData.domain ?? null,
subclass: checkbox.secondaryData.subclass ?? null,
tier: checkbox.tier,
level: levelKey
};
}
if (checkbox.type === 'subclass') {
subclasses.push({
tier: checkbox.tier,
level: levelKey
});
}
});
});
});
return { subclasses, multiclass };
}
get tiersForRendering() {
const tierKeys = Object.keys(this.tiers);
const selections = Object.keys(this.levels).reduce(
(acc, key) => {
const level = this.levels[key];
Object.keys(level.choices).forEach(optionKey => {
const choice = level.choices[optionKey];
Object.keys(choice).forEach(checkboxNr => {
const checkbox = choice[checkboxNr];
if (!acc[checkbox.tier][optionKey]) acc[checkbox.tier][optionKey] = {};
Object.keys(choice).forEach(checkboxNr => {
acc[checkbox.tier][optionKey][checkboxNr] = { ...checkbox, level: Number(key) };
});
});
});
return acc;
},
tierKeys.reduce((acc, key) => {
acc[key] = {};
return acc;
}, {})
);
const { multiclass, subclasses } = this.classUpgradeChoices;
return tierKeys.map((tierKey, tierIndex) => {
const tier = this.tiers[tierKey];
const multiclassInTier = multiclass?.tier === Number(tierKey);
const subclassInTier = subclasses.some(x => x.tier === Number(tierKey));
return {
name: game.i18n.localize(tier.name),
active: this.currentLevel >= Math.min(...tier.belongingLevels),
groups: Object.keys(tier.options).map(optionKey => {
const option = tier.options[optionKey];
const checkboxes = [...Array(option.checkboxSelections).keys()].flatMap(index => {
const checkboxNr = index + 1;
const checkboxData = selections[tierKey]?.[optionKey]?.[checkboxNr];
const checkbox = { ...option, checkboxNr, tier: tierKey };
if (checkboxData) {
checkbox.level = checkboxData.level;
checkbox.selected = true;
checkbox.disabled = checkbox.level !== this.currentLevel;
}
if (optionKey === 'multiclass') {
if ((multiclass && !multiclassInTier) || subclassInTier) {
checkbox.disabled = true;
}
}
if (optionKey === 'subclass' && multiclassInTier) {
checkbox.disabled = true;
}
return checkbox;
});
let label = game.i18n.localize(option.label);
if (optionKey === 'domainCard') {
const maxLevel = tier.belongingLevels[tier.belongingLevels.length - 1];
label = game.i18n.format(option.label, { maxLevel });
}
return {
label: label,
checkboxGroups: chunkify(checkboxes, option.minCost, chunkedBoxes => {
const anySelected = chunkedBoxes.some(x => x.selected);
const anyDisabled = chunkedBoxes.some(x => x.disabled);
return {
multi: option.minCost > 1,
checkboxes: chunkedBoxes.map(x => ({
...x,
selected: anySelected,
disabled: anyDisabled
}))
};
})
};
})
};
});
}
}
export class DhLevelupLevel extends foundry.abstract.DataModel {
static initializeData(levelData = { selections: [] }, maxSelections, achievements) {
return {
maxSelections: maxSelections,
achievements: {
experiences: levelData.achievements?.experiences ?? achievements.experiences ?? {},
domainCards: levelData.achievements?.domainCards
? levelData.achievements.domainCards.reduce((acc, card, index) => {
acc[index] = { ...card };
return acc;
}, {})
: (achievements.domainCards ?? {}),
proficiency: levelData.achievements?.proficiency ?? achievements.proficiency ?? null
},
choices: levelData.selections.reduce((acc, data) => {
if (!acc[data.optionKey]) acc[data.optionKey] = {};
acc[data.optionKey][data.checkboxNr] = { ...data };
return acc;
}, {})
};
}
static defineSchema() {
const fields = foundry.data.fields;
return {
maxSelections: new fields.NumberField({ required: true, integer: true }),
achievements: new fields.SchemaField({
experiences: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({ required: true }),
modifier: new fields.NumberField({ required: true, integer: true })
})
),
domainCards: new fields.TypedObjectField(
new fields.SchemaField({
uuid: new fields.StringField({ required: true, nullable: true, initial: null }),
itemUuid: new fields.StringField({ required: true }),
level: new fields.NumberField({ required: true, integer: true })
})
),
proficiency: new fields.NumberField({ integer: true })
}),
choices: new fields.TypedObjectField(
new fields.TypedObjectField(
new fields.SchemaField({
tier: new fields.NumberField({ required: true, integer: true }),
minCost: new fields.NumberField({ required: true, integer: true }),
amount: new fields.NumberField({ integer: true }),
value: new fields.StringField(),
data: new fields.ArrayField(new fields.StringField()),
secondaryData: new fields.TypedObjectField(new fields.StringField()),
type: new fields.StringField({ required: true })
})
)
)
};
}
get nrSelections() {
const selections = Object.keys(this.choices).reduce((acc, choiceKey) => {
const choice = this.choices[choiceKey];
acc += Object.values(choice).reduce((acc, x) => acc + x.minCost, 0);
return acc;
}, 0);
return {
selections: selections,
available: this.maxSelections - selections
};
}
}

View file

@ -9,3 +9,4 @@ export { default as BeastformField } from './beastformField.mjs';
export { default as DamageField } from './damageField.mjs';
export { default as RollField } from './rollField.mjs';
export { default as MacroField } from './macroField.mjs';
export { default as SummonField } from './summonField.mjs';

View file

@ -105,12 +105,22 @@ export default class DamageField extends fields.SchemaField {
damagePromises.push(
actor.takeHealing(config.damage).then(updates => targetDamage.push({ token, updates }))
);
else
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;
if (configDamage.hitPoints) {
for (const part of configDamage.hitPoints.parts) {
part.total = Math.ceil(part.total * hpDamageMultiplier * hpDamageTakenMultiplier);
}
}
damagePromises.push(
actor
.takeDamage(config.damage, config.isDirect)
.takeDamage(configDamage, config.isDirect)
.then(updates => targetDamage.push({ token, updates }))
);
}
}
Promise.all(damagePromises).then(async _ => {

View file

@ -87,7 +87,7 @@ export class DHActionRollData extends foundry.abstract.DataModel {
if (this.type === CONFIG.DH.GENERAL.rollTypes.attack.id)
modifiers.push({
label: 'Bonus to Hit',
value: this.bonus ?? this.parent.actor.system.attack.roll.bonus
value: this.bonus ?? this.parent.actor.system.attack.roll.bonus ?? 0
});
break;
default:

View file

@ -0,0 +1,89 @@
import FormulaField from '../formulaField.mjs';
const fields = foundry.data.fields;
export default class DHSummonField extends fields.ArrayField {
/**
* Action Workflow order
*/
static order = 120;
constructor(options = {}, context = {}) {
const summonFields = new fields.SchemaField({
actorUUID: new fields.DocumentUUIDField({
type: 'Actor',
required: true
}),
count: new FormulaField({
required: true,
default: '1'
})
});
super(summonFields, options, context);
}
static async execute() {
if (!canvas.scene) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.summon.error'));
return;
}
if (this.summon.length === 0) {
ui.notifications.warn('No actors configured for this Summon action.');
return;
}
const rolls = [];
const summonData = [];
for (const summon of this.summon) {
let count = summon.count;
const roll = new Roll(summon.count);
if (!roll.isDeterministic) {
await roll.evaluate();
if (game.modules.get('dice-so-nice')?.active) rolls.push(roll);
count = roll.total;
}
const actor = DHSummonField.getWorldActor(await foundry.utils.fromUuid(summon.actorUUID));
/* Extending summon data in memory so it's available in actionField.toChat. Think it's harmless, but ugly. Could maybe find a better way. */
summon.rolledCount = count;
summon.actor = actor.toObject();
summonData.push({ actor, count: count });
}
if (rolls.length) await Promise.all(rolls.map(roll => game.dice3d.showForRoll(roll, game.user, true)));
this.actor.sheet?.minimize();
DHSummonField.handleSummon(summonData, this.actor);
}
/* Check for any available instances of the actor present in the world if we're missing artwork in the compendium */
static getWorldActor(baseActor) {
const dataType = game.system.api.data.actors[`Dh${baseActor.type.capitalize()}`];
if (baseActor.inCompendium && dataType && baseActor.img === dataType.DEFAULT_ICON) {
const worldActorCopy = game.actors.find(x => x.name === baseActor.name);
return worldActorCopy ?? baseActor;
}
return baseActor;
}
static async handleSummon(summonData, actionActor, summonIndex = 0) {
const summon = summonData[summonIndex];
const result = await CONFIG.ux.TokenManager.createPreviewAsync(summon.actor, {
name: `${summon.actor.prototypeToken.name}${summon.count > 1 ? ` (${summon.count}x)` : ''}`
});
if (!result) return actionActor.sheet?.maximize();
summon.actor = result.actor;
summon.count--;
if (summon.count <= 0) {
summonIndex++;
if (summonIndex === summonData.length) return actionActor.sheet?.maximize();
}
DHSummonField.handleSummon(summonData, actionActor, summonIndex);
}
}

View file

@ -267,7 +267,8 @@ export function ActionMixin(Base) {
action: {
name: this.name,
img: this.baseAction ? this.parent.parent.img : this.img,
tags: this.tags ? this.tags : ['Spell', 'Arcana', 'Lv 10']
tags: this.tags ? this.tags : ['Spell', 'Arcana', 'Lv 10'],
summon: this.summon
},
itemOrigin: this.item,
description: this.description || (this.item instanceof Item ? this.item.system.description : '')

View file

@ -147,7 +147,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
return await foundry.applications.ux.TextEditor.implementation.enrichHTML(fullDescription, {
relativeTo: this,
rollData: this.getRollData(),
secrets: this.isOwner
secrets: this.parent.isOwner
});
}
@ -164,26 +164,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
prepareBaseData() {
super.prepareBaseData();
for (const action of this.actions ?? []) {
if (!action.actor) continue;
const actionsToRegister = [];
for (let i = 0; i < action.triggers.length; i++) {
const trigger = action.triggers[i];
const { args } = CONFIG.DH.TRIGGER.triggers[trigger.trigger];
const fn = new foundry.utils.AsyncFunction(...args, `{${trigger.command}\n}`);
actionsToRegister.push(fn.bind(action));
if (i === action.triggers.length - 1)
game.system.registeredTriggers.registerTriggers(
trigger.trigger,
action.actor?.uuid,
trigger.triggeringActorType,
this.parent.uuid,
actionsToRegister
);
}
}
game.system.registeredTriggers.registerItemTriggers(this.parent);
}
async _preCreate(data, options, user) {
@ -246,6 +227,28 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
const armorData = getScrollTextData(this.parent.parent.system.resources, changed.system.marks, 'armor');
options.scrollingTextData = [armorData];
}
if (changed.system?.actions) {
const triggersToRemove = Object.keys(changed.system.actions).reduce((acc, key) => {
if (!changed.system.actions[key]) {
const strippedKey = key.replace('-=', '');
acc.push(...this.actions.get(strippedKey).triggers.map(x => x.trigger));
}
return acc;
}, []);
game.system.registeredTriggers.unregisterTriggers(triggersToRemove, this.parent.uuid);
if (this.parent.parent && !(this.parent.parent.token instanceof game.system.api.documents.DhToken)) {
for (const token of this.parent.parent.getActiveTokens()) {
game.system.registeredTriggers.unregisterTriggers(
triggersToRemove,
`${token.document.uuid}.${this.parent.uuid}`
);
}
}
}
}
_onUpdate(changed, options, userId) {

View file

@ -49,6 +49,7 @@ export default class DHBeastform extends BaseDataItem {
choices: CONFIG.DH.ACTOR.tokenSize,
initial: CONFIG.DH.ACTOR.tokenSize.custom.id
}),
scale: new fields.NumberField({ nullable: false, min: 0.2, max: 3, step: 0.05, initial: 1 }),
height: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }),
width: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true })
}),
@ -184,6 +185,7 @@ export default class DHBeastform extends BaseDataItem {
tokenImg: this.parent.parent.prototypeToken.texture.src,
tokenRingImg: this.parent.parent.prototypeToken.ring.subject.texture,
tokenSize: {
scale: this.parent.parent.prototypeToken.texture.scaleX,
height: this.parent.parent.prototypeToken.height,
width: this.parent.parent.prototypeToken.width
}
@ -209,7 +211,9 @@ export default class DHBeastform extends BaseDataItem {
height,
width,
texture: {
src: this.tokenImg
src: this.tokenImg,
scaleX: this.tokenSize.scale,
scaleY: this.tokenSize.scale
},
ring: {
subject: {

View file

@ -29,7 +29,21 @@ export default class DHDomainCard extends BaseDataItem {
required: true,
initial: CONFIG.DH.DOMAIN.cardTypes.ability.id
}),
inVault: new fields.BooleanField({ initial: false })
inVault: new fields.BooleanField({ initial: false }),
vaultActive: new fields.BooleanField({
required: true,
nullable: false,
initial: false
}),
loadoutIgnore: new fields.BooleanField({
required: true,
nullable: false,
initial: false
}),
domainTouched: new fields.NumberField({
nullable: true,
initial: null
})
};
}
@ -38,6 +52,19 @@ export default class DHDomainCard extends BaseDataItem {
return game.i18n.localize(allDomainData[this.domain].label);
}
get isVaultSupressed() {
return this.inVault && !this.vaultActive;
}
get isDomainTouchedSuppressed() {
if (!this.parent.system.domainTouched || this.parent.parent?.type !== 'character') return false;
const matchingDomainCards = this.parent.parent.items.filter(
item => !item.system.inVault && item.system.domain === this.parent.system.domain
).length;
return matchingDomainCards < this.parent.system.domainTouched;
}
/* -------------------------------------------- */
/**@override */

View file

@ -0,0 +1,167 @@
export default class RegisteredTriggers extends Map {
constructor() {
super();
}
registerTriggers(triggers, actor, uuid) {
for (const triggerKey of Object.keys(CONFIG.DH.TRIGGER.triggers)) {
const match = triggers[triggerKey];
const existingTrigger = this.get(triggerKey);
if (!match) {
if (existingTrigger?.get(uuid)) this.get(triggerKey).delete(uuid);
} else {
const { trigger, triggeringActorType, commands } = match;
if (!existingTrigger) this.set(trigger, new Map());
this.get(trigger).set(uuid, { actor, triggeringActorType, commands });
}
}
}
registerItemTriggers(item, registerOverride) {
if (!item.actor || !item._stats.createdTime) return;
for (const action of item.system.actions ?? []) {
if (!action.actor) continue;
/* Non actor-linked should only prep synthetic actors so they're not registering triggers unless they're on the canvas */
if (
!registerOverride &&
!action.actor.prototypeToken.actorLink &&
(!(action.actor.parent instanceof game.system.api.documents.DhToken) || !action.actor.parent?.uuid)
)
continue;
const triggers = {};
for (const trigger of action.triggers) {
const { args } = CONFIG.DH.TRIGGER.triggers[trigger.trigger];
const fn = new foundry.utils.AsyncFunction(...args, `{${trigger.command}\n}`);
if (!triggers[trigger.trigger])
triggers[trigger.trigger] = {
trigger: trigger.trigger,
triggeringActorType: trigger.triggeringActorType,
commands: []
};
triggers[trigger.trigger].commands.push(fn.bind(action));
}
this.registerTriggers(triggers, action.actor?.uuid, item.uuid);
}
}
unregisterTriggers(triggerKeys, uuid) {
for (const triggerKey of triggerKeys) {
const existingTrigger = this.get(triggerKey);
if (!existingTrigger) return;
existingTrigger.delete(uuid);
}
}
unregisterItemTriggers(items) {
for (const item of items) {
if (!item.system.actions?.size) continue;
const triggers = (item.system.actions ?? []).reduce((acc, action) => {
acc.push(...action.triggers.map(x => x.trigger));
return acc;
}, []);
this.unregisterTriggers(triggers, item.uuid);
}
}
unregisterSceneEnvironmentTriggers(flagSystemData) {
const sceneData = new game.system.api.data.scenes.DHScene(flagSystemData);
for (const environment of sceneData.sceneEnvironments) {
if (environment.pack) continue;
this.unregisterItemTriggers(environment.system.features);
}
}
unregisterSceneTriggers(scene) {
this.unregisterSceneEnvironmentTriggers(scene.flags.daggerheart);
for (const triggerKey of Object.keys(CONFIG.DH.TRIGGER.triggers)) {
const existingTrigger = this.get(triggerKey);
if (!existingTrigger) continue;
const filtered = new Map();
for (const [uuid, data] of existingTrigger.entries()) {
if (!uuid.startsWith(scene.uuid)) filtered.set(uuid, data);
}
this.set(triggerKey, filtered);
}
}
registerSceneEnvironmentTriggers(flagSystemData) {
const sceneData = new game.system.api.data.scenes.DHScene(flagSystemData);
for (const environment of sceneData.sceneEnvironments) {
for (const feature of environment.system.features) {
if (feature) this.registerItemTriggers(feature, true);
}
}
}
registerSceneTriggers(scene) {
this.registerSceneEnvironmentTriggers(scene.flags.daggerheart);
for (const actor of scene.tokens.filter(x => x.actor).map(x => x.actor)) {
if (actor.prototypeToken.actorLink) continue;
for (const item of actor.items) {
this.registerItemTriggers(item);
}
}
}
async runTrigger(trigger, currentActor, ...args) {
const updates = [];
const triggerSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).triggers;
if (!triggerSettings.enabled) return updates;
const dualityTrigger = this.get(trigger);
if (dualityTrigger?.size) {
const triggerActors = ['character', 'adversary', 'environment'];
for (let [itemUuid, { actor: actorUuid, triggeringActorType, commands }] of dualityTrigger.entries()) {
const actor = await foundry.utils.fromUuid(actorUuid);
if (!actor || !triggerActors.includes(actor.type)) continue;
const triggerData = CONFIG.DH.TRIGGER.triggers[trigger];
if (triggerData.usesActor && triggeringActorType !== 'any') {
if (triggeringActorType === 'self' && currentActor?.uuid !== actorUuid) continue;
else if (triggeringActorType === 'other' && currentActor?.uuid === actorUuid) continue;
}
for (const command of commands) {
try {
if (CONFIG.debug.triggers) {
const item = await foundry.utils.fromUuid(itemUuid);
console.log(
game.i18n.format('DAGGERHEART.UI.ConsoleLogs.triggerRun', {
actor: actor.name ?? '<Missing Actor>',
item: item?.name ?? '<Missing Item>',
trigger: game.i18n.localize(triggerData.label)
})
);
}
const result = await command(...args);
if (result?.updates?.length) updates.push(...result.updates);
} catch (_) {
const triggerName = game.i18n.localize(triggerData.label);
ui.notifications.error(
game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerError', {
trigger: triggerName,
actor: currentActor?.name
})
);
}
}
}
}
return updates;
}
}

38
module/data/rollTable.mjs Normal file
View file

@ -0,0 +1,38 @@
import FormulaField from './fields/formulaField.mjs';
//Extra definitions for RollTable
export default class DhRollTable extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
formulaName: new fields.StringField({
required: true,
nullable: false,
initial: 'Roll Formula',
label: 'DAGGERHEART.ROLLTABLES.FIELDS.formulaName.label'
}),
altFormula: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({
required: true,
nullable: false,
initial: 'Roll Formula',
label: 'DAGGERHEART.ROLLTABLES.FIELDS.formulaName.label'
}),
formula: new FormulaField({ label: 'Formula Roll', initial: '1d20' })
})
),
activeAltFormula: new fields.StringField({ nullable: true, initial: null })
};
}
getActiveFormula(baseFormula) {
return this.activeAltFormula ? (this.altFormula[this.activeAltFormula]?.formula ?? baseFormula) : baseFormula;
}
static getDefaultFormula = () => ({
name: game.i18n.localize('Roll Formula'),
formula: '1d20'
});
}

View file

@ -55,15 +55,10 @@ export default class DhAutomation extends foundry.abstract.DataModel {
initial: true,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.resourceScrollTexts.label'
}),
playerCanEditSheet: new fields.BooleanField({
required: true,
initial: false,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.playerCanEditSheet.label'
}),
defeated: new fields.SchemaField({
enabled: new fields.BooleanField({
required: true,
initial: false,
initial: true,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.enabled.label'
}),
overlay: new fields.BooleanField({
@ -74,7 +69,7 @@ export default class DhAutomation extends foundry.abstract.DataModel {
characterDefault: new fields.StringField({
required: true,
choices: CONFIG.DH.GENERAL.defeatedConditionChoices,
initial: CONFIG.DH.GENERAL.defeatedConditionChoices.unconscious.id,
initial: CONFIG.DH.GENERAL.defeatedConditionChoices.deathMove.id,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.characterDefault.label'
}),
adversaryDefault: new fields.StringField({
@ -89,23 +84,29 @@ export default class DhAutomation extends foundry.abstract.DataModel {
initial: CONFIG.DH.GENERAL.defeatedConditionChoices.defeated.id,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.companionDefault.label'
}),
deathMoveIcon: new fields.FilePathField({
initial: 'icons/magic/life/heart-cross-purple-orange.webp',
categories: ['IMAGE'],
base64: false,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.deathMove.label'
}),
deadIcon: new fields.FilePathField({
initial: 'icons/magic/death/grave-tombstone-glow-teal.webp',
categories: ['IMAGE'],
base64: false,
label: 'Dead'
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.dead.label'
}),
defeatedIcon: new fields.FilePathField({
initial: 'icons/magic/control/fear-fright-mask-orange.webp',
categories: ['IMAGE'],
base64: false,
label: 'Defeated'
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.defeated.label'
}),
unconsciousIcon: new fields.FilePathField({
initial: 'icons/magic/control/sleep-bubble-purple.webp',
categories: ['IMAGE'],
base64: false,
label: 'Unconcious'
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.unconscious.label'
})
}),
roll: new fields.SchemaField({

View file

@ -23,6 +23,13 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
initial: 12,
label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxFear.label'
}),
maxHope: new fields.NumberField({
required: true,
integer: true,
min: 0,
initial: 6,
label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxHope.label'
}),
maxLoadout: new fields.NumberField({
required: true,
integer: true,

View file

@ -3,3 +3,4 @@ export { default as D20Roll } from './d20Roll.mjs';
export { default as DamageRoll } from './damageRoll.mjs';
export { default as DHRoll } from './dhRoll.mjs';
export { default as DualityRoll } from './dualityRoll.mjs';
export { default as FateRoll } from './fateRoll.mjs';

View file

@ -35,7 +35,9 @@ export default class D20Roll extends DHRoll {
get isCritical() {
if (!this.d20._evaluated) return;
return this.d20.total >= this.data.system.criticalThreshold;
const criticalThreshold = this.options.actionType === 'reaction' ? 20 : this.data.system.criticalThreshold;
return this.d20.total >= criticalThreshold;
}
get hasAdvantage() {
@ -97,11 +99,14 @@ export default class D20Roll extends DHRoll {
this.options.roll.modifiers = this.applyBaseBonus();
const actorExperiences = this.options.roll.companionRoll
? (this.options.data?.companion?.system.experiences ?? {})
: (this.options.data.system?.experiences ?? {});
this.options.experiences?.forEach(m => {
if (this.options.data.system?.experiences?.[m])
if (actorExperiences[m])
this.options.roll.modifiers.push({
label: this.options.data.system.experiences[m].name,
value: this.options.data.system.experiences[m].value
label: actorExperiences[m].name,
value: actorExperiences[m].value
});
});
@ -127,15 +132,55 @@ export default class D20Roll extends DHRoll {
const modifiers = foundry.utils.deepClone(this.options.roll.baseModifiers) ?? [];
modifiers.push(
...this.getBonus(`roll.${this.options.actionType}`, `${this.options.actionType?.capitalize()} Bonus`)
);
modifiers.push(
...this.getBonus(`roll.${this.options.roll.type}`, `${this.options.roll.type?.capitalize()} Bonus`)
...this.getBonus(
`system.bonuses.roll.${this.options.actionType}`,
`${this.options.actionType?.capitalize()} Bonus`
)
);
if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.attack.id) {
modifiers.push(
...this.getBonus(
`system.bonuses.roll.${this.options.roll.type}`,
`${this.options.roll.type?.capitalize()} Bonus`
)
);
}
if (
this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.attack.id ||
(this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id && this.options.hasDamage)
) {
modifiers.push(
...this.getBonus(`system.bonuses.roll.attack`, `${this.options.roll.type?.capitalize()} Bonus`)
);
}
return modifiers;
}
getActionChangeKeys() {
const changeKeys = new Set([`system.bonuses.roll.${this.options.actionType}`]);
if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.attack.id) {
changeKeys.add(`system.bonuses.roll.${this.options.roll.type}`);
}
if (
this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.attack.id ||
(this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id && this.options.hasDamage)
) {
changeKeys.add(`system.bonuses.roll.attack`);
}
if (this.options.roll.trait && this.data.traits?.[this.options.roll.trait]) {
if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.spellcast.id)
changeKeys.add('system.bonuses.roll.trait');
}
return changeKeys;
}
static postEvaluate(roll, config = {}) {
const data = super.postEvaluate(roll, config);
data.type = config.actionType;

View file

@ -93,7 +93,6 @@ export default class DamageRoll extends DHRoll {
type = this.options.messageType ?? (this.options.hasHealing ? 'healing' : 'damage'),
options = part ?? this.options;
modifiers.push(...this.getBonus(`${type}`, `${type.capitalize()} Bonus`));
if (!this.options.hasHealing) {
options.damageTypes?.forEach(t => {
modifiers.push(...this.getBonus(`${type}.${t}`, `${t.capitalize()} ${type.capitalize()} Bonus`));
@ -108,6 +107,31 @@ export default class DamageRoll extends DHRoll {
return modifiers;
}
getActionChangeKeys() {
const type = this.options.messageType ?? (this.options.hasHealing ? 'healing' : 'damage');
const changeKeys = [];
for (const roll of this.options.roll) {
for (const damageType of roll.damageTypes?.values?.() ?? []) {
changeKeys.push(`system.bonuses.${type}.${damageType}`);
}
}
const item = this.data.parent?.items?.get(this.options.source.item);
if (item) {
switch (item.type) {
case 'weapon':
if (!this.options.hasHealing)
['primaryWeapon', 'secondaryWeapon'].forEach(w =>
changeKeys.push(`system.bonuses.damage.${w}`)
);
break;
}
}
return changeKeys;
}
constructFormula(config) {
this.options.roll.forEach((part, index) => {
part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data));

View file

@ -3,7 +3,8 @@ import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
export default class DHRoll extends Roll {
baseTerms = [];
constructor(formula, data = {}, options = {}) {
super(formula, data, options);
super(formula, data, foundry.utils.mergeObject(options, { roll: [] }, { overwrite: false }));
options.bonusEffects = this.bonusEffectBuilder();
if (!this.data || !Object.keys(this.data).length) this.data = options.data;
}
@ -164,12 +165,18 @@ export default class DHRoll extends Roll {
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
...this.constructor.parse(modifier.join(' + '), this.options.data)
];
} else {
} else if (Number.isNumeric(modifier)) {
const numTerm = modifier < 0 ? '-' : '+';
return [
new foundry.dice.terms.OperatorTerm({ operator: numTerm }),
new foundry.dice.terms.NumericTerm({ number: Math.abs(modifier) })
];
} else {
const numTerm = modifier < 0 ? '-' : '+';
return [
new foundry.dice.terms.OperatorTerm({ operator: numTerm }),
...this.constructor.parse(modifier, this.options.data)
];
}
}
@ -185,18 +192,20 @@ export default class DHRoll extends Roll {
}
getBonus(path, label) {
const bonus = foundry.utils.getProperty(this.data.bonuses, path),
modifiers = [];
if (bonus?.bonus)
modifiers.push({
label: label,
value: bonus?.bonus
});
if (bonus?.dice?.length)
modifiers.push({
label: label,
value: bonus?.dice
});
const modifiers = [];
for (const effect of Object.values(this.options.bonusEffects)) {
if (!effect.selected) continue;
for (const change of effect.changes) {
if (!change.key.includes(path)) continue;
const changeValue = game.system.api.documents.DhActiveEffect.getChangeValue(
this.data,
change,
effect.origEffect
);
modifiers.push({ label: label, value: changeValue });
}
}
return modifiers;
}
@ -235,4 +244,28 @@ export default class DHRoll extends Roll {
static temporaryModifierBuilder(config) {
return {};
}
bonusEffectBuilder() {
const changeKeys = this.getActionChangeKeys();
return (
this.options.effects?.reduce((acc, effect) => {
if (effect.changes.some(x => changeKeys.some(key => x.key.includes(key)))) {
acc[effect.id] = {
id: effect.id,
name: effect.name,
description: effect.description,
changes: effect.changes,
origEffect: effect,
selected: !effect.disabled
};
}
return acc;
}, {}) ?? []
);
}
getActionChangeKeys() {
return [];
}
}

View file

@ -12,6 +12,7 @@ export default class DualityRoll extends D20Roll {
constructor(formula, data = {}, options = {}) {
super(formula, data, options);
this.rallyChoices = this.setRallyChoices();
this.guaranteedCritical = options.guaranteedCritical;
}
static messageType = 'dualityRoll';
@ -25,29 +26,23 @@ export default class DualityRoll extends D20Roll {
}
get dHope() {
// if ( !(this.terms[0] instanceof foundry.dice.terms.Die) ) return;
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
return this.dice[0];
// return this.#hopeDice;
}
set dHope(faces) {
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
this.terms[0].faces = this.getFaces(faces);
// this.#hopeDice = `d${face}`;
this.dice[0].faces = this.getFaces(faces);
}
get dFear() {
// if ( !(this.terms[1] instanceof foundry.dice.terms.Die) ) return;
if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice();
return this.dice[1];
// return this.#fearDice;
}
set dFear(faces) {
if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice();
this.dice[1].faces = this.getFaces(faces);
// this.#fearDice = `d${face}`;
}
get dAdvantage() {
@ -90,26 +85,29 @@ export default class DualityRoll extends D20Roll {
}
get isCritical() {
if (this.guaranteedCritical) return true;
if (!this.dHope._evaluated || !this.dFear._evaluated) return;
return this.dHope.total === this.dFear.total;
}
get withHope() {
if (!this._evaluated) return;
if (!this._evaluated || this.guaranteedCritical) return;
return this.dHope.total > this.dFear.total;
}
get withFear() {
if (!this._evaluated) return;
if (!this._evaluated || this.guaranteedCritical) return;
return this.dHope.total < this.dFear.total;
}
get totalLabel() {
const label = this.withHope
? 'DAGGERHEART.GENERAL.hope'
: this.withFear
? 'DAGGERHEART.GENERAL.fear'
: 'DAGGERHEART.GENERAL.criticalSuccess';
const label = this.guaranteedCritical
? 'DAGGERHEART.GENERAL.guaranteedCriticalSuccess'
: this.isCritical
? 'DAGGERHEART.GENERAL.criticalSuccess'
: this.withHope
? 'DAGGERHEART.GENERAL.hope'
: 'DAGGERHEART.GENERAL.fear';
return game.i18n.localize(label);
}
@ -130,9 +128,14 @@ export default class DualityRoll extends D20Roll {
this.terms = [this.terms[0], this.terms[1], this.terms[2]];
return;
}
this.terms[0] = new foundry.dice.terms.Die({ faces: 12 });
this.terms[0] = new foundry.dice.terms.Die({
faces: this.data.rules.dualityRoll?.defaultHopeDice ?? 12
});
this.terms[1] = new foundry.dice.terms.OperatorTerm({ operator: '+' });
this.terms[2] = new foundry.dice.terms.Die({ faces: 12 });
this.terms[2] = new foundry.dice.terms.Die({
faces: this.data.rules.dualityRoll?.defaultFearDice ?? 12
});
}
applyAdvantage() {
@ -173,6 +176,49 @@ export default class DualityRoll extends D20Roll {
return modifiers;
}
static async buildConfigure(config = {}, message = {}) {
config.dialog ??= {};
config.guaranteedCritical = config.data?.parent?.appliedEffects.reduce((a, c) => {
const change = c.changes.find(ch => ch.key === 'system.rules.roll.guaranteedCritical');
if (change) a = true;
return a;
}, false);
if (config.guaranteedCritical) {
config.dialog.configure = false;
}
return super.buildConfigure(config, message);
}
getActionChangeKeys() {
const changeKeys = new Set([`system.bonuses.roll.${this.options.actionType}`]);
if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.attack.id) {
changeKeys.add(`system.bonuses.roll.${this.options.roll.type}`);
}
if (
this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.attack.id ||
(this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id && this.options.hasDamage)
) {
changeKeys.add(`system.bonuses.roll.attack`);
}
if (this.options.roll.trait && this.data.traits?.[this.options.roll.trait]) {
if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.spellcast.id)
changeKeys.add('system.bonuses.roll.trait');
}
const weapons = ['primaryWeapon', 'secondaryWeapon'];
weapons.forEach(w => {
if (this.options.source.item && this.options.source.item === this.data[w]?.id)
changeKeys.add(`system.bonuses.roll.${w}`);
});
return changeKeys;
}
static async buildEvaluate(roll, config = {}, message = {}) {
await super.buildEvaluate(roll, config, message);
@ -190,7 +236,7 @@ export default class DualityRoll extends D20Roll {
data.hope = {
dice: roll.dHope.denomination,
value: roll.dHope.total,
value: this.guaranteedCritical ? 0 : roll.dHope.total,
rerolled: {
any: roll.dHope.results.some(x => x.rerolled),
rerolls: roll.dHope.results.filter(x => x.rerolled)
@ -198,7 +244,7 @@ export default class DualityRoll extends D20Roll {
};
data.fear = {
dice: roll.dFear.denomination,
value: roll.dFear.total,
value: this.guaranteedCritical ? 0 : roll.dFear.total,
rerolled: {
any: roll.dFear.results.some(x => x.rerolled),
rerolls: roll.dFear.results.filter(x => x.rerolled)
@ -210,7 +256,7 @@ export default class DualityRoll extends D20Roll {
};
data.result = {
duality: roll.withHope ? 1 : roll.withFear ? -1 : 0,
total: roll.dHope.total + roll.dFear.total,
total: this.guaranteedCritical ? 0 : roll.dHope.total + roll.dFear.total,
label: roll.totalLabel
};
@ -228,6 +274,8 @@ export default class DualityRoll extends D20Roll {
}
static async handleTriggers(roll, config) {
if (!config.source?.actor || config.skips?.triggers) return;
const updates = [];
const dualityUpdates = await game.system.registeredTriggers.runTrigger(
CONFIG.DH.TRIGGER.triggers.dualityRoll.id,

85
module/dice/fateRoll.mjs Normal file
View file

@ -0,0 +1,85 @@
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
import D20Roll from './d20Roll.mjs';
import { setDiceSoNiceForHopeFateRoll, setDiceSoNiceForFearFateRoll } from '../helpers/utils.mjs';
export default class FateRoll extends D20Roll {
constructor(formula, data = {}, options = {}) {
super(formula, data, options);
}
static messageType = 'fateRoll';
static DefaultDialog = D20RollDialog;
get title() {
return game.i18n.localize(`DAGGERHEART.GENERAL.fateRoll`);
}
get dHope() {
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
return this.dice[0];
}
set dHope(faces) {
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
this.dice[0].faces = this.getFaces(faces);
}
get dFear() {
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
return this.dice[0];
}
set dFear(faces) {
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
this.dice[0].faces = this.getFaces(faces);
}
get isCritical() {
return false;
}
get fateDie() {
return this.data.fateType;
}
static getHooks(hooks) {
return [...(hooks ?? []), 'Fate'];
}
/** @inheritDoc */
static fromData(data) {
data.terms[0].class = foundry.dice.terms.Die.name;
return super.fromData(data);
}
createBaseDice() {
if (this.dice[0] instanceof foundry.dice.terms.Die) {
this.terms = [this.terms[0]];
return;
}
this.terms[0] = new foundry.dice.terms.Die({ faces: 12 });
}
static async buildEvaluate(roll, config = {}, message = {}) {
await super.buildEvaluate(roll, config, message);
if (roll.fateDie === 'Hope') {
await setDiceSoNiceForHopeFateRoll(roll, config.roll.fate.dice);
} else {
await setDiceSoNiceForFearFateRoll(roll, config.roll.fate.dice);
}
}
static postEvaluate(roll, config = {}) {
const data = super.postEvaluate(roll, config);
data.fate = {
dice: roll.fateDie === 'Hope' ? roll.dHope.denomination : roll.dFear.denomination,
value: roll.fateDie === 'Hope' ? roll.dHope.total : roll.dFear.total,
fateDie: roll.fateDie
};
return data;
}
}

View file

@ -4,7 +4,9 @@ export { default as DhpCombat } from './combat.mjs';
export { default as DHCombatant } from './combatant.mjs';
export { default as DhActiveEffect } from './activeEffect.mjs';
export { default as DhChatMessage } from './chatMessage.mjs';
export { default as DhRollTable } from './rollTable.mjs';
export { default as DhScene } from './scene.mjs';
export { default as DhToken } from './token.mjs';
export { default as DhTooltipManager } from './tooltipManager.mjs';
export { default as DhTemplateManager } from './templateManager.mjs';
export { default as DhTokenManager } from './tokenManager.mjs';

View file

@ -20,7 +20,10 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
}
if (this.parent?.type === 'domainCard') {
return this.parent.system.inVault;
const isVaultSupressed = this.parent.system.isVaultSupressed;
const domainTouchedSupressed = this.parent.system.isDomainTouchedSuppressed;
return isVaultSupressed || domainTouchedSupressed;
}
return super.isSuppressed;
@ -106,23 +109,29 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
/**@inheritdoc*/
static applyField(model, change, field) {
const isOriginTarget = change.value.toLowerCase().includes('origin.@');
change.value = DhActiveEffect.getChangeValue(model, change, change.effect);
super.applyField(model, change, field);
}
/** */
static getChangeValue(model, change, effect) {
let value = change.value;
const isOriginTarget = value.toLowerCase().includes('origin.@');
let parseModel = model;
if (isOriginTarget && change.effect.origin) {
change.value = change.value.replaceAll(/origin\.@/gi, '@');
if (isOriginTarget && effect.origin) {
value = change.value.replaceAll(/origin\.@/gi, '@');
try {
const effect = foundry.utils.fromUuidSync(change.effect.origin);
const originEffect = foundry.utils.fromUuidSync(effect.origin);
const doc =
effect.parent?.parent instanceof game.system.api.documents.DhpActor
? effect.parent
: effect.parent.parent;
originEffect.parent?.parent instanceof game.system.api.documents.DhpActor
? originEffect.parent
: originEffect.parent.parent;
if (doc) parseModel = doc;
} catch (_) {}
}
const evalValue = this.effectSafeEval(itemAbleRollParse(change.value, parseModel, change.effect.parent));
change.value = evalValue ?? change.value;
super.applyField(model, change, field);
const evalValue = this.effectSafeEval(itemAbleRollParse(value, parseModel, effect.parent));
return evalValue ?? value;
}
/**

View file

@ -104,6 +104,16 @@ export default class DhpActor extends Actor {
}
}
async _preDelete() {
if (this.prototypeToken.actorLink) {
game.system.registeredTriggers.unregisterItemTriggers(this.items);
} else {
for (const token of this.getActiveTokens()) {
game.system.registeredTriggers.unregisterItemTriggers(token.actor.items);
}
}
}
_onDelete(options, userId) {
super._onDelete(options, userId);
for (const party of this.parties) {
@ -231,6 +241,11 @@ export default class DhpActor extends Actor {
}
}
});
if (this.system.companion) {
this.system.companion.updateLevel(usedLevel);
}
this.sheet.render();
}
}
@ -597,7 +612,7 @@ export default class DhpActor extends Actor {
if (!updates.length) return;
const hpDamage = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
if (hpDamage) {
if (hpDamage?.value) {
hpDamage.value = this.convertDamageToThreshold(hpDamage.value);
if (
this.type === 'character' &&
@ -754,16 +769,24 @@ export default class DhpActor extends Actor {
};
}
} else {
const valueFunc = (base, resource, baseMax) => {
if (resource.clear) return baseMax && base.inverted ? baseMax : 0;
return (base.value ?? base) + resource.value;
};
switch (r.key) {
case 'fear':
ui.resources.updateFear(
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) + r.value
valueFunc(
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
r
)
);
break;
case 'armor':
if (this.system.armor?.system?.marks) {
updates.armor.resources['system.marks.value'] = Math.max(
Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore),
Math.min(valueFunc(this.system.armor.system.marks, r), this.system.armorScore),
0
);
}
@ -772,7 +795,7 @@ export default class DhpActor extends Actor {
if (this.system.resources?.[r.key]) {
updates.actor.resources[`system.resources.${r.key}.value`] = Math.max(
Math.min(
this.system.resources[r.key].value + r.value,
valueFunc(this.system.resources[r.key], r, this.system.resources[r.key].max),
this.system.resources[r.key].max
),
0
@ -831,8 +854,8 @@ export default class DhpActor extends Actor {
async toggleDefeated(defeatedState) {
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).defeated;
const { unconscious, defeated, dead } = CONFIG.DH.GENERAL.conditions();
const defeatedConditions = new Set([unconscious.id, defeated.id, dead.id]);
const { deathMove, unconscious, defeated, dead } = CONFIG.DH.GENERAL.conditions();
const defeatedConditions = new Set([deathMove.id, unconscious.id, defeated.id, dead.id]);
if (!defeatedState) {
for (let defeatedId of defeatedConditions) {
await this.toggleStatusEffect(defeatedId, { overlay: settings.overlay, active: defeatedState });
@ -846,6 +869,18 @@ export default class DhpActor extends Actor {
}
}
async setDeathMoveDefeated(defeatedIconId) {
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).defeated;
const actorDefault = settings[`${this.type}Default`];
if (!settings.enabled || !settings.enabled || !actorDefault || actorDefault === defeatedIconId) return;
for (let defeatedId of Object.keys(CONFIG.DH.GENERAL.defeatedConditionChoices)) {
await this.toggleStatusEffect(defeatedId, { overlay: settings.overlay, active: false });
}
if (defeatedIconId) await this.toggleStatusEffect(defeatedIconId, { overlay: settings.overlay, active: true });
}
queueScrollText(scrollingTextData) {
this.#scrollTextQueue.push(...scrollingTextData.map(data => () => createScrollText(this, data)));
if (!this.#scrollTextInterval) {

View file

@ -87,6 +87,15 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
break;
}
}
if (this.type === 'fateRoll') {
html.classList.add('fate');
if (this.system.roll?.fate.fateDie == 'Hope') {
html.classList.add('hope');
}
if (this.system.roll?.fate.fateDie == 'Fear') {
html.classList.add('fear');
}
}
const autoExpandRoll = game.settings.get(
CONFIG.DH.id,
@ -157,7 +166,12 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
event.stopPropagation();
const config = foundry.utils.deepClone(this.system);
config.event = event;
await this.system.action?.workflow.get('damage')?.execute(config, this._id, true);
if (this.system.action) {
const actor = await foundry.utils.fromUuid(config.source.actor);
const item = actor?.items.get(config.source.item) ?? null;
config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(actor, item);
await this.system.action.workflow.get('damage')?.execute(config, this._id, true);
}
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
@ -174,7 +188,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
config = foundry.utils.deepClone(this.system);
config.event = event;
if (this.system.onSave) {
if (config.hasSave) {
const pendingingSaves = targets.filter(t => t.saved.success === null);
if (pendingingSaves.length) {
const confirm = await foundry.applications.api.DialogV2.confirm({
@ -189,7 +203,16 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm'));
this.consumeOnSuccess();
this.system.action?.workflow.get('applyDamage')?.execute(config, targets, true);
if (this.system.action) this.system.action.workflow.get('applyDamage')?.execute(config, targets, true);
else {
for (const target of targets) {
const actor = await foundry.utils.fromUuid(target.actorId);
if (!actor) continue;
if (this.system.hasHealing) actor.takeHealing(this.system.damage);
else actor.takeDamage(this.system.damage);
}
}
}
async onRollSave(event) {

View file

@ -0,0 +1 @@
export { default as DhActorCollection } from './actorCollection.mjs';

View file

@ -0,0 +1,14 @@
export default class DhActorCollection extends foundry.documents.collections.Actors {
/** Ensure companions are initialized after all other subtypes. */
_initialize() {
super._initialize();
const companions = [];
for (const actor of this.values()) {
if (actor.type === 'companion') companions.push(actor);
}
for (const actor of companions) {
this.delete(actor.id);
this.set(actor.id, actor);
}
}
}

View file

@ -31,7 +31,7 @@ export default class DHItem extends foundry.documents.Item {
static async createDocuments(sources, operation) {
// Ensure that items being created are valid to the actor its being added to
const actor = operation.parent;
sources = actor?.system?.isItemValid ? sources.filter((s) => actor.system.isItemValid(s)) : sources;
sources = actor?.system?.isItemValid ? sources.filter(s => actor.system.isItemValid(s)) : sources;
return super.createDocuments(sources, operation);
}
@ -146,6 +146,16 @@ export default class DHItem extends foundry.documents.Item {
/* -------------------------------------------- */
async use(event) {
/* DomainCard check. Can be expanded or made neater */
if (this.system.isDomainTouchedSuppressed) {
return ui.notifications.warn(
game.i18n.format('DAGGERHEART.UI.Notifications.domainTouchRequirement', {
nr: this.domainTouched,
domain: game.i18n.localize(CONFIG.DH.DOMAIN.allDomains()[this.domain].label)
})
);
}
const actions = new Set(this.system.actionsList);
if (actions?.size) {
let action = actions.first();
@ -198,4 +208,23 @@ export default class DHItem extends foundry.documents.Item {
cls.create(msg);
}
deleteTriggers() {
const actions = Array.from(this.system.actions ?? []);
if (!actions.length) return;
const triggerKeys = actions.flatMap(action => action.triggers.map(x => x.trigger));
game.system.registeredTriggers.unregisterTriggers(triggerKeys, this.uuid);
if (this.actor && !(this.actor.parent instanceof game.system.api.documents.DhToken)) {
for (const token of this.actor.getActiveTokens()) {
game.system.registeredTriggers.unregisterTriggers(triggerKeys, `${token.document.uuid}.${this.uuid}`);
}
}
}
async _preDelete() {
this.deleteTriggers();
}
}

View file

@ -0,0 +1,122 @@
export default class DhRollTable extends foundry.documents.RollTable {
async roll({ selectedFormula, roll, recursive = true, _depth = 0 } = {}) {
// Prevent excessive recursion
if (_depth > 5) {
throw new Error(`Maximum recursion depth exceeded when attempting to draw from RollTable ${this.id}`);
}
const formula = selectedFormula ?? this.formula;
// If there is no formula, automatically calculate an even distribution
if (!this.formula) {
await this.normalize();
}
// Reference the provided roll formula
roll = roll instanceof Roll ? roll : Roll.create(formula);
let results = [];
// Ensure that at least one non-drawn result remains
const available = this.results.filter(r => !r.drawn);
if (!available.length) {
ui.notifications.warn(game.i18n.localize('TABLE.NoAvailableResults'));
return { roll, results };
}
// Ensure that results are available within the minimum/maximum range
const minRoll = (await roll.reroll({ minimize: true })).total;
const maxRoll = (await roll.reroll({ maximize: true })).total;
const availableRange = available.reduce(
(range, result) => {
const r = result.range;
if (!range[0] || r[0] < range[0]) range[0] = r[0];
if (!range[1] || r[1] > range[1]) range[1] = r[1];
return range;
},
[null, null]
);
if (availableRange[0] > maxRoll || availableRange[1] < minRoll) {
ui.notifications.warn('No results can possibly be drawn from this table and formula.');
return { roll, results };
}
// Continue rolling until one or more results are recovered
let iter = 0;
while (!results.length) {
if (iter >= 10000) {
ui.notifications.error(
`Failed to draw an available entry from Table ${this.name}, maximum iteration reached`
);
break;
}
roll = await roll.reroll();
results = this.getResultsForRoll(roll.total);
iter++;
}
// Draw results recursively from any inner Roll Tables
if (recursive) {
const inner = [];
for (const result of results) {
const { type, documentUuid } = result;
const documentName = foundry.utils.parseUuid(documentUuid)?.type;
if (type === 'document' && documentName === 'RollTable') {
const innerTable = await fromUuid(documentUuid);
if (innerTable) {
const innerRoll = await innerTable.roll({ _depth: _depth + 1 });
inner.push(...innerRoll.results);
}
} else inner.push(result);
}
results = inner;
}
// Return the Roll and the results
return { roll, results };
}
async toMessage(results, { roll, messageData = {}, messageOptions = {} } = {}) {
messageOptions.rollMode ??= game.settings.get('core', 'rollMode');
// Construct chat data
messageData = foundry.utils.mergeObject(
{
author: game.user.id,
speaker: foundry.documents.ChatMessage.implementation.getSpeaker(),
rolls: [],
sound: roll ? CONFIG.sounds.dice : null,
flags: { 'core.RollTable': this.id }
},
messageData
);
if (roll) messageData.rolls.push(roll);
// Render the chat card which combines the dice roll with the drawn results
const detailsPromises = await Promise.allSettled(results.map(r => r.getHTML()));
const flavorKey = `TABLE.DrawFlavor${results.length > 1 ? 'Plural' : ''}`;
const flavor = game.i18n.format(flavorKey, {
number: results.length,
name: foundry.utils.escapeHTML(this.name)
});
messageData.content = await foundry.applications.handlebars.renderTemplate(CONFIG.RollTable.resultTemplate, {
description: await TextEditor.implementation.enrichHTML(this.description, {
documents: true,
secrets: this.isOwner
}),
flavor: flavor,
results: results.map((result, i) => {
const r = result.toObject(false);
r.details = detailsPromises[i].value ?? '';
const useTableIcon =
result.icon === CONFIG.RollTable.resultIcon && this.img !== this.constructor.DEFAULT_ICON;
r.icon = useTableIcon ? this.img : result.icon;
return r;
}),
rollHTML: this.displayRoll && roll ? await roll.render() : null,
table: this
});
// Create the chat message
return foundry.documents.ChatMessage.implementation.create(messageData, messageOptions);
}
}

View file

@ -51,6 +51,27 @@ export default class DhScene extends Scene {
}
}
async _preUpdate(changes, options, user) {
const allowed = await super._preUpdate(changes, options, user);
if (allowed === false) return false;
if (changes.flags?.daggerheart) {
if (this._source.flags.daggerheart) {
const unregisterTriggerData = (this._source.flags.daggerheart.sceneEnvironments ?? []).reduce(
(acc, env) => {
if (!changes.flags.daggerheart.sceneEnvironments.includes(env)) acc.sceneEnvironments.push(env);
return acc;
},
{ ...this._source.flags.daggerheart, sceneEnvironments: [] }
);
game.system.registeredTriggers.unregisterSceneEnvironmentTriggers(unregisterTriggerData);
}
game.system.registeredTriggers.registerSceneEnvironmentTriggers(changes.flags.daggerheart);
}
}
_onDelete(options, userId) {
super._onDelete(options, userId);

View file

@ -536,4 +536,10 @@ export default class DHToken extends CONFIG.Token.documentClass {
};
}
//#endregion
async _preDelete() {
if (this.actor && !this.actor.prototypeToken?.actorLink) {
game.system.registeredTriggers.unregisterItemTriggers(this.actor.items);
}
}
}

View file

@ -0,0 +1,104 @@
/**
* A singleton class that handles preview tokens.
*/
export default class DhTokenManager {
#activePreview;
#actor;
#resolve;
/**
* Create a template preview, deactivating any existing ones.
* @param {object} data
*/
async createPreview(actor, tokenData) {
this.#actor = actor;
const token = await canvas.tokens._createPreview(
{
...actor.prototypeToken,
displayName: 50,
...tokenData
},
{ renderSheet: false, actor }
);
this.#activePreview = {
document: token.document,
object: token,
origin: { x: token.document.x, y: token.document.y }
};
this.#activePreview.events = {
contextmenu: this.#cancelTemplate.bind(this),
mousedown: this.#confirmTemplate.bind(this),
mousemove: this.#onDragMouseMove.bind(this)
};
canvas.stage.on('mousemove', this.#activePreview.events.mousemove);
canvas.stage.on('mousedown', this.#activePreview.events.mousedown);
canvas.app.view.addEventListener('contextmenu', this.#activePreview.events.contextmenu);
}
/* Currently intended for using as a preview of where to create a token. (note the flag) */
async createPreviewAsync(actor, tokenData = {}) {
return new Promise(resolve => {
this.#resolve = resolve;
this.createPreview(actor, { ...tokenData, flags: { daggerheart: { createPlacement: true } } });
});
}
/**
* Handles the movement of the token preview on mousedrag.
* @param {mousemove Event} event
*/
#onDragMouseMove(event) {
event.stopPropagation();
const { moveTime, object } = this.#activePreview;
const update = {};
const now = Date.now();
if (now - (moveTime || 0) <= 16) return;
this.#activePreview.moveTime = now;
let cursor = event.getLocalPosition(canvas.templates);
Object.assign(update, canvas.grid.getTopLeftPoint(cursor));
object.document.updateSource(update);
object.renderFlags.set({ refresh: true });
}
/**
* Cancels the preview token on right-click.
* @param {contextmenu Event} event
*/
#cancelTemplate(_event, resolved) {
const { mousemove, mousedown, contextmenu } = this.#activePreview.events;
this.#activePreview.object.destroy();
canvas.stage.off('mousemove', mousemove);
canvas.stage.off('mousedown', mousedown);
canvas.app.view.removeEventListener('contextmenu', contextmenu);
if (this.#resolve && !resolved) this.#resolve(false);
}
/**
* Creates a real Actor and token at the preview location and cancels the preview.
* @param {click Event} event
*/
async #confirmTemplate(event) {
event.stopPropagation();
this.#cancelTemplate(event, true);
const actor = this.#actor.inCompendium
? await game.system.api.documents.DhpActor.create(this.#actor.toObject())
: this.#actor;
const tokenData = await actor.getTokenDocument();
const result = await canvas.scene.createEmbeddedDocuments('Token', [
{ ...tokenData, x: this.#activePreview.document.x, y: this.#activePreview.document.y }
]);
this.#activePreview = undefined;
if (this.#resolve && result.length) this.#resolve(result[0]);
}
}

View file

@ -67,7 +67,7 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
if (item) {
const isAction = item instanceof game.system.api.models.actions.actionsTypes.base;
const isEffect = item instanceof ActiveEffect;
await this.enrichText(item, isAction || isEffect);
await this.enrichText(item);
const type = isAction ? 'action' : isEffect ? 'effect' : item.type;
html = await foundry.applications.handlebars.renderTemplate(
@ -202,10 +202,20 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
}
}
async enrichText(item, flatStructure) {
async enrichText(item) {
const { TextEditor } = foundry.applications.ux;
if (item.system?.metadata?.hasDescription) {
const enrichedValue =
(await item.system?.getEnrichedDescription?.()) ??
(await TextEditor.enrichHTML(item.system.description));
foundry.utils.setProperty(item, 'system.enrichedDescription', enrichedValue);
} else if (item.description) {
const enrichedValue = await TextEditor.enrichHTML(item.description);
foundry.utils.setProperty(item, 'enrichedDescription', enrichedValue);
}
const enrichPaths = [
{ path: flatStructure ? '' : 'system', name: 'description' },
{ path: 'system', name: 'features' },
{ path: 'system', name: 'actions' },
{ path: 'system', name: 'customActions' }

View file

@ -2,7 +2,7 @@ import { abilities } from '../config/actorConfig.mjs';
import { getCommandTarget, rollCommandToJSON } from '../helpers/utils.mjs';
export default function DhDualityRollEnricher(match, _options) {
const roll = rollCommandToJSON(match[1], match[0]);
const roll = rollCommandToJSON(match[0]);
if (!roll) return match[0];
return getDualityMessage(roll.result, roll.flavor);
@ -47,6 +47,7 @@ function getDualityMessage(roll, flavor) {
${roll?.trait && abilities[roll.trait] ? `data-trait="${roll.trait}"` : ''}
${roll?.advantage ? 'data-advantage="true"' : ''}
${roll?.disadvantage ? 'data-disadvantage="true"' : ''}
${roll?.grantResources ? 'data-grant-resources="true"' : ''}
>
${roll?.reaction ? '<i class="fa-solid fa-reply"></i>' : '<i class="fa-solid fa-circle-half-stroke"></i>'}
${label}
@ -63,7 +64,8 @@ export const renderDualityButton = async event => {
traitValue = button.dataset.trait?.toLowerCase(),
target = getCommandTarget({ allowNull: true }),
difficulty = button.dataset.difficulty,
advantage = button.dataset.advantage ? Number(button.dataset.advantage) : undefined;
advantage = button.dataset.advantage ? Number(button.dataset.advantage) : undefined,
grantResources = Boolean(button.dataset?.grantResources);
await enrichedDualityRoll(
{
@ -73,36 +75,45 @@ export const renderDualityButton = async event => {
difficulty,
title: button.dataset.title,
label: button.dataset.label,
advantage
advantage,
grantResources
},
event
);
};
export const enrichedDualityRoll = async (
{ reaction, traitValue, target, difficulty, title, label, advantage },
{ reaction, traitValue, target, difficulty, title, label, advantage, grantResources, customConfig },
event
) => {
const config = {
event: event ?? {},
title: title,
headerTitle: label,
roll: {
trait: traitValue && target ? traitValue : null,
label: label,
difficulty: difficulty,
advantage,
type: reaction ? 'reaction' : null
},
skips: {
resources: !grantResources,
triggers: !grantResources
},
type: 'trait',
hasRoll: true
hasRoll: true,
...(customConfig ?? {})
};
if (target) {
await target.diceRoll(config);
const result = await target.diceRoll(config);
if (!result) return;
result.resourceUpdates.updateResources();
} else {
// For no target, call DualityRoll directly with basic data
config.data = { experiences: {}, traits: {} };
config.data = { experiences: {}, traits: {}, rules: {} };
config.source = { actor: null };
await CONFIG.Dice.daggerheart.DualityRoll.build(config);
}
return config;
};

View file

@ -0,0 +1,80 @@
import { getCommandTarget, rollCommandToJSON } from '../helpers/utils.mjs';
export default function DhFateRollEnricher(match, _options) {
const roll = rollCommandToJSON(match[0]);
if (!roll) return match[0];
return getFateMessage(roll.result, roll?.flavor);
}
export function getFateTypeData(fateTypeValue) {
const value = fateTypeValue ? fateTypeValue.capitalize() : 'Hope';
const lowercased = fateTypeValue?.toLowerCase?.() ?? 'hope';
switch (lowercased) {
case 'hope':
case 'fear':
return { value, label: game.i18n.localize(`DAGGERHEART.GENERAL.${lowercased}`) };
default:
return null;
}
}
function getFateMessage(roll, flavor) {
const fateTypeData = getFateTypeData(roll?.type);
if (!fateTypeData)
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing'));
const { value: fateType, label: fateTypeLabel } = fateTypeData;
const title = flavor ?? game.i18n.localize('DAGGERHEART.GENERAL.fateRoll');
const fateElement = document.createElement('span');
fateElement.innerHTML = `
<button type="button" class="fate-roll-button${roll?.inline ? ' inline' : ''}"
data-title="${title}"
data-label="${fateTypeLabel}"
data-fateType="${fateType}"
>
${title}
</button>
`;
return fateElement;
}
export const renderFateButton = async event => {
const button = event.currentTarget,
target = getCommandTarget({ allowNull: true });
const fateTypeData = getFateTypeData(button.dataset?.fatetype);
if (!fateTypeData) ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing'));
const { value: fateType, label: fateTypeLabel } = fateTypeData;
await enrichedFateRoll(
{
target,
title: button.dataset.title,
label: button.dataset.label,
fateType: fateType
},
event
);
};
export const enrichedFateRoll = async ({ target, title, label, fateType }, event) => {
const config = {
event: event ?? {},
title: title,
headerTitle: label,
roll: {},
hasRoll: true,
fateType: fateType,
skips: { reaction: true }
};
config.data = { experiences: {}, traits: {}, fateType: fateType };
config.source = { actor: target?.uuid };
await CONFIG.Dice.daggerheart.FateRoll.build(config);
return config;
};

View file

@ -1,10 +1,11 @@
import { default as DhDamageEnricher, renderDamageButton } from './DamageEnricher.mjs';
import { default as DhDualityRollEnricher, renderDualityButton } from './DualityRollEnricher.mjs';
import { default as DhFateRollEnricher, renderFateButton } from './FateRollEnricher.mjs';
import { default as DhEffectEnricher } from './EffectEnricher.mjs';
import { default as DhTemplateEnricher, renderMeasuredTemplate } from './TemplateEnricher.mjs';
import { default as DhLookupEnricher } from './LookupEnricher.mjs';
export { DhDamageEnricher, DhDualityRollEnricher, DhEffectEnricher, DhTemplateEnricher };
export { DhDamageEnricher, DhDualityRollEnricher, DhEffectEnricher, DhTemplateEnricher, DhFateRollEnricher };
export const enricherConfig = [
{
@ -15,6 +16,10 @@ export const enricherConfig = [
pattern: /\[\[\/dr\s?(.*?)\]\]({[^}]*})?/g,
enricher: DhDualityRollEnricher
},
{
pattern: /\[\[\/fr\s?(.*?)\]\]({[^}]*})?/g,
enricher: DhFateRollEnricher
},
{
pattern: /@Effect\[([^\[\]]*)\]({[^}]*})?/g,
enricher: DhEffectEnricher
@ -38,6 +43,10 @@ export const enricherRenderSetup = element => {
.querySelectorAll('.duality-roll-button')
.forEach(element => element.addEventListener('click', renderDualityButton));
element
.querySelectorAll('.fate-roll-button')
.forEach(element => element.addEventListener('click', renderFateButton));
element
.querySelectorAll('.measured-template-button')
.forEach(element => element.addEventListener('click', renderMeasuredTemplate));

View file

@ -1,14 +1,14 @@
import { diceTypes, getDiceSoNicePresets, range } from '../config/generalConfig.mjs';
import { diceTypes, getDiceSoNicePresets, getDiceSoNicePreset, range } from '../config/generalConfig.mjs';
import Tagify from '@yaireo/tagify';
export const capitalize = string => {
return string.charAt(0).toUpperCase() + string.slice(1);
};
export function rollCommandToJSON(text, raw) {
export function rollCommandToJSON(text) {
if (!text) return {};
const flavorMatch = raw?.match(/{(.*)}$/);
const flavorMatch = text?.match(/{(.*)}$/);
const flavor = flavorMatch ? flavorMatch[1] : null;
// Match key="quoted string" OR key=unquotedValue
@ -31,7 +31,7 @@ export function rollCommandToJSON(text, raw) {
}
result[key] = value;
}
return Object.keys(result).length > 0 ? { result, flavor } : null;
return { result, flavor };
}
export const getCommandTarget = (options = {}) => {
@ -69,6 +69,20 @@ export const setDiceSoNiceForDualityRoll = async (rollResult, advantageState, ho
}
};
export const setDiceSoNiceForHopeFateRoll = async (rollResult, hopeFaces) => {
if (!game.modules.get('dice-so-nice')?.active) return;
const { diceSoNice } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
const diceSoNicePresets = await getDiceSoNicePreset(diceSoNice.hope, hopeFaces);
rollResult.dice[0].options = diceSoNicePresets;
};
export const setDiceSoNiceForFearFateRoll = async (rollResult, fearFaces) => {
if (!game.modules.get('dice-so-nice')?.active) return;
const { diceSoNice } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
const diceSoNicePresets = await getDiceSoNicePreset(diceSoNice.fear, fearFaces);
rollResult.dice[0].options = diceSoNicePresets;
};
export const chunkify = (array, chunkSize, mappingFunc) => {
var chunkifiedArray = [];
for (let i = 0; i < array.length; i += chunkSize) {

View file

@ -32,6 +32,7 @@ export const preloadHandlebarsTemplates = async function () {
'systems/daggerheart/templates/actionTypes/effect.hbs',
'systems/daggerheart/templates/actionTypes/beastform.hbs',
'systems/daggerheart/templates/actionTypes/countdown.hbs',
'systems/daggerheart/templates/actionTypes/summon.hbs',
'systems/daggerheart/templates/settings/components/settings-item-line.hbs',
'systems/daggerheart/templates/ui/tooltip/parts/tooltipChips.hbs',
'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs',

View file

@ -210,6 +210,42 @@ export async function runMigrations() {
lastMigrationVersion = '1.2.7';
}
if (foundry.utils.isNewerVersion('1.5.5', lastMigrationVersion)) {
/* Clear out Environments that were added directly from compendium */
for (const scene of game.scenes) {
if (!scene.flags.daggerheart) continue;
const systemData = new game.system.api.data.scenes.DHScene(scene.flags.daggerheart);
const sceneEnvironments = systemData.sceneEnvironments;
const newEnvironments = sceneEnvironments.filter(x => !x?.pack);
if (newEnvironments.length !== sceneEnvironments.length)
await scene.update({ 'flags.daggerheart.sceneEnvironments': newEnvironments });
}
ui.nav.render(true);
lastMigrationVersion = '1.5.5';
}
if (foundry.utils.isNewerVersion('1.6.0', lastMigrationVersion)) {
/* Delevel any companions that are higher level than their partner character */
for (const companion of game.actors.filter(x => x.type === 'companion')) {
if (companion.system.levelData.level.current <= 1) continue;
if (!companion.system.partner) {
await companion.updateLevel(1);
} else {
const endLevel = companion.system.partner.system.levelData.level.current;
if (endLevel < companion.system.levelData.level.current) {
companion.system.levelData.level.changed = companion.system.levelData.level.current;
await companion.updateLevel(endLevel);
}
}
}
lastMigrationVersion = '1.6.0';
}
//#endregion
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion, lastMigrationVersion);

View file

@ -93,10 +93,6 @@ export const registerSocketHooks = () => {
}
}
});
Hooks.on(socketEvent.RefreshDocument, async data => {
const document = await foundry.utils.fromUuid(data.uuid);
document.sheet.render();
});
};
export const registerUserQueries = () => {

View file

@ -533,33 +533,31 @@
"description": "<p><strong>Spend a Fear</strong> to summon a @UUID[Compendium.daggerheart.adversaries.Actor.YhJrP7rTBiRdX5Fp]{Zombie Legion}, which appears at Close range and immediately takes the spotlight.</p>",
"resource": null,
"actions": {
"gZg3AkzCYUTExjE6": {
"type": "effect",
"_id": "gZg3AkzCYUTExjE6",
"qSuWxC8xQOhnbBx9": {
"type": "summon",
"_id": "qSuWxC8xQOhnbBx9",
"systemPath": "actions",
"baseAction": false,
"description": "",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action",
"cost": [
{
"scalable": false,
"key": "fear",
"value": 1,
"step": null
}
],
"cost": [],
"uses": {
"value": null,
"max": "",
"recovery": null
},
"effects": [],
"target": {
"type": "any",
"amount": null
"recovery": null,
"consumeOnSuccess": false
},
"summon": [
{
"actorUUID": "Compendium.daggerheart.adversaries.Actor.YhJrP7rTBiRdX5Fp",
"count": "1"
}
],
"name": "Spend Fear",
"img": "icons/magic/death/undead-zombie-grave-green.webp",
"range": ""
}
},

View file

@ -235,7 +235,51 @@
},
"_id": "2ESeh4tPhr6DI5ty",
"img": "icons/magic/death/skull-horned-worn-fire-blue.webp",
"effects": [],
"effects": [
{
"name": "Depths Of Despair",
"type": "base",
"system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
}
},
"_id": "nofxm1vGZ2TmceA2",
"img": "icons/magic/death/skull-horned-worn-fire-blue.webp",
"changes": [
{
"key": "system.rules.attack.damage.hpDamageMultiplier",
"mode": 5,
"value": "2",
"priority": null
}
],
"disabled": true,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
},
"description": "<p><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\">The </span><span style=\"box-sizing:border-box;scrollbar-width:thin;scrollbar-color:rgb(93, 20, 43) rgba(0, 0, 0, 0);font-family:Montserrat, sans-serif;color:rgb(239, 230, 216);font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial\">Demon of Despair</span><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\"> deals double damage to PCs with 0 Hope.</span></p>",
"origin": null,
"tint": "#ffffff",
"transfer": true,
"statuses": [],
"sort": 0,
"flags": {},
"_stats": {
"compendiumSource": null
},
"_key": "!actors.items.effects!kE4dfhqmIQpNd44e.2ESeh4tPhr6DI5ty.nofxm1vGZ2TmceA2"
}
],
"folder": null,
"sort": 0,
"ownership": {
@ -312,7 +356,14 @@
"range": "melee"
}
},
"changes": [],
"changes": [
{
"key": "system.rules.dualityRoll.defaultHopeDice",
"mode": 5,
"value": "d8",
"priority": null
}
],
"disabled": false,
"duration": {
"startTime": null,
@ -323,7 +374,7 @@
"startRound": null,
"startTurn": null
},
"description": "<p>All targets aff ected replace their Hope Die with a <strong>d8</strong> until they roll a success with Hope or their next rest.</p>",
"description": "<p>All targets affected replace their Hope Die with a <strong>d8</strong> until they roll a success with Hope or their next rest.</p>",
"tint": "#ffffff",
"statuses": [],
"sort": 0,

View file

@ -256,34 +256,45 @@
"description": "<p><strong>Spend a Fear</strong> to boil the blood of all PCs within Far range. They use a d20 as their Fear Die until the end of the scene.</p><p>@Template[type:emanation|range:f]</p>",
"resource": null,
"actions": {
"V142qYppCGJn8OiN": {
"jKvzbQT0vp66DDOH": {
"type": "effect",
"_id": "V142qYppCGJn8OiN",
"_id": "jKvzbQT0vp66DDOH",
"systemPath": "actions",
"baseAction": false,
"description": "",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action",
"cost": [
{
"scalable": false,
"key": "fear",
"value": 1,
"step": null
"itemId": null,
"step": null,
"consumeOnSuccess": false
}
],
"uses": {
"value": null,
"max": "",
"recovery": null
"recovery": null,
"consumeOnSuccess": false
},
"effects": [],
"effects": [
{
"_id": "gFeHLGgeRoDdd3VG",
"onSave": false
}
],
"target": {
"type": "self",
"type": "hostile",
"amount": null
},
"name": "Spend Fear",
"img": "icons/skills/melee/maneuver-greatsword-yellow.webp",
"range": ""
"range": "far"
}
},
"originItemType": null,
@ -292,7 +303,51 @@
},
"_id": "a33PW8UkziliowlR",
"img": "icons/skills/melee/maneuver-greatsword-yellow.webp",
"effects": [],
"effects": [
{
"name": "Battle Lust",
"img": "icons/skills/melee/maneuver-greatsword-yellow.webp",
"origin": "Compendium.daggerheart.adversaries.Actor.5lphJAgzoqZI3VoG.Item.a33PW8UkziliowlR",
"transfer": false,
"_id": "gFeHLGgeRoDdd3VG",
"type": "base",
"system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
}
},
"changes": [
{
"key": "system.rules.dualityRoll.defaultFearDice",
"mode": 5,
"value": "d20",
"priority": null
}
],
"disabled": false,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
},
"description": "<p>You use a d20 as your Fear Die until the end of the scene.</p>",
"tint": "#ffffff",
"statuses": [],
"sort": 0,
"flags": {},
"_stats": {
"compendiumSource": null
},
"_key": "!actors.items.effects!5lphJAgzoqZI3VoG.a33PW8UkziliowlR.gFeHLGgeRoDdd3VG"
}
],
"folder": null,
"sort": 0,
"ownership": {
@ -457,11 +512,12 @@
"img": "icons/creatures/unholy/demon-fire-horned-clawed.webp",
"range": ""
},
"7G6uWlFEeOLsJIWY": {
"type": "effect",
"_id": "7G6uWlFEeOLsJIWY",
"FlE6i0tbKEguF9wz": {
"type": "summon",
"_id": "FlE6i0tbKEguF9wz",
"systemPath": "actions",
"description": "<p>Summon [[/r 1d4]]@UUID[Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb]{Minor Demons}, who appear at Close range.</p>",
"baseAction": false,
"description": "",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
@ -474,13 +530,13 @@
"recovery": null,
"consumeOnSuccess": false
},
"effects": [],
"target": {
"type": "any",
"amount": null
},
"summon": [
{
"actorUUID": "Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb",
"count": "1d4"
}
],
"name": "Summon",
"img": "icons/creatures/unholy/demon-fire-horned-clawed.webp",
"range": ""
}
},

View file

@ -363,33 +363,31 @@
"description": "<p><strong>Spend a Fear</strong> to grow three @UUID[Compendium.daggerheart.adversaries.Actor.o63nS0k3wHu6EgKP]{Treant Sapling Minions}, who appear at Close range and immediately take the spotlight.</p>",
"resource": null,
"actions": {
"84Q2b0zIY9c7Yhho": {
"type": "effect",
"_id": "84Q2b0zIY9c7Yhho",
"R84DdS0OIx2cUt1w": {
"type": "summon",
"_id": "R84DdS0OIx2cUt1w",
"systemPath": "actions",
"baseAction": false,
"description": "",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action",
"cost": [
{
"scalable": false,
"key": "fear",
"value": 1,
"step": null
}
],
"cost": [],
"uses": {
"value": null,
"max": "",
"recovery": null
},
"effects": [],
"target": {
"type": "self",
"amount": null
"recovery": null,
"consumeOnSuccess": false
},
"summon": [
{
"actorUUID": "Compendium.daggerheart.adversaries.Actor.o63nS0k3wHu6EgKP",
"count": "3"
}
],
"name": "Spend Fear",
"img": "icons/magic/unholy/orb-hands-pink.webp",
"range": ""
}
},

View file

@ -304,7 +304,51 @@
},
"_id": "1fE6xo8yIOmZkGNE",
"img": "icons/skills/melee/strike-slashes-orange.webp",
"effects": [],
"effects": [
{
"name": "Overwhelm",
"type": "base",
"system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
}
},
"_id": "eGB9G0ljYCcdGbOx",
"img": "icons/skills/melee/strike-slashes-orange.webp",
"changes": [
{
"key": "system.rules.attack.damage.hpDamageMultiplier",
"mode": 5,
"value": "2",
"priority": null
}
],
"disabled": true,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
},
"description": "<p><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\">When a target the </span><span style=\"box-sizing:border-box;scrollbar-width:thin;scrollbar-color:rgb(93, 20, 43) rgba(0, 0, 0, 0);font-family:Montserrat, sans-serif;color:rgb(239, 230, 216);font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial\">Failed Experiment</span><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\"> attacks has other adversaries within Very Close range, the </span><span style=\"box-sizing:border-box;scrollbar-width:thin;scrollbar-color:rgb(93, 20, 43) rgba(0, 0, 0, 0);font-family:Montserrat, sans-serif;color:rgb(239, 230, 216);font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial\">Failed Experiment</span><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\"> deals double damage.</span></p>",
"origin": null,
"tint": "#ffffff",
"transfer": true,
"statuses": [],
"sort": 0,
"flags": {},
"_stats": {
"compendiumSource": null
},
"_key": "!actors.items.effects!ChwwVqowFw8hJQwT.1fE6xo8yIOmZkGNE.eGB9G0ljYCcdGbOx"
}
],
"folder": null,
"sort": 0,
"ownership": {

View file

@ -510,34 +510,41 @@
"description": "<p>When the @Lookup[@name] has 3 or more HP marked, you can <strong>spend a Fear</strong> to split them into two @UUID[Compendium.daggerheart.adversaries.Actor.aLkLFuVoKz2NLoBK]{Tiny Green Oozes} (with no marked HP or Stress). Immediately spotlight both of them.</p>",
"resource": null,
"actions": {
"s5mLw6DRGd76MLcC": {
"type": "effect",
"_id": "s5mLw6DRGd76MLcC",
"J8U7dw3cDSsEirr5": {
"type": "summon",
"_id": "J8U7dw3cDSsEirr5",
"systemPath": "actions",
"baseAction": false,
"description": "",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action",
"cost": [
{
"scalable": false,
"key": "fear",
"value": 1,
"step": null
"itemId": null,
"step": null,
"consumeOnSuccess": false
}
],
"uses": {
"value": null,
"max": "",
"recovery": null
},
"effects": [],
"target": {
"type": "self",
"amount": null
"recovery": null,
"consumeOnSuccess": false
},
"summon": [
{
"actorUUID": "Compendium.daggerheart.adversaries.Actor.aLkLFuVoKz2NLoBK",
"count": "2"
}
],
"name": "Spend Fear",
"img": "icons/creatures/slimes/slime-movement-pseudopods-green.webp",
"range": ""
"range": "self"
}
},
"originItemType": null,

View file

@ -229,7 +229,51 @@
},
"_id": "FGJTAeL38zTVd4fA",
"img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp",
"effects": [],
"effects": [
{
"name": "Punish the Guilty",
"type": "base",
"system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
}
},
"_id": "ID85zoIa5GfhNMti",
"img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp",
"changes": [
{
"key": "system.rules.attack.damage.hpDamageMultiplier",
"mode": 5,
"value": "2",
"priority": null
}
],
"disabled": true,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
},
"description": "<p><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\">The </span><span style=\"box-sizing:border-box;scrollbar-width:thin;scrollbar-color:rgb(93, 20, 43) rgba(0, 0, 0, 0);font-family:Montserrat, sans-serif;color:rgb(239, 230, 216);font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial\">Hallowed Archer</span><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\"> deals double damage to targets marked Guilty by a High Seraph.</span></p>",
"origin": null,
"tint": "#ffffff",
"transfer": true,
"statuses": [],
"sort": 0,
"flags": {},
"_stats": {
"compendiumSource": null
},
"_key": "!actors.items.effects!kabueAo6BALApWqp.FGJTAeL38zTVd4fA.ID85zoIa5GfhNMti"
}
],
"folder": null,
"sort": 0,
"ownership": {

View file

@ -474,33 +474,31 @@
"description": "<p><strong>Spend 2 Fear</strong> to summon [[/r 1d4]] @UUID[Compendium.daggerheart.adversaries.Actor.WWyUp6Mxl1S3KYUG]{Vampires}, who appear at Far range and immediately take the spotlight.</p>",
"resource": null,
"actions": {
"5Q6RMUTiauKw0tDj": {
"type": "effect",
"_id": "5Q6RMUTiauKw0tDj",
"jGFOnU6PNdWU6iF4": {
"type": "summon",
"_id": "jGFOnU6PNdWU6iF4",
"systemPath": "actions",
"baseAction": false,
"description": "",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action",
"cost": [
{
"scalable": false,
"key": "fear",
"value": 2,
"step": null
}
],
"cost": [],
"uses": {
"value": null,
"max": "",
"recovery": null
"recovery": null,
"consumeOnSuccess": false
},
"effects": [],
"target": {
"type": "any",
"amount": null
},
"name": "Summon Vampires",
"img": "icons/creatures/mammals/bat-giant-tattered-purple.webp",
"summon": [
{
"actorUUID": "Compendium.daggerheart.adversaries.Actor.WWyUp6Mxl1S3KYUG",
"count": "1d4"
}
],
"name": "Spend Fear",
"range": ""
}
},

View file

@ -479,33 +479,31 @@
"description": "<p>When the @Lookup[@name] has 4 or more HP marked, you can <strong>spend a Fear</strong> to split them into two @UUID[Compendium.daggerheart.adversaries.Actor.SHXedd9zZPVfUgUa]{Green Oozes}(with no marked HP or Stress). Immediately spotlight both of them.</p>",
"resource": null,
"actions": {
"iQsYAqpUFvJslRDr": {
"type": "effect",
"_id": "iQsYAqpUFvJslRDr",
"aeRdkiRsDNagTKhp": {
"type": "summon",
"_id": "aeRdkiRsDNagTKhp",
"systemPath": "actions",
"baseAction": false,
"description": "",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action",
"cost": [
{
"scalable": false,
"key": "fear",
"value": 1,
"step": null
}
],
"cost": [],
"uses": {
"value": null,
"max": "",
"recovery": null
},
"effects": [],
"target": {
"type": "any",
"amount": null
"recovery": null,
"consumeOnSuccess": false
},
"summon": [
{
"actorUUID": "Compendium.daggerheart.adversaries.Actor.SHXedd9zZPVfUgUa",
"count": "2"
}
],
"name": "Spend Fear",
"img": "icons/creatures/slimes/slime-movement-pseudopods-green.webp",
"range": ""
}
},

View file

@ -336,7 +336,14 @@
"range": "melee"
}
},
"changes": [],
"changes": [
{
"key": "system.rules.attack.damage.hpDamageTakenMultiplier",
"mode": 5,
"value": "2",
"priority": null
}
],
"disabled": false,
"duration": {
"startTime": null,
@ -350,8 +357,8 @@
"description": "",
"tint": "#ffffff",
"statuses": [
"restrained",
"vulnerable"
"vulnerable",
"restrained"
],
"sort": 0,
"flags": {},

View file

@ -287,7 +287,35 @@
"system": {
"description": "<p>Summon three @Compendium[daggerheart.adversaries.Actor.C0OMQqV7pN6t7ouR], who appear at Far range.</p>",
"resource": null,
"actions": {},
"actions": {
"MCTBsw9lusUdubj0": {
"type": "summon",
"_id": "MCTBsw9lusUdubj0",
"systemPath": "actions",
"baseAction": false,
"description": "",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action",
"cost": [],
"uses": {
"value": null,
"max": "",
"recovery": null,
"consumeOnSuccess": false
},
"summon": [
{
"actorUUID": "Compendium.daggerheart.adversaries.Actor.C0OMQqV7pN6t7ouR",
"count": "3"
}
],
"name": "Summon",
"range": ""
}
},
"originItemType": null,
"subType": null,
"originId": null,

View file

@ -258,57 +258,40 @@
"description": "<p>Once per scene, <strong>mark a Stress</strong> to summon <strong>1d4</strong> @UUID[Compendium.daggerheart.adversaries.Actor.B4LZcGuBAHzyVdzy]{Bladed Guards}, who appear at Far range to enforce the @Lookup[@name]s will.</p>",
"resource": null,
"actions": {
"cUKwhq1imsTVru8D": {
"type": "attack",
"_id": "cUKwhq1imsTVru8D",
"tioTtYfIGFIXRITN": {
"type": "summon",
"_id": "tioTtYfIGFIXRITN",
"systemPath": "actions",
"description": "<p>Once per scene, <strong>mark a Stress</strong> to summon <strong>1d4</strong> @UUID[Compendium.daggerheart.adversaries.Actor.B4LZcGuBAHzyVdzy]{Bladed Guards}, who appear at Far range to enforce the Nobles will.</p>",
"baseAction": false,
"description": "",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action",
"cost": [
{
"scalable": false,
"key": "stress",
"value": 1,
"step": null
"itemId": null,
"step": null,
"consumeOnSuccess": false
}
],
"uses": {
"value": null,
"max": "",
"recovery": null
},
"damage": {
"parts": [],
"includeBase": false
},
"target": {
"type": "any",
"amount": null
},
"effects": [],
"roll": {
"type": "diceSet",
"trait": null,
"difficulty": null,
"bonus": null,
"advState": "neutral",
"diceRolling": {
"multiplier": "prof",
"flatMultiplier": 1,
"dice": "d4",
"compare": null,
"treshold": null
},
"useDefault": false
},
"save": {
"trait": null,
"difficulty": null,
"damageMod": "none"
"max": "1",
"recovery": "scene",
"consumeOnSuccess": false
},
"summon": [
{
"actorUUID": "Compendium.daggerheart.adversaries.Actor.B4LZcGuBAHzyVdzy",
"count": "1d4"
}
],
"name": "Summon Guards",
"img": "icons/environment/people/infantry-armored.webp",
"range": ""
}
},

View file

@ -313,36 +313,43 @@
"_id": "WGEGO0DSOs5cF0EL",
"img": "icons/environment/people/charge.webp",
"system": {
"description": "<p>Once per scene, <strong>mark a Stress</strong> to summon a Pirate Raiders Horde, which appears at Far range.</p>",
"description": "<p>Once per scene, <strong>mark a Stress</strong> to summon a @UUID[Compendium.daggerheart.adversaries.Actor.5YgEajn0wa4i85kC]{Pirate Raider Horde}, which appears at Far range.</p>",
"resource": null,
"actions": {
"NlgIp0KrmZoS27Xy": {
"type": "effect",
"_id": "NlgIp0KrmZoS27Xy",
"nuYk5WeLLpIKa69q": {
"type": "summon",
"_id": "nuYk5WeLLpIKa69q",
"systemPath": "actions",
"baseAction": false,
"description": "",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action",
"cost": [
{
"scalable": false,
"key": "stress",
"value": 1,
"step": null
"itemId": null,
"step": null,
"consumeOnSuccess": false
}
],
"uses": {
"value": null,
"max": "",
"recovery": null
},
"effects": [],
"target": {
"type": "any",
"amount": null
"recovery": null,
"consumeOnSuccess": false
},
"summon": [
{
"actorUUID": "Compendium.daggerheart.adversaries.Actor.5YgEajn0wa4i85kC",
"count": "1"
}
],
"name": "Mark Stress",
"img": "icons/environment/people/charge.webp",
"range": ""
}
},

View file

@ -454,33 +454,40 @@
"description": "<p>When the @Lookup[@name] has 3 or more HP marked, you can <strong>spend a Fear</strong> to split them into two @UUID[Compendium.daggerheart.adversaries.Actor.1fkLQXVtmILqfJ44]{Tiny Red Oozes} (with no marked HP or Stress). Immediately spotlight both of them.</p>",
"resource": null,
"actions": {
"dw6Juw8mriH7sg0e": {
"type": "effect",
"_id": "dw6Juw8mriH7sg0e",
"BMEr77hDxaQyYBna": {
"type": "summon",
"_id": "BMEr77hDxaQyYBna",
"systemPath": "actions",
"baseAction": false,
"description": "",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action",
"cost": [
{
"scalable": false,
"key": "fear",
"value": 1,
"step": null
"itemId": null,
"step": null,
"consumeOnSuccess": false
}
],
"uses": {
"value": null,
"max": "",
"recovery": null
},
"effects": [],
"target": {
"type": "any",
"amount": null
"recovery": null,
"consumeOnSuccess": false
},
"summon": [
{
"actorUUID": "Compendium.daggerheart.adversaries.Actor.1fkLQXVtmILqfJ44",
"count": "2"
}
],
"name": "Spend Fear",
"img": "icons/creatures/slimes/slime-movement-splashing-red.webp",
"range": ""
}
},

View file

@ -416,28 +416,6 @@
"description": "<p><em>Countdown (6)</em>. When the @Lookup[@name] is in the spotlight for the first time, activate the countdown. When they mark HP, tick down this countdown by the number of HP marked. When it triggers, summon a @UUID[Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb]{Minor Demon} who appears at Close range.</p>",
"resource": null,
"actions": {
"0rixG6jLRynAYNqA": {
"type": "effect",
"_id": "0rixG6jLRynAYNqA",
"systemPath": "actions",
"description": "<p>Summon a @UUID[Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb]{Minor Demon} who appears at Close range.</p>",
"chatDisplay": true,
"actionType": "action",
"cost": [],
"uses": {
"value": null,
"max": "",
"recovery": null
},
"effects": [],
"target": {
"type": "any",
"amount": null
},
"name": "Summon",
"img": "icons/magic/unholy/silhouette-light-fire-blue.webp",
"range": "close"
},
"ZVXHY2fpomoKV7jG": {
"type": "countdown",
"_id": "ZVXHY2fpomoKV7jG",
@ -474,6 +452,33 @@
"name": "Start Countdown",
"img": "icons/magic/unholy/silhouette-light-fire-blue.webp",
"range": ""
},
"YReYG6DrWp4QGSij": {
"type": "summon",
"_id": "YReYG6DrWp4QGSij",
"systemPath": "actions",
"baseAction": false,
"description": "",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action",
"cost": [],
"uses": {
"value": null,
"max": "",
"recovery": null,
"consumeOnSuccess": false
},
"summon": [
{
"actorUUID": "Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb",
"count": "1"
}
],
"name": "Summon",
"range": ""
}
},
"originItemType": null,
@ -502,33 +507,31 @@
"description": "<p>Once per scene, when the @Lookup[@name] marks 2 or more HP, you can <strong>mark a Stress</strong> to summon a @UUID[Compendium.daggerheart.adversaries.Actor.NoRZ1PqB8N5wcIw0]{Demonic Hound Pack}, which appears at Close range and is immediately spotlighted.</p>",
"resource": null,
"actions": {
"JBuQUJhif2A7IlJd": {
"type": "effect",
"_id": "JBuQUJhif2A7IlJd",
"tfmY6HYkkY27NBaF": {
"type": "summon",
"_id": "tfmY6HYkkY27NBaF",
"systemPath": "actions",
"baseAction": false,
"description": "",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action",
"cost": [
{
"scalable": false,
"key": "stress",
"value": 1,
"step": null
}
],
"cost": [],
"uses": {
"value": null,
"max": "1",
"recovery": "scene"
},
"effects": [],
"target": {
"type": "self",
"amount": null
"max": "",
"recovery": null,
"consumeOnSuccess": false
},
"summon": [
{
"actorUUID": "Compendium.daggerheart.adversaries.Actor.NoRZ1PqB8N5wcIw0",
"count": "1"
}
],
"name": "Mark Stress",
"img": "icons/creatures/unholy/demon-fire-horned-clawed.webp",
"range": ""
}
},

View file

@ -230,7 +230,51 @@
"subType": null,
"originId": null
},
"effects": [],
"effects": [
{
"name": "Opportunist",
"type": "base",
"system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
}
},
"_id": "O03vYbyNLO3YPZGo",
"img": "icons/skills/targeting/crosshair-triple-strike-orange.webp",
"changes": [
{
"key": "system.rules.attack.damage.hpDamageMultiplier",
"mode": 5,
"value": "2",
"priority": null
}
],
"disabled": true,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
},
"description": "<p><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\">When two or more adversaries are within Very Close range of a creature, all damage the </span><span style=\"box-sizing:border-box;scrollbar-width:thin;scrollbar-color:rgb(93, 20, 43) rgba(0, 0, 0, 0);font-family:Montserrat, sans-serif;color:rgb(239, 230, 216);font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial\">Skeleton Archer</span><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\"> deals to that creature is doubled.</span></p>",
"origin": null,
"tint": "#ffffff",
"transfer": true,
"statuses": [],
"sort": 0,
"flags": {},
"_stats": {
"compendiumSource": null
},
"_key": "!actors.items.effects!7X5q7a6ueeHs5oA9.6mL2FQ9pQdfoDNzG.O03vYbyNLO3YPZGo"
}
],
"folder": null,
"sort": 0,
"ownership": {

View file

@ -340,7 +340,35 @@
"system": {
"description": "<p>When an attack from the @Lookup[@name] causes a target to mark HP and there are three or more @Lookup[@name] Minions within Close range, you can combine the Minions into a @UUID[Compendium.daggerheart.adversaries.Actor.PKSXFuaIHUCoH63A]{Tangle Bramble Swarm Horde}. The Hordes HP is equal to the number of Minions combined.</p>",
"resource": null,
"actions": {},
"actions": {
"g1OQ5xlMHFWsoktd": {
"type": "summon",
"_id": "g1OQ5xlMHFWsoktd",
"systemPath": "actions",
"baseAction": false,
"description": "",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action",
"cost": [],
"uses": {
"value": null,
"max": "",
"recovery": null,
"consumeOnSuccess": false
},
"summon": [
{
"actorUUID": "Compendium.daggerheart.adversaries.Actor.PKSXFuaIHUCoH63A",
"count": "1"
}
],
"name": "Summon",
"range": ""
}
},
"originItemType": null,
"subType": null,
"originId": null,

View file

@ -9,13 +9,47 @@
"resource": {
"type": "simple",
"value": 0,
"max": "",
"icon": "",
"recovery": null,
"max": "@system.levelData.level.current",
"icon": "fa-solid fa-water",
"recovery": "session",
"diceStates": {},
"dieFaces": "d4"
},
"actions": {},
"actions": {
"tFlus34KotJjHfTe": {
"type": "effect",
"_id": "tFlus34KotJjHfTe",
"systemPath": "actions",
"baseAction": false,
"description": "",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action",
"triggers": [
{
"trigger": "fearRoll",
"triggeringActorType": "self",
"command": "const { max, value } = this.item.system.resource;\nconst maxValue = actor.system.levelData.level.current;\nconst afterUpdate = value+1;\nif (afterUpdate > maxValue) return;\n\nui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.knowTheTide'));\nreturn { updates: [{\n key: 'resource',\n itemId: this.item.id,\n target: this.item,\n value: 1,\n}]};"
}
],
"cost": [],
"uses": {
"value": null,
"max": "",
"recovery": null,
"consumeOnSuccess": false
},
"effects": [],
"target": {
"type": "any",
"amount": null
},
"name": "Know The Tide",
"range": ""
}
},
"originItemType": null,
"subType": null,
"originId": null,

View file

@ -54,7 +54,8 @@
"source": "Daggerheart SRD",
"page": 120,
"artist": ""
}
},
"domainTouched": 4
},
"flags": {},
"_id": "5PvMQKCjrgSxzstn",

View file

@ -13,7 +13,8 @@
"source": "Daggerheart SRD",
"page": 121,
"artist": ""
}
},
"domainTouched": 4
},
"flags": {},
"_id": "Gb5bqpFSBiuBxUix",

View file

@ -81,7 +81,7 @@
"name": "Bold Presence",
"img": "icons/magic/holy/barrier-shield-winged-blue.webp",
"origin": "Compendium.daggerheart.domains.Item.tdsL00yTSLNgZWs6",
"transfer": false,
"transfer": true,
"_id": "2XEYhuAcRGTtqvED",
"type": "base",
"system": {

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