Compare commits

..

No commits in common. "286944d2e63da423f66bdf87a2516ba41b6a1d05" and "7614001bddf691c0870673140009d6dffb8dc370" have entirely different histories.

207 changed files with 1072 additions and 4908 deletions

View file

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

View file

@ -69,11 +69,7 @@
}, },
"summon": { "summon": {
"name": "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": { "Config": {
@ -126,9 +122,6 @@
}, },
"cost": { "cost": {
"stepTooltip": "+{step} per step" "stepTooltip": "+{step} per step"
},
"summon": {
"dropSummonsHere": "Drop Summons Here"
} }
} }
}, },
@ -203,8 +196,6 @@
"unequip": "Unequip", "unequip": "Unequip",
"useItem": "Use Item" "useItem": "Use Item"
}, },
"defaultHopeDice": "Default Hope Dice",
"defaultFearDice": "Default Fear Dice",
"disadvantageSources": { "disadvantageSources": {
"label": "Disadvantage Sources", "label": "Disadvantage Sources",
"hint": "Add single words or short text as reminders and hints of what a character has disadvantage on." "hint": "Add single words or short text as reminders and hints of what a character has disadvantage on."
@ -237,14 +228,11 @@
"confirmText": "Would you like to level up your companion {name} by {levelChange} levels at this time? (You can do it manually later)" "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", "viewLevelups": "View Levelups",
"resetCharacter": "Reset Character",
"viewParty": "View Party", "viewParty": "View Party",
"InvalidOldCharacterImportTitle": "Old Character Import", "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?", "InvalidOldCharacterImportText": "Character data exported prior to system version 1.1 will not generate a complete character. Do you wish to continue?",
"cancelBeastform": "Cancel Beastform", "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": { "Companion": {
"FIELDS": { "FIELDS": {
@ -318,8 +306,6 @@
"selectPrimaryWeapon": "Select Primary Weapon", "selectPrimaryWeapon": "Select Primary Weapon",
"selectSecondaryWeapon": "Select Secondary Weapon", "selectSecondaryWeapon": "Select Secondary Weapon",
"selectSubclass": "Select Subclass", "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", "startingItems": "Starting Items",
"story": "Story", "story": "Story",
"storyExplanation": "Select which background and connection prompts you want to copy into your character's background.", "storyExplanation": "Select which background and connection prompts you want to copy into your character's background.",
@ -331,12 +317,6 @@
"title": "{actor} - Character Setup", "title": "{actor} - Character Setup",
"traitIncreases": "Trait Increases" "traitIncreases": "Trait Increases"
}, },
"CharacterReset": {
"title": "Reset Character",
"alwaysDeleteSection": "Deleted Data",
"optionalDeleteSection": "Optional Data",
"headerTitle": "Select which data you'd like to keep"
},
"CombatTracker": { "CombatTracker": {
"combatStarted": "Active", "combatStarted": "Active",
"giveSpotlight": "Give The Spotlight", "giveSpotlight": "Give The Spotlight",
@ -489,9 +469,7 @@
"tokenHUD": { "tokenHUD": {
"genericEffects": "Foundry Effects", "genericEffects": "Foundry Effects",
"depositPartyTokens": "Deposit Party Tokens", "depositPartyTokens": "Deposit Party Tokens",
"retrievePartyTokens": "Retrieve Party Tokens", "retrievePartyTokens": "Retrieve Party Tokens"
"depositCompanionTokens": "Deposit Companion Token",
"retrieveCompanionTokens": "Retrieve Companion Token"
} }
}, },
"ImageSelect": { "ImageSelect": {
@ -621,7 +599,6 @@
}, },
"RerollDialog": { "RerollDialog": {
"title": "Reroll", "title": "Reroll",
"damageTitle": "Reroll Damage",
"deselectDiceNotification": "Deselect one of the selected dice first", "deselectDiceNotification": "Deselect one of the selected dice first",
"acceptCurrentRolls": "Accept Current Rolls" "acceptCurrentRolls": "Accept Current Rolls"
}, },
@ -629,13 +606,6 @@
"title": "{name} Resource", "title": "{name} Resource",
"rerollDice": "Reroll Dice" "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": { "TagTeamSelect": {
"title": "Tag Team Roll", "title": "Tag Team Roll",
"leaderTitle": "Initiating Character", "leaderTitle": "Initiating Character",
@ -983,10 +953,6 @@
"outsideRange": "Outside Range" "outsideRange": "Outside Range"
}, },
"Condition": { "Condition": {
"deathMove": {
"name": "Death Move",
"description": "The character is about to make a Death Move"
},
"dead": { "dead": {
"name": "Dead", "name": "Dead",
"description": "The character is dead" "description": "The character is dead"
@ -1038,15 +1004,15 @@
"DeathMoves": { "DeathMoves": {
"avoidDeath": { "avoidDeath": {
"name": "Avoid Death", "name": "Avoid Death",
"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." "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."
}, },
"riskItAll": { "riskItAll": {
"name": "Risk It All", "name": "Risk It All",
"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." "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."
}, },
"blazeOfGlory": { "blazeOfGlory": {
"name": "Blaze Of Glory", "name": "Blaze Of Glory",
"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." "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."
} }
}, },
"DomainCardTypes": { "DomainCardTypes": {
@ -2087,7 +2053,6 @@
"description": "Description", "description": "Description",
"main": "Data", "main": "Data",
"information": "Information", "information": "Information",
"itemFeatures": "Item Features",
"notes": "Notes", "notes": "Notes",
"inventory": "Inventory", "inventory": "Inventory",
"loadout": "Loadout", "loadout": "Loadout",
@ -2112,7 +2077,6 @@
"tier4": "tier 4", "tier4": "tier 4",
"domains": "Domains", "domains": "Domains",
"downtime": "Downtime", "downtime": "Downtime",
"itemFeatures": "Item Features",
"roll": "Roll", "roll": "Roll",
"rules": "Rules", "rules": "Rules",
"partyMembers": "Party Members", "partyMembers": "Party Members",
@ -2163,7 +2127,6 @@
"dropActorsHere": "Drop Actors here", "dropActorsHere": "Drop Actors here",
"dropFeaturesHere": "Drop Features here", "dropFeaturesHere": "Drop Features here",
"duality": "Duality", "duality": "Duality",
"dualityDice": "Duality Dice",
"dualityRoll": "Duality Roll", "dualityRoll": "Duality Roll",
"enabled": "Enabled", "enabled": "Enabled",
"evasion": "Evasion", "evasion": "Evasion",
@ -2177,14 +2140,11 @@
"single": "Favorite", "single": "Favorite",
"plural": "Favorites" "plural": "Favorites"
}, },
"fate": "Fate",
"fateRoll": "Fate Roll",
"fear": "Fear", "fear": "Fear",
"features": "Features", "features": "Features",
"formula": "Formula", "formula": "Formula",
"general": "General", "general": "General",
"gm": "GM", "gm": "GM",
"guaranteedCriticalSuccess": "Guaranteed Critical Success",
"healing": "Healing", "healing": "Healing",
"healingRoll": "Healing Roll", "healingRoll": "Healing Roll",
"hit": { "hit": {
@ -2228,7 +2188,6 @@
"single": "Player", "single": "Player",
"plurial": "Players" "plurial": "Players"
}, },
"portrait": "Portrait",
"proficiency": "Proficiency", "proficiency": "Proficiency",
"quantity": "Quantity", "quantity": "Quantity",
"range": "Range", "range": "Range",
@ -2245,17 +2204,12 @@
"rollWith": "{roll} Roll", "rollWith": "{roll} Roll",
"save": "Save", "save": "Save",
"scalable": "Scalable", "scalable": "Scalable",
"scars": "Scars",
"situationalBonus": "Situational Bonus", "situationalBonus": "Situational Bonus",
"spent": "Spent", "spent": "Spent",
"step": "Step", "step": "Step",
"stress": "Stress", "stress": "Stress",
"subclasses": "Subclasses", "subclasses": "Subclasses",
"success": "Success", "success": "Success",
"summon": {
"single": "Summon",
"plural": "Summons"
},
"take": "Take", "take": "Take",
"Target": { "Target": {
"single": "Target", "single": "Target",
@ -2322,8 +2276,7 @@
"placeholder": "Using character dimensions", "placeholder": "Using character dimensions",
"disabledPlaceholder": "Set by character size", "disabledPlaceholder": "Set by character size",
"height": { "label": "Height" }, "height": { "label": "Height" },
"width": { "label": "Width" }, "width": { "label": "Width" }
"scale": { "label": "Token Scale" }
}, },
"evolved": { "evolved": {
"maximumTier": { "label": "Maximum Tier" }, "maximumTier": { "label": "Maximum Tier" },
@ -2372,9 +2325,6 @@
"DomainCard": { "DomainCard": {
"type": "Type", "type": "Type",
"recallCost": "Recall Cost", "recallCost": "Recall Cost",
"vaultActive": "Active In Vault",
"loadoutIgnore": "Ignores Loadout Limits",
"domainTouched": "Domain Touched",
"foundationTitle": "Foundation", "foundationTitle": "Foundation",
"specializationTitle": "Specialization", "specializationTitle": "Specialization",
"masteryTitle": "Mastery" "masteryTitle": "Mastery"
@ -2388,12 +2338,6 @@
"secondaryWeapon": "Secondary Weapon" "secondaryWeapon": "Secondary Weapon"
} }
}, },
"ROLLTABLES": {
"FIELDS": {
"formulaName": { "label": "Formula Name" }
},
"formula": "Formula"
},
"SETTINGS": { "SETTINGS": {
"Appearance": { "Appearance": {
"FIELDS": { "FIELDS": {
@ -2460,11 +2404,7 @@
"overlay": { "label": "Overlay Effect" }, "overlay": { "label": "Overlay Effect" },
"characterDefault": { "label": "Character Default Defeated Status" }, "characterDefault": { "label": "Character Default Defeated Status" },
"adversaryDefault": { "label": "Adversary 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": { "hopeFear": {
"label": "Hope & Fear", "label": "Hope & Fear",
@ -2497,6 +2437,10 @@
"label": "Show Resource Change Scrolltexts", "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." "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": {
"roll": { "roll": {
"label": "Roll", "label": "Roll",
@ -2549,11 +2493,9 @@
"itemFeatures": "Item Features", "itemFeatures": "Item Features",
"nrChoices": "# Moves Per Rest", "nrChoices": "# Moves Per Rest",
"resetMovesTitle": "Reset {type} Downtime Moves", "resetMovesTitle": "Reset {type} Downtime Moves",
"resetItemFeaturesTitle": "Reset {type}",
"resetMovesText": "Are you sure you want to reset?", "resetMovesText": "Are you sure you want to reset?",
"FIELDS": { "FIELDS": {
"maxFear": { "label": "Max Fear" }, "maxFear": { "label": "Max Fear" },
"maxHope": { "label": "Max Hope" },
"traitArray": { "label": "Initial Trait Modifiers" }, "traitArray": { "label": "Initial Trait Modifiers" },
"maxLoadout": { "maxLoadout": {
"label": "Max Cards in Loadout", "label": "Max Cards in Loadout",
@ -2701,16 +2643,7 @@
"currentTarget": "Current" "currentTarget": "Current"
}, },
"deathMove": { "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": { "dicePool": {
"title": "Dice Pool" "title": "Dice Pool"
@ -2764,9 +2697,6 @@
"rerollDamage": "Reroll Damage", "rerollDamage": "Reroll Damage",
"assignTagRoll": "Assign as Tag Roll" "assignTagRoll": "Assign as Tag Roll"
}, },
"ConsoleLogs": {
"triggerRun": "DH TRIGGER | Item '{item}' on actor '{actor}' ran a '{trigger}' trigger."
},
"Countdowns": { "Countdowns": {
"title": "Countdowns", "title": "Countdowns",
"toggleIconMode": "Toggle Icon Only", "toggleIconMode": "Toggle Icon Only",
@ -2832,9 +2762,7 @@
"noAssignedPlayerCharacter": "You have no assigned character.", "noAssignedPlayerCharacter": "You have no assigned character.",
"noSelectedToken": "You have no selected token", "noSelectedToken": "You have no selected token",
"onlyUseableByPC": "This can only be used with a PC token", "onlyUseableByPC": "This can only be used with a PC token",
"dualityParsing": "Duality roll not properly formatted", "dualityParsing": "Duality roll not properly formated",
"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", "attributeFaulty": "The supplied Attribute doesn't exist",
"domainCardWrongDomain": "You don't have access to that Domain", "domainCardWrongDomain": "You don't have access to that Domain",
"domainCardToHighLevel": "The Domain Card is too high level to be selected", "domainCardToHighLevel": "The Domain Card is too high level to be selected",
@ -2897,9 +2825,7 @@
"noActorOwnership": "You do not have permissions for this character", "noActorOwnership": "You do not have permissions for this character",
"documentIsMissing": "The {documentType} is missing from the world.", "documentIsMissing": "The {documentType} is missing from the world.",
"tokenActorMissing": "{name} is missing an Actor", "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": { "Sidebar": {
"actorDirectory": { "actorDirectory": {
@ -2944,8 +2870,7 @@
"deleteItem": "Delete Item", "deleteItem": "Delete Item",
"immune": "Immune", "immune": "Immune",
"middleClick": "[Middle Click] Keep tooltip view", "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,6 +1,5 @@
export { default as AttributionDialog } from './attributionDialog.mjs'; export { default as AttributionDialog } from './attributionDialog.mjs';
export { default as BeastformDialog } from './beastformDialog.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 d20RollDialog } from './d20RollDialog.mjs';
export { default as DamageDialog } from './damageDialog.mjs'; export { default as DamageDialog } from './damageDialog.mjs';
export { default as DamageReductionDialog } from './damageReductionDialog.mjs'; export { default as DamageReductionDialog } from './damageReductionDialog.mjs';
@ -15,4 +14,3 @@ export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs';
export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs'; export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs';
export { default as GroupRollDialog } from './group-roll-dialog.mjs'; export { default as GroupRollDialog } from './group-roll-dialog.mjs';
export { default as TagTeamDialog } from './tagTeamDialog.mjs'; export { default as TagTeamDialog } from './tagTeamDialog.mjs';
export { default as RiskItAllDialog } from './riskItAllDialog.mjs';

View file

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

View file

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

View file

@ -1,16 +1,11 @@
import { enrichedFateRoll } from '../../enrichers/FateRollEnricher.mjs';
import { enrichedDualityRoll } from '../../enrichers/DualityRollEnricher.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV2) {
export default class DhpDeathMove extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(actor) { constructor(actor) {
super({}); super({});
this.actor = actor; this.actor = actor;
this.selectedMove = null; this.selectedMove = null;
this.showRiskItAllButton = false;
this.riskItAllButtonLabel = '';
this.riskItAllHope = 0;
} }
get title() { get title() {
@ -43,111 +38,6 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
return context; 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) { static selectMove(_, button) {
const move = button.dataset.move; const move = button.dataset.move;
this.selectedMove = CONFIG.DH.GENERAL.deathMoves[move]; this.selectedMove = CONFIG.DH.GENERAL.deathMoves[move];
@ -156,49 +46,23 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
} }
static async takeMove() { 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 cls = getDocumentClass('ChatMessage');
const msg = { const msg = {
user: game.user.id, user: game.user.id,
content: await foundry.applications.handlebars.renderTemplate( content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/deathMove.hbs', 'systems/daggerheart/templates/ui/chat/deathMove.hbs',
{ {
player: this.actor.name, player: this.actor.name,
actor: this.actor, actor: { name: this.actor.name, img: this.actor.img },
actorId: this.actor._id,
author: game.users.get(game.user.id), author: game.users.get(game.user.id),
title: game.i18n.localize(this.selectedMove.name), title: game.i18n.localize(this.selectedMove.name),
img: this.selectedMove.img, 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(), speaker: cls.getSpeaker(),
flags: { flags: {
daggerheart: { daggerheart: {
@ -208,5 +72,7 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
}; };
cls.create(msg); cls.create(msg);
this.close();
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -31,7 +31,6 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
editEffect: this.editEffect, editEffect: this.editEffect,
addDamage: this.addDamage, addDamage: this.addDamage,
removeDamage: this.removeDamage, removeDamage: this.removeDamage,
editDoc: this.editDoc,
addTrigger: this.addTrigger, addTrigger: this.addTrigger,
removeTrigger: this.removeTrigger, removeTrigger: this.removeTrigger,
expandTrigger: this.expandTrigger expandTrigger: this.expandTrigger
@ -40,8 +39,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
handler: this.updateForm, handler: this.updateForm,
submitOnChange: true, submitOnChange: true,
closeOnSubmit: false closeOnSubmit: false
}, }
dragDrop: [{ dragSelector: null, dropSelector: '#summon-drop-zone', handlers: ['_onDrop'] }]
}; };
static PARTS = { static PARTS = {
@ -103,7 +101,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
} }
}; };
static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects', 'summon']; static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects'];
_getTabs(tabs) { _getTabs(tabs) {
for (const v of Object.values(tabs)) { for (const v of Object.values(tabs)) {
@ -114,25 +112,9 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
return tabs; 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) { async _prepareContext(_options) {
const context = await super._prepareContext(_options, 'action'); const context = await super._prepareContext(_options, 'action');
context.source = this.action.toObject(true); 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.openSection = this.openSection;
context.tabs = this._getTabs(this.constructor.TABS); context.tabs = this._getTabs(this.constructor.TABS);
context.config = CONFIG.DH; context.config = CONFIG.DH;
@ -225,9 +207,8 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
} }
static async updateForm(event, _, formData) { static async updateForm(event, _, formData) {
const submitData = this._prepareSubmitData(event, formData); const submitData = this._prepareSubmitData(event, formData),
data = foundry.utils.mergeObject(this.action.toObject(), submitData);
const data = foundry.utils.mergeObject(this.action.toObject(), submitData);
this.action = await this.action.update(data); this.action = await this.action.update(data);
this.sheetUpdate?.(this.action); this.sheetUpdate?.(this.action);
@ -246,26 +227,12 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
static removeElement(event, button) { static removeElement(event, button) {
event.stopPropagation(); event.stopPropagation();
const data = this.action.toObject(), const data = this.action.toObject(),
key = event.target.closest('[data-key]').dataset.key; key = event.target.closest('[data-key]').dataset.key,
index = button.dataset.index;
// 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); data[key].splice(index, 1);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); 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) { static addDamage(_event) {
if (!this.action.damage.parts) return; if (!this.action.damage.parts) return;
const data = this.action.toObject(), const data = this.action.toObject(),
@ -337,15 +304,6 @@ 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 **/ /** Specific implementation in extending classes **/
static async addEffect(_event) {} static async addEffect(_event) {}
static removeEffect(_event, _button) {} static removeEffect(_event, _button) {}
@ -355,29 +313,4 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
this.tabGroups.primary = 'base'; this.tabGroups.primary = 'base';
await super.close(options); 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,4 +1,3 @@
export * as actors from './actors/_module.mjs'; export * as actors from './actors/_module.mjs';
export * as api from './api/_modules.mjs'; export * as api from './api/_modules.mjs';
export * as items from './items/_module.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"]', dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
dropSelector: null dropSelector: null
} }
] ],
}; };
static PARTS = { static PARTS = {
@ -185,6 +185,7 @@ export default class AdversarySheet extends DHBaseActorSheet {
super._onDragStart(event); super._onDragStart(event);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Application Clicks Actions */ /* Application Clicks Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */

View file

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

View file

@ -38,6 +38,15 @@ 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 */ /* Application Clicks Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -62,10 +71,10 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
title: `${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}: ${this.actor.name}`, title: `${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}: ${this.actor.name}`,
headerTitle: `Companion ${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}`, headerTitle: `Companion ${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}`,
roll: { roll: {
trait: partner.system.spellcastModifierTrait?.key, trait: partner.system.spellcastModifierTrait?.key
companionRoll: true
}, },
hasRoll: true hasRoll: true,
data: partner.getRollData()
}; };
const result = await partner.diceRoll(config); const result = await partner.diceRoll(config);

View file

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

View file

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

View file

@ -1,191 +0,0 @@
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 */ /** @override */
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['dh-style', 'directory'], classes: ['dh-style'],
window: { window: {
title: 'SIDEBAR.TabSettings' title: 'SIDEBAR.TabSettings'
}, },

View file

@ -81,9 +81,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
html.querySelectorAll('.group-roll-header-expand-section').forEach(element => html.querySelectorAll('.group-roll-header-expand-section').forEach(element =>
element.addEventListener('click', this.groupRollExpandSection) element.addEventListener('click', this.groupRollExpandSection)
); );
html.querySelectorAll('.risk-it-all-button').forEach(element =>
element.addEventListener('click', event => this.riskItAllClearStressAndHitPoints(event, data))
);
}; };
setupHooks() { setupHooks() {
@ -95,21 +92,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
super.close(options); 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) { async onRollSimple(event, message) {
const buttonType = event.target.dataset.type ?? 'damage', const buttonType = event.target.dataset.type ?? 'damage',
total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0), total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0),
@ -153,7 +135,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
async actionUseButton(event, message) { async actionUseButton(event, message) {
const { moveIndex, actionIndex, movePath } = event.currentTarget.dataset; const { moveIndex, actionIndex, movePath } = event.currentTarget.dataset;
const targetUuid = event.currentTarget.closest('.action-use-button-parent').querySelector('select')?.value; 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 actionType = message.system.moves[moveIndex].actions[actionIndex];
const cls = game.system.api.models.actions.actionsTypes[actionType.type]; const cls = game.system.api.models.actions.actionsTypes[actionType.type];
@ -388,10 +370,4 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
}); });
event.target.closest('.group-roll-section').querySelector('.group-roll-content').classList.toggle('closed'); 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 this.combats
.find(x => x.active) .find(x => x.active)
?.system?.extendedBattleToggles?.reduce((acc, toggle) => (acc ?? 0) + toggle.category, null) ?? null; ?.system?.extendedBattleToggles?.reduce((acc, toggle) => (acc ?? 0) + toggle.category, null) ?? null;
const maxBP = CONFIG.DH.ENCOUNTER.BaseBPPerEncounter(context.allCharacters.length) + modifierBP; const maxBP = CONFIG.DH.ENCOUNTER.BaseBPPerEncounter(context.characters.length) + modifierBP;
const currentBP = AdversaryBPPerEncounter(context.adversaries, context.allCharacters); const currentBP = AdversaryBPPerEncounter(context.adversaries, context.characters);
Object.assign(context, { Object.assign(context, {
fear: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), fear: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
@ -73,8 +73,9 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
Object.assign(context, { Object.assign(context, {
actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens, actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens,
adversaries, adversaries,
allCharacters: characters, characters: characters
characters: characters.filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0), ?.filter(x => !x.isNPC)
.filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0),
spotlightRequests spotlightRequests
}); });
} }

View file

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

View file

@ -230,14 +230,6 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
result.flatMap(r => r), result.flatMap(r => r),
'name' '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(); this.fieldFilter = this._createFieldFilter();
if (this.presets?.filter) { if (this.presets?.filter) {

View file

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

View file

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

View file

@ -1,12 +1,4 @@
export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { 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 */ /** @inheritDoc */
async _drawEffects() { async _drawEffects() {
this.effects.renderable = false; this.effects.renderable = false;
@ -42,7 +34,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
this.renderFlags.set({ refreshEffects: true }); this.renderFlags.set({ refreshEffects: true });
} }
/** /**
* Returns the distance from this token to another token object. * Returns the distance from this token to another token object.
* This value is corrected to handle alternate token sizes and other grid types * This value is corrected to handle alternate token sizes and other grid types
* according to the diagonal rules. * according to the diagonal rules.
@ -55,11 +47,11 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
const destinationPoint = target.center; const destinationPoint = target.center;
// Compute for gridless. This version returns circular edge to edge + grid distance, // 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) { if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
const boundsCorrection = canvas.grid.distance / canvas.grid.size; const boundsCorrection = canvas.grid.distance / canvas.grid.size;
const originRadius = (this.bounds.width * boundsCorrection) / 2; const originRadius = this.bounds.width * boundsCorrection / 2;
const targetRadius = (target.bounds.width * boundsCorrection) / 2; const targetRadius = target.bounds.width * boundsCorrection / 2;
const distance = canvas.grid.measurePath([originPoint, destinationPoint]).distance; const distance = canvas.grid.measurePath([originPoint, destinationPoint]).distance;
return distance - originRadius - targetRadius + canvas.grid.distance; return distance - originRadius - targetRadius + canvas.grid.distance;
} }
@ -69,11 +61,11 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
const targetEdge = this.#getEdgeBoundary(target.bounds, originPoint, destinationPoint); const targetEdge = this.#getEdgeBoundary(target.bounds, originPoint, destinationPoint);
const adjustedOriginPoint = canvas.grid.getTopLeftPoint({ const adjustedOriginPoint = canvas.grid.getTopLeftPoint({
x: originEdge.x + Math.sign(originPoint.x - originEdge.x), 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({ const adjustDestinationPoint = canvas.grid.getTopLeftPoint({
x: targetEdge.x + Math.sign(destinationPoint.x - targetEdge.x), 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; return canvas.grid.measurePath([adjustedOriginPoint, adjustDestinationPoint]).distance;
} }
@ -102,7 +94,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 */ /** Tests if the token is at least adjacent with another, with some leeway for diagonals */
isAdjacentWith(token) { isAdjacentWith(token) {
return this.distanceTo(token) <= canvas.grid.distance * 1.5; return this.distanceTo(token) <= (canvas.grid.distance * 1.5);
} }
/** @inheritDoc */ /** @inheritDoc */
@ -140,25 +132,4 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
bar.position.set(0, posY); bar.position.set(0, posY);
return true; 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;
}
} }

View file

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

View file

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

View file

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

View file

@ -166,6 +166,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
*/ */
getRollData(data = {}) { getRollData(data = {}) {
const actorData = this.actor ? this.actor.getRollData(false) : {}; const actorData = this.actor ? this.actor.getRollData(false) : {};
actorData.result = data.roll?.total ?? 1; actorData.result = data.roll?.total ?? 1;
actorData.scale = data.costs?.length // Right now only return the first scalable cost. actorData.scale = data.costs?.length // Right now only return the first scalable cost.
? (data.costs.find(c => c.scalable)?.total ?? 1) ? (data.costs.find(c => c.scalable)?.total ?? 1)
@ -198,8 +199,6 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
let config = this.prepareConfig(event); let config = this.prepareConfig(event);
if (!config) return; 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; if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return;
// Display configuration window if necessary // Display configuration window if necessary
@ -241,7 +240,6 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
hasHealing: this.hasHealing, hasHealing: this.hasHealing,
hasEffect: this.hasEffect, hasEffect: this.hasEffect,
hasSave: this.hasSave, hasSave: this.hasSave,
onSave: this.save?.damageMod,
isDirect: !!this.damage?.direct, isDirect: !!this.damage?.direct,
selectedRollMode: game.settings.get('core', 'rollMode'), selectedRollMode: game.settings.get('core', 'rollMode'),
data: this.getRollData(), data: this.getRollData(),
@ -267,28 +265,6 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return config; 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. * 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. * @param {*} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
@ -377,14 +353,14 @@ export class ResourceUpdateMap extends Map {
if (!resource.key) continue; if (!resource.key) continue;
const existing = this.get(resource.key); const existing = this.get(resource.key);
if (!existing || resource.clear) { if (existing) {
this.set(resource.key, resource);
} else if (!existing?.clear) {
this.set(resource.key, { this.set(resource.key, {
...existing, ...existing,
value: existing.value + (resource.value ?? 0), value: existing.value + (resource.value ?? 0),
total: existing.total + (resource.total ?? 0) total: existing.total + (resource.total ?? 0)
}); });
} else {
this.set(resource.key, resource);
} }
} }
} }

View file

@ -1,5 +1,19 @@
import DHBaseAction from './baseAction.mjs'; import DHBaseAction from './baseAction.mjs';
export default class DHSummonAction extends DHBaseAction { export default class DHSummonAction extends DHBaseAction {
static extraSchemas = [...super.extraSchemas, 'summon']; 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');
}
} }

View file

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

View file

@ -27,7 +27,7 @@ const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) =>
}); });
/* Common rules applying to Characters and Adversaries */ /* Common rules applying to Characters and Adversaries */
export const commonActorRules = (extendedData = { damageReduction: {}, attack: { damage: {} } }) => ({ export const commonActorRules = (extendedData = { damageReduction: {} }) => ({
conditionImmunities: new fields.SchemaField({ conditionImmunities: new fields.SchemaField({
hidden: new fields.BooleanField({ initial: false }), hidden: new fields.BooleanField({ initial: false }),
restrained: new fields.BooleanField({ initial: false }), restrained: new fields.BooleanField({ initial: false }),
@ -41,23 +41,7 @@ export const commonActorRules = (extendedData = { damageReduction: {}, attack: {
magical: new fields.NumberField({ initial: 0, min: 0 }), magical: new fields.NumberField({ initial: 0, min: 0 }),
physical: 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,14 +36,7 @@ export default class DhCharacter extends BaseDataActor {
'DAGGERHEART.ACTORS.Character.maxHPBonus' 'DAGGERHEART.ACTORS.Character.maxHPBonus'
), ),
stress: resourceField(6, 0, 'DAGGERHEART.GENERAL.stress', true), stress: resourceField(6, 0, 'DAGGERHEART.GENERAL.stress', true),
hope: new fields.SchemaField({ hope: resourceField(6, 2, 'DAGGERHEART.GENERAL.hope')
value: new fields.NumberField({
initial: 2,
min: 0,
integer: true,
label: 'DAGGERHEART.GENERAL.hope'
})
})
}), }),
traits: new fields.SchemaField({ traits: new fields.SchemaField({
agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'), agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'),
@ -86,7 +79,12 @@ export default class DhCharacter extends BaseDataActor {
bags: new fields.NumberField({ initial: 0, integer: true }), bags: new fields.NumberField({ initial: 0, integer: true }),
chests: new fields.NumberField({ initial: 0, integer: true }) chests: new fields.NumberField({ initial: 0, integer: true })
}), }),
scars: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.scars' }), scars: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({}),
description: new fields.StringField()
})
),
biography: new fields.SchemaField({ biography: new fields.SchemaField({
background: new fields.HTMLField(), background: new fields.HTMLField(),
connections: new fields.HTMLField(), connections: new fields.HTMLField(),
@ -254,59 +252,38 @@ export default class DhCharacter extends BaseDataActor {
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint' hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint'
}), }),
disabledArmor: new fields.BooleanField({ intial: false }) 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'
})
})
} }
}), }),
dualityRoll: new fields.SchemaField({ attack: new fields.SchemaField({
defaultHopeDice: new fields.NumberField({ damage: new fields.SchemaField({
nullable: false, diceIndex: new fields.NumberField({
required: true, integer: true,
integer: true, min: 0,
choices: CONFIG.DH.GENERAL.dieFaces, max: 5,
initial: 12, initial: 0,
label: 'DAGGERHEART.ACTORS.Character.defaultHopeDice' 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'
})
}), }),
defaultFearDice: new fields.NumberField({ roll: new fields.SchemaField({
nullable: false, trait: new fields.StringField({
required: true, required: true,
integer: true, choices: CONFIG.DH.ACTOR.abilities,
choices: CONFIG.DH.GENERAL.dieFaces, nullable: true,
initial: 12, initial: null,
label: 'DAGGERHEART.ACTORS.Character.defaultFearDice' label: 'DAGGERHEART.GENERAL.Rules.attack.roll.trait.label'
})
}) })
}), }),
runeWard: new fields.BooleanField({ initial: false }), runeWard: new fields.BooleanField({ initial: false }),
burden: new fields.SchemaField({ burden: new fields.SchemaField({
ignore: new fields.BooleanField() ignore: new fields.BooleanField()
}),
roll: new fields.SchemaField({
guaranteedCritical: new fields.BooleanField()
}) })
}), }),
sidebarFavorites: new ForeignDocumentUUIDArrayField({ type: 'Item' }) sidebarFavorites: new ForeignDocumentUUIDArrayField({ type: 'Item' })
@ -370,7 +347,7 @@ export default class DhCharacter extends BaseDataActor {
const modifiers = subClasses const modifiers = subClasses
?.map(sc => ({ ...this.traits[sc.system.spellcastingTrait], key: sc.system.spellcastingTrait })) ?.map(sc => ({ ...this.traits[sc.system.spellcastingTrait], key: sc.system.spellcastingTrait }))
.filter(x => x); .filter(x => x);
return modifiers.sort((a, b) => (b.value ?? 0) - (a.value ?? 0))[0]; return modifiers.sort((a, b) => a.value - b.value)[0];
} }
get spellcastModifier() { get spellcastModifier() {
@ -551,18 +528,7 @@ export default class DhCharacter extends BaseDataActor {
} }
get deathMoveViable() { get deathMoveViable() {
const { characterDefault } = game.settings.get( return this.resources.hitPoints.max > 0 && this.resources.hitPoints.value >= this.resources.hitPoints.max;
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() { get armorApplicableDamageTypes() {
@ -660,15 +626,8 @@ export default class DhCharacter extends BaseDataActor {
? armor.system.baseThresholds.severe + this.levelData.level.current ? armor.system.baseThresholds.severe + this.levelData.level.current
: this.levelData.level.current * 2 : 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; this.resources.hitPoints.max += this.class.value?.system?.hitPoints ?? 0;
/* Companion Related Data */
this.companionData = {
levelupChoices: this.levelData.level.current - 1
};
} }
prepareDerivedData() { prepareDerivedData() {
@ -724,30 +683,6 @@ export default class DhCharacter extends BaseDataActor {
changes.system.experiences[experience].core = true; 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() { async _preDelete() {
@ -763,11 +698,4 @@ export default class DhCharacter extends BaseDataActor {
t => !!t 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,11 +108,7 @@ export default class DhCompanion extends BaseDataActor {
get proficiency() { get proficiency() {
return this.partner?.system?.proficiency ?? 1; return this.partner?.system?.proficiency ?? 1;
} }
get canLevelUp() {
return this.levelupChoicesLeft > 0;
}
isItemValid() { isItemValid() {
return false; return false;
} }
@ -131,7 +127,7 @@ export default class DhCompanion extends BaseDataActor {
if (selection.data[0] === 'damage') { if (selection.data[0] === 'damage') {
this.attack.damage.parts[0].value.dice = adjustDice(this.attack.damage.parts[0].value.dice); this.attack.damage.parts[0].value.dice = adjustDice(this.attack.damage.parts[0].value.dice);
} else { } else {
this.attack.range = adjustRange(this.attack.range).id; this.attack.range = adjustRange(this.attack.range);
} }
break; break;
case 'stress': case 'stress':
@ -151,17 +147,6 @@ 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) { async _preUpdate(changes, options, userId) {
const allowed = await super._preUpdate(changes, options, userId); const allowed = await super._preUpdate(changes, options, userId);
if (allowed === false) return; if (allowed === false) return;
@ -177,16 +162,6 @@ export default class DhCompanion extends BaseDataActor {
changes.system.experiences[experience].core = true; 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() { async _preDelete() {

View file

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

View file

@ -1,370 +0,0 @@
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,4 +9,3 @@ export { default as BeastformField } from './beastformField.mjs';
export { default as DamageField } from './damageField.mjs'; export { default as DamageField } from './damageField.mjs';
export { default as RollField } from './rollField.mjs'; export { default as RollField } from './rollField.mjs';
export { default as MacroField } from './macroField.mjs'; export { default as MacroField } from './macroField.mjs';
export { default as SummonField } from './summonField.mjs';

View file

@ -105,22 +105,12 @@ export default class DamageField extends fields.SchemaField {
damagePromises.push( damagePromises.push(
actor.takeHealing(config.damage).then(updates => targetDamage.push({ token, updates })) 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( damagePromises.push(
actor actor
.takeDamage(configDamage, config.isDirect) .takeDamage(config.damage, config.isDirect)
.then(updates => targetDamage.push({ token, updates })) .then(updates => targetDamage.push({ token, updates }))
); );
}
} }
Promise.all(damagePromises).then(async _ => { 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) if (this.type === CONFIG.DH.GENERAL.rollTypes.attack.id)
modifiers.push({ modifiers.push({
label: 'Bonus to Hit', label: 'Bonus to Hit',
value: this.bonus ?? this.parent.actor.system.attack.roll.bonus ?? 0 value: this.bonus ?? this.parent.actor.system.attack.roll.bonus
}); });
break; break;
default: default:

View file

@ -1,89 +0,0 @@
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,8 +267,7 @@ export function ActionMixin(Base) {
action: { action: {
name: this.name, name: this.name,
img: this.baseAction ? this.parent.parent.img : this.img, 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, itemOrigin: this.item,
description: this.description || (this.item instanceof Item ? this.item.system.description : '') 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, { return await foundry.applications.ux.TextEditor.implementation.enrichHTML(fullDescription, {
relativeTo: this, relativeTo: this,
rollData: this.getRollData(), rollData: this.getRollData(),
secrets: this.parent.isOwner secrets: this.isOwner
}); });
} }
@ -164,7 +164,26 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
prepareBaseData() { prepareBaseData() {
super.prepareBaseData(); super.prepareBaseData();
game.system.registeredTriggers.registerItemTriggers(this.parent);
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
);
}
}
} }
async _preCreate(data, options, user) { async _preCreate(data, options, user) {
@ -227,28 +246,6 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
const armorData = getScrollTextData(this.parent.parent.system.resources, changed.system.marks, 'armor'); const armorData = getScrollTextData(this.parent.parent.system.resources, changed.system.marks, 'armor');
options.scrollingTextData = [armorData]; 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) { _onUpdate(changed, options, userId) {

View file

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

View file

@ -29,21 +29,7 @@ export default class DHDomainCard extends BaseDataItem {
required: true, required: true,
initial: CONFIG.DH.DOMAIN.cardTypes.ability.id 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
})
}; };
} }
@ -52,19 +38,6 @@ export default class DHDomainCard extends BaseDataItem {
return game.i18n.localize(allDomainData[this.domain].label); 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 */ /**@override */

View file

@ -1,167 +0,0 @@
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;
}
}

View file

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

View file

@ -23,13 +23,6 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
initial: 12, initial: 12,
label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxFear.label' 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({ maxLoadout: new fields.NumberField({
required: true, required: true,
integer: true, integer: true,

View file

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

View file

@ -35,9 +35,7 @@ export default class D20Roll extends DHRoll {
get isCritical() { get isCritical() {
if (!this.d20._evaluated) return; 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() { get hasAdvantage() {
@ -99,14 +97,11 @@ export default class D20Roll extends DHRoll {
this.options.roll.modifiers = this.applyBaseBonus(); 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 => { this.options.experiences?.forEach(m => {
if (actorExperiences[m]) if (this.options.data.system?.experiences?.[m])
this.options.roll.modifiers.push({ this.options.roll.modifiers.push({
label: actorExperiences[m].name, label: this.options.data.system.experiences[m].name,
value: actorExperiences[m].value value: this.options.data.system.experiences[m].value
}); });
}); });
@ -132,55 +127,15 @@ export default class D20Roll extends DHRoll {
const modifiers = foundry.utils.deepClone(this.options.roll.baseModifiers) ?? []; const modifiers = foundry.utils.deepClone(this.options.roll.baseModifiers) ?? [];
modifiers.push( modifiers.push(
...this.getBonus( ...this.getBonus(`roll.${this.options.actionType}`, `${this.options.actionType?.capitalize()} Bonus`)
`system.bonuses.roll.${this.options.actionType}`, );
`${this.options.actionType?.capitalize()} Bonus` modifiers.push(
) ...this.getBonus(`roll.${this.options.roll.type}`, `${this.options.roll.type?.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; 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 = {}) { static postEvaluate(roll, config = {}) {
const data = super.postEvaluate(roll, config); const data = super.postEvaluate(roll, config);
data.type = config.actionType; data.type = config.actionType;

View file

@ -93,6 +93,7 @@ export default class DamageRoll extends DHRoll {
type = this.options.messageType ?? (this.options.hasHealing ? 'healing' : 'damage'), type = this.options.messageType ?? (this.options.hasHealing ? 'healing' : 'damage'),
options = part ?? this.options; options = part ?? this.options;
modifiers.push(...this.getBonus(`${type}`, `${type.capitalize()} Bonus`));
if (!this.options.hasHealing) { if (!this.options.hasHealing) {
options.damageTypes?.forEach(t => { options.damageTypes?.forEach(t => {
modifiers.push(...this.getBonus(`${type}.${t}`, `${t.capitalize()} ${type.capitalize()} Bonus`)); modifiers.push(...this.getBonus(`${type}.${t}`, `${t.capitalize()} ${type.capitalize()} Bonus`));
@ -107,31 +108,6 @@ export default class DamageRoll extends DHRoll {
return modifiers; 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) { constructFormula(config) {
this.options.roll.forEach((part, index) => { this.options.roll.forEach((part, index) => {
part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data)); part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data));

View file

@ -3,8 +3,7 @@ import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
export default class DHRoll extends Roll { export default class DHRoll extends Roll {
baseTerms = []; baseTerms = [];
constructor(formula, data = {}, options = {}) { constructor(formula, data = {}, options = {}) {
super(formula, data, foundry.utils.mergeObject(options, { roll: [] }, { overwrite: false })); super(formula, data, options);
options.bonusEffects = this.bonusEffectBuilder();
if (!this.data || !Object.keys(this.data).length) this.data = options.data; if (!this.data || !Object.keys(this.data).length) this.data = options.data;
} }
@ -165,17 +164,11 @@ export default class DHRoll extends Roll {
new foundry.dice.terms.OperatorTerm({ operator: '+' }), new foundry.dice.terms.OperatorTerm({ operator: '+' }),
...this.constructor.parse(modifier.join(' + '), this.options.data) ...this.constructor.parse(modifier.join(' + '), this.options.data)
]; ];
} 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 { } else {
const numTerm = modifier < 0 ? '-' : '+'; const numTerm = modifier < 0 ? '-' : '+';
return [ return [
new foundry.dice.terms.OperatorTerm({ operator: numTerm }), new foundry.dice.terms.OperatorTerm({ operator: numTerm }),
...this.constructor.parse(modifier, this.options.data) new foundry.dice.terms.NumericTerm({ number: Math.abs(modifier) })
]; ];
} }
} }
@ -192,20 +185,18 @@ export default class DHRoll extends Roll {
} }
getBonus(path, label) { getBonus(path, label) {
const modifiers = []; const bonus = foundry.utils.getProperty(this.data.bonuses, path),
for (const effect of Object.values(this.options.bonusEffects)) { modifiers = [];
if (!effect.selected) continue; if (bonus?.bonus)
for (const change of effect.changes) { modifiers.push({
if (!change.key.includes(path)) continue; label: label,
const changeValue = game.system.api.documents.DhActiveEffect.getChangeValue( value: bonus?.bonus
this.data, });
change, if (bonus?.dice?.length)
effect.origEffect modifiers.push({
); label: label,
modifiers.push({ label: label, value: changeValue }); value: bonus?.dice
} });
}
return modifiers; return modifiers;
} }
@ -244,28 +235,4 @@ export default class DHRoll extends Roll {
static temporaryModifierBuilder(config) { static temporaryModifierBuilder(config) {
return {}; 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,7 +12,6 @@ export default class DualityRoll extends D20Roll {
constructor(formula, data = {}, options = {}) { constructor(formula, data = {}, options = {}) {
super(formula, data, options); super(formula, data, options);
this.rallyChoices = this.setRallyChoices(); this.rallyChoices = this.setRallyChoices();
this.guaranteedCritical = options.guaranteedCritical;
} }
static messageType = 'dualityRoll'; static messageType = 'dualityRoll';
@ -26,23 +25,29 @@ export default class DualityRoll extends D20Roll {
} }
get dHope() { get dHope() {
// if ( !(this.terms[0] instanceof foundry.dice.terms.Die) ) return;
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
return this.dice[0]; return this.dice[0];
// return this.#hopeDice;
} }
set dHope(faces) { set dHope(faces) {
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
this.dice[0].faces = this.getFaces(faces); this.terms[0].faces = this.getFaces(faces);
// this.#hopeDice = `d${face}`;
} }
get dFear() { get dFear() {
// if ( !(this.terms[1] instanceof foundry.dice.terms.Die) ) return;
if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice(); if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice();
return this.dice[1]; return this.dice[1];
// return this.#fearDice;
} }
set dFear(faces) { set dFear(faces) {
if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice(); if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice();
this.dice[1].faces = this.getFaces(faces); this.dice[1].faces = this.getFaces(faces);
// this.#fearDice = `d${face}`;
} }
get dAdvantage() { get dAdvantage() {
@ -85,29 +90,26 @@ export default class DualityRoll extends D20Roll {
} }
get isCritical() { get isCritical() {
if (this.guaranteedCritical) return true;
if (!this.dHope._evaluated || !this.dFear._evaluated) return; if (!this.dHope._evaluated || !this.dFear._evaluated) return;
return this.dHope.total === this.dFear.total; return this.dHope.total === this.dFear.total;
} }
get withHope() { get withHope() {
if (!this._evaluated || this.guaranteedCritical) return; if (!this._evaluated) return;
return this.dHope.total > this.dFear.total; return this.dHope.total > this.dFear.total;
} }
get withFear() { get withFear() {
if (!this._evaluated || this.guaranteedCritical) return; if (!this._evaluated) return;
return this.dHope.total < this.dFear.total; return this.dHope.total < this.dFear.total;
} }
get totalLabel() { get totalLabel() {
const label = this.guaranteedCritical const label = this.withHope
? 'DAGGERHEART.GENERAL.guaranteedCriticalSuccess' ? 'DAGGERHEART.GENERAL.hope'
: this.isCritical : this.withFear
? 'DAGGERHEART.GENERAL.criticalSuccess' ? 'DAGGERHEART.GENERAL.fear'
: this.withHope : 'DAGGERHEART.GENERAL.criticalSuccess';
? 'DAGGERHEART.GENERAL.hope'
: 'DAGGERHEART.GENERAL.fear';
return game.i18n.localize(label); return game.i18n.localize(label);
} }
@ -128,14 +130,9 @@ export default class DualityRoll extends D20Roll {
this.terms = [this.terms[0], this.terms[1], this.terms[2]]; this.terms = [this.terms[0], this.terms[1], this.terms[2]];
return; 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[1] = new foundry.dice.terms.OperatorTerm({ operator: '+' });
this.terms[2] = new foundry.dice.terms.Die({ this.terms[2] = new foundry.dice.terms.Die({ faces: 12 });
faces: this.data.rules.dualityRoll?.defaultFearDice ?? 12
});
} }
applyAdvantage() { applyAdvantage() {
@ -176,49 +173,6 @@ export default class DualityRoll extends D20Roll {
return modifiers; 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 = {}) { static async buildEvaluate(roll, config = {}, message = {}) {
await super.buildEvaluate(roll, config, message); await super.buildEvaluate(roll, config, message);
@ -236,7 +190,7 @@ export default class DualityRoll extends D20Roll {
data.hope = { data.hope = {
dice: roll.dHope.denomination, dice: roll.dHope.denomination,
value: this.guaranteedCritical ? 0 : roll.dHope.total, value: roll.dHope.total,
rerolled: { rerolled: {
any: roll.dHope.results.some(x => x.rerolled), any: roll.dHope.results.some(x => x.rerolled),
rerolls: roll.dHope.results.filter(x => x.rerolled) rerolls: roll.dHope.results.filter(x => x.rerolled)
@ -244,7 +198,7 @@ export default class DualityRoll extends D20Roll {
}; };
data.fear = { data.fear = {
dice: roll.dFear.denomination, dice: roll.dFear.denomination,
value: this.guaranteedCritical ? 0 : roll.dFear.total, value: roll.dFear.total,
rerolled: { rerolled: {
any: roll.dFear.results.some(x => x.rerolled), any: roll.dFear.results.some(x => x.rerolled),
rerolls: roll.dFear.results.filter(x => x.rerolled) rerolls: roll.dFear.results.filter(x => x.rerolled)
@ -256,7 +210,7 @@ export default class DualityRoll extends D20Roll {
}; };
data.result = { data.result = {
duality: roll.withHope ? 1 : roll.withFear ? -1 : 0, duality: roll.withHope ? 1 : roll.withFear ? -1 : 0,
total: this.guaranteedCritical ? 0 : roll.dHope.total + roll.dFear.total, total: roll.dHope.total + roll.dFear.total,
label: roll.totalLabel label: roll.totalLabel
}; };
@ -274,8 +228,6 @@ export default class DualityRoll extends D20Roll {
} }
static async handleTriggers(roll, config) { static async handleTriggers(roll, config) {
if (!config.source?.actor || config.skips?.triggers) return;
const updates = []; const updates = [];
const dualityUpdates = await game.system.registeredTriggers.runTrigger( const dualityUpdates = await game.system.registeredTriggers.runTrigger(
CONFIG.DH.TRIGGER.triggers.dualityRoll.id, CONFIG.DH.TRIGGER.triggers.dualityRoll.id,

View file

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

View file

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

View file

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

View file

@ -87,15 +87,6 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
break; 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( const autoExpandRoll = game.settings.get(
CONFIG.DH.id, CONFIG.DH.id,
@ -166,12 +157,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
event.stopPropagation(); event.stopPropagation();
const config = foundry.utils.deepClone(this.system); const config = foundry.utils.deepClone(this.system);
config.event = event; config.event = event;
if (this.system.action) { await this.system.action?.workflow.get('damage')?.execute(config, this._id, true);
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 }); Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, { await game.socket.emit(`system.${CONFIG.DH.id}`, {
@ -188,7 +174,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
config = foundry.utils.deepClone(this.system); config = foundry.utils.deepClone(this.system);
config.event = event; config.event = event;
if (config.hasSave) { if (this.system.onSave) {
const pendingingSaves = targets.filter(t => t.saved.success === null); const pendingingSaves = targets.filter(t => t.saved.success === null);
if (pendingingSaves.length) { if (pendingingSaves.length) {
const confirm = await foundry.applications.api.DialogV2.confirm({ const confirm = await foundry.applications.api.DialogV2.confirm({
@ -203,16 +189,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm')); return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm'));
this.consumeOnSuccess(); this.consumeOnSuccess();
if (this.system.action) this.system.action.workflow.get('applyDamage')?.execute(config, targets, true); 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) { async onRollSave(event) {

View file

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

View file

@ -1,14 +0,0 @@
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) { static async createDocuments(sources, operation) {
// Ensure that items being created are valid to the actor its being added to // Ensure that items being created are valid to the actor its being added to
const actor = operation.parent; 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); return super.createDocuments(sources, operation);
} }
@ -146,16 +146,6 @@ export default class DHItem extends foundry.documents.Item {
/* -------------------------------------------- */ /* -------------------------------------------- */
async use(event) { 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); const actions = new Set(this.system.actionsList);
if (actions?.size) { if (actions?.size) {
let action = actions.first(); let action = actions.first();
@ -208,23 +198,4 @@ export default class DHItem extends foundry.documents.Item {
cls.create(msg); 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

@ -1,122 +0,0 @@
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,27 +51,6 @@ 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) { _onDelete(options, userId) {
super._onDelete(options, userId); super._onDelete(options, userId);

View file

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

View file

@ -1,104 +0,0 @@
/**
* 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) { if (item) {
const isAction = item instanceof game.system.api.models.actions.actionsTypes.base; const isAction = item instanceof game.system.api.models.actions.actionsTypes.base;
const isEffect = item instanceof ActiveEffect; const isEffect = item instanceof ActiveEffect;
await this.enrichText(item); await this.enrichText(item, isAction || isEffect);
const type = isAction ? 'action' : isEffect ? 'effect' : item.type; const type = isAction ? 'action' : isEffect ? 'effect' : item.type;
html = await foundry.applications.handlebars.renderTemplate( html = await foundry.applications.handlebars.renderTemplate(
@ -202,20 +202,10 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
} }
} }
async enrichText(item) { async enrichText(item, flatStructure) {
const { TextEditor } = foundry.applications.ux; 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 = [ const enrichPaths = [
{ path: flatStructure ? '' : 'system', name: 'description' },
{ path: 'system', name: 'features' }, { path: 'system', name: 'features' },
{ path: 'system', name: 'actions' }, { path: 'system', name: 'actions' },
{ path: 'system', name: 'customActions' } { path: 'system', name: 'customActions' }

View file

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

View file

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

View file

@ -1,14 +1,14 @@
import { diceTypes, getDiceSoNicePresets, getDiceSoNicePreset, range } from '../config/generalConfig.mjs'; import { diceTypes, getDiceSoNicePresets, range } from '../config/generalConfig.mjs';
import Tagify from '@yaireo/tagify'; import Tagify from '@yaireo/tagify';
export const capitalize = string => { export const capitalize = string => {
return string.charAt(0).toUpperCase() + string.slice(1); return string.charAt(0).toUpperCase() + string.slice(1);
}; };
export function rollCommandToJSON(text) { export function rollCommandToJSON(text, raw) {
if (!text) return {}; if (!text) return {};
const flavorMatch = text?.match(/{(.*)}$/); const flavorMatch = raw?.match(/{(.*)}$/);
const flavor = flavorMatch ? flavorMatch[1] : null; const flavor = flavorMatch ? flavorMatch[1] : null;
// Match key="quoted string" OR key=unquotedValue // Match key="quoted string" OR key=unquotedValue
@ -31,7 +31,7 @@ export function rollCommandToJSON(text) {
} }
result[key] = value; result[key] = value;
} }
return { result, flavor }; return Object.keys(result).length > 0 ? { result, flavor } : null;
} }
export const getCommandTarget = (options = {}) => { export const getCommandTarget = (options = {}) => {
@ -69,20 +69,6 @@ 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) => { export const chunkify = (array, chunkSize, mappingFunc) => {
var chunkifiedArray = []; var chunkifiedArray = [];
for (let i = 0; i < array.length; i += chunkSize) { for (let i = 0; i < array.length; i += chunkSize) {

View file

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

View file

@ -210,42 +210,6 @@ export async function runMigrations() {
lastMigrationVersion = '1.2.7'; 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 //#endregion
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion, lastMigrationVersion); await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion, lastMigrationVersion);

View file

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

View file

@ -533,31 +533,33 @@
"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>", "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, "resource": null,
"actions": { "actions": {
"qSuWxC8xQOhnbBx9": { "gZg3AkzCYUTExjE6": {
"type": "summon", "type": "effect",
"_id": "qSuWxC8xQOhnbBx9", "_id": "gZg3AkzCYUTExjE6",
"systemPath": "actions", "systemPath": "actions",
"baseAction": false,
"description": "", "description": "",
"chatDisplay": true, "chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action", "actionType": "action",
"cost": [], "cost": [
{
"scalable": false,
"key": "fear",
"value": 1,
"step": null
}
],
"uses": { "uses": {
"value": null, "value": null,
"max": "", "max": "",
"recovery": null, "recovery": null
"consumeOnSuccess": false },
"effects": [],
"target": {
"type": "any",
"amount": null
}, },
"summon": [
{
"actorUUID": "Compendium.daggerheart.adversaries.Actor.YhJrP7rTBiRdX5Fp",
"count": "1"
}
],
"name": "Spend Fear", "name": "Spend Fear",
"img": "icons/magic/death/undead-zombie-grave-green.webp",
"range": "" "range": ""
} }
}, },

View file

@ -235,51 +235,7 @@
}, },
"_id": "2ESeh4tPhr6DI5ty", "_id": "2ESeh4tPhr6DI5ty",
"img": "icons/magic/death/skull-horned-worn-fire-blue.webp", "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, "folder": null,
"sort": 0, "sort": 0,
"ownership": { "ownership": {
@ -356,14 +312,7 @@
"range": "melee" "range": "melee"
} }
}, },
"changes": [ "changes": [],
{
"key": "system.rules.dualityRoll.defaultHopeDice",
"mode": 5,
"value": "d8",
"priority": null
}
],
"disabled": false, "disabled": false,
"duration": { "duration": {
"startTime": null, "startTime": null,
@ -374,7 +323,7 @@
"startRound": null, "startRound": null,
"startTurn": null "startTurn": null
}, },
"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>", "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>",
"tint": "#ffffff", "tint": "#ffffff",
"statuses": [], "statuses": [],
"sort": 0, "sort": 0,

View file

@ -256,45 +256,34 @@
"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>", "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, "resource": null,
"actions": { "actions": {
"jKvzbQT0vp66DDOH": { "V142qYppCGJn8OiN": {
"type": "effect", "type": "effect",
"_id": "jKvzbQT0vp66DDOH", "_id": "V142qYppCGJn8OiN",
"systemPath": "actions", "systemPath": "actions",
"baseAction": false,
"description": "", "description": "",
"chatDisplay": true, "chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action", "actionType": "action",
"cost": [ "cost": [
{ {
"scalable": false, "scalable": false,
"key": "fear", "key": "fear",
"value": 1, "value": 1,
"itemId": null, "step": null
"step": null,
"consumeOnSuccess": false
} }
], ],
"uses": { "uses": {
"value": null, "value": null,
"max": "", "max": "",
"recovery": null, "recovery": null
"consumeOnSuccess": false
}, },
"effects": [ "effects": [],
{
"_id": "gFeHLGgeRoDdd3VG",
"onSave": false
}
],
"target": { "target": {
"type": "hostile", "type": "self",
"amount": null "amount": null
}, },
"name": "Spend Fear", "name": "Spend Fear",
"range": "far" "img": "icons/skills/melee/maneuver-greatsword-yellow.webp",
"range": ""
} }
}, },
"originItemType": null, "originItemType": null,
@ -303,51 +292,7 @@
}, },
"_id": "a33PW8UkziliowlR", "_id": "a33PW8UkziliowlR",
"img": "icons/skills/melee/maneuver-greatsword-yellow.webp", "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, "folder": null,
"sort": 0, "sort": 0,
"ownership": { "ownership": {
@ -512,12 +457,11 @@
"img": "icons/creatures/unholy/demon-fire-horned-clawed.webp", "img": "icons/creatures/unholy/demon-fire-horned-clawed.webp",
"range": "" "range": ""
}, },
"FlE6i0tbKEguF9wz": { "7G6uWlFEeOLsJIWY": {
"type": "summon", "type": "effect",
"_id": "FlE6i0tbKEguF9wz", "_id": "7G6uWlFEeOLsJIWY",
"systemPath": "actions", "systemPath": "actions",
"baseAction": false, "description": "<p>Summon [[/r 1d4]]@UUID[Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb]{Minor Demons}, who appear at Close range.</p>",
"description": "",
"chatDisplay": true, "chatDisplay": true,
"originItem": { "originItem": {
"type": "itemCollection" "type": "itemCollection"
@ -530,13 +474,13 @@
"recovery": null, "recovery": null,
"consumeOnSuccess": false "consumeOnSuccess": false
}, },
"summon": [ "effects": [],
{ "target": {
"actorUUID": "Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb", "type": "any",
"count": "1d4" "amount": null
} },
],
"name": "Summon", "name": "Summon",
"img": "icons/creatures/unholy/demon-fire-horned-clawed.webp",
"range": "" "range": ""
} }
}, },

View file

@ -363,31 +363,33 @@
"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>", "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, "resource": null,
"actions": { "actions": {
"R84DdS0OIx2cUt1w": { "84Q2b0zIY9c7Yhho": {
"type": "summon", "type": "effect",
"_id": "R84DdS0OIx2cUt1w", "_id": "84Q2b0zIY9c7Yhho",
"systemPath": "actions", "systemPath": "actions",
"baseAction": false,
"description": "", "description": "",
"chatDisplay": true, "chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action", "actionType": "action",
"cost": [], "cost": [
{
"scalable": false,
"key": "fear",
"value": 1,
"step": null
}
],
"uses": { "uses": {
"value": null, "value": null,
"max": "", "max": "",
"recovery": null, "recovery": null
"consumeOnSuccess": false },
"effects": [],
"target": {
"type": "self",
"amount": null
}, },
"summon": [
{
"actorUUID": "Compendium.daggerheart.adversaries.Actor.o63nS0k3wHu6EgKP",
"count": "3"
}
],
"name": "Spend Fear", "name": "Spend Fear",
"img": "icons/magic/unholy/orb-hands-pink.webp",
"range": "" "range": ""
} }
}, },

View file

@ -304,51 +304,7 @@
}, },
"_id": "1fE6xo8yIOmZkGNE", "_id": "1fE6xo8yIOmZkGNE",
"img": "icons/skills/melee/strike-slashes-orange.webp", "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, "folder": null,
"sort": 0, "sort": 0,
"ownership": { "ownership": {

View file

@ -510,41 +510,34 @@
"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>", "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, "resource": null,
"actions": { "actions": {
"J8U7dw3cDSsEirr5": { "s5mLw6DRGd76MLcC": {
"type": "summon", "type": "effect",
"_id": "J8U7dw3cDSsEirr5", "_id": "s5mLw6DRGd76MLcC",
"systemPath": "actions", "systemPath": "actions",
"baseAction": false,
"description": "", "description": "",
"chatDisplay": true, "chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action", "actionType": "action",
"cost": [ "cost": [
{ {
"scalable": false, "scalable": false,
"key": "fear", "key": "fear",
"value": 1, "value": 1,
"itemId": null, "step": null
"step": null,
"consumeOnSuccess": false
} }
], ],
"uses": { "uses": {
"value": null, "value": null,
"max": "", "max": "",
"recovery": null, "recovery": null
"consumeOnSuccess": false },
"effects": [],
"target": {
"type": "self",
"amount": null
}, },
"summon": [
{
"actorUUID": "Compendium.daggerheart.adversaries.Actor.aLkLFuVoKz2NLoBK",
"count": "2"
}
],
"name": "Spend Fear", "name": "Spend Fear",
"range": "self" "img": "icons/creatures/slimes/slime-movement-pseudopods-green.webp",
"range": ""
} }
}, },
"originItemType": null, "originItemType": null,

View file

@ -229,51 +229,7 @@
}, },
"_id": "FGJTAeL38zTVd4fA", "_id": "FGJTAeL38zTVd4fA",
"img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", "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, "folder": null,
"sort": 0, "sort": 0,
"ownership": { "ownership": {

View file

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

View file

@ -479,31 +479,33 @@
"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>", "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, "resource": null,
"actions": { "actions": {
"aeRdkiRsDNagTKhp": { "iQsYAqpUFvJslRDr": {
"type": "summon", "type": "effect",
"_id": "aeRdkiRsDNagTKhp", "_id": "iQsYAqpUFvJslRDr",
"systemPath": "actions", "systemPath": "actions",
"baseAction": false,
"description": "", "description": "",
"chatDisplay": true, "chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action", "actionType": "action",
"cost": [], "cost": [
{
"scalable": false,
"key": "fear",
"value": 1,
"step": null
}
],
"uses": { "uses": {
"value": null, "value": null,
"max": "", "max": "",
"recovery": null, "recovery": null
"consumeOnSuccess": false },
"effects": [],
"target": {
"type": "any",
"amount": null
}, },
"summon": [
{
"actorUUID": "Compendium.daggerheart.adversaries.Actor.SHXedd9zZPVfUgUa",
"count": "2"
}
],
"name": "Spend Fear", "name": "Spend Fear",
"img": "icons/creatures/slimes/slime-movement-pseudopods-green.webp",
"range": "" "range": ""
} }
}, },

View file

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

View file

@ -287,35 +287,7 @@
"system": { "system": {
"description": "<p>Summon three @Compendium[daggerheart.adversaries.Actor.C0OMQqV7pN6t7ouR], who appear at Far range.</p>", "description": "<p>Summon three @Compendium[daggerheart.adversaries.Actor.C0OMQqV7pN6t7ouR], who appear at Far range.</p>",
"resource": null, "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, "originItemType": null,
"subType": null, "subType": null,
"originId": null, "originId": null,

View file

@ -258,40 +258,57 @@
"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>", "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, "resource": null,
"actions": { "actions": {
"tioTtYfIGFIXRITN": { "cUKwhq1imsTVru8D": {
"type": "summon", "type": "attack",
"_id": "tioTtYfIGFIXRITN", "_id": "cUKwhq1imsTVru8D",
"systemPath": "actions", "systemPath": "actions",
"baseAction": false, "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>",
"description": "",
"chatDisplay": true, "chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action", "actionType": "action",
"cost": [ "cost": [
{ {
"scalable": false, "scalable": false,
"key": "stress", "key": "stress",
"value": 1, "value": 1,
"itemId": null, "step": null
"step": null,
"consumeOnSuccess": false
} }
], ],
"uses": { "uses": {
"value": null, "value": null,
"max": "1", "max": "",
"recovery": "scene", "recovery": null
"consumeOnSuccess": false },
"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"
}, },
"summon": [
{
"actorUUID": "Compendium.daggerheart.adversaries.Actor.B4LZcGuBAHzyVdzy",
"count": "1d4"
}
],
"name": "Summon Guards", "name": "Summon Guards",
"img": "icons/environment/people/infantry-armored.webp",
"range": "" "range": ""
} }
}, },

View file

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

View file

@ -454,40 +454,33 @@
"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>", "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, "resource": null,
"actions": { "actions": {
"BMEr77hDxaQyYBna": { "dw6Juw8mriH7sg0e": {
"type": "summon", "type": "effect",
"_id": "BMEr77hDxaQyYBna", "_id": "dw6Juw8mriH7sg0e",
"systemPath": "actions", "systemPath": "actions",
"baseAction": false,
"description": "", "description": "",
"chatDisplay": true, "chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action", "actionType": "action",
"cost": [ "cost": [
{ {
"scalable": false, "scalable": false,
"key": "fear", "key": "fear",
"value": 1, "value": 1,
"itemId": null, "step": null
"step": null,
"consumeOnSuccess": false
} }
], ],
"uses": { "uses": {
"value": null, "value": null,
"max": "", "max": "",
"recovery": null, "recovery": null
"consumeOnSuccess": false },
"effects": [],
"target": {
"type": "any",
"amount": null
}, },
"summon": [
{
"actorUUID": "Compendium.daggerheart.adversaries.Actor.1fkLQXVtmILqfJ44",
"count": "2"
}
],
"name": "Spend Fear", "name": "Spend Fear",
"img": "icons/creatures/slimes/slime-movement-splashing-red.webp",
"range": "" "range": ""
} }
}, },

View file

@ -416,6 +416,28 @@
"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>", "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, "resource": null,
"actions": { "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": { "ZVXHY2fpomoKV7jG": {
"type": "countdown", "type": "countdown",
"_id": "ZVXHY2fpomoKV7jG", "_id": "ZVXHY2fpomoKV7jG",
@ -452,33 +474,6 @@
"name": "Start Countdown", "name": "Start Countdown",
"img": "icons/magic/unholy/silhouette-light-fire-blue.webp", "img": "icons/magic/unholy/silhouette-light-fire-blue.webp",
"range": "" "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, "originItemType": null,
@ -507,31 +502,33 @@
"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>", "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, "resource": null,
"actions": { "actions": {
"tfmY6HYkkY27NBaF": { "JBuQUJhif2A7IlJd": {
"type": "summon", "type": "effect",
"_id": "tfmY6HYkkY27NBaF", "_id": "JBuQUJhif2A7IlJd",
"systemPath": "actions", "systemPath": "actions",
"baseAction": false,
"description": "", "description": "",
"chatDisplay": true, "chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action", "actionType": "action",
"cost": [], "cost": [
"uses": {
"value": null,
"max": "",
"recovery": null,
"consumeOnSuccess": false
},
"summon": [
{ {
"actorUUID": "Compendium.daggerheart.adversaries.Actor.NoRZ1PqB8N5wcIw0", "scalable": false,
"count": "1" "key": "stress",
"value": 1,
"step": null
} }
], ],
"uses": {
"value": null,
"max": "1",
"recovery": "scene"
},
"effects": [],
"target": {
"type": "self",
"amount": null
},
"name": "Mark Stress", "name": "Mark Stress",
"img": "icons/creatures/unholy/demon-fire-horned-clawed.webp",
"range": "" "range": ""
} }
}, },

View file

@ -230,51 +230,7 @@
"subType": null, "subType": null,
"originId": 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, "folder": null,
"sort": 0, "sort": 0,
"ownership": { "ownership": {

View file

@ -340,35 +340,7 @@
"system": { "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>", "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, "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, "originItemType": null,
"subType": null, "subType": null,
"originId": null, "originId": null,

View file

@ -9,47 +9,13 @@
"resource": { "resource": {
"type": "simple", "type": "simple",
"value": 0, "value": 0,
"max": "@system.levelData.level.current", "max": "",
"icon": "fa-solid fa-water", "icon": "",
"recovery": "session", "recovery": null,
"diceStates": {}, "diceStates": {},
"dieFaces": "d4" "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, "originItemType": null,
"subType": null, "subType": null,
"originId": null, "originId": null,

View file

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

View file

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

View file

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

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