diff --git a/daggerheart.mjs b/daggerheart.mjs index 861d16ca..b1b2a0ca 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -3,36 +3,41 @@ import * as applications from './module/applications/_module.mjs'; import * as data from './module/data/_module.mjs'; import * as models from './module/data/_module.mjs'; import * as documents from './module/documents/_module.mjs'; +import * as collections from './module/documents/collections/_module.mjs'; import * as dice from './module/dice/_module.mjs'; import * as fields from './module/data/fields/_module.mjs'; import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs'; import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs'; import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs'; -import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll } from './module/dice/_module.mjs'; +import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll, FateRoll } from './module/dice/_module.mjs'; import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs'; +import { enrichedFateRoll, getFateTypeData } from './module/enrichers/FateRollEnricher.mjs'; import { handlebarsRegistration, runMigrations, settingsRegistration, socketRegistration } from './module/systemRegistration/_module.mjs'; -import { placeables } from './module/canvas/_module.mjs'; +import { placeables, DhTokenLayer } from './module/canvas/_module.mjs'; import './node_modules/@yaireo/tagify/dist/tagify.css'; import TemplateManager from './module/documents/templateManager.mjs'; +import TokenManager from './module/documents/tokenManager.mjs'; CONFIG.DH = SYSTEM; CONFIG.TextEditor.enrichers.push(...enricherConfig); -CONFIG.Dice.rolls = [BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll]; +CONFIG.Dice.rolls = [BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll, FateRoll]; CONFIG.Dice.daggerheart = { DHRoll: DHRoll, DualityRoll: DualityRoll, D20Roll: D20Roll, - DamageRoll: DamageRoll + DamageRoll: DamageRoll, + FateRoll: FateRoll }; CONFIG.Actor.documentClass = documents.DhpActor; CONFIG.Actor.dataModels = models.actors.config; +CONFIG.Actor.collection = collections.DhActorCollection; CONFIG.Item.documentClass = documents.DHItem; CONFIG.Item.dataModels = models.items.config; @@ -51,8 +56,13 @@ CONFIG.ChatMessage.template = 'systems/daggerheart/templates/ui/chat/chat-messag CONFIG.Canvas.rulerClass = placeables.DhRuler; CONFIG.Canvas.layers.templates.layerClass = placeables.DhTemplateLayer; +CONFIG.Canvas.layers.tokens.layerClass = DhTokenLayer; + CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate; +CONFIG.RollTable.documentClass = documents.DhRollTable; +CONFIG.RollTable.resultTemplate = 'systems/daggerheart/templates/ui/chat/table-result.hbs'; + CONFIG.Scene.documentClass = documents.DhScene; CONFIG.Token.documentClass = documents.DhToken; @@ -74,6 +84,8 @@ CONFIG.ui.countdowns = applications.ui.DhCountdowns; CONFIG.ux.ContextMenu = applications.ux.DHContextMenu; CONFIG.ux.TooltipManager = documents.DhTooltipManager; CONFIG.ux.TemplateManager = new TemplateManager(); +CONFIG.ux.TokenManager = new TokenManager(); +CONFIG.debug.triggers = false; Hooks.once('init', () => { game.system.api = { @@ -85,7 +97,7 @@ Hooks.once('init', () => { fields }; - game.system.registeredTriggers = new RegisteredTriggers(); + game.system.registeredTriggers = new game.system.api.data.RegisteredTriggers(); const { DocumentSheetConfig } = foundry.applications.apps; DocumentSheetConfig.unregisterSheet(TokenDocument, 'core', foundry.applications.sheets.TokenConfig); @@ -98,7 +110,7 @@ Hooks.once('init', () => { type: game.i18n.localize(typePath) }); - const { Items, Actors } = foundry.documents.collections; + const { Items, Actors, RollTables } = foundry.documents.collections; Items.unregisterSheet('core', foundry.applications.sheets.ItemSheetV2); Items.registerSheet(SYSTEM.id, applications.sheets.items.Ancestry, { types: ['ancestry'], @@ -183,6 +195,12 @@ Hooks.once('init', () => { label: sheetLabel('TYPES.Actor.party') }); + RollTables.unregisterSheet('core', foundry.applications.sheets.RollTableSheet); + RollTables.registerSheet(SYSTEM.id, applications.sheets.rollTables.RollTableSheet, { + types: ['base'], + makeDefault: true + }); + DocumentSheetConfig.unregisterSheet( CONFIG.ActiveEffect.documentClass, 'core', @@ -291,13 +309,15 @@ Hooks.on('chatMessage', (_, message) => { ? CONFIG.DH.ACTIONS.advantageState.disadvantage.value : undefined; const difficulty = rollCommand.difficulty; + const grantResources = Boolean(rollCommand.grantResources); const target = getCommandTarget({ allowNull: true }); - const title = traitValue - ? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { - ability: game.i18n.localize(SYSTEM.ACTOR.abilities[traitValue].label) - }) - : game.i18n.localize('DAGGERHEART.GENERAL.duality'); + const title = + (flavor ?? traitValue) + ? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { + ability: game.i18n.localize(SYSTEM.ACTOR.abilities[traitValue].label) + }) + : game.i18n.localize('DAGGERHEART.GENERAL.duality'); enrichedDualityRoll({ reaction, @@ -305,9 +325,38 @@ Hooks.on('chatMessage', (_, message) => { target, difficulty, title, - label: 'test', + label: game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll'), actionType: null, - advantage + advantage, + grantResources + }); + return false; + } + + if (message.startsWith('/fr')) { + const result = + message.trim().toLowerCase() === '/fr' ? { result: {} } : rollCommandToJSON(message.replace(/\/fr\s?/, '')); + + if (!result) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateParsing')); + return false; + } + + const { result: rollCommand, flavor } = result; + const fateTypeData = getFateTypeData(rollCommand?.type); + + if (!fateTypeData) + return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing')); + + const { value: fateType, label: fateTypeLabel } = fateTypeData; + const target = getCommandTarget({ allowNull: true }); + const title = flavor ?? game.i18n.localize('DAGGERHEART.GENERAL.fateRoll'); + + enrichedFateRoll({ + target, + title, + label: fateTypeLabel, + fateType }); return false; } @@ -354,7 +403,9 @@ const updateAllRangeDependentEffects = async () => { const effectsAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).effects; if (!effectsAutomation.rangeDependent) return; - const tokens = canvas.scene.tokens; + const tokens = canvas.scene?.tokens; + if (!tokens) return; + if (game.user.character) { // The character updates their character's token. There can be only one token. const characterToken = tokens.find(x => x.actor === game.user.character); @@ -374,8 +425,8 @@ Hooks.on('targetToken', () => { debouncedRangeEffectCall(); }); -Hooks.on('refreshToken', (_, options) => { - if (options.refreshPosition) { +Hooks.on('refreshToken', (token, options) => { + if (options.refreshPosition && !token._original) { debouncedRangeEffectCall(); } }); @@ -383,49 +434,12 @@ Hooks.on('refreshToken', (_, options) => { Hooks.on('renderCompendiumDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html)); Hooks.on('renderDocumentDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html)); -class RegisteredTriggers extends Map { - constructor() { - super(); - } +/* Non actor-linked Actors should unregister the triggers of their tokens if a scene's token layer is torn down */ +Hooks.on('canvasTearDown', canvas => { + game.system.registeredTriggers.unregisterSceneTriggers(canvas.scene); +}); - async registerTriggers(trigger, actor, triggeringActorType, uuid, commands) { - const existingTrigger = this.get(trigger); - if (!existingTrigger) this.set(trigger, new Map()); - - this.get(trigger).set(uuid, { actor, triggeringActorType, commands }); - } - - async runTrigger(trigger, currentActor, ...args) { - const updates = []; - const triggerSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).triggers; - if (!triggerSettings.enabled) return updates; - - const dualityTrigger = this.get(trigger); - if (dualityTrigger) { - for (let { actor, triggeringActorType, commands } of dualityTrigger.values()) { - const triggerData = CONFIG.DH.TRIGGER.triggers[trigger]; - if (triggerData.usesActor && triggeringActorType !== 'any') { - if (triggeringActorType === 'self' && currentActor?.uuid !== actor) continue; - else if (triggeringActorType === 'other' && currentActor?.uuid === actor) continue; - } - - for (let command of commands) { - try { - const result = await command(...args); - if (result?.updates?.length) updates.push(...result.updates); - } catch (_) { - const triggerName = game.i18n.localize(triggerData.label); - ui.notifications.error( - game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerError', { - trigger: triggerName, - actor: currentActor?.name - }) - ); - } - } - } - } - - return updates; - } -} +/* Non actor-linked Actors should register the triggers of their tokens on a readied scene */ +Hooks.on('canvasReady', canas => { + game.system.registeredTriggers.registerSceneTriggers(canvas.scene); +}); diff --git a/lang/en.json b/lang/en.json index cb297398..e68b5a60 100755 --- a/lang/en.json +++ b/lang/en.json @@ -69,7 +69,11 @@ }, "summon": { "name": "Summon", - "tooltip": "Create tokens in the scene." + "tooltip": "Create tokens in the scene.", + "error": "You do not have permission to summon tokens or there is no active scene.", + "invalidDrop": "You can only drop Actor entities to summon.", + "chatMessageTitle": "Test2", + "chatMessageHeaderTitle": "Summoning" } }, "Config": { @@ -122,6 +126,9 @@ }, "cost": { "stepTooltip": "+{step} per step" + }, + "summon": { + "dropSummonsHere": "Drop Summons Here" } } }, @@ -196,6 +203,8 @@ "unequip": "Unequip", "useItem": "Use Item" }, + "defaultHopeDice": "Default Hope Dice", + "defaultFearDice": "Default Fear Dice", "disadvantageSources": { "label": "Disadvantage Sources", "hint": "Add single words or short text as reminders and hints of what a character has disadvantage on." @@ -228,11 +237,14 @@ "confirmText": "Would you like to level up your companion {name} by {levelChange} levels at this time? (You can do it manually later)" }, "viewLevelups": "View Levelups", + "resetCharacter": "Reset Character", "viewParty": "View Party", "InvalidOldCharacterImportTitle": "Old Character Import", "InvalidOldCharacterImportText": "Character data exported prior to system version 1.1 will not generate a complete character. Do you wish to continue?", "cancelBeastform": "Cancel Beastform", - "sidebarFavoritesHint": "Drag items, features and domain cards from the sheet to here" + "sidebarFavoritesHint": "Drag items, features and domain cards from the sheet to here", + "resetCharacterConfirmationTitle": "Reset Character", + "resetCharacterConfirmationContent": "You are reseting all character data except name and portrait. Are you sure?" }, "Companion": { "FIELDS": { @@ -306,6 +318,8 @@ "selectPrimaryWeapon": "Select Primary Weapon", "selectSecondaryWeapon": "Select Secondary Weapon", "selectSubclass": "Select Subclass", + "setupSkipTitle": "Skipping Character Setup", + "setupSkipContent": "You are skipping the Character Setup by adding this manually. The character setup is the blinking arrows in the top-right. Are you sure you want to continue?", "startingItems": "Starting Items", "story": "Story", "storyExplanation": "Select which background and connection prompts you want to copy into your character's background.", @@ -317,6 +331,12 @@ "title": "{actor} - Character Setup", "traitIncreases": "Trait Increases" }, + "CharacterReset": { + "title": "Reset Character", + "alwaysDeleteSection": "Deleted Data", + "optionalDeleteSection": "Optional Data", + "headerTitle": "Select which data you'd like to keep" + }, "CombatTracker": { "combatStarted": "Active", "giveSpotlight": "Give The Spotlight", @@ -469,7 +489,9 @@ "tokenHUD": { "genericEffects": "Foundry Effects", "depositPartyTokens": "Deposit Party Tokens", - "retrievePartyTokens": "Retrieve Party Tokens" + "retrievePartyTokens": "Retrieve Party Tokens", + "depositCompanionTokens": "Deposit Companion Token", + "retrieveCompanionTokens": "Retrieve Companion Token" } }, "ImageSelect": { @@ -599,6 +621,7 @@ }, "RerollDialog": { "title": "Reroll", + "damageTitle": "Reroll Damage", "deselectDiceNotification": "Deselect one of the selected dice first", "acceptCurrentRolls": "Accept Current Rolls" }, @@ -606,6 +629,13 @@ "title": "{name} Resource", "rerollDice": "Reroll Dice" }, + "RiskItAllDialog": { + "title": "{name} - Risk It All", + "subtitle": "Clear Stress and Hit Points", + "remainingTitle": "Remaining Points", + "clearResource": "Clear {resource}", + "finalTitle": "Final Character Resources" + }, "TagTeamSelect": { "title": "Tag Team Roll", "leaderTitle": "Initiating Character", @@ -953,6 +983,10 @@ "outsideRange": "Outside Range" }, "Condition": { + "deathMove": { + "name": "Death Move", + "description": "The character is about to make a Death Move" + }, "dead": { "name": "Dead", "description": "The character is dead" @@ -1004,15 +1038,15 @@ "DeathMoves": { "avoidDeath": { "name": "Avoid Death", - "description": "You drop unconscious temporarily and work with the GM to describe how the situation gets much worse because of it. Then roll your Fear die; if its value is equal to or under your Level, take a Scar." + "description": "Your character avoids death and faces the consequences. They temporarily drop unconscious, and then you work with the GM to describe how the situation worsens. While unconscious, your character can't move or act, and they can't be targeted by an attack. They return to consciousness when an ally clears 1 or more of their marked Hit Points or when the party finishes a long rest. After your character falls unconscious, roll your Hope Die. If its value is equal to or less than your character's level, they gain a scar: permanently cross out a Hope slot and work with the GM to determine its lasting narrative impact and how, if possible, it can be restored. If you ever cross out your last Hope slot, your character's journey ends." }, "riskItAll": { "name": "Risk It All", - "description": "Roll your Duality Dice. If Hope is higher, you stay on your feet and clear an amount of Hit Points and/or Stress equal to the value of the Hope die (divide the Hope die value up between these however you’d prefer). If your Fear die is higher, you cross through the veil of death. If the Duality Dice are tied, you stay on your feet and clear all Hit Points and Stress." + "description": "Roll your Duality Dice. If the Hope Die is higher, your character stays on their feet and clears a number of Hit Points or Stress equal to the value of the Hope Die (you can divide the Hope Die value between Hit Points and Stress however you'd prefer). If the Fear Die is higher, your character crosses through the veil of death. If the Duality Dice show matching results, your character stays up and clears all Hit Points and Stress." }, "blazeOfGlory": { "name": "Blaze Of Glory", - "description": "With Blaze of Glory, the player is accepting death for the character. Take one action (at GM discretion), which becomes an automatic critical success, then cross through the veil of death." + "description": "Your character embraces death and goes out in a blaze of glory. Take one final action. It automatically critically succeeds (with GM approval), and then you cross through the veil of death. NOTE: A Blaze of Glory effect has been added to your character. Any Duality Roll will automatically be a critical." } }, "DomainCardTypes": { @@ -2053,6 +2087,7 @@ "description": "Description", "main": "Data", "information": "Information", + "itemFeatures": "Item Features", "notes": "Notes", "inventory": "Inventory", "loadout": "Loadout", @@ -2077,6 +2112,7 @@ "tier4": "tier 4", "domains": "Domains", "downtime": "Downtime", + "itemFeatures": "Item Features", "roll": "Roll", "rules": "Rules", "partyMembers": "Party Members", @@ -2127,6 +2163,7 @@ "dropActorsHere": "Drop Actors here", "dropFeaturesHere": "Drop Features here", "duality": "Duality", + "dualityDice": "Duality Dice", "dualityRoll": "Duality Roll", "enabled": "Enabled", "evasion": "Evasion", @@ -2140,11 +2177,14 @@ "single": "Favorite", "plural": "Favorites" }, + "fate": "Fate", + "fateRoll": "Fate Roll", "fear": "Fear", "features": "Features", "formula": "Formula", "general": "General", "gm": "GM", + "guaranteedCriticalSuccess": "Guaranteed Critical Success", "healing": "Healing", "healingRoll": "Healing Roll", "hit": { @@ -2188,6 +2228,7 @@ "single": "Player", "plurial": "Players" }, + "portrait": "Portrait", "proficiency": "Proficiency", "quantity": "Quantity", "range": "Range", @@ -2204,12 +2245,17 @@ "rollWith": "{roll} Roll", "save": "Save", "scalable": "Scalable", + "scars": "Scars", "situationalBonus": "Situational Bonus", "spent": "Spent", "step": "Step", "stress": "Stress", "subclasses": "Subclasses", "success": "Success", + "summon": { + "single": "Summon", + "plural": "Summons" + }, "take": "Take", "Target": { "single": "Target", @@ -2276,7 +2322,8 @@ "placeholder": "Using character dimensions", "disabledPlaceholder": "Set by character size", "height": { "label": "Height" }, - "width": { "label": "Width" } + "width": { "label": "Width" }, + "scale": { "label": "Token Scale" } }, "evolved": { "maximumTier": { "label": "Maximum Tier" }, @@ -2325,6 +2372,9 @@ "DomainCard": { "type": "Type", "recallCost": "Recall Cost", + "vaultActive": "Active In Vault", + "loadoutIgnore": "Ignores Loadout Limits", + "domainTouched": "Domain Touched", "foundationTitle": "Foundation", "specializationTitle": "Specialization", "masteryTitle": "Mastery" @@ -2338,6 +2388,12 @@ "secondaryWeapon": "Secondary Weapon" } }, + "ROLLTABLES": { + "FIELDS": { + "formulaName": { "label": "Formula Name" } + }, + "formula": "Formula" + }, "SETTINGS": { "Appearance": { "FIELDS": { @@ -2404,7 +2460,11 @@ "overlay": { "label": "Overlay Effect" }, "characterDefault": { "label": "Character Default Defeated Status" }, "adversaryDefault": { "label": "Adversary Default Defeated Status" }, - "companionDefault": { "label": "Companion Default Defeated Status" } + "companionDefault": { "label": "Companion Default Defeated Status" }, + "deathMove": { "label": "Death Move" }, + "dead": { "label": "Dead" }, + "defeated": { "label": "Defeated" }, + "unconscious": { "label": "Unconscious" } }, "hopeFear": { "label": "Hope & Fear", @@ -2437,10 +2497,6 @@ "label": "Show Resource Change Scrolltexts", "hint": "When a character is damaged, uses armor etc, a scrolling text will briefly appear by the token to signify this." }, - "playerCanEditSheet": { - "label": "Players Can Manually Edit Character Settings", - "hint": "Players are allowed to access the manual Character Settings and change their statistics beyond the rules." - }, "roll": { "roll": { "label": "Roll", @@ -2493,9 +2549,11 @@ "itemFeatures": "Item Features", "nrChoices": "# Moves Per Rest", "resetMovesTitle": "Reset {type} Downtime Moves", + "resetItemFeaturesTitle": "Reset {type}", "resetMovesText": "Are you sure you want to reset?", "FIELDS": { "maxFear": { "label": "Max Fear" }, + "maxHope": { "label": "Max Hope" }, "traitArray": { "label": "Initial Trait Modifiers" }, "maxLoadout": { "label": "Max Cards in Loadout", @@ -2643,7 +2701,16 @@ "currentTarget": "Current" }, "deathMove": { - "title": "Death Move" + "title": "Death Move", + "gainScar": "You gained a scar.", + "avoidScar": "You have avoided a new scar.", + "journeysEnd": "You have {scars} Scars and have crossed out your last Hope slot. Your character's journey ends.", + "riskItAllCritical": "Critical Rolled, clearing all marked Stress and Hit Points.", + "riskItAllFailure": "The fear die rolled higher. You have crossed through the veil of death.", + "blazeOfGlory": "Blaze of Glory Effect Added!", + "riskItAllDialogButton": "Clear Stress And Hit Points.", + "riskItAllSuccessWithEnoughHope": "The Hope value is more than the marked Stress and Hit Points. Both are cleared fully.", + "riskItAllSuccess": "The hope die rolled higher, clear up to {hope} Stress And Hit Points." }, "dicePool": { "title": "Dice Pool" @@ -2697,6 +2764,9 @@ "rerollDamage": "Reroll Damage", "assignTagRoll": "Assign as Tag Roll" }, + "ConsoleLogs": { + "triggerRun": "DH TRIGGER | Item '{item}' on actor '{actor}' ran a '{trigger}' trigger." + }, "Countdowns": { "title": "Countdowns", "toggleIconMode": "Toggle Icon Only", @@ -2762,7 +2832,9 @@ "noAssignedPlayerCharacter": "You have no assigned character.", "noSelectedToken": "You have no selected token", "onlyUseableByPC": "This can only be used with a PC token", - "dualityParsing": "Duality roll not properly formated", + "dualityParsing": "Duality roll not properly formatted", + "fateParsing": "Fate roll not properly formatted", + "fateTypeParsing": "Fate roll not properly formatted, bad fate type. Valid types are 'Hope' and 'Fear'", "attributeFaulty": "The supplied Attribute doesn't exist", "domainCardWrongDomain": "You don't have access to that Domain", "domainCardToHighLevel": "The Domain Card is too high level to be selected", @@ -2825,7 +2897,9 @@ "noActorOwnership": "You do not have permissions for this character", "documentIsMissing": "The {documentType} is missing from the world.", "tokenActorMissing": "{name} is missing an Actor", - "tokenActorsMissing": "[{names}] missing Actors" + "tokenActorsMissing": "[{names}] missing Actors", + "domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used", + "knowTheTide": "Know The Tide gained a token" }, "Sidebar": { "actorDirectory": { @@ -2870,7 +2944,8 @@ "deleteItem": "Delete Item", "immune": "Immune", "middleClick": "[Middle Click] Keep tooltip view", - "tokenSize": "The token size used on the canvas" + "tokenSize": "The token size used on the canvas", + "previewTokenHelp": "Left-click to place, right-click to cancel" } } } diff --git a/module/applications/dialogs/_module.mjs b/module/applications/dialogs/_module.mjs index 92038c41..4eda8579 100644 --- a/module/applications/dialogs/_module.mjs +++ b/module/applications/dialogs/_module.mjs @@ -1,5 +1,6 @@ export { default as AttributionDialog } from './attributionDialog.mjs'; export { default as BeastformDialog } from './beastformDialog.mjs'; +export { default as CharacterResetDialog } from './characterResetDialog.mjs'; export { default as d20RollDialog } from './d20RollDialog.mjs'; export { default as DamageDialog } from './damageDialog.mjs'; export { default as DamageReductionDialog } from './damageReductionDialog.mjs'; @@ -14,3 +15,4 @@ export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs'; export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs'; export { default as GroupRollDialog } from './group-roll-dialog.mjs'; export { default as TagTeamDialog } from './tagTeamDialog.mjs'; +export { default as RiskItAllDialog } from './riskItAllDialog.mjs'; diff --git a/module/applications/dialogs/characterResetDialog.mjs b/module/applications/dialogs/characterResetDialog.mjs new file mode 100644 index 00000000..0836af9c --- /dev/null +++ b/module/applications/dialogs/characterResetDialog.mjs @@ -0,0 +1,105 @@ +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; + +export default class CharacterResetDialog extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(actor, options = {}) { + super(options); + + this.actor = actor; + this.data = { + delete: { + class: { keep: false, label: 'TYPES.Item.class' }, + subclass: { keep: false, label: 'TYPES.Item.subclass' }, + ancestry: { keep: false, label: 'TYPES.Item.ancestry' }, + community: { keep: false, label: 'TYPES.Item.community' } + }, + optional: { + portrait: { keep: true, label: 'DAGGERHEART.GENERAL.portrait' }, + name: { keep: true, label: 'Name' }, + biography: { keep: true, label: 'DAGGERHEART.GENERAL.Tabs.biography' }, + inventory: { keep: true, label: 'DAGGERHEART.GENERAL.inventory' } + } + }; + } + + static DEFAULT_OPTIONS = { + tag: 'form', + classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'character-reset'], + window: { + icon: 'fa-solid fa-arrow-rotate-left', + title: 'DAGGERHEART.APPLICATIONS.CharacterReset.title' + }, + actions: { + finishSelection: this.#finishSelection + }, + form: { + handler: this.updateData, + submitOnChange: true, + submitOnClose: false + } + }; + + /** @override */ + static PARTS = { + resourceDice: { + id: 'resourceDice', + template: 'systems/daggerheart/templates/dialogs/characterReset.hbs' + } + }; + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.data = this.data; + + return context; + } + + static async updateData(event, _, formData) { + const { data } = foundry.utils.expandObject(formData.object); + + this.data = foundry.utils.mergeObject(this.data, data); + this.render(); + } + + static getUpdateData() { + const update = {}; + if (!this.data.optional.portrait) update.if(!this.data.optional.biography); + + if (!this.data.optional.inventory) return update; + } + + static async #finishSelection() { + const update = {}; + if (!this.data.optional.name.keep) { + const defaultName = game.system.api.documents.DhpActor.defaultName({ type: 'character' }); + foundry.utils.setProperty(update, 'name', defaultName); + foundry.utils.setProperty(update, 'prototypeToken.name', defaultName); + } + + if (!this.data.optional.portrait.keep) { + foundry.utils.setProperty(update, 'img', this.actor.schema.fields.img.initial(this.actor)); + foundry.utils.setProperty(update, 'prototypeToken.==texture', {}); + foundry.utils.setProperty(update, 'prototypeToken.==ring', {}); + } + + if (this.data.optional.biography.keep) + foundry.utils.setProperty(update, 'system.biography', this.actor.system.biography); + + if (this.data.optional.inventory.keep) foundry.utils.setProperty(update, 'system.gold', this.actor.system.gold); + + const { system, ...rest } = update; + await this.actor.update({ + ...rest, + '==system': system ?? {} + }); + + const inventoryItemTypes = ['weapon', 'armor', 'consumable', 'loot']; + await this.actor.deleteEmbeddedDocuments( + 'Item', + this.actor.items + .filter(x => !inventoryItemTypes.includes(x.type) || !this.data.optional.inventory.keep) + .map(x => x.id) + ); + + this.close(); + } +} diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index 34ca02cd..4a4b1556 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -10,6 +10,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio this.config = config; this.config.experiences = []; this.reactionOverride = config.actionType === 'reaction'; + this.selectedEffects = this.config.bonusEffects; if (config.source?.action) { this.item = config.data.parent.items.get(config.source.item) ?? config.data.parent; @@ -35,6 +36,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio selectExperience: this.selectExperience, toggleReaction: this.toggleReaction, toggleTagTeamRoll: this.toggleTagTeamRoll, + toggleSelectedEffect: this.toggleSelectedEffect, submitRoll: this.submitRoll }, form: { @@ -76,6 +78,9 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio icon })); + context.hasSelectedEffects = Boolean(this.selectedEffects && Object.keys(this.selectedEffects).length); + context.selectedEffects = this.selectedEffects; + this.config.costs ??= []; if (this.config.costs?.length) { const updatedCosts = game.system.api.fields.ActionFields.CostField.calcCosts.call( @@ -104,11 +109,17 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio context.roll = this.roll; context.rollType = this.roll?.constructor.name; context.rallyDie = this.roll.rallyChoices; - const experiences = this.config.data?.system?.experiences || {}; + + const actorExperiences = this.config.data?.system?.experiences || {}; + const companionExperiences = this.config.roll.companionRoll + ? (this.config.data?.companion?.system.experiences ?? {}) + : null; + const experiences = companionExperiences ?? actorExperiences; context.experiences = Object.keys(experiences).map(id => ({ id, ...experiences[id] })); + context.selectedExperiences = this.config.experiences; context.advantage = this.config.roll?.advantage; context.disadvantage = this.config.roll?.disadvantage; @@ -118,7 +129,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio context.formula = this.roll.constructFormula(this.config); if (this.actor?.system?.traits) context.abilities = this.getTraitModifiers(); - context.showReaction = !this.config.roll?.type && context.rollType === 'DualityRoll'; + context.showReaction = !this.config.skips?.reaction && context.rollType === 'DualityRoll'; context.reactionOverride = this.reactionOverride; } @@ -208,6 +219,11 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio this.render(); } + static toggleSelectedEffect(_event, button) { + this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected; + this.render(); + } + static async submitRoll() { await this.close({ submitted: true }); } diff --git a/module/applications/dialogs/damageDialog.mjs b/module/applications/dialogs/damageDialog.mjs index fbc584e4..b24570cc 100644 --- a/module/applications/dialogs/damageDialog.mjs +++ b/module/applications/dialogs/damageDialog.mjs @@ -6,6 +6,7 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application this.roll = roll; this.config = config; + this.selectedEffects = this.config.bonusEffects; } static DEFAULT_OPTIONS = { @@ -20,6 +21,7 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application icon: 'fa-solid fa-dice' }, actions: { + toggleSelectedEffect: this.toggleSelectedEffect, submitRoll: this.submitRoll }, form: { @@ -57,6 +59,9 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application icon })); context.modifiers = this.config.modifiers; + context.hasSelectedEffects = Boolean(Object.keys(this.selectedEffects).length); + context.selectedEffects = this.selectedEffects; + return context; } @@ -69,6 +74,11 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application this.render(); } + static toggleSelectedEffect(_event, button) { + this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected; + this.render(); + } + static async submitRoll() { await this.close({ submitted: true }); } diff --git a/module/applications/dialogs/deathMove.mjs b/module/applications/dialogs/deathMove.mjs index d0686d2b..3eadede6 100644 --- a/module/applications/dialogs/deathMove.mjs +++ b/module/applications/dialogs/deathMove.mjs @@ -1,11 +1,16 @@ -const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; +import { enrichedFateRoll } from '../../enrichers/FateRollEnricher.mjs'; +import { enrichedDualityRoll } from '../../enrichers/DualityRollEnricher.mjs'; -export default class DhpDeathMove extends HandlebarsApplicationMixin(ApplicationV2) { +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; +export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV2) { constructor(actor) { super({}); this.actor = actor; this.selectedMove = null; + this.showRiskItAllButton = false; + this.riskItAllButtonLabel = ''; + this.riskItAllHope = 0; } get title() { @@ -38,6 +43,111 @@ export default class DhpDeathMove extends HandlebarsApplicationMixin(Application return context; } + async handleAvoidDeath() { + const target = this.actor.uuid; + const config = await enrichedFateRoll({ + target, + title: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.avoidDeath.name'), + label: `${game.i18n.localize('DAGGERHEART.GENERAL.hope')} ${game.i18n.localize('DAGGERHEART.GENERAL.fateRoll')}`, + fateType: 'Hope' + }); + + if (!config.roll.fate) return; + + let returnMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.avoidScar'); + if (config.roll.fate.value <= this.actor.system.levelData.level.current) { + const newScarAmount = this.actor.system.scars + 1; + await this.actor.update({ + system: { + scars: newScarAmount + } + }); + + if (newScarAmount >= this.actor.system.resources.hope.max) { + await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id); + return game.i18n.format('DAGGERHEART.UI.Chat.deathMove.journeysEnd', { scars: newScarAmount }); + } + + returnMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.gainScar'); + } + + await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.unconscious.id); + return returnMessage; + } + + async handleRiskItAll() { + const config = await enrichedDualityRoll({ + reaction: true, + traitValue: null, + target: this.actor, + difficulty: null, + title: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.riskItAll.name'), + label: game.i18n.localize('DAGGERHEART.GENERAL.dualityDice'), + actionType: null, + advantage: null, + grantResources: false, + customConfig: { skips: { resources: true, reaction: true } } + }); + + if (!config.roll.result) return; + + const clearAllStressAndHitpointsUpdates = [ + { key: 'hitPoints', clear: true }, + { key: 'stress', clear: true } + ]; + + let chatMessage = ''; + if (config.roll.isCritical) { + config.resourceUpdates.addResources(clearAllStressAndHitpointsUpdates); + chatMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.riskItAllCritical'); + } + + if (config.roll.result.duality == 1) { + if ( + config.roll.hope.value >= + this.actor.system.resources.hitPoints.value + this.actor.system.resources.stress.value + ) { + config.resourceUpdates.addResources(clearAllStressAndHitpointsUpdates); + chatMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.riskItAllSuccessWithEnoughHope'); + } else { + chatMessage = game.i18n.format('DAGGERHEART.UI.Chat.deathMove.riskItAllSuccess', { + hope: config.roll.hope.value + }); + this.showRiskItAllButton = true; + this.riskItAllHope = config.roll.hope.value; + this.riskItAllButtonLabel = game.i18n.format('DAGGERHEART.UI.Chat.deathMove.riskItAllDialogButton'); + } + } + + if (config.roll.result.duality == -1) { + await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id); + chatMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.riskItAllFailure'); + } + + await config.resourceUpdates.updateResources(); + return chatMessage; + } + + async handleBlazeOfGlory() { + this.actor.createEmbeddedDocuments('ActiveEffect', [ + { + name: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.blazeOfGlory.name'), + description: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.blazeOfGlory.description'), + img: CONFIG.DH.GENERAL.deathMoves.blazeOfGlory.img, + changes: [ + { + key: 'system.rules.roll.guaranteedCritical', + mode: 2, + value: 'true' + } + ] + } + ]); + + await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id); + return game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.blazeOfGlory'); + } + static selectMove(_, button) { const move = button.dataset.move; this.selectedMove = CONFIG.DH.GENERAL.deathMoves[move]; @@ -46,23 +156,49 @@ export default class DhpDeathMove extends HandlebarsApplicationMixin(Application } static async takeMove() { + this.close(); + + let result = ''; + + if (CONFIG.DH.GENERAL.deathMoves.blazeOfGlory === this.selectedMove) { + result = await this.handleBlazeOfGlory(); + } + + if (CONFIG.DH.GENERAL.deathMoves.avoidDeath === this.selectedMove) { + result = await this.handleAvoidDeath(); + } + + if (CONFIG.DH.GENERAL.deathMoves.riskItAll === this.selectedMove) { + result = await this.handleRiskItAll(); + } + + if (!result) return; + + const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance) + .expandRollMessage?.desc; const cls = getDocumentClass('ChatMessage'); + const msg = { user: game.user.id, content: await foundry.applications.handlebars.renderTemplate( 'systems/daggerheart/templates/ui/chat/deathMove.hbs', { player: this.actor.name, - actor: { name: this.actor.name, img: this.actor.img }, + actor: this.actor, + actorId: this.actor._id, author: game.users.get(game.user.id), title: game.i18n.localize(this.selectedMove.name), img: this.selectedMove.img, - description: game.i18n.localize(this.selectedMove.description) + description: game.i18n.localize(this.selectedMove.description), + result: result, + open: autoExpandDescription ? 'open' : '', + chevron: autoExpandDescription ? 'fa-chevron-up' : 'fa-chevron-down', + showRiskItAllButton: this.showRiskItAllButton, + riskItAllButtonLabel: this.riskItAllButtonLabel, + riskItAllHope: this.riskItAllHope } ), - title: game.i18n.localize( - 'DAGGERHEART.UI.Chat.deathMove.title' - ), + title: game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.title'), speaker: cls.getSpeaker(), flags: { daggerheart: { @@ -72,7 +208,5 @@ export default class DhpDeathMove extends HandlebarsApplicationMixin(Application }; cls.create(msg); - - this.close(); } } diff --git a/module/applications/dialogs/downtime.mjs b/module/applications/dialogs/downtime.mjs index f03524f0..9a9a9ddb 100644 --- a/module/applications/dialogs/downtime.mjs +++ b/module/applications/dialogs/downtime.mjs @@ -93,27 +93,29 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV } getRefreshables() { - const actionItems = this.actor.items.filter(x => this.actor.system.isItemAvailable(x)).reduce((acc, x) => { - if (x.system.actions) { - const recoverable = x.system.actions.reduce((acc, action) => { - if (refreshIsAllowed([this.shortrest ? 'shortRest' : 'longRest'], action.uses.recovery)) { - acc.push({ - title: x.name, - name: action.name, - uuid: action.uuid - }); + const actionItems = this.actor.items + .filter(x => this.actor.system.isItemAvailable(x)) + .reduce((acc, x) => { + if (x.system.actions) { + const recoverable = x.system.actions.reduce((acc, action) => { + if (refreshIsAllowed([this.shortrest ? 'shortRest' : 'longRest'], action.uses.recovery)) { + acc.push({ + title: x.name, + name: action.name, + uuid: action.uuid + }); + } + + return acc; + }, []); + + if (recoverable) { + acc.push(...recoverable); } - - return acc; - }, []); - - if (recoverable) { - acc.push(...recoverable); } - } - return acc; - }, []); + return acc; + }, []); const resourceItems = this.actor.items.reduce((acc, x) => { if ( x.system.resource && @@ -189,7 +191,8 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV })); }); }); - const characters = game.actors.filter(x => x.type === 'character') + const characters = game.actors + .filter(x => x.type === 'character') .filter(x => x.testUserPermission(game.user, 'LIMITED')) .filter(x => x.uuid !== this.actor.uuid); diff --git a/module/applications/dialogs/riskItAllDialog.mjs b/module/applications/dialogs/riskItAllDialog.mjs new file mode 100644 index 00000000..10fa1bb4 --- /dev/null +++ b/module/applications/dialogs/riskItAllDialog.mjs @@ -0,0 +1,94 @@ +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; +export default class RiskItAllDialog extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(actor, resourceValue) { + super({}); + + this.actor = actor; + this.resourceValue = resourceValue; + this.choices = { + hitPoints: 0, + stress: 0 + }; + } + + get title() { + return game.i18n.format('DAGGERHEART.APPLICATIONS.RiskItAllDialog.title', { name: this.actor.name }); + } + + static DEFAULT_OPTIONS = { + classes: ['daggerheart', 'dh-style', 'dialog', 'views', 'risk-it-all'], + position: { width: 280, height: 'auto' }, + window: { icon: 'fa-solid fa-dice fa-xl' }, + actions: { + finish: RiskItAllDialog.#finish + } + }; + + static PARTS = { + application: { + id: 'risk-it-all', + template: 'systems/daggerheart/templates/dialogs/riskItAllDialog.hbs' + } + }; + + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + for (const input of htmlElement.querySelectorAll('.resource-container input')) + input.addEventListener('change', this.updateChoice.bind(this)); + } + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.resourceValue = this.resourceValue; + context.maxHitPointsValue = Math.min(this.resourceValue, this.actor.system.resources.hitPoints.max); + context.maxStressValue = Math.min(this.resourceValue, this.actor.system.resources.stress.max); + context.remainingResource = this.resourceValue - this.choices.hitPoints - this.choices.stress; + context.unfinished = context.remainingResource !== 0; + + context.choices = this.choices; + context.final = { + hitPoints: { + value: this.actor.system.resources.hitPoints.value - this.choices.hitPoints, + max: this.actor.system.resources.hitPoints.max + }, + stress: { + value: this.actor.system.resources.stress.value - this.choices.stress, + max: this.actor.system.resources.stress.max + } + }; + + context; + + return context; + } + + updateChoice(event) { + let value = Number.parseInt(event.target.value); + const choiceKey = event.target.dataset.choice; + const actorValue = this.actor.system.resources[choiceKey].value; + const remaining = this.resourceValue - this.choices.hitPoints - this.choices.stress; + const changeAmount = value - this.choices[choiceKey]; + + /* If trying to increase beyond remaining resource points, just increase to max available */ + if (remaining - changeAmount < 0) value = this.choices[choiceKey] + remaining; + else if (actorValue - value < 0) value = actorValue; + + this.choices[choiceKey] = value; + this.render(); + } + + static async #finish() { + const resourceUpdate = Object.keys(this.choices).reduce((acc, resourceKey) => { + const value = this.actor.system.resources[resourceKey].value - this.choices[resourceKey]; + acc[resourceKey] = { value }; + return acc; + }, {}); + + await this.actor.update({ + 'system.resources': resourceUpdate + }); + + this.close(); + } +} diff --git a/module/applications/hud/tokenHUD.mjs b/module/applications/hud/tokenHUD.mjs index 87c3e88e..77caaaff 100644 --- a/module/applications/hud/tokenHUD.mjs +++ b/module/applications/hud/tokenHUD.mjs @@ -5,7 +5,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { classes: ['daggerheart'], actions: { combat: DHTokenHUD.#onToggleCombat, - togglePartyTokens: DHTokenHUD.#togglePartyTokens + togglePartyTokens: DHTokenHUD.#togglePartyTokens, + toggleCompanions: DHTokenHUD.#toggleCompanions } }; @@ -26,7 +27,7 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { context.partyOnCanvas = this.actor.type === 'party' && this.actor.system.partyMembers.some(member => member.getActiveTokens().length > 0); - context.icons.toggleParty = 'systems/daggerheart/assets/icons/arrow-dunk.png'; + context.icons.toggleClowncar = 'systems/daggerheart/assets/icons/arrow-dunk.png'; context.actorType = this.actor.type; context.usesEffects = this.actor.type !== 'party'; context.canToggleCombat = DHTokenHUD.#nonCombatTypes.includes(this.actor.type) @@ -56,6 +57,9 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { }, {}) : null; + context.hasCompanion = this.actor.system.companion; + context.companionOnCanvas = context.hasCompanion && this.actor.system.companion.getActiveTokens().length > 0; + return context; } @@ -101,8 +105,24 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { : 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.depositPartyTokens' ); + await this.toggleClowncar(this.actor.system.partyMembers); + } + + static async #toggleCompanions(_, button) { + const icon = button.querySelector('img'); + icon.classList.toggle('flipped'); + button.dataset.tooltip = game.i18n.localize( + icon.classList.contains('flipped') + ? 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.retrieveCompanionTokens' + : 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.depositCompanionTokens' + ); + + await this.toggleClowncar([this.actor.system.companion]); + } + + async toggleClowncar(actors) { const animationDuration = 500; - const activeTokens = this.actor.system.partyMembers.flatMap(member => member.getActiveTokens()); + const activeTokens = actors.flatMap(member => member.getActiveTokens()); const { x: actorX, y: actorY } = this.document; if (activeTokens.length > 0) { for (let token of activeTokens) { @@ -114,14 +134,15 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { } } else { const activeScene = game.scenes.find(x => x.id === game.user.viewedScene); - const partyTokenData = []; - for (let member of this.actor.system.partyMembers) { + const tokenData = []; + for (let member of actors) { const data = await member.getTokenDocument(); - partyTokenData.push(data.toObject()); + tokenData.push(data.toObject()); } + const newTokens = await activeScene.createEmbeddedDocuments( 'Token', - partyTokenData.map(tokenData => ({ + tokenData.map(tokenData => ({ ...tokenData, alpha: 0, x: actorX, diff --git a/module/applications/levelup/companionLevelup.mjs b/module/applications/levelup/companionLevelup.mjs index 4b8f9b47..7f11ccff 100644 --- a/module/applications/levelup/companionLevelup.mjs +++ b/module/applications/levelup/companionLevelup.mjs @@ -1,6 +1,6 @@ import BaseLevelUp from './levelup.mjs'; import { defaultCompanionTier, LevelOptionType } from '../../data/levelTier.mjs'; -import { DhLevelup } from '../../data/levelup.mjs'; +import { DhCompanionLevelup as DhLevelup } from '../../data/companionLevelup.mjs'; import { diceTypes, range } from '../../config/generalConfig.mjs'; export default class DhCompanionLevelUp extends BaseLevelUp { @@ -9,7 +9,9 @@ export default class DhCompanionLevelUp extends BaseLevelUp { this.levelTiers = this.addBonusChoices(defaultCompanionTier); const playerLevelupData = actor.system.levelData; - this.levelup = new DhLevelup(DhLevelup.initializeData(this.levelTiers, playerLevelupData)); + this.levelup = new DhLevelup( + DhLevelup.initializeData(this.levelTiers, playerLevelupData, actor.system.levelupChoicesLeft) + ); } async _preparePartContext(partId, context) { diff --git a/module/applications/levelup/levelupViewMode.mjs b/module/applications/levelup/levelupViewMode.mjs index b3d7c30f..afd7dbc4 100644 --- a/module/applications/levelup/levelupViewMode.mjs +++ b/module/applications/levelup/levelupViewMode.mjs @@ -70,7 +70,10 @@ export default class DhlevelUpViewMode extends HandlebarsApplicationMixin(Applic return checkbox; }); - let label = game.i18n.localize(option.label); + let label = + optionKey === 'domainCard' + ? game.i18n.format(option.label, { maxLevel: tier.levels.end }) + : game.i18n.localize(option.label); return { label: label, checkboxGroups: chunkify(checkboxes, option.minCost, chunkedBoxes => { diff --git a/module/applications/scene/sceneConfigSettings.mjs b/module/applications/scene/sceneConfigSettings.mjs index 1b93aa8c..98e18f09 100644 --- a/module/applications/scene/sceneConfigSettings.mjs +++ b/module/applications/scene/sceneConfigSettings.mjs @@ -5,10 +5,7 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S super(options); Hooks.on(socketEvent.Refresh, ({ refreshType }) => { - if (refreshType === RefreshType.Scene) { - this.daggerheartFlag = new game.system.api.data.scenes.DHScene(this.document.flags.daggerheart); - this.render(); - } + if (refreshType === RefreshType.Scene) this.render(); }); } @@ -42,7 +39,9 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S async _preRender(context, options) { await super._preFirstRender(context, options); - this.daggerheartFlag = new game.system.api.data.scenes.DHScene(this.document.flags.daggerheart); + + if (!options.internalRefresh) + this.daggerheartFlag = new game.system.api.data.scenes.DHScene(this.document.flags.daggerheart); } _attachPartListeners(partId, htmlElement, options) { @@ -52,7 +51,7 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S case 'dh': htmlElement.querySelector('#rangeMeasurementSetting')?.addEventListener('change', async event => { this.daggerheartFlag.updateSource({ rangeMeasurement: { setting: event.target.value } }); - this.render(); + this.render({ internalRefresh: true }); }); const dragArea = htmlElement.querySelector('.scene-environments'); @@ -66,10 +65,17 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const item = await foundry.utils.fromUuid(data.uuid); if (item instanceof game.system.api.documents.DhpActor && item.type === 'environment') { + let sceneUuid = data.uuid; + if (item.pack) { + const inWorldActor = await game.system.api.documents.DhpActor.create([item.toObject()]); + if (!inWorldActor.length) return; + sceneUuid = inWorldActor[0].uuid; + } + await this.daggerheartFlag.updateSource({ - sceneEnvironments: [...this.daggerheartFlag.sceneEnvironments, data.uuid] + sceneEnvironments: [...this.daggerheartFlag.sceneEnvironments, sceneUuid] }); - this.render(); + this.render({ internalRefresh: true }); } } @@ -92,12 +98,16 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S (_, index) => index !== Number.parseInt(button.dataset.index) ) }); - this.render(); + this.render({ internalRefresh: true }); } /** @override */ async _processSubmitData(event, form, submitData, options) { submitData.flags.daggerheart = this.daggerheartFlag.toObject(); + submitData.flags.daggerheart.sceneEnvironments = submitData.flags.daggerheart.sceneEnvironments.filter(x => + foundry.utils.fromUuidSync(x) + ); + for (const key of Object.keys(this.document._source.flags.daggerheart?.sceneEnvironments ?? {})) { if (!submitData.flags.daggerheart.sceneEnvironments[key]) { submitData.flags.daggerheart.sceneEnvironments[`-=${key}`] = null; diff --git a/module/applications/settings/homebrewSettings.mjs b/module/applications/settings/homebrewSettings.mjs index 3c4486c1..6e2e665d 100644 --- a/module/applications/settings/homebrewSettings.mjs +++ b/module/applications/settings/homebrewSettings.mjs @@ -36,7 +36,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli addItem: this.addItem, editItem: this.editItem, removeItem: this.removeItem, - resetMoves: this.resetMoves, + resetDowntimeMoves: this.resetDowntimeMoves, + resetItemFeatures: this.resetItemFeatures, addDomain: this.addDomain, toggleSelectedDomain: this.toggleSelectedDomain, deleteDomain: this.deleteDomain, @@ -232,7 +233,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli this.render(); } - static async resetMoves(_, target) { + static async resetDowntimeMoves(_, target) { const confirmed = await foundry.applications.api.DialogV2.confirm({ window: { title: game.i18n.format('DAGGERHEART.SETTINGS.Homebrew.resetMovesTitle', { @@ -266,7 +267,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli ...move, name: game.i18n.localize(move.name), description: game.i18n.localize(move.description), - actions: move.actions.reduce((acc, key) => { + actions: Object.keys(move.actions).reduce((acc, key) => { const action = move.actions[key]; acc[key] = { ...action, @@ -293,6 +294,31 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli this.render(); } + static async resetItemFeatures(_, target) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.format('DAGGERHEART.SETTINGS.Homebrew.resetItemFeaturesTitle', { + type: game.i18n.localize(`DAGGERHEART.GENERAL.${target.dataset.type}`) + }) + }, + content: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.resetMovesText') + }); + + if (!confirmed) return; + + await this.settings.updateSource({ + [`itemFeatures.${target.dataset.type}`]: Object.keys( + this.settings.itemFeatures[target.dataset.type] + ).reduce((acc, key) => { + acc[`-=${key}`] = null; + + return acc; + }, {}) + }); + + this.render(); + } + static async addDomain(event) { event.preventDefault(); const content = new foundry.data.fields.StringField({ diff --git a/module/applications/sheets-configs/action-base-config.mjs b/module/applications/sheets-configs/action-base-config.mjs index 16ebcab5..42252362 100644 --- a/module/applications/sheets-configs/action-base-config.mjs +++ b/module/applications/sheets-configs/action-base-config.mjs @@ -31,6 +31,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) editEffect: this.editEffect, addDamage: this.addDamage, removeDamage: this.removeDamage, + editDoc: this.editDoc, addTrigger: this.addTrigger, removeTrigger: this.removeTrigger, expandTrigger: this.expandTrigger @@ -39,7 +40,8 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) handler: this.updateForm, submitOnChange: true, closeOnSubmit: false - } + }, + dragDrop: [{ dragSelector: null, dropSelector: '#summon-drop-zone', handlers: ['_onDrop'] }] }; static PARTS = { @@ -101,7 +103,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) } }; - static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects']; + static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects', 'summon']; _getTabs(tabs) { for (const v of Object.values(tabs)) { @@ -112,9 +114,25 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) return tabs; } + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + htmlElement.querySelectorAll('.summon-count-wrapper input').forEach(element => { + element.addEventListener('change', this.updateSummonCount.bind(this)); + }); + } + async _prepareContext(_options) { const context = await super._prepareContext(_options, 'action'); context.source = this.action.toObject(true); + context.action = this.action; + + context.summons = []; + for (const summon of context.source.summon ?? []) { + const actor = await foundry.utils.fromUuid(summon.actorUUID); + context.summons.push({ actor, count: summon.count }); + } + context.openSection = this.openSection; context.tabs = this._getTabs(this.constructor.TABS); context.config = CONFIG.DH; @@ -207,8 +225,9 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) } static async updateForm(event, _, formData) { - const submitData = this._prepareSubmitData(event, formData), - data = foundry.utils.mergeObject(this.action.toObject(), submitData); + const submitData = this._prepareSubmitData(event, formData); + + const data = foundry.utils.mergeObject(this.action.toObject(), submitData); this.action = await this.action.update(data); this.sheetUpdate?.(this.action); @@ -227,12 +246,26 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) static removeElement(event, button) { event.stopPropagation(); const data = this.action.toObject(), - key = event.target.closest('[data-key]').dataset.key, - index = button.dataset.index; + key = event.target.closest('[data-key]').dataset.key; + + // Prefer explicit index, otherwise find by uuid + let index = button?.dataset.index; + if (index === undefined || index === null || index === '') { + const uuid = button?.dataset.uuid ?? button?.dataset.itemUuid; + index = data[key].findIndex(e => (e?.actorUUID ?? e?.uuid) === uuid); + if (index === -1) return; + } else index = Number(index); + data[key].splice(index, 1); this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); } + static async editDoc(_event, target) { + const element = target.closest('[data-item-uuid]'); + const doc = (await foundry.utils.fromUuid(element.dataset.itemUuid)) ?? null; + if (doc) return doc.sheet.render({ force: true }); + } + static addDamage(_event) { if (!this.action.damage.parts) return; const data = this.action.toObject(), @@ -304,6 +337,15 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) } } + updateSummonCount(event) { + event.stopPropagation(); + const wrapper = event.target.closest('.summon-count-wrapper'); + const index = wrapper.dataset.index; + const data = this.action.toObject(); + data.summon[index].count = event.target.value; + this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); + } + /** Specific implementation in extending classes **/ static async addEffect(_event) {} static removeEffect(_event, _button) {} @@ -313,4 +355,29 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) this.tabGroups.primary = 'base'; await super.close(options); } + + async _onDrop(event) { + const data = foundry.applications.ux.TextEditor.getDragEventData(event); + const item = await foundry.utils.fromUuid(data.uuid); + if (!(item instanceof game.system.api.documents.DhpActor)) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.summon.invalidDrop')); + return; + } + + const actionData = this.action.toObject(); + let countvalue = 1; + for (const entry of actionData.summon) { + if (entry.actorUUID === data.uuid) { + entry.count += 1; + countvalue = entry.count; + await this.constructor.updateForm.bind(this)(null, null, { + object: foundry.utils.flattenObject(actionData) + }); + return; + } + } + + actionData.summon.push({ actorUUID: data.uuid, count: countvalue }); + await this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(actionData) }); + } } diff --git a/module/applications/sheets/_module.mjs b/module/applications/sheets/_module.mjs index c503e054..390267d5 100644 --- a/module/applications/sheets/_module.mjs +++ b/module/applications/sheets/_module.mjs @@ -1,3 +1,4 @@ export * as actors from './actors/_module.mjs'; export * as api from './api/_modules.mjs'; export * as items from './items/_module.mjs'; +export * as rollTables from './rollTables/_module.mjs'; diff --git a/module/applications/sheets/actors/adversary.mjs b/module/applications/sheets/actors/adversary.mjs index 98282d9f..d8a3df29 100644 --- a/module/applications/sheets/actors/adversary.mjs +++ b/module/applications/sheets/actors/adversary.mjs @@ -31,7 +31,7 @@ export default class AdversarySheet extends DHBaseActorSheet { dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]', dropSelector: null } - ], + ] }; static PARTS = { @@ -185,7 +185,6 @@ export default class AdversarySheet extends DHBaseActorSheet { super._onDragStart(event); } - /* -------------------------------------------- */ /* Application Clicks Actions */ /* -------------------------------------------- */ diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index db97de64..29e838ba 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -1,5 +1,5 @@ import DHBaseActorSheet from '../api/base-actor.mjs'; -import DhpDeathMove from '../../dialogs/deathMove.mjs'; +import DhDeathMove from '../../dialogs/deathMove.mjs'; import { abilities } from '../../../config/actorConfig.mjs'; import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs'; import DhCharacterCreation from '../../characterCreation/characterCreation.mjs'; @@ -27,6 +27,7 @@ export default class CharacterSheet extends DHBaseActorSheet { makeDeathMove: CharacterSheet.#makeDeathMove, levelManagement: CharacterSheet.#levelManagement, viewLevelups: CharacterSheet.#viewLevelups, + resetCharacter: CharacterSheet.#resetCharacter, toggleEquipItem: CharacterSheet.#toggleEquipItem, toggleResourceDice: CharacterSheet.#toggleResourceDice, handleResourceDice: CharacterSheet.#handleResourceDice, @@ -42,6 +43,11 @@ export default class CharacterSheet extends DHBaseActorSheet { icon: 'fa-solid fa-angles-up', label: 'DAGGERHEART.ACTORS.Character.viewLevelups', action: 'viewLevelups' + }, + { + icon: 'fa-solid fa-arrow-rotate-left', + label: 'DAGGERHEART.ACTORS.Character.resetCharacter', + action: 'resetCharacter' } ] }, @@ -224,13 +230,6 @@ export default class CharacterSheet extends DHBaseActorSheet { async _preparePartContext(partId, context, options) { context = await super._preparePartContext(partId, context, options); switch (partId) { - case 'header': - const { playerCanEditSheet, levelupAuto } = game.settings.get( - CONFIG.DH.id, - CONFIG.DH.SETTINGS.gameSettings.Automation - ); - context.showSettings = game.user.isGM || !levelupAuto || (levelupAuto && playerCanEditSheet); - break; case 'loadout': await this._prepareLoadoutContext(context, options); break; @@ -679,12 +678,19 @@ export default class CharacterSheet extends DHBaseActorSheet { new LevelupViewMode(this.document).render({ force: true }); } + /** + * Resets the character data and removes all embedded documents. + */ + static async #resetCharacter() { + new game.system.api.applications.dialogs.CharacterResetDialog(this.document).render({ force: true }); + } + /** * Opens the Death Move interface for the character. * @type {ApplicationClickAction} */ static async #makeDeathMove() { - await new DhpDeathMove(this.document).render({ force: true }); + await new DhDeathMove(this.document).render({ force: true }); } /** @@ -725,8 +731,10 @@ export default class CharacterSheet extends DHBaseActorSheet { headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { ability: abilityLabel }), + effects: await game.system.api.data.actions.actionsTypes.base.getEffects(this.document), roll: { - trait: button.dataset.attribute + trait: button.dataset.attribute, + type: 'trait' }, hasRoll: true, actionType: 'action', @@ -736,11 +744,12 @@ export default class CharacterSheet extends DHBaseActorSheet { }) }; const result = await this.document.diceRoll(config); + if (!result) return; /* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */ - const costResources = result.costs - .filter(x => x.enabled) - .map(cost => ({ ...cost, value: -cost.value, total: -cost.total })); + const costResources = + result.costs?.filter(x => x.enabled).map(cost => ({ ...cost, value: -cost.value, total: -cost.total })) || + {}; config.resourceUpdates.addResources(costResources); await config.resourceUpdates.updateResources(); } @@ -840,7 +849,7 @@ export default class CharacterSheet extends DHBaseActorSheet { static async #toggleVault(_event, button) { const doc = await getDocFromElement(button); const { available } = this.document.system.loadoutSlot; - if (doc.system.inVault && !available) { + if (doc.system.inVault && !available && !doc.system.loadoutIgnore) { return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.loadoutMaxReached')); } @@ -971,6 +980,18 @@ export default class CharacterSheet extends DHBaseActorSheet { return this._onSidebarDrop(event, item); } + const setupCriticalItemTypes = ['class', 'subclass', 'ancestry', 'community']; + if (this.document.system.needsCharacterSetup && setupCriticalItemTypes.includes(item.type)) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.localize('DAGGERHEART.APPLICATIONS.CharacterCreation.setupSkipTitle') + }, + content: game.i18n.localize('DAGGERHEART.APPLICATIONS.CharacterCreation.setupSkipContent') + }); + + if (!confirmed) return; + } + if (this.document.uuid === item.parent?.uuid) { return super._onDropItem(event, item); } diff --git a/module/applications/sheets/actors/companion.mjs b/module/applications/sheets/actors/companion.mjs index 9b85f622..b30b9c07 100644 --- a/module/applications/sheets/actors/companion.mjs +++ b/module/applications/sheets/actors/companion.mjs @@ -38,15 +38,6 @@ export default class DhCompanionSheet extends DHBaseActorSheet { } }; - /** @inheritDoc */ - async _onRender(context, options) { - await super._onRender(context, options); - - this.element - .querySelector('.level-value') - ?.addEventListener('change', event => this.document.updateLevel(Number(event.currentTarget.value))); - } - /* -------------------------------------------- */ /* Application Clicks Actions */ /* -------------------------------------------- */ @@ -71,10 +62,10 @@ export default class DhCompanionSheet extends DHBaseActorSheet { title: `${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}: ${this.actor.name}`, headerTitle: `Companion ${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}`, roll: { - trait: partner.system.spellcastModifierTrait?.key + trait: partner.system.spellcastModifierTrait?.key, + companionRoll: true }, - hasRoll: true, - data: partner.getRollData() + hasRoll: true }; const result = await partner.diceRoll(config); diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index 2d83b54b..5c285bc9 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -505,6 +505,10 @@ export default function DHApplicationMixin(Base) { const doc = await getDocFromElement(target), action = doc?.system?.attack ?? doc; const config = action.prepareConfig(event); + config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects( + this.document, + doc + ); config.hasRoll = false; return action && action.workflow.get('damage').execute(config, null, true); } @@ -629,7 +633,7 @@ export default function DHApplicationMixin(Base) { { relativeTo: isAction ? doc.parent : doc, rollData: doc.getRollData?.(), - secrets: isAction ? doc.parent.isOwner : doc.isOwner + secrets: isAction ? doc.parent.parent.isOwner : doc.isOwner } ); } diff --git a/module/applications/sheets/rollTables/_module.mjs b/module/applications/sheets/rollTables/_module.mjs new file mode 100644 index 00000000..73067b64 --- /dev/null +++ b/module/applications/sheets/rollTables/_module.mjs @@ -0,0 +1 @@ +export { default as RollTableSheet } from './rollTable.mjs'; diff --git a/module/applications/sheets/rollTables/rollTable.mjs b/module/applications/sheets/rollTables/rollTable.mjs new file mode 100644 index 00000000..9ead6814 --- /dev/null +++ b/module/applications/sheets/rollTables/rollTable.mjs @@ -0,0 +1,191 @@ +export default class DhRollTableSheet extends foundry.applications.sheets.RollTableSheet { + static DEFAULT_OPTIONS = { + ...super.DEFAULT_OPTIONS, + actions: { + changeMode: DhRollTableSheet.#onChangeMode, + drawResult: DhRollTableSheet.#onDrawResult, + resetResults: DhRollTableSheet.#onResetResults, + addFormula: DhRollTableSheet.#addFormula, + removeFormula: DhRollTableSheet.#removeFormula + } + }; + + static buildParts() { + const { footer, header, sheet, results, ...parts } = super.PARTS; + return { + sheet: { + ...sheet, + template: 'systems/daggerheart/templates/sheets/rollTable/sheet.hbs' + }, + header: { template: 'systems/daggerheart/templates/sheets/rollTable/header.hbs' }, + ...parts, + results: { + template: 'systems/daggerheart/templates/sheets/rollTable/results.hbs', + templates: ['templates/sheets/roll-table/result-details.hbs'], + scrollable: ['table[data-results] tbody'] + }, + summary: { template: 'systems/daggerheart/templates/sheets/rollTable/summary.hbs' }, + footer + }; + } + + static PARTS = DhRollTableSheet.buildParts(); + + async _preRender(context, options) { + await super._preRender(context, options); + + if (!options.internalRefresh) + this.daggerheartFlag = new game.system.api.data.DhRollTable(this.document.flags.daggerheart); + } + + /* root PART has a blank element on _attachPartListeners, so it cannot be used to set the eventListeners for the view mode */ + async _onRender(context, options) { + super._onRender(context, options); + + for (const element of this.element.querySelectorAll('.system-update-field')) + element.addEventListener('change', this.updateSystemField.bind(this)); + } + + async _preparePartContext(partId, context, options) { + context = await super._preparePartContext(partId, context, options); + + switch (partId) { + case 'sheet': + context.altFormula = this.daggerheartFlag.altFormula; + context.usesAltFormula = Object.keys(this.daggerheartFlag.altFormula).length > 0; + context.altFormulaOptions = { + '': { name: this.daggerheartFlag.formulaName }, + ...this.daggerheartFlag.altFormula + }; + context.activeAltFormula = this.daggerheartFlag.activeAltFormula; + context.selectedFormula = this.daggerheartFlag.getActiveFormula(this.document.formula); + context.results = this.getExtendedResults(context.results); + break; + case 'header': + context.altFormula = this.daggerheartFlag.altFormula; + context.usesAltFormula = Object.keys(this.daggerheartFlag.altFormula).length > 0; + context.altFormulaOptions = { + '': { name: this.daggerheartFlag.formulaName }, + ...this.daggerheartFlag.altFormula + }; + context.activeAltFormula = this.daggerheartFlag.activeAltFormula; + break; + case 'summary': + context.systemFields = this.daggerheartFlag.schema.fields; + context.altFormula = this.daggerheartFlag.altFormula; + context.formulaName = this.daggerheartFlag.formulaName; + break; + case 'results': + context.results = this.getExtendedResults(context.results); + break; + } + + return context; + } + + getExtendedResults(results) { + const bodyDarkMode = document.body.classList.contains('theme-dark'); + const elementLightMode = this.element.classList.contains('theme-light'); + const elementDarkMode = this.element.classList.contains('theme-dark'); + const isDarkMode = elementDarkMode || (!elementLightMode && bodyDarkMode); + + return results.map(x => ({ + ...x, + displayImg: isDarkMode && x.img === 'icons/svg/d20-black.svg' ? 'icons/svg/d20.svg' : x.img + })); + } + + /* -------------------------------------------- */ + /* Flag SystemData update methods */ + /* -------------------------------------------- */ + + async updateSystemField(event) { + const { dataset, value } = event.target; + await this.daggerheartFlag.updateSource({ [dataset.path]: value }); + this.render({ internalRefresh: true }); + } + + getSystemFlagUpdate() { + const deleteUpdate = Object.keys(this.document._source.flags.daggerheart?.altFormula ?? {}).reduce( + (acc, formulaKey) => { + if (!this.daggerheartFlag.altFormula[formulaKey]) acc.altFormula[`-=${formulaKey}`] = null; + + return acc; + }, + { altFormula: {} } + ); + + return { ['flags.daggerheart']: foundry.utils.mergeObject(this.daggerheartFlag.toObject(), deleteUpdate) }; + } + + static async #addFormula() { + await this.daggerheartFlag.updateSource({ + [`altFormula.${foundry.utils.randomID()}`]: game.system.api.data.DhRollTable.getDefaultFormula() + }); + this.render({ internalRefresh: true }); + } + + static async #removeFormula(_event, target) { + await this.daggerheartFlag.updateSource({ + [`altFormula.-=${target.dataset.key}`]: null + }); + this.render({ internalRefresh: true }); + } + + /* -------------------------------------------- */ + /* Extended RollTable methods */ + /* -------------------------------------------- */ + + /** + * Alternate between view and edit modes. + * @this {RollTableSheet} + * @type {ApplicationClickAction} + */ + static async #onChangeMode() { + this.mode = this.isEditMode ? 'view' : 'edit'; + await this.document.update(this.getSystemFlagUpdate()); + await this.render({ internalRefresh: true }); + } + + /** @inheritdoc */ + async _processSubmitData(event, form, submitData, options) { + /* RollTable sends an empty dummy event when swapping from view/edit first time */ + if (Object.keys(submitData).length) { + if (!submitData.flags) submitData.flags = { daggerheart: {} }; + submitData.flags.daggerheart = this.getSystemFlagUpdate(); + } + + super._processSubmitData(event, form, submitData, options); + } + + /** @inheritdoc */ + static async #onResetResults() { + await this.document.update(this.getSystemFlagUpdate()); + await this.document.resetResults(); + } + + /** + * Roll and draw a TableResult. + * @this {RollTableSheet} + * @type {ApplicationClickAction} + */ + static async #onDrawResult(_event, button) { + if (this.form) await this.submit({ operation: { render: false } }); + button.disabled = true; + const table = this.document; + + await this.document.update(this.getSystemFlagUpdate()); + + /* Sending in the currently selectd activeFormula to table.roll to use as the formula */ + const selectedFormula = this.daggerheartFlag.getActiveFormula(this.document.formula); + const tableRoll = await table.roll({ selectedFormula }); + const draws = table.getResultsForRoll(tableRoll.roll.total); + if (draws.length > 0) { + if (game.settings.get('core', 'animateRollTable')) await this._animateRoll(draws); + await table.draw(tableRoll); + } + + // Reenable the button if drawing with replacement since the draw won't trigger a sheet re-render + if (table.replacement) button.disabled = false; + } +} diff --git a/module/applications/sidebar/tabs/daggerheartMenu.mjs b/module/applications/sidebar/tabs/daggerheartMenu.mjs index 6c7a9df1..b29437bf 100644 --- a/module/applications/sidebar/tabs/daggerheartMenu.mjs +++ b/module/applications/sidebar/tabs/daggerheartMenu.mjs @@ -25,7 +25,7 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract /** @override */ static DEFAULT_OPTIONS = { - classes: ['dh-style'], + classes: ['dh-style', 'directory'], window: { title: 'SIDEBAR.TabSettings' }, diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index cc42df2f..2b489f58 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -81,6 +81,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo html.querySelectorAll('.group-roll-header-expand-section').forEach(element => element.addEventListener('click', this.groupRollExpandSection) ); + html.querySelectorAll('.risk-it-all-button').forEach(element => + element.addEventListener('click', event => this.riskItAllClearStressAndHitPoints(event, data)) + ); }; setupHooks() { @@ -92,6 +95,21 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo super.close(options); } + /** Ensure the chat theme inherits the interface theme */ + _replaceHTML(result, content, options) { + const themedElement = result.log?.querySelector('.chat-log'); + themedElement?.classList.remove('themed', 'theme-light', 'theme-dark'); + super._replaceHTML(result, content, options); + } + + /** Remove chat log theme from notifications area */ + async _onFirstRender(result, content) { + await super._onFirstRender(result, content); + document + .querySelector('#chat-notifications .chat-log') + ?.classList.remove('themed', 'theme-light', 'theme-dark'); + } + async onRollSimple(event, message) { const buttonType = event.target.dataset.type ?? 'damage', total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0), @@ -135,7 +153,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo async actionUseButton(event, message) { const { moveIndex, actionIndex, movePath } = event.currentTarget.dataset; const targetUuid = event.currentTarget.closest('.action-use-button-parent').querySelector('select')?.value; - const parent = await foundry.utils.fromUuid(targetUuid || message.system.actor) + const parent = await foundry.utils.fromUuid(targetUuid || message.system.actor); const actionType = message.system.moves[moveIndex].actions[actionIndex]; const cls = game.system.api.models.actions.actionsTypes[actionType.type]; @@ -370,4 +388,10 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo }); event.target.closest('.group-roll-section').querySelector('.group-roll-content').classList.toggle('closed'); } + + async riskItAllClearStressAndHitPoints(event, data) { + const resourceValue = event.target.dataset.resourceValue; + const actor = game.actors.get(event.target.dataset.actorId); + new game.system.api.applications.dialogs.RiskItAllDialog(actor, resourceValue).render({ force: true }); + } } diff --git a/module/applications/ui/combatTracker.mjs b/module/applications/ui/combatTracker.mjs index 288ba8ad..fc47f085 100644 --- a/module/applications/ui/combatTracker.mjs +++ b/module/applications/ui/combatTracker.mjs @@ -42,8 +42,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C this.combats .find(x => x.active) ?.system?.extendedBattleToggles?.reduce((acc, toggle) => (acc ?? 0) + toggle.category, null) ?? null; - const maxBP = CONFIG.DH.ENCOUNTER.BaseBPPerEncounter(context.characters.length) + modifierBP; - const currentBP = AdversaryBPPerEncounter(context.adversaries, context.characters); + const maxBP = CONFIG.DH.ENCOUNTER.BaseBPPerEncounter(context.allCharacters.length) + modifierBP; + const currentBP = AdversaryBPPerEncounter(context.adversaries, context.allCharacters); Object.assign(context, { fear: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), @@ -73,9 +73,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C Object.assign(context, { actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens, adversaries, - characters: characters - ?.filter(x => !x.isNPC) - .filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0), + allCharacters: characters, + characters: characters.filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0), spotlightRequests }); } diff --git a/module/applications/ui/effectsDisplay.mjs b/module/applications/ui/effectsDisplay.mjs index 0875e783..8c0c939c 100644 --- a/module/applications/ui/effectsDisplay.mjs +++ b/module/applications/ui/effectsDisplay.mjs @@ -76,6 +76,8 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica }; toggleHidden(token, focused) { + if (!this.element) return; + const effects = DhEffectsDisplay.getTokenEffects(focused ? token : null); this.element.hidden = effects.length === 0; diff --git a/module/applications/ui/itemBrowser.mjs b/module/applications/ui/itemBrowser.mjs index 794c3fb6..b35573f7 100644 --- a/module/applications/ui/itemBrowser.mjs +++ b/module/applications/ui/itemBrowser.mjs @@ -230,6 +230,14 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { result.flatMap(r => r), 'name' ); + + /* If any noticeable slowdown occurs, consider replacing with enriching description on clicking to expand descriptions */ + for (const item of this.items) { + item.system.enrichedDescription = + (await item.system.getEnrichedDescription?.()) ?? + (await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.description)); + } + this.fieldFilter = this._createFieldFilter(); if (this.presets?.filter) { diff --git a/module/applications/ui/sceneNavigation.mjs b/module/applications/ui/sceneNavigation.mjs index ac16ac99..0a3e08a5 100644 --- a/module/applications/ui/sceneNavigation.mjs +++ b/module/applications/ui/sceneNavigation.mjs @@ -31,7 +31,7 @@ export default class DhSceneNavigation extends foundry.applications.ui.SceneNavi const environments = daggerheartInfo.sceneEnvironments.filter( x => x && x.testUserPermission(game.user, 'LIMITED') ); - const hasEnvironments = environments.length > 0; + const hasEnvironments = environments.length > 0 && x.isView; return { ...x, hasEnvironments, diff --git a/module/canvas/_module.mjs b/module/canvas/_module.mjs index 6b8885f4..c211b549 100644 --- a/module/canvas/_module.mjs +++ b/module/canvas/_module.mjs @@ -1 +1,2 @@ export * as placeables from './placeables/_module.mjs'; +export { default as DhTokenLayer } from './tokens.mjs'; diff --git a/module/canvas/placeables/token.mjs b/module/canvas/placeables/token.mjs index e8b85938..2266d0da 100644 --- a/module/canvas/placeables/token.mjs +++ b/module/canvas/placeables/token.mjs @@ -1,4 +1,12 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { + /** @inheritdoc */ + async _draw(options) { + await super._draw(options); + + if (this.document.flags.daggerheart?.createPlacement) + this.previewHelp ||= this.addChild(this.#drawPreviewHelp()); + } + /** @inheritDoc */ async _drawEffects() { this.effects.renderable = false; @@ -34,7 +42,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { this.renderFlags.set({ refreshEffects: true }); } - /** + /** * Returns the distance from this token to another token object. * This value is corrected to handle alternate token sizes and other grid types * according to the diagonal rules. @@ -47,11 +55,11 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { const destinationPoint = target.center; // Compute for gridless. This version returns circular edge to edge + grid distance, - // so that tokens that are touching return 5. + // so that tokens that are touching return 5. if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) { const boundsCorrection = canvas.grid.distance / canvas.grid.size; - const originRadius = this.bounds.width * boundsCorrection / 2; - const targetRadius = target.bounds.width * boundsCorrection / 2; + const originRadius = (this.bounds.width * boundsCorrection) / 2; + const targetRadius = (target.bounds.width * boundsCorrection) / 2; const distance = canvas.grid.measurePath([originPoint, destinationPoint]).distance; return distance - originRadius - targetRadius + canvas.grid.distance; } @@ -61,11 +69,11 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { const targetEdge = this.#getEdgeBoundary(target.bounds, originPoint, destinationPoint); const adjustedOriginPoint = canvas.grid.getTopLeftPoint({ x: originEdge.x + Math.sign(originPoint.x - originEdge.x), - y: originEdge.y + Math.sign(originPoint.y - originEdge.y) + y: originEdge.y + Math.sign(originPoint.y - originEdge.y) }); const adjustDestinationPoint = canvas.grid.getTopLeftPoint({ x: targetEdge.x + Math.sign(destinationPoint.x - targetEdge.x), - y: targetEdge.y + Math.sign(destinationPoint.y - targetEdge.y) + y: targetEdge.y + Math.sign(destinationPoint.y - targetEdge.y) }); return canvas.grid.measurePath([adjustedOriginPoint, adjustDestinationPoint]).distance; } @@ -94,7 +102,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { /** Tests if the token is at least adjacent with another, with some leeway for diagonals */ isAdjacentWith(token) { - return this.distanceTo(token) <= (canvas.grid.distance * 1.5); + return this.distanceTo(token) <= canvas.grid.distance * 1.5; } /** @inheritDoc */ @@ -132,4 +140,25 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { bar.position.set(0, posY); return true; } + + /** + * Draw a helptext for previews as a text object + * @returns {PreciseText} The Text object for the preview helper + */ + #drawPreviewHelp() { + const { uiScale } = canvas.dimensions; + + const textStyle = CONFIG.canvasTextStyle.clone(); + textStyle.fontSize = 18; + textStyle.wordWrapWidth = this.w * 2.5; + textStyle.fontStyle = 'italic'; + + const helpText = new foundry.canvas.containers.PreciseText( + `(${game.i18n.localize('DAGGERHEART.UI.Tooltip.previewTokenHelp')})`, + textStyle + ); + helpText.anchor.set(helpText.width / 900, 1); + helpText.scale.set(uiScale, uiScale); + return helpText; + } } diff --git a/module/canvas/tokens.mjs b/module/canvas/tokens.mjs new file mode 100644 index 00000000..9813cd48 --- /dev/null +++ b/module/canvas/tokens.mjs @@ -0,0 +1,16 @@ +export default class DhTokenLayer extends foundry.canvas.layers.TokenLayer { + async _createPreview(createData, options) { + if (options.actor) { + const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes; + if (options.actor?.system.metadata.usesSize) { + const tokenSize = tokenSizes[options.actor.system.size]; + if (tokenSize && options.actor.system.size !== CONFIG.DH.ACTOR.tokenSize.custom.id) { + createData.width = tokenSize; + createData.height = tokenSize; + } + } + } + + return super._createPreview(createData, options); + } +} diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 3f49f7aa..be1dfce1 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -171,7 +171,7 @@ export const defeatedConditions = () => { acc[key] = { ...choice, img: defeated[`${choice.id}Icon`], - description: `DAGGERHEART.CONFIG.Condition.${choice.id}.description` + description: game.i18n.localize(`DAGGERHEART.CONFIG.Condition.${choice.id}.description`) }; return acc; @@ -179,6 +179,10 @@ export const defeatedConditions = () => { }; export const defeatedConditionChoices = { + deathMove: { + id: 'deathMove', + name: 'DAGGERHEART.CONFIG.Condition.deathMove.name' + }, defeated: { id: 'defeated', name: 'DAGGERHEART.CONFIG.Condition.defeated.name' @@ -496,6 +500,8 @@ export const diceTypes = { d20: 'd20' }; +export const dieFaces = [4, 6, 8, 10, 12, 20]; + export const multiplierTypes = { prof: 'Proficiency', cast: 'Spellcast', diff --git a/module/data/_module.mjs b/module/data/_module.mjs index 0a476ee9..f7e25a4e 100644 --- a/module/data/_module.mjs +++ b/module/data/_module.mjs @@ -1,6 +1,8 @@ export { default as DhCombat } from './combat.mjs'; export { default as DhCombatant } from './combatant.mjs'; export { default as DhTagTeamRoll } from './tagTeamRoll.mjs'; +export { default as DhRollTable } from './rollTable.mjs'; +export { default as RegisteredTriggers } from './registeredTriggers.mjs'; export * as countdowns from './countdowns.mjs'; export * as actions from './action/_module.mjs'; diff --git a/module/data/action/attackAction.mjs b/module/data/action/attackAction.mjs index ed97072f..7be7461d 100644 --- a/module/data/action/attackAction.mjs +++ b/module/data/action/attackAction.mjs @@ -36,6 +36,7 @@ export default class DHAttackAction extends DHDamageAction { async use(event, options) { const result = await super.use(event, options); + if (!result.message) return; if (result.message.system.action.roll?.type === 'attack') { const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns; diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index 3bf97564..115e6463 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -166,7 +166,6 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel */ getRollData(data = {}) { const actorData = this.actor ? this.actor.getRollData(false) : {}; - actorData.result = data.roll?.total ?? 1; actorData.scale = data.costs?.length // Right now only return the first scalable cost. ? (data.costs.find(c => c.scalable)?.total ?? 1) @@ -199,6 +198,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel let config = this.prepareConfig(event); if (!config) return; + config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(this.actor, this.item); + if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return; // Display configuration window if necessary @@ -240,6 +241,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel hasHealing: this.hasHealing, hasEffect: this.hasEffect, hasSave: this.hasSave, + onSave: this.save?.damageMod, isDirect: !!this.damage?.direct, selectedRollMode: game.settings.get('core', 'rollMode'), data: this.getRollData(), @@ -265,6 +267,28 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel return config; } + /** + * Get the all potentially applicable effects on the actor + * @param {DHActor} actor The actor performing the action + * @param {DHItem|DhActor} effectParent The parent of the effect + * @returns {DhActiveEffect[]} + */ + static async getEffects(actor, effectParent) { + if (!actor) return []; + + return Array.from(await actor.allApplicableEffects()).filter(effect => { + /* Effects on weapons only ever apply for the weapon itself */ + if (effect.parent.type === 'weapon') { + /* Unless they're secondary - then they apply only to other primary weapons */ + if (effect.parent.system.secondary) { + if (effectParent?.type !== 'weapon' || effectParent?.system.secondary) return false; + } else if (effectParent?.id !== effect.parent.id) return false; + } + + return !effect.isSuppressed; + }); + } + /** * Method used to know if a configuration dialog must be shown or not when there is no roll. * @param {*} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. @@ -353,14 +377,14 @@ export class ResourceUpdateMap extends Map { if (!resource.key) continue; const existing = this.get(resource.key); - if (existing) { + if (!existing || resource.clear) { + this.set(resource.key, resource); + } else if (!existing?.clear) { this.set(resource.key, { ...existing, value: existing.value + (resource.value ?? 0), total: existing.total + (resource.total ?? 0) }); - } else { - this.set(resource.key, resource); } } } diff --git a/module/data/action/summonAction.mjs b/module/data/action/summonAction.mjs index b06f1d38..1505ce2d 100644 --- a/module/data/action/summonAction.mjs +++ b/module/data/action/summonAction.mjs @@ -1,19 +1,5 @@ import DHBaseAction from './baseAction.mjs'; export default class DHSummonAction extends DHBaseAction { - static defineSchema() { - const fields = foundry.data.fields; - return { - ...super.defineSchema(), - documentUUID: new fields.DocumentUUIDField({ type: 'Actor' }) - }; - } - - async trigger(event, ...args) { - if (!this.canSummon || !canvas.scene) return; - } - - get canSummon() { - return game.user.can('TOKEN_CREATE'); - } + static extraSchemas = [...super.extraSchemas, 'summon']; } diff --git a/module/data/activeEffect/beastformEffect.mjs b/module/data/activeEffect/beastformEffect.mjs index 5311b827..65e36606 100644 --- a/module/data/activeEffect/beastformEffect.mjs +++ b/module/data/activeEffect/beastformEffect.mjs @@ -19,6 +19,7 @@ export default class BeastformEffect extends BaseEffect { base64: false }), tokenSize: new fields.SchemaField({ + scale: new fields.NumberField({ nullable: false, initial: 1 }), height: new fields.NumberField({ integer: false, nullable: true }), width: new fields.NumberField({ integer: false, nullable: true }) }) @@ -55,7 +56,9 @@ export default class BeastformEffect extends BaseEffect { const update = { ...baseUpdate, texture: { - src: this.characterTokenData.tokenImg + src: this.characterTokenData.tokenImg, + scaleX: this.characterTokenData.tokenSize.scale, + scaleY: this.characterTokenData.tokenSize.scale }, ring: { enabled: this.characterTokenData.usesDynamicToken, @@ -86,7 +89,9 @@ export default class BeastformEffect extends BaseEffect { y, 'texture': { enabled: this.characterTokenData.usesDynamicToken, - src: token.flags.daggerheart?.beastformTokenImg ?? this.characterTokenData.tokenImg + src: token.flags.daggerheart?.beastformTokenImg ?? this.characterTokenData.tokenImg, + scaleX: this.characterTokenData.tokenSize.scale, + scaleY: this.characterTokenData.tokenSize.scale }, 'ring': { subject: { diff --git a/module/data/actor/base.mjs b/module/data/actor/base.mjs index b90361e2..08308eab 100644 --- a/module/data/actor/base.mjs +++ b/module/data/actor/base.mjs @@ -27,7 +27,7 @@ const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) => }); /* Common rules applying to Characters and Adversaries */ -export const commonActorRules = (extendedData = { damageReduction: {} }) => ({ +export const commonActorRules = (extendedData = { damageReduction: {}, attack: { damage: {} } }) => ({ conditionImmunities: new fields.SchemaField({ hidden: new fields.BooleanField({ initial: false }), restrained: new fields.BooleanField({ initial: false }), @@ -41,7 +41,23 @@ export const commonActorRules = (extendedData = { damageReduction: {} }) => ({ magical: new fields.NumberField({ initial: 0, min: 0 }), physical: new fields.NumberField({ initial: 0, min: 0 }) }), - ...extendedData.damageReduction + ...(extendedData.damageReduction ?? {}) + }), + attack: new fields.SchemaField({ + ...extendedData.attack, + damage: new fields.SchemaField({ + hpDamageMultiplier: new fields.NumberField({ + required: true, + nullable: false, + initial: 1 + }), + hpDamageTakenMultiplier: new fields.NumberField({ + required: true, + nullable: false, + initial: 1 + }), + ...(extendedData.attack?.damage ?? {}) + }) }) }); diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 4bb47ebc..b557492e 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -36,7 +36,14 @@ export default class DhCharacter extends BaseDataActor { 'DAGGERHEART.ACTORS.Character.maxHPBonus' ), stress: resourceField(6, 0, 'DAGGERHEART.GENERAL.stress', true), - hope: resourceField(6, 2, 'DAGGERHEART.GENERAL.hope') + hope: new fields.SchemaField({ + value: new fields.NumberField({ + initial: 2, + min: 0, + integer: true, + label: 'DAGGERHEART.GENERAL.hope' + }) + }) }), traits: new fields.SchemaField({ agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'), @@ -79,12 +86,7 @@ export default class DhCharacter extends BaseDataActor { bags: new fields.NumberField({ initial: 0, integer: true }), chests: new fields.NumberField({ initial: 0, integer: true }) }), - scars: new fields.TypedObjectField( - new fields.SchemaField({ - name: new fields.StringField({}), - description: new fields.StringField() - }) - ), + scars: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.scars' }), biography: new fields.SchemaField({ background: new fields.HTMLField(), connections: new fields.HTMLField(), @@ -252,38 +254,59 @@ export default class DhCharacter extends BaseDataActor { hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint' }), disabledArmor: new fields.BooleanField({ intial: false }) + }, + attack: { + damage: { + diceIndex: new fields.NumberField({ + integer: true, + min: 0, + max: 5, + initial: 0, + label: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.label', + hint: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.hint' + }), + bonus: new fields.NumberField({ + required: true, + initial: 0, + min: 0, + label: 'DAGGERHEART.GENERAL.Rules.attack.damage.bonus.label' + }) + }, + roll: new fields.SchemaField({ + trait: new fields.StringField({ + required: true, + choices: CONFIG.DH.ACTOR.abilities, + nullable: true, + initial: null, + label: 'DAGGERHEART.GENERAL.Rules.attack.roll.trait.label' + }) + }) } }), - attack: new fields.SchemaField({ - damage: new fields.SchemaField({ - diceIndex: new fields.NumberField({ - integer: true, - min: 0, - max: 5, - initial: 0, - label: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.label', - hint: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.hint' - }), - bonus: new fields.NumberField({ - required: true, - initial: 0, - min: 0, - label: 'DAGGERHEART.GENERAL.Rules.attack.damage.bonus.label' - }) + dualityRoll: new fields.SchemaField({ + defaultHopeDice: new fields.NumberField({ + nullable: false, + required: true, + integer: true, + choices: CONFIG.DH.GENERAL.dieFaces, + initial: 12, + label: 'DAGGERHEART.ACTORS.Character.defaultHopeDice' }), - roll: new fields.SchemaField({ - trait: new fields.StringField({ - required: true, - choices: CONFIG.DH.ACTOR.abilities, - nullable: true, - initial: null, - label: 'DAGGERHEART.GENERAL.Rules.attack.roll.trait.label' - }) + defaultFearDice: new fields.NumberField({ + nullable: false, + required: true, + integer: true, + choices: CONFIG.DH.GENERAL.dieFaces, + initial: 12, + label: 'DAGGERHEART.ACTORS.Character.defaultFearDice' }) }), runeWard: new fields.BooleanField({ initial: false }), burden: new fields.SchemaField({ ignore: new fields.BooleanField() + }), + roll: new fields.SchemaField({ + guaranteedCritical: new fields.BooleanField() }) }), sidebarFavorites: new ForeignDocumentUUIDArrayField({ type: 'Item' }) @@ -347,7 +370,7 @@ export default class DhCharacter extends BaseDataActor { const modifiers = subClasses ?.map(sc => ({ ...this.traits[sc.system.spellcastingTrait], key: sc.system.spellcastingTrait })) .filter(x => x); - return modifiers.sort((a, b) => a.value - b.value)[0]; + return modifiers.sort((a, b) => (b.value ?? 0) - (a.value ?? 0))[0]; } get spellcastModifier() { @@ -528,7 +551,18 @@ export default class DhCharacter extends BaseDataActor { } get deathMoveViable() { - return this.resources.hitPoints.max > 0 && this.resources.hitPoints.value >= this.resources.hitPoints.max; + const { characterDefault } = game.settings.get( + CONFIG.DH.id, + CONFIG.DH.SETTINGS.gameSettings.Automation + ).defeated; + const deathMoveOutcomeStatuses = Object.keys(CONFIG.DH.GENERAL.defeatedConditionChoices).filter( + key => key !== characterDefault + ); + const deathMoveNotResolved = this.parent.statuses.every(status => !deathMoveOutcomeStatuses.includes(status)); + + const allHitPointsMarked = + this.resources.hitPoints.max > 0 && this.resources.hitPoints.value >= this.resources.hitPoints.max; + return deathMoveNotResolved && allHitPointsMarked; } get armorApplicableDamageTypes() { @@ -626,8 +660,15 @@ export default class DhCharacter extends BaseDataActor { ? armor.system.baseThresholds.severe + this.levelData.level.current : this.levelData.level.current * 2 }; - this.resources.hope.max -= Object.keys(this.scars).length; + + const globalHopeMax = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxHope; + this.resources.hope.max = globalHopeMax - this.scars; this.resources.hitPoints.max += this.class.value?.system?.hitPoints ?? 0; + + /* Companion Related Data */ + this.companionData = { + levelupChoices: this.levelData.level.current - 1 + }; } prepareDerivedData() { @@ -683,6 +724,30 @@ export default class DhCharacter extends BaseDataActor { changes.system.experiences[experience].core = true; } } + + /* Scars can alter the amount of current hope */ + if (changes.system?.scars) { + const diff = this.system.scars - changes.system.scars; + const newHopeMax = this.system.resources.hope.max + diff; + const newHopeValue = Math.min(newHopeMax, this.system.resources.hope.value); + if (newHopeValue != this.system.resources.hope.value) { + if (!changes.system.resources) changes.system.resources = { hope: { value: 0 } }; + changes.system.resources.hope = { + ...changes.system.resources.hope, + value: changes.system.resources.hope.value + newHopeValue + }; + } + } + + /* Force companion data prep */ + if (this.companion) { + if ( + changes.system?.levelData?.level?.current !== undefined && + changes.system.levelData.level.current !== this._source.levelData.level.current + ) { + this.companion.update(this.companion.toObject(), { diff: false, recursive: false }); + } + } } async _preDelete() { @@ -698,4 +763,11 @@ export default class DhCharacter extends BaseDataActor { t => !!t ); } + + static migrateData(source) { + if (typeof source.scars === 'object') source.scars = 0; + if (source.resources?.hope?.max) source.scars = Math.max(6 - source.resources.hope.max, 0); + + return super.migrateData(source); + } } diff --git a/module/data/actor/companion.mjs b/module/data/actor/companion.mjs index fa1965bd..1c25b48c 100644 --- a/module/data/actor/companion.mjs +++ b/module/data/actor/companion.mjs @@ -108,7 +108,11 @@ export default class DhCompanion extends BaseDataActor { get proficiency() { return this.partner?.system?.proficiency ?? 1; } - + + get canLevelUp() { + return this.levelupChoicesLeft > 0; + } + isItemValid() { return false; } @@ -127,7 +131,7 @@ export default class DhCompanion extends BaseDataActor { if (selection.data[0] === 'damage') { this.attack.damage.parts[0].value.dice = adjustDice(this.attack.damage.parts[0].value.dice); } else { - this.attack.range = adjustRange(this.attack.range); + this.attack.range = adjustRange(this.attack.range).id; } break; case 'stress': @@ -147,6 +151,17 @@ export default class DhCompanion extends BaseDataActor { } } + prepareDerivedData() { + /* Partner Related Setup */ + if (this.partner) { + this.levelData.level.changed = this.partner.system.levelData.level.current; + this.levelupChoicesLeft = Object.values(this.levelData.levelups).reduce((acc, curr) => { + acc = Math.max(acc - curr.selections.length, 0); + return acc; + }, this.partner.system.companionData.levelupChoices); + } + } + async _preUpdate(changes, options, userId) { const allowed = await super._preUpdate(changes, options, userId); if (allowed === false) return; @@ -162,6 +177,16 @@ export default class DhCompanion extends BaseDataActor { changes.system.experiences[experience].core = true; } } + + /* Force partner data prep */ + if (this.partner) { + if ( + changes.system?.levelData?.level?.current !== undefined && + changes.system.levelData.level.current !== this._source.levelData.level.current + ) { + this.partner.update(this.partner.toObject(), { diff: false, recursive: false }); + } + } } async _preDelete() { diff --git a/module/data/chat-message/_modules.mjs b/module/data/chat-message/_modules.mjs index ec095aac..c671de31 100644 --- a/module/data/chat-message/_modules.mjs +++ b/module/data/chat-message/_modules.mjs @@ -8,6 +8,7 @@ export const config = { adversaryRoll: DHActorRoll, damageRoll: DHActorRoll, dualityRoll: DHActorRoll, + fateRoll: DHActorRoll, groupRoll: DHGroupRoll, systemMessage: DHSystemMessage }; diff --git a/module/data/companionLevelup.mjs b/module/data/companionLevelup.mjs new file mode 100644 index 00000000..7ab61210 --- /dev/null +++ b/module/data/companionLevelup.mjs @@ -0,0 +1,370 @@ +import { abilities } from '../config/actorConfig.mjs'; +import { chunkify } from '../helpers/utils.mjs'; +import { LevelOptionType } from './levelTier.mjs'; + +export class DhCompanionLevelup extends foundry.abstract.DataModel { + static initializeData(levelTierData, pcLevelData, origChoicesLeft) { + let choicesLeft = origChoicesLeft; + + const { current, changed } = pcLevelData.level; + const bonusChoicesOnly = current === changed; + const startLevel = bonusChoicesOnly ? current : current + 1; + const endLevel = bonusChoicesOnly ? startLevel : changed; + + const tiers = {}; + const levels = {}; + const tierKeys = Object.keys(levelTierData.tiers); + tierKeys.forEach(key => { + const tier = levelTierData.tiers[key]; + const belongingLevels = []; + for (var i = tier.levels.start; i <= tier.levels.end; i++) { + if (i <= endLevel) { + const initialAchievements = i === tier.levels.start ? tier.initialAchievements : {}; + const experiences = initialAchievements.experience + ? [...Array(initialAchievements.experience.nr).keys()].reduce((acc, _) => { + acc[foundry.utils.randomID()] = { + name: '', + modifier: initialAchievements.experience.modifier + }; + return acc; + }, {}) + : {}; + + const currentChoices = pcLevelData.levelups[i]?.selections?.length; + const maxSelections = + i === endLevel + ? choicesLeft + (currentChoices ?? 0) + : (currentChoices ?? tier.maxSelections[i]); + if (!pcLevelData.levelups[i]) choicesLeft -= maxSelections; + + levels[i] = DhLevelupLevel.initializeData(pcLevelData.levelups[i], maxSelections, { + ...initialAchievements, + experiences, + domainCards: {} + }); + } + + belongingLevels.push(i); + } + + /* Improve. Temporary handling for Companion new experiences */ + Object.keys(tier.extraAchievements ?? {}).forEach(key => { + const level = Number(key); + if (level >= startLevel && level <= endLevel) { + const levelExtras = tier.extraAchievements[level]; + if (levelExtras.experience) { + levels[level].achievements.experiences[foundry.utils.randomID()] = { + name: '', + modifier: levelExtras.experience.modifier + }; + } + } + }); + + tiers[key] = { + name: tier.name, + belongingLevels: belongingLevels, + options: Object.keys(tier.options).reduce((acc, key) => { + acc[key] = tier.options[key].toObject?.() ?? tier.options[key]; + return acc; + }, {}) + }; + }); + + return { + tiers, + levels, + startLevel, + currentLevel: startLevel, + endLevel + }; + } + + static defineSchema() { + const fields = foundry.data.fields; + + return { + tiers: new fields.TypedObjectField( + new fields.SchemaField({ + name: new fields.StringField({ required: true }), + belongingLevels: new fields.ArrayField(new fields.NumberField({ required: true, integer: true })), + options: new fields.TypedObjectField( + new fields.SchemaField({ + label: new fields.StringField({ required: true }), + checkboxSelections: new fields.NumberField({ required: true, integer: true }), + minCost: new fields.NumberField({ required: true, integer: true }), + type: new fields.StringField({ required: true, choices: LevelOptionType }), + value: new fields.NumberField({ integer: true }), + amount: new fields.NumberField({ integer: true }) + }) + ) + }) + ), + levels: new fields.TypedObjectField(new fields.EmbeddedDataField(DhLevelupLevel)), + startLevel: new fields.NumberField({ required: true, integer: true }), + currentLevel: new fields.NumberField({ required: true, integer: true }), + endLevel: new fields.NumberField({ required: true, integer: true }) + }; + } + + #levelFinished(levelKey) { + const allSelectionsMade = this.levels[levelKey].nrSelections.available === 0; + const allChoicesMade = Object.keys(this.levels[levelKey].choices).every(choiceKey => { + const choice = this.levels[levelKey].choices[choiceKey]; + return Object.values(choice).every(checkbox => { + switch (choiceKey) { + case 'trait': + case 'experience': + case 'domainCard': + case 'subclass': + case 'vicious': + return checkbox.data.length === (checkbox.amount ?? 1); + case 'multiclass': + const classSelected = checkbox.data.length === 1; + const domainSelected = checkbox.secondaryData.domain; + const subclassSelected = checkbox.secondaryData.subclass; + return classSelected && domainSelected && subclassSelected; + default: + return true; + } + }); + }); + const experiencesSelected = !this.levels[levelKey].achievements.experiences + ? true + : Object.values(this.levels[levelKey].achievements.experiences).every(exp => exp.name); + const domainCardsSelected = Object.values(this.levels[levelKey].achievements.domainCards) + .filter(x => x.level <= this.endLevel) + .every(card => card.uuid); + const allAchievementsSelected = experiencesSelected && domainCardsSelected; + + return allSelectionsMade && allChoicesMade && allAchievementsSelected; + } + + get currentLevelFinished() { + return this.#levelFinished(this.currentLevel); + } + + get allLevelsFinished() { + return Object.keys(this.levels) + .filter(level => Number(level) >= this.startLevel) + .every(this.#levelFinished.bind(this)); + } + + get unmarkedTraits() { + const possibleLevels = Object.values(this.tiers).reduce((acc, tier) => { + if (tier.belongingLevels.includes(this.currentLevel)) acc = tier.belongingLevels; + return acc; + }, []); + + return Object.keys(this.levels) + .filter(key => possibleLevels.some(x => x === Number(key))) + .reduce( + (acc, levelKey) => { + const level = this.levels[levelKey]; + Object.values(level.choices).forEach(choice => + Object.values(choice).forEach(checkbox => { + if ( + checkbox.type === 'trait' && + checkbox.data.length > 0 && + Number(levelKey) !== this.currentLevel + ) { + checkbox.data.forEach(data => delete acc[data]); + } + }) + ); + + return acc; + }, + { ...abilities } + ); + } + + get classUpgradeChoices() { + let subclasses = []; + let multiclass = null; + Object.keys(this.levels).forEach(levelKey => { + const level = this.levels[levelKey]; + Object.values(level.choices).forEach(choice => { + Object.values(choice).forEach(checkbox => { + if (checkbox.type === 'multiclass') { + multiclass = { + class: checkbox.data.length > 0 ? checkbox.data[0] : null, + domain: checkbox.secondaryData.domain ?? null, + subclass: checkbox.secondaryData.subclass ?? null, + tier: checkbox.tier, + level: levelKey + }; + } + if (checkbox.type === 'subclass') { + subclasses.push({ + tier: checkbox.tier, + level: levelKey + }); + } + }); + }); + }); + return { subclasses, multiclass }; + } + + get tiersForRendering() { + const tierKeys = Object.keys(this.tiers); + const selections = Object.keys(this.levels).reduce( + (acc, key) => { + const level = this.levels[key]; + Object.keys(level.choices).forEach(optionKey => { + const choice = level.choices[optionKey]; + Object.keys(choice).forEach(checkboxNr => { + const checkbox = choice[checkboxNr]; + if (!acc[checkbox.tier][optionKey]) acc[checkbox.tier][optionKey] = {}; + Object.keys(choice).forEach(checkboxNr => { + acc[checkbox.tier][optionKey][checkboxNr] = { ...checkbox, level: Number(key) }; + }); + }); + }); + + return acc; + }, + tierKeys.reduce((acc, key) => { + acc[key] = {}; + return acc; + }, {}) + ); + + const { multiclass, subclasses } = this.classUpgradeChoices; + return tierKeys.map((tierKey, tierIndex) => { + const tier = this.tiers[tierKey]; + const multiclassInTier = multiclass?.tier === Number(tierKey); + const subclassInTier = subclasses.some(x => x.tier === Number(tierKey)); + + return { + name: game.i18n.localize(tier.name), + active: this.currentLevel >= Math.min(...tier.belongingLevels), + groups: Object.keys(tier.options).map(optionKey => { + const option = tier.options[optionKey]; + + const checkboxes = [...Array(option.checkboxSelections).keys()].flatMap(index => { + const checkboxNr = index + 1; + const checkboxData = selections[tierKey]?.[optionKey]?.[checkboxNr]; + const checkbox = { ...option, checkboxNr, tier: tierKey }; + + if (checkboxData) { + checkbox.level = checkboxData.level; + checkbox.selected = true; + checkbox.disabled = checkbox.level !== this.currentLevel; + } + + if (optionKey === 'multiclass') { + if ((multiclass && !multiclassInTier) || subclassInTier) { + checkbox.disabled = true; + } + } + + if (optionKey === 'subclass' && multiclassInTier) { + checkbox.disabled = true; + } + + return checkbox; + }); + + let label = game.i18n.localize(option.label); + if (optionKey === 'domainCard') { + const maxLevel = tier.belongingLevels[tier.belongingLevels.length - 1]; + label = game.i18n.format(option.label, { maxLevel }); + } + + return { + label: label, + checkboxGroups: chunkify(checkboxes, option.minCost, chunkedBoxes => { + const anySelected = chunkedBoxes.some(x => x.selected); + const anyDisabled = chunkedBoxes.some(x => x.disabled); + return { + multi: option.minCost > 1, + checkboxes: chunkedBoxes.map(x => ({ + ...x, + selected: anySelected, + disabled: anyDisabled + })) + }; + }) + }; + }) + }; + }); + } +} + +export class DhLevelupLevel extends foundry.abstract.DataModel { + static initializeData(levelData = { selections: [] }, maxSelections, achievements) { + return { + maxSelections: maxSelections, + achievements: { + experiences: levelData.achievements?.experiences ?? achievements.experiences ?? {}, + domainCards: levelData.achievements?.domainCards + ? levelData.achievements.domainCards.reduce((acc, card, index) => { + acc[index] = { ...card }; + return acc; + }, {}) + : (achievements.domainCards ?? {}), + proficiency: levelData.achievements?.proficiency ?? achievements.proficiency ?? null + }, + choices: levelData.selections.reduce((acc, data) => { + if (!acc[data.optionKey]) acc[data.optionKey] = {}; + acc[data.optionKey][data.checkboxNr] = { ...data }; + + return acc; + }, {}) + }; + } + + static defineSchema() { + const fields = foundry.data.fields; + + return { + maxSelections: new fields.NumberField({ required: true, integer: true }), + achievements: new fields.SchemaField({ + experiences: new fields.TypedObjectField( + new fields.SchemaField({ + name: new fields.StringField({ required: true }), + modifier: new fields.NumberField({ required: true, integer: true }) + }) + ), + domainCards: new fields.TypedObjectField( + new fields.SchemaField({ + uuid: new fields.StringField({ required: true, nullable: true, initial: null }), + itemUuid: new fields.StringField({ required: true }), + level: new fields.NumberField({ required: true, integer: true }) + }) + ), + proficiency: new fields.NumberField({ integer: true }) + }), + choices: new fields.TypedObjectField( + new fields.TypedObjectField( + new fields.SchemaField({ + tier: new fields.NumberField({ required: true, integer: true }), + minCost: new fields.NumberField({ required: true, integer: true }), + amount: new fields.NumberField({ integer: true }), + value: new fields.StringField(), + data: new fields.ArrayField(new fields.StringField()), + secondaryData: new fields.TypedObjectField(new fields.StringField()), + type: new fields.StringField({ required: true }) + }) + ) + ) + }; + } + + get nrSelections() { + const selections = Object.keys(this.choices).reduce((acc, choiceKey) => { + const choice = this.choices[choiceKey]; + acc += Object.values(choice).reduce((acc, x) => acc + x.minCost, 0); + + return acc; + }, 0); + + return { + selections: selections, + available: this.maxSelections - selections + }; + } +} diff --git a/module/data/fields/action/_module.mjs b/module/data/fields/action/_module.mjs index ef69394a..0bdffca2 100644 --- a/module/data/fields/action/_module.mjs +++ b/module/data/fields/action/_module.mjs @@ -9,3 +9,4 @@ export { default as BeastformField } from './beastformField.mjs'; export { default as DamageField } from './damageField.mjs'; export { default as RollField } from './rollField.mjs'; export { default as MacroField } from './macroField.mjs'; +export { default as SummonField } from './summonField.mjs'; diff --git a/module/data/fields/action/damageField.mjs b/module/data/fields/action/damageField.mjs index bb81c702..ef91c64e 100644 --- a/module/data/fields/action/damageField.mjs +++ b/module/data/fields/action/damageField.mjs @@ -105,12 +105,22 @@ export default class DamageField extends fields.SchemaField { damagePromises.push( actor.takeHealing(config.damage).then(updates => targetDamage.push({ token, updates })) ); - else + else { + const configDamage = foundry.utils.deepClone(config.damage); + const hpDamageMultiplier = config.actionActor?.system.rules.attack.damage.hpDamageMultiplier ?? 1; + const hpDamageTakenMultiplier = actor.system.rules.attack.damage.hpDamageTakenMultiplier; + if (configDamage.hitPoints) { + for (const part of configDamage.hitPoints.parts) { + part.total = Math.ceil(part.total * hpDamageMultiplier * hpDamageTakenMultiplier); + } + } + damagePromises.push( actor - .takeDamage(config.damage, config.isDirect) + .takeDamage(configDamage, config.isDirect) .then(updates => targetDamage.push({ token, updates })) ); + } } Promise.all(damagePromises).then(async _ => { diff --git a/module/data/fields/action/rollField.mjs b/module/data/fields/action/rollField.mjs index e2196c1c..63d48990 100644 --- a/module/data/fields/action/rollField.mjs +++ b/module/data/fields/action/rollField.mjs @@ -87,7 +87,7 @@ export class DHActionRollData extends foundry.abstract.DataModel { if (this.type === CONFIG.DH.GENERAL.rollTypes.attack.id) modifiers.push({ label: 'Bonus to Hit', - value: this.bonus ?? this.parent.actor.system.attack.roll.bonus + value: this.bonus ?? this.parent.actor.system.attack.roll.bonus ?? 0 }); break; default: diff --git a/module/data/fields/action/summonField.mjs b/module/data/fields/action/summonField.mjs new file mode 100644 index 00000000..dce6414c --- /dev/null +++ b/module/data/fields/action/summonField.mjs @@ -0,0 +1,89 @@ +import FormulaField from '../formulaField.mjs'; + +const fields = foundry.data.fields; + +export default class DHSummonField extends fields.ArrayField { + /** + * Action Workflow order + */ + static order = 120; + + constructor(options = {}, context = {}) { + const summonFields = new fields.SchemaField({ + actorUUID: new fields.DocumentUUIDField({ + type: 'Actor', + required: true + }), + count: new FormulaField({ + required: true, + default: '1' + }) + }); + super(summonFields, options, context); + } + + static async execute() { + if (!canvas.scene) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.summon.error')); + return; + } + + if (this.summon.length === 0) { + ui.notifications.warn('No actors configured for this Summon action.'); + return; + } + + const rolls = []; + const summonData = []; + for (const summon of this.summon) { + let count = summon.count; + const roll = new Roll(summon.count); + if (!roll.isDeterministic) { + await roll.evaluate(); + if (game.modules.get('dice-so-nice')?.active) rolls.push(roll); + count = roll.total; + } + + const actor = DHSummonField.getWorldActor(await foundry.utils.fromUuid(summon.actorUUID)); + /* Extending summon data in memory so it's available in actionField.toChat. Think it's harmless, but ugly. Could maybe find a better way. */ + summon.rolledCount = count; + summon.actor = actor.toObject(); + + summonData.push({ actor, count: count }); + } + + if (rolls.length) await Promise.all(rolls.map(roll => game.dice3d.showForRoll(roll, game.user, true))); + + this.actor.sheet?.minimize(); + DHSummonField.handleSummon(summonData, this.actor); + } + + /* Check for any available instances of the actor present in the world if we're missing artwork in the compendium */ + static getWorldActor(baseActor) { + const dataType = game.system.api.data.actors[`Dh${baseActor.type.capitalize()}`]; + if (baseActor.inCompendium && dataType && baseActor.img === dataType.DEFAULT_ICON) { + const worldActorCopy = game.actors.find(x => x.name === baseActor.name); + return worldActorCopy ?? baseActor; + } + + return baseActor; + } + + static async handleSummon(summonData, actionActor, summonIndex = 0) { + const summon = summonData[summonIndex]; + const result = await CONFIG.ux.TokenManager.createPreviewAsync(summon.actor, { + name: `${summon.actor.prototypeToken.name}${summon.count > 1 ? ` (${summon.count}x)` : ''}` + }); + + if (!result) return actionActor.sheet?.maximize(); + summon.actor = result.actor; + + summon.count--; + if (summon.count <= 0) { + summonIndex++; + if (summonIndex === summonData.length) return actionActor.sheet?.maximize(); + } + + DHSummonField.handleSummon(summonData, actionActor, summonIndex); + } +} diff --git a/module/data/fields/actionField.mjs b/module/data/fields/actionField.mjs index d0d04721..0d71ab86 100644 --- a/module/data/fields/actionField.mjs +++ b/module/data/fields/actionField.mjs @@ -267,7 +267,8 @@ export function ActionMixin(Base) { action: { name: this.name, img: this.baseAction ? this.parent.parent.img : this.img, - tags: this.tags ? this.tags : ['Spell', 'Arcana', 'Lv 10'] + tags: this.tags ? this.tags : ['Spell', 'Arcana', 'Lv 10'], + summon: this.summon }, itemOrigin: this.item, description: this.description || (this.item instanceof Item ? this.item.system.description : '') diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index 415fc8d4..84f39103 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -147,7 +147,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { return await foundry.applications.ux.TextEditor.implementation.enrichHTML(fullDescription, { relativeTo: this, rollData: this.getRollData(), - secrets: this.isOwner + secrets: this.parent.isOwner }); } @@ -164,26 +164,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { prepareBaseData() { super.prepareBaseData(); - - for (const action of this.actions ?? []) { - if (!action.actor) continue; - - const actionsToRegister = []; - for (let i = 0; i < action.triggers.length; i++) { - const trigger = action.triggers[i]; - const { args } = CONFIG.DH.TRIGGER.triggers[trigger.trigger]; - const fn = new foundry.utils.AsyncFunction(...args, `{${trigger.command}\n}`); - actionsToRegister.push(fn.bind(action)); - if (i === action.triggers.length - 1) - game.system.registeredTriggers.registerTriggers( - trigger.trigger, - action.actor?.uuid, - trigger.triggeringActorType, - this.parent.uuid, - actionsToRegister - ); - } - } + game.system.registeredTriggers.registerItemTriggers(this.parent); } async _preCreate(data, options, user) { @@ -246,6 +227,28 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { const armorData = getScrollTextData(this.parent.parent.system.resources, changed.system.marks, 'armor'); options.scrollingTextData = [armorData]; } + + if (changed.system?.actions) { + const triggersToRemove = Object.keys(changed.system.actions).reduce((acc, key) => { + if (!changed.system.actions[key]) { + const strippedKey = key.replace('-=', ''); + acc.push(...this.actions.get(strippedKey).triggers.map(x => x.trigger)); + } + + return acc; + }, []); + + game.system.registeredTriggers.unregisterTriggers(triggersToRemove, this.parent.uuid); + + if (this.parent.parent && !(this.parent.parent.token instanceof game.system.api.documents.DhToken)) { + for (const token of this.parent.parent.getActiveTokens()) { + game.system.registeredTriggers.unregisterTriggers( + triggersToRemove, + `${token.document.uuid}.${this.parent.uuid}` + ); + } + } + } } _onUpdate(changed, options, userId) { diff --git a/module/data/item/beastform.mjs b/module/data/item/beastform.mjs index 1840e26a..dd491169 100644 --- a/module/data/item/beastform.mjs +++ b/module/data/item/beastform.mjs @@ -49,6 +49,7 @@ export default class DHBeastform extends BaseDataItem { choices: CONFIG.DH.ACTOR.tokenSize, initial: CONFIG.DH.ACTOR.tokenSize.custom.id }), + scale: new fields.NumberField({ nullable: false, min: 0.2, max: 3, step: 0.05, initial: 1 }), height: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }), width: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }) }), @@ -184,6 +185,7 @@ export default class DHBeastform extends BaseDataItem { tokenImg: this.parent.parent.prototypeToken.texture.src, tokenRingImg: this.parent.parent.prototypeToken.ring.subject.texture, tokenSize: { + scale: this.parent.parent.prototypeToken.texture.scaleX, height: this.parent.parent.prototypeToken.height, width: this.parent.parent.prototypeToken.width } @@ -209,7 +211,9 @@ export default class DHBeastform extends BaseDataItem { height, width, texture: { - src: this.tokenImg + src: this.tokenImg, + scaleX: this.tokenSize.scale, + scaleY: this.tokenSize.scale }, ring: { subject: { diff --git a/module/data/item/domainCard.mjs b/module/data/item/domainCard.mjs index 571d682c..2c272f75 100644 --- a/module/data/item/domainCard.mjs +++ b/module/data/item/domainCard.mjs @@ -29,7 +29,21 @@ export default class DHDomainCard extends BaseDataItem { required: true, initial: CONFIG.DH.DOMAIN.cardTypes.ability.id }), - inVault: new fields.BooleanField({ initial: false }) + inVault: new fields.BooleanField({ initial: false }), + vaultActive: new fields.BooleanField({ + required: true, + nullable: false, + initial: false + }), + loadoutIgnore: new fields.BooleanField({ + required: true, + nullable: false, + initial: false + }), + domainTouched: new fields.NumberField({ + nullable: true, + initial: null + }) }; } @@ -38,6 +52,19 @@ export default class DHDomainCard extends BaseDataItem { return game.i18n.localize(allDomainData[this.domain].label); } + get isVaultSupressed() { + return this.inVault && !this.vaultActive; + } + + get isDomainTouchedSuppressed() { + if (!this.parent.system.domainTouched || this.parent.parent?.type !== 'character') return false; + + const matchingDomainCards = this.parent.parent.items.filter( + item => !item.system.inVault && item.system.domain === this.parent.system.domain + ).length; + return matchingDomainCards < this.parent.system.domainTouched; + } + /* -------------------------------------------- */ /**@override */ diff --git a/module/data/registeredTriggers.mjs b/module/data/registeredTriggers.mjs new file mode 100644 index 00000000..ee4f3b49 --- /dev/null +++ b/module/data/registeredTriggers.mjs @@ -0,0 +1,167 @@ +export default class RegisteredTriggers extends Map { + constructor() { + super(); + } + + registerTriggers(triggers, actor, uuid) { + for (const triggerKey of Object.keys(CONFIG.DH.TRIGGER.triggers)) { + const match = triggers[triggerKey]; + const existingTrigger = this.get(triggerKey); + + if (!match) { + if (existingTrigger?.get(uuid)) this.get(triggerKey).delete(uuid); + } else { + const { trigger, triggeringActorType, commands } = match; + + if (!existingTrigger) this.set(trigger, new Map()); + this.get(trigger).set(uuid, { actor, triggeringActorType, commands }); + } + } + } + + registerItemTriggers(item, registerOverride) { + if (!item.actor || !item._stats.createdTime) return; + for (const action of item.system.actions ?? []) { + if (!action.actor) continue; + + /* Non actor-linked should only prep synthetic actors so they're not registering triggers unless they're on the canvas */ + if ( + !registerOverride && + !action.actor.prototypeToken.actorLink && + (!(action.actor.parent instanceof game.system.api.documents.DhToken) || !action.actor.parent?.uuid) + ) + continue; + + const triggers = {}; + for (const trigger of action.triggers) { + const { args } = CONFIG.DH.TRIGGER.triggers[trigger.trigger]; + const fn = new foundry.utils.AsyncFunction(...args, `{${trigger.command}\n}`); + + if (!triggers[trigger.trigger]) + triggers[trigger.trigger] = { + trigger: trigger.trigger, + triggeringActorType: trigger.triggeringActorType, + commands: [] + }; + triggers[trigger.trigger].commands.push(fn.bind(action)); + } + + this.registerTriggers(triggers, action.actor?.uuid, item.uuid); + } + } + + unregisterTriggers(triggerKeys, uuid) { + for (const triggerKey of triggerKeys) { + const existingTrigger = this.get(triggerKey); + if (!existingTrigger) return; + + existingTrigger.delete(uuid); + } + } + + unregisterItemTriggers(items) { + for (const item of items) { + if (!item.system.actions?.size) continue; + + const triggers = (item.system.actions ?? []).reduce((acc, action) => { + acc.push(...action.triggers.map(x => x.trigger)); + return acc; + }, []); + + this.unregisterTriggers(triggers, item.uuid); + } + } + + unregisterSceneEnvironmentTriggers(flagSystemData) { + const sceneData = new game.system.api.data.scenes.DHScene(flagSystemData); + for (const environment of sceneData.sceneEnvironments) { + if (environment.pack) continue; + this.unregisterItemTriggers(environment.system.features); + } + } + + unregisterSceneTriggers(scene) { + this.unregisterSceneEnvironmentTriggers(scene.flags.daggerheart); + + for (const triggerKey of Object.keys(CONFIG.DH.TRIGGER.triggers)) { + const existingTrigger = this.get(triggerKey); + if (!existingTrigger) continue; + + const filtered = new Map(); + for (const [uuid, data] of existingTrigger.entries()) { + if (!uuid.startsWith(scene.uuid)) filtered.set(uuid, data); + } + this.set(triggerKey, filtered); + } + } + + registerSceneEnvironmentTriggers(flagSystemData) { + const sceneData = new game.system.api.data.scenes.DHScene(flagSystemData); + for (const environment of sceneData.sceneEnvironments) { + for (const feature of environment.system.features) { + if (feature) this.registerItemTriggers(feature, true); + } + } + } + + registerSceneTriggers(scene) { + this.registerSceneEnvironmentTriggers(scene.flags.daggerheart); + + for (const actor of scene.tokens.filter(x => x.actor).map(x => x.actor)) { + if (actor.prototypeToken.actorLink) continue; + + for (const item of actor.items) { + this.registerItemTriggers(item); + } + } + } + + async runTrigger(trigger, currentActor, ...args) { + const updates = []; + const triggerSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).triggers; + if (!triggerSettings.enabled) return updates; + + const dualityTrigger = this.get(trigger); + if (dualityTrigger?.size) { + const triggerActors = ['character', 'adversary', 'environment']; + for (let [itemUuid, { actor: actorUuid, triggeringActorType, commands }] of dualityTrigger.entries()) { + const actor = await foundry.utils.fromUuid(actorUuid); + if (!actor || !triggerActors.includes(actor.type)) continue; + + const triggerData = CONFIG.DH.TRIGGER.triggers[trigger]; + if (triggerData.usesActor && triggeringActorType !== 'any') { + if (triggeringActorType === 'self' && currentActor?.uuid !== actorUuid) continue; + else if (triggeringActorType === 'other' && currentActor?.uuid === actorUuid) continue; + } + + for (const command of commands) { + try { + if (CONFIG.debug.triggers) { + const item = await foundry.utils.fromUuid(itemUuid); + console.log( + game.i18n.format('DAGGERHEART.UI.ConsoleLogs.triggerRun', { + actor: actor.name ?? '', + item: item?.name ?? '', + 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; + } +} diff --git a/module/data/rollTable.mjs b/module/data/rollTable.mjs new file mode 100644 index 00000000..78f7e6dd --- /dev/null +++ b/module/data/rollTable.mjs @@ -0,0 +1,38 @@ +import FormulaField from './fields/formulaField.mjs'; + +//Extra definitions for RollTable +export default class DhRollTable extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + formulaName: new fields.StringField({ + required: true, + nullable: false, + initial: 'Roll Formula', + label: 'DAGGERHEART.ROLLTABLES.FIELDS.formulaName.label' + }), + altFormula: new fields.TypedObjectField( + new fields.SchemaField({ + name: new fields.StringField({ + required: true, + nullable: false, + initial: 'Roll Formula', + label: 'DAGGERHEART.ROLLTABLES.FIELDS.formulaName.label' + }), + formula: new FormulaField({ label: 'Formula Roll', initial: '1d20' }) + }) + ), + activeAltFormula: new fields.StringField({ nullable: true, initial: null }) + }; + } + + getActiveFormula(baseFormula) { + return this.activeAltFormula ? (this.altFormula[this.activeAltFormula]?.formula ?? baseFormula) : baseFormula; + } + + static getDefaultFormula = () => ({ + name: game.i18n.localize('Roll Formula'), + formula: '1d20' + }); +} diff --git a/module/data/settings/Automation.mjs b/module/data/settings/Automation.mjs index 3376b153..436f0eb7 100644 --- a/module/data/settings/Automation.mjs +++ b/module/data/settings/Automation.mjs @@ -55,15 +55,10 @@ export default class DhAutomation extends foundry.abstract.DataModel { initial: true, label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.resourceScrollTexts.label' }), - playerCanEditSheet: new fields.BooleanField({ - required: true, - initial: false, - label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.playerCanEditSheet.label' - }), defeated: new fields.SchemaField({ enabled: new fields.BooleanField({ required: true, - initial: false, + initial: true, label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.enabled.label' }), overlay: new fields.BooleanField({ @@ -74,7 +69,7 @@ export default class DhAutomation extends foundry.abstract.DataModel { characterDefault: new fields.StringField({ required: true, choices: CONFIG.DH.GENERAL.defeatedConditionChoices, - initial: CONFIG.DH.GENERAL.defeatedConditionChoices.unconscious.id, + initial: CONFIG.DH.GENERAL.defeatedConditionChoices.deathMove.id, label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.characterDefault.label' }), adversaryDefault: new fields.StringField({ @@ -89,23 +84,29 @@ export default class DhAutomation extends foundry.abstract.DataModel { initial: CONFIG.DH.GENERAL.defeatedConditionChoices.defeated.id, label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.companionDefault.label' }), + deathMoveIcon: new fields.FilePathField({ + initial: 'icons/magic/life/heart-cross-purple-orange.webp', + categories: ['IMAGE'], + base64: false, + label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.deathMove.label' + }), deadIcon: new fields.FilePathField({ initial: 'icons/magic/death/grave-tombstone-glow-teal.webp', categories: ['IMAGE'], base64: false, - label: 'Dead' + label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.dead.label' }), defeatedIcon: new fields.FilePathField({ initial: 'icons/magic/control/fear-fright-mask-orange.webp', categories: ['IMAGE'], base64: false, - label: 'Defeated' + label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.defeated.label' }), unconsciousIcon: new fields.FilePathField({ initial: 'icons/magic/control/sleep-bubble-purple.webp', categories: ['IMAGE'], base64: false, - label: 'Unconcious' + label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.unconscious.label' }) }), roll: new fields.SchemaField({ diff --git a/module/data/settings/Homebrew.mjs b/module/data/settings/Homebrew.mjs index 7572c9ea..0138713c 100644 --- a/module/data/settings/Homebrew.mjs +++ b/module/data/settings/Homebrew.mjs @@ -23,6 +23,13 @@ export default class DhHomebrew extends foundry.abstract.DataModel { initial: 12, label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxFear.label' }), + maxHope: new fields.NumberField({ + required: true, + integer: true, + min: 0, + initial: 6, + label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxHope.label' + }), maxLoadout: new fields.NumberField({ required: true, integer: true, diff --git a/module/dice/_module.mjs b/module/dice/_module.mjs index e6755a74..b9339d87 100644 --- a/module/dice/_module.mjs +++ b/module/dice/_module.mjs @@ -3,3 +3,4 @@ export { default as D20Roll } from './d20Roll.mjs'; export { default as DamageRoll } from './damageRoll.mjs'; export { default as DHRoll } from './dhRoll.mjs'; export { default as DualityRoll } from './dualityRoll.mjs'; +export { default as FateRoll } from './fateRoll.mjs'; diff --git a/module/dice/d20Roll.mjs b/module/dice/d20Roll.mjs index 0256f281..f117ff65 100644 --- a/module/dice/d20Roll.mjs +++ b/module/dice/d20Roll.mjs @@ -35,7 +35,9 @@ export default class D20Roll extends DHRoll { get isCritical() { if (!this.d20._evaluated) return; - return this.d20.total >= this.data.system.criticalThreshold; + + const criticalThreshold = this.options.actionType === 'reaction' ? 20 : this.data.system.criticalThreshold; + return this.d20.total >= criticalThreshold; } get hasAdvantage() { @@ -97,11 +99,14 @@ export default class D20Roll extends DHRoll { this.options.roll.modifiers = this.applyBaseBonus(); + const actorExperiences = this.options.roll.companionRoll + ? (this.options.data?.companion?.system.experiences ?? {}) + : (this.options.data.system?.experiences ?? {}); this.options.experiences?.forEach(m => { - if (this.options.data.system?.experiences?.[m]) + if (actorExperiences[m]) this.options.roll.modifiers.push({ - label: this.options.data.system.experiences[m].name, - value: this.options.data.system.experiences[m].value + label: actorExperiences[m].name, + value: actorExperiences[m].value }); }); @@ -127,15 +132,55 @@ export default class D20Roll extends DHRoll { const modifiers = foundry.utils.deepClone(this.options.roll.baseModifiers) ?? []; modifiers.push( - ...this.getBonus(`roll.${this.options.actionType}`, `${this.options.actionType?.capitalize()} Bonus`) - ); - modifiers.push( - ...this.getBonus(`roll.${this.options.roll.type}`, `${this.options.roll.type?.capitalize()} Bonus`) + ...this.getBonus( + `system.bonuses.roll.${this.options.actionType}`, + `${this.options.actionType?.capitalize()} Bonus` + ) ); + if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.attack.id) { + modifiers.push( + ...this.getBonus( + `system.bonuses.roll.${this.options.roll.type}`, + `${this.options.roll.type?.capitalize()} Bonus` + ) + ); + } + + if ( + this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.attack.id || + (this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id && this.options.hasDamage) + ) { + modifiers.push( + ...this.getBonus(`system.bonuses.roll.attack`, `${this.options.roll.type?.capitalize()} Bonus`) + ); + } + return modifiers; } + getActionChangeKeys() { + const changeKeys = new Set([`system.bonuses.roll.${this.options.actionType}`]); + + if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.attack.id) { + changeKeys.add(`system.bonuses.roll.${this.options.roll.type}`); + } + + if ( + this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.attack.id || + (this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id && this.options.hasDamage) + ) { + changeKeys.add(`system.bonuses.roll.attack`); + } + + if (this.options.roll.trait && this.data.traits?.[this.options.roll.trait]) { + if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.spellcast.id) + changeKeys.add('system.bonuses.roll.trait'); + } + + return changeKeys; + } + static postEvaluate(roll, config = {}) { const data = super.postEvaluate(roll, config); data.type = config.actionType; diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index c10ee6ff..cd26eb21 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -93,7 +93,6 @@ export default class DamageRoll extends DHRoll { type = this.options.messageType ?? (this.options.hasHealing ? 'healing' : 'damage'), options = part ?? this.options; - modifiers.push(...this.getBonus(`${type}`, `${type.capitalize()} Bonus`)); if (!this.options.hasHealing) { options.damageTypes?.forEach(t => { modifiers.push(...this.getBonus(`${type}.${t}`, `${t.capitalize()} ${type.capitalize()} Bonus`)); @@ -108,6 +107,31 @@ export default class DamageRoll extends DHRoll { return modifiers; } + getActionChangeKeys() { + const type = this.options.messageType ?? (this.options.hasHealing ? 'healing' : 'damage'); + const changeKeys = []; + + for (const roll of this.options.roll) { + for (const damageType of roll.damageTypes?.values?.() ?? []) { + changeKeys.push(`system.bonuses.${type}.${damageType}`); + } + } + + const item = this.data.parent?.items?.get(this.options.source.item); + if (item) { + switch (item.type) { + case 'weapon': + if (!this.options.hasHealing) + ['primaryWeapon', 'secondaryWeapon'].forEach(w => + changeKeys.push(`system.bonuses.damage.${w}`) + ); + break; + } + } + + return changeKeys; + } + constructFormula(config) { this.options.roll.forEach((part, index) => { part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data)); diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index ea24f238..1977c7ea 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -3,7 +3,8 @@ import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; export default class DHRoll extends Roll { baseTerms = []; constructor(formula, data = {}, options = {}) { - super(formula, data, options); + super(formula, data, foundry.utils.mergeObject(options, { roll: [] }, { overwrite: false })); + options.bonusEffects = this.bonusEffectBuilder(); if (!this.data || !Object.keys(this.data).length) this.data = options.data; } @@ -164,12 +165,18 @@ export default class DHRoll extends Roll { new foundry.dice.terms.OperatorTerm({ operator: '+' }), ...this.constructor.parse(modifier.join(' + '), this.options.data) ]; - } else { + } else if (Number.isNumeric(modifier)) { const numTerm = modifier < 0 ? '-' : '+'; return [ new foundry.dice.terms.OperatorTerm({ operator: numTerm }), new foundry.dice.terms.NumericTerm({ number: Math.abs(modifier) }) ]; + } else { + const numTerm = modifier < 0 ? '-' : '+'; + return [ + new foundry.dice.terms.OperatorTerm({ operator: numTerm }), + ...this.constructor.parse(modifier, this.options.data) + ]; } } @@ -185,18 +192,20 @@ export default class DHRoll extends Roll { } getBonus(path, label) { - const bonus = foundry.utils.getProperty(this.data.bonuses, path), - modifiers = []; - if (bonus?.bonus) - modifiers.push({ - label: label, - value: bonus?.bonus - }); - if (bonus?.dice?.length) - modifiers.push({ - label: label, - value: bonus?.dice - }); + const modifiers = []; + for (const effect of Object.values(this.options.bonusEffects)) { + if (!effect.selected) continue; + for (const change of effect.changes) { + if (!change.key.includes(path)) continue; + const changeValue = game.system.api.documents.DhActiveEffect.getChangeValue( + this.data, + change, + effect.origEffect + ); + modifiers.push({ label: label, value: changeValue }); + } + } + return modifiers; } @@ -235,4 +244,28 @@ export default class DHRoll extends Roll { static temporaryModifierBuilder(config) { return {}; } + + bonusEffectBuilder() { + const changeKeys = this.getActionChangeKeys(); + return ( + this.options.effects?.reduce((acc, effect) => { + if (effect.changes.some(x => changeKeys.some(key => x.key.includes(key)))) { + acc[effect.id] = { + id: effect.id, + name: effect.name, + description: effect.description, + changes: effect.changes, + origEffect: effect, + selected: !effect.disabled + }; + } + + return acc; + }, {}) ?? [] + ); + } + + getActionChangeKeys() { + return []; + } } diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index f15e0b09..e65d0ff5 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -12,6 +12,7 @@ export default class DualityRoll extends D20Roll { constructor(formula, data = {}, options = {}) { super(formula, data, options); this.rallyChoices = this.setRallyChoices(); + this.guaranteedCritical = options.guaranteedCritical; } static messageType = 'dualityRoll'; @@ -25,29 +26,23 @@ export default class DualityRoll extends D20Roll { } get dHope() { - // if ( !(this.terms[0] instanceof foundry.dice.terms.Die) ) return; if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); return this.dice[0]; - // return this.#hopeDice; } set dHope(faces) { if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); - this.terms[0].faces = this.getFaces(faces); - // this.#hopeDice = `d${face}`; + this.dice[0].faces = this.getFaces(faces); } get dFear() { - // if ( !(this.terms[1] instanceof foundry.dice.terms.Die) ) return; if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice(); return this.dice[1]; - // return this.#fearDice; } set dFear(faces) { if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice(); this.dice[1].faces = this.getFaces(faces); - // this.#fearDice = `d${face}`; } get dAdvantage() { @@ -90,26 +85,29 @@ export default class DualityRoll extends D20Roll { } get isCritical() { + if (this.guaranteedCritical) return true; if (!this.dHope._evaluated || !this.dFear._evaluated) return; return this.dHope.total === this.dFear.total; } get withHope() { - if (!this._evaluated) return; + if (!this._evaluated || this.guaranteedCritical) return; return this.dHope.total > this.dFear.total; } get withFear() { - if (!this._evaluated) return; + if (!this._evaluated || this.guaranteedCritical) return; return this.dHope.total < this.dFear.total; } get totalLabel() { - const label = this.withHope - ? 'DAGGERHEART.GENERAL.hope' - : this.withFear - ? 'DAGGERHEART.GENERAL.fear' - : 'DAGGERHEART.GENERAL.criticalSuccess'; + const label = this.guaranteedCritical + ? 'DAGGERHEART.GENERAL.guaranteedCriticalSuccess' + : this.isCritical + ? 'DAGGERHEART.GENERAL.criticalSuccess' + : this.withHope + ? 'DAGGERHEART.GENERAL.hope' + : 'DAGGERHEART.GENERAL.fear'; return game.i18n.localize(label); } @@ -130,9 +128,14 @@ export default class DualityRoll extends D20Roll { this.terms = [this.terms[0], this.terms[1], this.terms[2]]; return; } - this.terms[0] = new foundry.dice.terms.Die({ faces: 12 }); + + this.terms[0] = new foundry.dice.terms.Die({ + faces: this.data.rules.dualityRoll?.defaultHopeDice ?? 12 + }); this.terms[1] = new foundry.dice.terms.OperatorTerm({ operator: '+' }); - this.terms[2] = new foundry.dice.terms.Die({ faces: 12 }); + this.terms[2] = new foundry.dice.terms.Die({ + faces: this.data.rules.dualityRoll?.defaultFearDice ?? 12 + }); } applyAdvantage() { @@ -173,6 +176,49 @@ export default class DualityRoll extends D20Roll { return modifiers; } + static async buildConfigure(config = {}, message = {}) { + config.dialog ??= {}; + config.guaranteedCritical = config.data?.parent?.appliedEffects.reduce((a, c) => { + const change = c.changes.find(ch => ch.key === 'system.rules.roll.guaranteedCritical'); + if (change) a = true; + return a; + }, false); + + if (config.guaranteedCritical) { + config.dialog.configure = false; + } + + return super.buildConfigure(config, message); + } + + getActionChangeKeys() { + const changeKeys = new Set([`system.bonuses.roll.${this.options.actionType}`]); + + if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.attack.id) { + changeKeys.add(`system.bonuses.roll.${this.options.roll.type}`); + } + + if ( + this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.attack.id || + (this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id && this.options.hasDamage) + ) { + changeKeys.add(`system.bonuses.roll.attack`); + } + + if (this.options.roll.trait && this.data.traits?.[this.options.roll.trait]) { + if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.spellcast.id) + changeKeys.add('system.bonuses.roll.trait'); + } + + const weapons = ['primaryWeapon', 'secondaryWeapon']; + weapons.forEach(w => { + if (this.options.source.item && this.options.source.item === this.data[w]?.id) + changeKeys.add(`system.bonuses.roll.${w}`); + }); + + return changeKeys; + } + static async buildEvaluate(roll, config = {}, message = {}) { await super.buildEvaluate(roll, config, message); @@ -190,7 +236,7 @@ export default class DualityRoll extends D20Roll { data.hope = { dice: roll.dHope.denomination, - value: roll.dHope.total, + value: this.guaranteedCritical ? 0 : roll.dHope.total, rerolled: { any: roll.dHope.results.some(x => x.rerolled), rerolls: roll.dHope.results.filter(x => x.rerolled) @@ -198,7 +244,7 @@ export default class DualityRoll extends D20Roll { }; data.fear = { dice: roll.dFear.denomination, - value: roll.dFear.total, + value: this.guaranteedCritical ? 0 : roll.dFear.total, rerolled: { any: roll.dFear.results.some(x => x.rerolled), rerolls: roll.dFear.results.filter(x => x.rerolled) @@ -210,7 +256,7 @@ export default class DualityRoll extends D20Roll { }; data.result = { duality: roll.withHope ? 1 : roll.withFear ? -1 : 0, - total: roll.dHope.total + roll.dFear.total, + total: this.guaranteedCritical ? 0 : roll.dHope.total + roll.dFear.total, label: roll.totalLabel }; @@ -228,6 +274,8 @@ export default class DualityRoll extends D20Roll { } static async handleTriggers(roll, config) { + if (!config.source?.actor || config.skips?.triggers) return; + const updates = []; const dualityUpdates = await game.system.registeredTriggers.runTrigger( CONFIG.DH.TRIGGER.triggers.dualityRoll.id, diff --git a/module/dice/fateRoll.mjs b/module/dice/fateRoll.mjs new file mode 100644 index 00000000..418c8465 --- /dev/null +++ b/module/dice/fateRoll.mjs @@ -0,0 +1,85 @@ +import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; +import D20Roll from './d20Roll.mjs'; +import { setDiceSoNiceForHopeFateRoll, setDiceSoNiceForFearFateRoll } from '../helpers/utils.mjs'; + +export default class FateRoll extends D20Roll { + constructor(formula, data = {}, options = {}) { + super(formula, data, options); + } + + static messageType = 'fateRoll'; + + static DefaultDialog = D20RollDialog; + + get title() { + return game.i18n.localize(`DAGGERHEART.GENERAL.fateRoll`); + } + + get dHope() { + if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); + return this.dice[0]; + } + + set dHope(faces) { + if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); + this.dice[0].faces = this.getFaces(faces); + } + + get dFear() { + if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); + return this.dice[0]; + } + + set dFear(faces) { + if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); + this.dice[0].faces = this.getFaces(faces); + } + + get isCritical() { + return false; + } + + get fateDie() { + return this.data.fateType; + } + + static getHooks(hooks) { + return [...(hooks ?? []), 'Fate']; + } + + /** @inheritDoc */ + static fromData(data) { + data.terms[0].class = foundry.dice.terms.Die.name; + return super.fromData(data); + } + + createBaseDice() { + if (this.dice[0] instanceof foundry.dice.terms.Die) { + this.terms = [this.terms[0]]; + return; + } + this.terms[0] = new foundry.dice.terms.Die({ faces: 12 }); + } + + static async buildEvaluate(roll, config = {}, message = {}) { + await super.buildEvaluate(roll, config, message); + + if (roll.fateDie === 'Hope') { + await setDiceSoNiceForHopeFateRoll(roll, config.roll.fate.dice); + } else { + await setDiceSoNiceForFearFateRoll(roll, config.roll.fate.dice); + } + } + + static postEvaluate(roll, config = {}) { + const data = super.postEvaluate(roll, config); + + data.fate = { + dice: roll.fateDie === 'Hope' ? roll.dHope.denomination : roll.dFear.denomination, + value: roll.fateDie === 'Hope' ? roll.dHope.total : roll.dFear.total, + fateDie: roll.fateDie + }; + + return data; + } +} diff --git a/module/documents/_module.mjs b/module/documents/_module.mjs index 22718bea..b9cfd3f2 100644 --- a/module/documents/_module.mjs +++ b/module/documents/_module.mjs @@ -4,7 +4,9 @@ export { default as DhpCombat } from './combat.mjs'; export { default as DHCombatant } from './combatant.mjs'; export { default as DhActiveEffect } from './activeEffect.mjs'; export { default as DhChatMessage } from './chatMessage.mjs'; +export { default as DhRollTable } from './rollTable.mjs'; export { default as DhScene } from './scene.mjs'; export { default as DhToken } from './token.mjs'; export { default as DhTooltipManager } from './tooltipManager.mjs'; export { default as DhTemplateManager } from './templateManager.mjs'; +export { default as DhTokenManager } from './tokenManager.mjs'; diff --git a/module/documents/activeEffect.mjs b/module/documents/activeEffect.mjs index 2297ea27..5e9b0c3b 100644 --- a/module/documents/activeEffect.mjs +++ b/module/documents/activeEffect.mjs @@ -20,7 +20,10 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { } if (this.parent?.type === 'domainCard') { - return this.parent.system.inVault; + const isVaultSupressed = this.parent.system.isVaultSupressed; + const domainTouchedSupressed = this.parent.system.isDomainTouchedSuppressed; + + return isVaultSupressed || domainTouchedSupressed; } return super.isSuppressed; @@ -106,23 +109,29 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { /**@inheritdoc*/ static applyField(model, change, field) { - const isOriginTarget = change.value.toLowerCase().includes('origin.@'); + change.value = DhActiveEffect.getChangeValue(model, change, change.effect); + super.applyField(model, change, field); + } + + /** */ + static getChangeValue(model, change, effect) { + let value = change.value; + const isOriginTarget = value.toLowerCase().includes('origin.@'); let parseModel = model; - if (isOriginTarget && change.effect.origin) { - change.value = change.value.replaceAll(/origin\.@/gi, '@'); + if (isOriginTarget && effect.origin) { + value = change.value.replaceAll(/origin\.@/gi, '@'); try { - const effect = foundry.utils.fromUuidSync(change.effect.origin); + const originEffect = foundry.utils.fromUuidSync(effect.origin); const doc = - effect.parent?.parent instanceof game.system.api.documents.DhpActor - ? effect.parent - : effect.parent.parent; + originEffect.parent?.parent instanceof game.system.api.documents.DhpActor + ? originEffect.parent + : originEffect.parent.parent; if (doc) parseModel = doc; } catch (_) {} } - const evalValue = this.effectSafeEval(itemAbleRollParse(change.value, parseModel, change.effect.parent)); - change.value = evalValue ?? change.value; - super.applyField(model, change, field); + const evalValue = this.effectSafeEval(itemAbleRollParse(value, parseModel, effect.parent)); + return evalValue ?? value; } /** diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 1a4153ad..e8bea0bf 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -104,6 +104,16 @@ export default class DhpActor extends Actor { } } + async _preDelete() { + if (this.prototypeToken.actorLink) { + game.system.registeredTriggers.unregisterItemTriggers(this.items); + } else { + for (const token of this.getActiveTokens()) { + game.system.registeredTriggers.unregisterItemTriggers(token.actor.items); + } + } + } + _onDelete(options, userId) { super._onDelete(options, userId); for (const party of this.parties) { @@ -231,6 +241,11 @@ export default class DhpActor extends Actor { } } }); + + if (this.system.companion) { + this.system.companion.updateLevel(usedLevel); + } + this.sheet.render(); } } @@ -597,7 +612,7 @@ export default class DhpActor extends Actor { if (!updates.length) return; const hpDamage = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id); - if (hpDamage) { + if (hpDamage?.value) { hpDamage.value = this.convertDamageToThreshold(hpDamage.value); if ( this.type === 'character' && @@ -754,16 +769,24 @@ export default class DhpActor extends Actor { }; } } else { + const valueFunc = (base, resource, baseMax) => { + if (resource.clear) return baseMax && base.inverted ? baseMax : 0; + + return (base.value ?? base) + resource.value; + }; switch (r.key) { case 'fear': ui.resources.updateFear( - game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) + r.value + valueFunc( + game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), + r + ) ); break; case 'armor': if (this.system.armor?.system?.marks) { updates.armor.resources['system.marks.value'] = Math.max( - Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore), + Math.min(valueFunc(this.system.armor.system.marks, r), this.system.armorScore), 0 ); } @@ -772,7 +795,7 @@ export default class DhpActor extends Actor { if (this.system.resources?.[r.key]) { updates.actor.resources[`system.resources.${r.key}.value`] = Math.max( Math.min( - this.system.resources[r.key].value + r.value, + valueFunc(this.system.resources[r.key], r, this.system.resources[r.key].max), this.system.resources[r.key].max ), 0 @@ -831,8 +854,8 @@ export default class DhpActor extends Actor { async toggleDefeated(defeatedState) { const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).defeated; - const { unconscious, defeated, dead } = CONFIG.DH.GENERAL.conditions(); - const defeatedConditions = new Set([unconscious.id, defeated.id, dead.id]); + const { deathMove, unconscious, defeated, dead } = CONFIG.DH.GENERAL.conditions(); + const defeatedConditions = new Set([deathMove.id, unconscious.id, defeated.id, dead.id]); if (!defeatedState) { for (let defeatedId of defeatedConditions) { await this.toggleStatusEffect(defeatedId, { overlay: settings.overlay, active: defeatedState }); @@ -846,6 +869,18 @@ export default class DhpActor extends Actor { } } + async setDeathMoveDefeated(defeatedIconId) { + const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).defeated; + const actorDefault = settings[`${this.type}Default`]; + if (!settings.enabled || !settings.enabled || !actorDefault || actorDefault === defeatedIconId) return; + + for (let defeatedId of Object.keys(CONFIG.DH.GENERAL.defeatedConditionChoices)) { + await this.toggleStatusEffect(defeatedId, { overlay: settings.overlay, active: false }); + } + + if (defeatedIconId) await this.toggleStatusEffect(defeatedIconId, { overlay: settings.overlay, active: true }); + } + queueScrollText(scrollingTextData) { this.#scrollTextQueue.push(...scrollingTextData.map(data => () => createScrollText(this, data))); if (!this.#scrollTextInterval) { diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index 7e313891..1d2c6c41 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -87,6 +87,15 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { break; } } + if (this.type === 'fateRoll') { + html.classList.add('fate'); + if (this.system.roll?.fate.fateDie == 'Hope') { + html.classList.add('hope'); + } + if (this.system.roll?.fate.fateDie == 'Fear') { + html.classList.add('fear'); + } + } const autoExpandRoll = game.settings.get( CONFIG.DH.id, @@ -157,7 +166,12 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { event.stopPropagation(); const config = foundry.utils.deepClone(this.system); config.event = event; - await this.system.action?.workflow.get('damage')?.execute(config, this._id, true); + if (this.system.action) { + const actor = await foundry.utils.fromUuid(config.source.actor); + const item = actor?.items.get(config.source.item) ?? null; + config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(actor, item); + await this.system.action.workflow.get('damage')?.execute(config, this._id, true); + } Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll }); await game.socket.emit(`system.${CONFIG.DH.id}`, { @@ -174,7 +188,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { config = foundry.utils.deepClone(this.system); config.event = event; - if (this.system.onSave) { + if (config.hasSave) { const pendingingSaves = targets.filter(t => t.saved.success === null); if (pendingingSaves.length) { const confirm = await foundry.applications.api.DialogV2.confirm({ @@ -189,7 +203,16 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm')); this.consumeOnSuccess(); - this.system.action?.workflow.get('applyDamage')?.execute(config, targets, true); + if (this.system.action) this.system.action.workflow.get('applyDamage')?.execute(config, targets, true); + else { + for (const target of targets) { + const actor = await foundry.utils.fromUuid(target.actorId); + if (!actor) continue; + + if (this.system.hasHealing) actor.takeHealing(this.system.damage); + else actor.takeDamage(this.system.damage); + } + } } async onRollSave(event) { diff --git a/module/documents/collections/_module.mjs b/module/documents/collections/_module.mjs new file mode 100644 index 00000000..a24c1d85 --- /dev/null +++ b/module/documents/collections/_module.mjs @@ -0,0 +1 @@ +export { default as DhActorCollection } from './actorCollection.mjs'; diff --git a/module/documents/collections/actorCollection.mjs b/module/documents/collections/actorCollection.mjs new file mode 100644 index 00000000..a3714b30 --- /dev/null +++ b/module/documents/collections/actorCollection.mjs @@ -0,0 +1,14 @@ +export default class DhActorCollection extends foundry.documents.collections.Actors { + /** Ensure companions are initialized after all other subtypes. */ + _initialize() { + super._initialize(); + const companions = []; + for (const actor of this.values()) { + if (actor.type === 'companion') companions.push(actor); + } + for (const actor of companions) { + this.delete(actor.id); + this.set(actor.id, actor); + } + } +} diff --git a/module/documents/item.mjs b/module/documents/item.mjs index 2c6d68b5..fe62c5bd 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -31,7 +31,7 @@ export default class DHItem extends foundry.documents.Item { static async createDocuments(sources, operation) { // Ensure that items being created are valid to the actor its being added to const actor = operation.parent; - sources = actor?.system?.isItemValid ? sources.filter((s) => actor.system.isItemValid(s)) : sources; + sources = actor?.system?.isItemValid ? sources.filter(s => actor.system.isItemValid(s)) : sources; return super.createDocuments(sources, operation); } @@ -146,6 +146,16 @@ export default class DHItem extends foundry.documents.Item { /* -------------------------------------------- */ async use(event) { + /* DomainCard check. Can be expanded or made neater */ + if (this.system.isDomainTouchedSuppressed) { + return ui.notifications.warn( + game.i18n.format('DAGGERHEART.UI.Notifications.domainTouchRequirement', { + nr: this.domainTouched, + domain: game.i18n.localize(CONFIG.DH.DOMAIN.allDomains()[this.domain].label) + }) + ); + } + const actions = new Set(this.system.actionsList); if (actions?.size) { let action = actions.first(); @@ -198,4 +208,23 @@ export default class DHItem extends foundry.documents.Item { cls.create(msg); } + + deleteTriggers() { + const actions = Array.from(this.system.actions ?? []); + if (!actions.length) return; + + const triggerKeys = actions.flatMap(action => action.triggers.map(x => x.trigger)); + + game.system.registeredTriggers.unregisterTriggers(triggerKeys, this.uuid); + + if (this.actor && !(this.actor.parent instanceof game.system.api.documents.DhToken)) { + for (const token of this.actor.getActiveTokens()) { + game.system.registeredTriggers.unregisterTriggers(triggerKeys, `${token.document.uuid}.${this.uuid}`); + } + } + } + + async _preDelete() { + this.deleteTriggers(); + } } diff --git a/module/documents/rollTable.mjs b/module/documents/rollTable.mjs new file mode 100644 index 00000000..50b8fe63 --- /dev/null +++ b/module/documents/rollTable.mjs @@ -0,0 +1,122 @@ +export default class DhRollTable extends foundry.documents.RollTable { + async roll({ selectedFormula, roll, recursive = true, _depth = 0 } = {}) { + // Prevent excessive recursion + if (_depth > 5) { + throw new Error(`Maximum recursion depth exceeded when attempting to draw from RollTable ${this.id}`); + } + + const formula = selectedFormula ?? this.formula; + + // If there is no formula, automatically calculate an even distribution + if (!this.formula) { + await this.normalize(); + } + + // Reference the provided roll formula + roll = roll instanceof Roll ? roll : Roll.create(formula); + let results = []; + + // Ensure that at least one non-drawn result remains + const available = this.results.filter(r => !r.drawn); + if (!available.length) { + ui.notifications.warn(game.i18n.localize('TABLE.NoAvailableResults')); + return { roll, results }; + } + + // Ensure that results are available within the minimum/maximum range + const minRoll = (await roll.reroll({ minimize: true })).total; + const maxRoll = (await roll.reroll({ maximize: true })).total; + const availableRange = available.reduce( + (range, result) => { + const r = result.range; + if (!range[0] || r[0] < range[0]) range[0] = r[0]; + if (!range[1] || r[1] > range[1]) range[1] = r[1]; + return range; + }, + [null, null] + ); + if (availableRange[0] > maxRoll || availableRange[1] < minRoll) { + ui.notifications.warn('No results can possibly be drawn from this table and formula.'); + return { roll, results }; + } + + // Continue rolling until one or more results are recovered + let iter = 0; + while (!results.length) { + if (iter >= 10000) { + ui.notifications.error( + `Failed to draw an available entry from Table ${this.name}, maximum iteration reached` + ); + break; + } + roll = await roll.reroll(); + results = this.getResultsForRoll(roll.total); + iter++; + } + + // Draw results recursively from any inner Roll Tables + if (recursive) { + const inner = []; + for (const result of results) { + const { type, documentUuid } = result; + const documentName = foundry.utils.parseUuid(documentUuid)?.type; + if (type === 'document' && documentName === 'RollTable') { + const innerTable = await fromUuid(documentUuid); + if (innerTable) { + const innerRoll = await innerTable.roll({ _depth: _depth + 1 }); + inner.push(...innerRoll.results); + } + } else inner.push(result); + } + results = inner; + } + + // Return the Roll and the results + return { roll, results }; + } + + async toMessage(results, { roll, messageData = {}, messageOptions = {} } = {}) { + messageOptions.rollMode ??= game.settings.get('core', 'rollMode'); + + // Construct chat data + messageData = foundry.utils.mergeObject( + { + author: game.user.id, + speaker: foundry.documents.ChatMessage.implementation.getSpeaker(), + rolls: [], + sound: roll ? CONFIG.sounds.dice : null, + flags: { 'core.RollTable': this.id } + }, + messageData + ); + if (roll) messageData.rolls.push(roll); + + // Render the chat card which combines the dice roll with the drawn results + const detailsPromises = await Promise.allSettled(results.map(r => r.getHTML())); + const flavorKey = `TABLE.DrawFlavor${results.length > 1 ? 'Plural' : ''}`; + const flavor = game.i18n.format(flavorKey, { + number: results.length, + name: foundry.utils.escapeHTML(this.name) + }); + messageData.content = await foundry.applications.handlebars.renderTemplate(CONFIG.RollTable.resultTemplate, { + description: await TextEditor.implementation.enrichHTML(this.description, { + documents: true, + secrets: this.isOwner + }), + flavor: flavor, + results: results.map((result, i) => { + const r = result.toObject(false); + r.details = detailsPromises[i].value ?? ''; + const useTableIcon = + result.icon === CONFIG.RollTable.resultIcon && this.img !== this.constructor.DEFAULT_ICON; + r.icon = useTableIcon ? this.img : result.icon; + return r; + }), + rollHTML: this.displayRoll && roll ? await roll.render() : null, + table: this + }); + + // Create the chat message + return foundry.documents.ChatMessage.implementation.create(messageData, messageOptions); + } +} diff --git a/module/documents/scene.mjs b/module/documents/scene.mjs index 7f880b1d..1c2faa34 100644 --- a/module/documents/scene.mjs +++ b/module/documents/scene.mjs @@ -51,6 +51,27 @@ export default class DhScene extends Scene { } } + async _preUpdate(changes, options, user) { + const allowed = await super._preUpdate(changes, options, user); + if (allowed === false) return false; + + if (changes.flags?.daggerheart) { + if (this._source.flags.daggerheart) { + const unregisterTriggerData = (this._source.flags.daggerheart.sceneEnvironments ?? []).reduce( + (acc, env) => { + if (!changes.flags.daggerheart.sceneEnvironments.includes(env)) acc.sceneEnvironments.push(env); + + return acc; + }, + { ...this._source.flags.daggerheart, sceneEnvironments: [] } + ); + game.system.registeredTriggers.unregisterSceneEnvironmentTriggers(unregisterTriggerData); + } + + game.system.registeredTriggers.registerSceneEnvironmentTriggers(changes.flags.daggerheart); + } + } + _onDelete(options, userId) { super._onDelete(options, userId); diff --git a/module/documents/token.mjs b/module/documents/token.mjs index 4ac29264..317f3acf 100644 --- a/module/documents/token.mjs +++ b/module/documents/token.mjs @@ -536,4 +536,10 @@ export default class DHToken extends CONFIG.Token.documentClass { }; } //#endregion + + async _preDelete() { + if (this.actor && !this.actor.prototypeToken?.actorLink) { + game.system.registeredTriggers.unregisterItemTriggers(this.actor.items); + } + } } diff --git a/module/documents/tokenManager.mjs b/module/documents/tokenManager.mjs new file mode 100644 index 00000000..be5467da --- /dev/null +++ b/module/documents/tokenManager.mjs @@ -0,0 +1,104 @@ +/** + * A singleton class that handles preview tokens. + */ + +export default class DhTokenManager { + #activePreview; + #actor; + #resolve; + + /** + * Create a template preview, deactivating any existing ones. + * @param {object} data + */ + async createPreview(actor, tokenData) { + this.#actor = actor; + const token = await canvas.tokens._createPreview( + { + ...actor.prototypeToken, + displayName: 50, + ...tokenData + }, + { renderSheet: false, actor } + ); + + this.#activePreview = { + document: token.document, + object: token, + origin: { x: token.document.x, y: token.document.y } + }; + + this.#activePreview.events = { + contextmenu: this.#cancelTemplate.bind(this), + mousedown: this.#confirmTemplate.bind(this), + mousemove: this.#onDragMouseMove.bind(this) + }; + + canvas.stage.on('mousemove', this.#activePreview.events.mousemove); + canvas.stage.on('mousedown', this.#activePreview.events.mousedown); + canvas.app.view.addEventListener('contextmenu', this.#activePreview.events.contextmenu); + } + + /* Currently intended for using as a preview of where to create a token. (note the flag) */ + async createPreviewAsync(actor, tokenData = {}) { + return new Promise(resolve => { + this.#resolve = resolve; + this.createPreview(actor, { ...tokenData, flags: { daggerheart: { createPlacement: true } } }); + }); + } + + /** + * Handles the movement of the token preview on mousedrag. + * @param {mousemove Event} event + */ + #onDragMouseMove(event) { + event.stopPropagation(); + const { moveTime, object } = this.#activePreview; + const update = {}; + + const now = Date.now(); + if (now - (moveTime || 0) <= 16) return; + this.#activePreview.moveTime = now; + + let cursor = event.getLocalPosition(canvas.templates); + + Object.assign(update, canvas.grid.getTopLeftPoint(cursor)); + + object.document.updateSource(update); + object.renderFlags.set({ refresh: true }); + } + + /** + * Cancels the preview token on right-click. + * @param {contextmenu Event} event + */ + #cancelTemplate(_event, resolved) { + const { mousemove, mousedown, contextmenu } = this.#activePreview.events; + this.#activePreview.object.destroy(); + + canvas.stage.off('mousemove', mousemove); + canvas.stage.off('mousedown', mousedown); + canvas.app.view.removeEventListener('contextmenu', contextmenu); + if (this.#resolve && !resolved) this.#resolve(false); + } + + /** + * Creates a real Actor and token at the preview location and cancels the preview. + * @param {click Event} event + */ + async #confirmTemplate(event) { + event.stopPropagation(); + this.#cancelTemplate(event, true); + + const actor = this.#actor.inCompendium + ? await game.system.api.documents.DhpActor.create(this.#actor.toObject()) + : this.#actor; + const tokenData = await actor.getTokenDocument(); + const result = await canvas.scene.createEmbeddedDocuments('Token', [ + { ...tokenData, x: this.#activePreview.document.x, y: this.#activePreview.document.y } + ]); + + this.#activePreview = undefined; + if (this.#resolve && result.length) this.#resolve(result[0]); + } +} diff --git a/module/documents/tooltipManager.mjs b/module/documents/tooltipManager.mjs index dac5aea3..c4b52bb5 100644 --- a/module/documents/tooltipManager.mjs +++ b/module/documents/tooltipManager.mjs @@ -67,7 +67,7 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti if (item) { const isAction = item instanceof game.system.api.models.actions.actionsTypes.base; const isEffect = item instanceof ActiveEffect; - await this.enrichText(item, isAction || isEffect); + await this.enrichText(item); const type = isAction ? 'action' : isEffect ? 'effect' : item.type; html = await foundry.applications.handlebars.renderTemplate( @@ -202,10 +202,20 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti } } - async enrichText(item, flatStructure) { + async enrichText(item) { const { TextEditor } = foundry.applications.ux; + + if (item.system?.metadata?.hasDescription) { + const enrichedValue = + (await item.system?.getEnrichedDescription?.()) ?? + (await TextEditor.enrichHTML(item.system.description)); + foundry.utils.setProperty(item, 'system.enrichedDescription', enrichedValue); + } else if (item.description) { + const enrichedValue = await TextEditor.enrichHTML(item.description); + foundry.utils.setProperty(item, 'enrichedDescription', enrichedValue); + } + const enrichPaths = [ - { path: flatStructure ? '' : 'system', name: 'description' }, { path: 'system', name: 'features' }, { path: 'system', name: 'actions' }, { path: 'system', name: 'customActions' } diff --git a/module/enrichers/DualityRollEnricher.mjs b/module/enrichers/DualityRollEnricher.mjs index 1d6404ff..f6de8107 100644 --- a/module/enrichers/DualityRollEnricher.mjs +++ b/module/enrichers/DualityRollEnricher.mjs @@ -2,7 +2,7 @@ import { abilities } from '../config/actorConfig.mjs'; import { getCommandTarget, rollCommandToJSON } from '../helpers/utils.mjs'; export default function DhDualityRollEnricher(match, _options) { - const roll = rollCommandToJSON(match[1], match[0]); + const roll = rollCommandToJSON(match[0]); if (!roll) return match[0]; return getDualityMessage(roll.result, roll.flavor); @@ -47,6 +47,7 @@ function getDualityMessage(roll, flavor) { ${roll?.trait && abilities[roll.trait] ? `data-trait="${roll.trait}"` : ''} ${roll?.advantage ? 'data-advantage="true"' : ''} ${roll?.disadvantage ? 'data-disadvantage="true"' : ''} + ${roll?.grantResources ? 'data-grant-resources="true"' : ''} > ${roll?.reaction ? '' : ''} ${label} @@ -63,7 +64,8 @@ export const renderDualityButton = async event => { traitValue = button.dataset.trait?.toLowerCase(), target = getCommandTarget({ allowNull: true }), difficulty = button.dataset.difficulty, - advantage = button.dataset.advantage ? Number(button.dataset.advantage) : undefined; + advantage = button.dataset.advantage ? Number(button.dataset.advantage) : undefined, + grantResources = Boolean(button.dataset?.grantResources); await enrichedDualityRoll( { @@ -73,36 +75,45 @@ export const renderDualityButton = async event => { difficulty, title: button.dataset.title, label: button.dataset.label, - advantage + advantage, + grantResources }, event ); }; export const enrichedDualityRoll = async ( - { reaction, traitValue, target, difficulty, title, label, advantage }, + { reaction, traitValue, target, difficulty, title, label, advantage, grantResources, customConfig }, event ) => { const config = { event: event ?? {}, title: title, + headerTitle: label, roll: { trait: traitValue && target ? traitValue : null, - label: label, difficulty: difficulty, advantage, type: reaction ? 'reaction' : null }, + skips: { + resources: !grantResources, + triggers: !grantResources + }, type: 'trait', - hasRoll: true + hasRoll: true, + ...(customConfig ?? {}) }; if (target) { - await target.diceRoll(config); + const result = await target.diceRoll(config); + if (!result) return; + result.resourceUpdates.updateResources(); } else { // For no target, call DualityRoll directly with basic data - config.data = { experiences: {}, traits: {} }; + config.data = { experiences: {}, traits: {}, rules: {} }; config.source = { actor: null }; await CONFIG.Dice.daggerheart.DualityRoll.build(config); } + return config; }; diff --git a/module/enrichers/FateRollEnricher.mjs b/module/enrichers/FateRollEnricher.mjs new file mode 100644 index 00000000..c82bbcb2 --- /dev/null +++ b/module/enrichers/FateRollEnricher.mjs @@ -0,0 +1,80 @@ +import { getCommandTarget, rollCommandToJSON } from '../helpers/utils.mjs'; + +export default function DhFateRollEnricher(match, _options) { + const roll = rollCommandToJSON(match[0]); + if (!roll) return match[0]; + + return getFateMessage(roll.result, roll?.flavor); +} + +export function getFateTypeData(fateTypeValue) { + const value = fateTypeValue ? fateTypeValue.capitalize() : 'Hope'; + const lowercased = fateTypeValue?.toLowerCase?.() ?? 'hope'; + switch (lowercased) { + case 'hope': + case 'fear': + return { value, label: game.i18n.localize(`DAGGERHEART.GENERAL.${lowercased}`) }; + default: + return null; + } +} + +function getFateMessage(roll, flavor) { + const fateTypeData = getFateTypeData(roll?.type); + + if (!fateTypeData) + return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing')); + + const { value: fateType, label: fateTypeLabel } = fateTypeData; + const title = flavor ?? game.i18n.localize('DAGGERHEART.GENERAL.fateRoll'); + + const fateElement = document.createElement('span'); + fateElement.innerHTML = ` + + `; + + 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; +}; diff --git a/module/enrichers/_module.mjs b/module/enrichers/_module.mjs index 0dcd870e..b80f166c 100644 --- a/module/enrichers/_module.mjs +++ b/module/enrichers/_module.mjs @@ -1,10 +1,11 @@ import { default as DhDamageEnricher, renderDamageButton } from './DamageEnricher.mjs'; import { default as DhDualityRollEnricher, renderDualityButton } from './DualityRollEnricher.mjs'; +import { default as DhFateRollEnricher, renderFateButton } from './FateRollEnricher.mjs'; import { default as DhEffectEnricher } from './EffectEnricher.mjs'; import { default as DhTemplateEnricher, renderMeasuredTemplate } from './TemplateEnricher.mjs'; import { default as DhLookupEnricher } from './LookupEnricher.mjs'; -export { DhDamageEnricher, DhDualityRollEnricher, DhEffectEnricher, DhTemplateEnricher }; +export { DhDamageEnricher, DhDualityRollEnricher, DhEffectEnricher, DhTemplateEnricher, DhFateRollEnricher }; export const enricherConfig = [ { @@ -15,6 +16,10 @@ export const enricherConfig = [ pattern: /\[\[\/dr\s?(.*?)\]\]({[^}]*})?/g, enricher: DhDualityRollEnricher }, + { + pattern: /\[\[\/fr\s?(.*?)\]\]({[^}]*})?/g, + enricher: DhFateRollEnricher + }, { pattern: /@Effect\[([^\[\]]*)\]({[^}]*})?/g, enricher: DhEffectEnricher @@ -38,6 +43,10 @@ export const enricherRenderSetup = element => { .querySelectorAll('.duality-roll-button') .forEach(element => element.addEventListener('click', renderDualityButton)); + element + .querySelectorAll('.fate-roll-button') + .forEach(element => element.addEventListener('click', renderFateButton)); + element .querySelectorAll('.measured-template-button') .forEach(element => element.addEventListener('click', renderMeasuredTemplate)); diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index a28725b1..1cce581a 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -1,14 +1,14 @@ -import { diceTypes, getDiceSoNicePresets, range } from '../config/generalConfig.mjs'; +import { diceTypes, getDiceSoNicePresets, getDiceSoNicePreset, range } from '../config/generalConfig.mjs'; import Tagify from '@yaireo/tagify'; export const capitalize = string => { return string.charAt(0).toUpperCase() + string.slice(1); }; -export function rollCommandToJSON(text, raw) { +export function rollCommandToJSON(text) { if (!text) return {}; - const flavorMatch = raw?.match(/{(.*)}$/); + const flavorMatch = text?.match(/{(.*)}$/); const flavor = flavorMatch ? flavorMatch[1] : null; // Match key="quoted string" OR key=unquotedValue @@ -31,7 +31,7 @@ export function rollCommandToJSON(text, raw) { } result[key] = value; } - return Object.keys(result).length > 0 ? { result, flavor } : null; + return { result, flavor }; } export const getCommandTarget = (options = {}) => { @@ -69,6 +69,20 @@ export const setDiceSoNiceForDualityRoll = async (rollResult, advantageState, ho } }; +export const setDiceSoNiceForHopeFateRoll = async (rollResult, hopeFaces) => { + if (!game.modules.get('dice-so-nice')?.active) return; + const { diceSoNice } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance); + const diceSoNicePresets = await getDiceSoNicePreset(diceSoNice.hope, hopeFaces); + rollResult.dice[0].options = diceSoNicePresets; +}; + +export const setDiceSoNiceForFearFateRoll = async (rollResult, fearFaces) => { + if (!game.modules.get('dice-so-nice')?.active) return; + const { diceSoNice } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance); + const diceSoNicePresets = await getDiceSoNicePreset(diceSoNice.fear, fearFaces); + rollResult.dice[0].options = diceSoNicePresets; +}; + export const chunkify = (array, chunkSize, mappingFunc) => { var chunkifiedArray = []; for (let i = 0; i < array.length; i += chunkSize) { diff --git a/module/systemRegistration/handlebars.mjs b/module/systemRegistration/handlebars.mjs index 32e047fd..97769181 100644 --- a/module/systemRegistration/handlebars.mjs +++ b/module/systemRegistration/handlebars.mjs @@ -32,6 +32,7 @@ export const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/actionTypes/effect.hbs', 'systems/daggerheart/templates/actionTypes/beastform.hbs', 'systems/daggerheart/templates/actionTypes/countdown.hbs', + 'systems/daggerheart/templates/actionTypes/summon.hbs', 'systems/daggerheart/templates/settings/components/settings-item-line.hbs', 'systems/daggerheart/templates/ui/tooltip/parts/tooltipChips.hbs', 'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs', diff --git a/module/systemRegistration/migrations.mjs b/module/systemRegistration/migrations.mjs index b3116459..743d42a4 100644 --- a/module/systemRegistration/migrations.mjs +++ b/module/systemRegistration/migrations.mjs @@ -210,6 +210,42 @@ export async function runMigrations() { lastMigrationVersion = '1.2.7'; } + + if (foundry.utils.isNewerVersion('1.5.5', lastMigrationVersion)) { + /* Clear out Environments that were added directly from compendium */ + for (const scene of game.scenes) { + if (!scene.flags.daggerheart) continue; + const systemData = new game.system.api.data.scenes.DHScene(scene.flags.daggerheart); + const sceneEnvironments = systemData.sceneEnvironments; + + const newEnvironments = sceneEnvironments.filter(x => !x?.pack); + if (newEnvironments.length !== sceneEnvironments.length) + await scene.update({ 'flags.daggerheart.sceneEnvironments': newEnvironments }); + } + + ui.nav.render(true); + + lastMigrationVersion = '1.5.5'; + } + + if (foundry.utils.isNewerVersion('1.6.0', lastMigrationVersion)) { + /* Delevel any companions that are higher level than their partner character */ + for (const companion of game.actors.filter(x => x.type === 'companion')) { + if (companion.system.levelData.level.current <= 1) continue; + + if (!companion.system.partner) { + await companion.updateLevel(1); + } else { + const endLevel = companion.system.partner.system.levelData.level.current; + if (endLevel < companion.system.levelData.level.current) { + companion.system.levelData.level.changed = companion.system.levelData.level.current; + await companion.updateLevel(endLevel); + } + } + } + + lastMigrationVersion = '1.6.0'; + } //#endregion await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion, lastMigrationVersion); diff --git a/module/systemRegistration/socket.mjs b/module/systemRegistration/socket.mjs index 82ca2e1c..a9e86917 100644 --- a/module/systemRegistration/socket.mjs +++ b/module/systemRegistration/socket.mjs @@ -93,10 +93,6 @@ export const registerSocketHooks = () => { } } }); - Hooks.on(socketEvent.RefreshDocument, async data => { - const document = await foundry.utils.fromUuid(data.uuid); - document.sheet.render(); - }); }; export const registerUserQueries = () => { diff --git a/src/packs/adversaries/adversary_Arch_Necromancer_WPEOIGfclNJxWb87.json b/src/packs/adversaries/adversary_Arch_Necromancer_WPEOIGfclNJxWb87.json index 4fc58990..d4e506cb 100644 --- a/src/packs/adversaries/adversary_Arch_Necromancer_WPEOIGfclNJxWb87.json +++ b/src/packs/adversaries/adversary_Arch_Necromancer_WPEOIGfclNJxWb87.json @@ -533,33 +533,31 @@ "description": "

Spend a Fear to summon a @UUID[Compendium.daggerheart.adversaries.Actor.YhJrP7rTBiRdX5Fp]{Zombie Legion}, which appears at Close range and immediately takes the spotlight.

", "resource": null, "actions": { - "gZg3AkzCYUTExjE6": { - "type": "effect", - "_id": "gZg3AkzCYUTExjE6", + "qSuWxC8xQOhnbBx9": { + "type": "summon", + "_id": "qSuWxC8xQOhnbBx9", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", - "cost": [ - { - "scalable": false, - "key": "fear", - "value": 1, - "step": null - } - ], + "cost": [], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "any", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.YhJrP7rTBiRdX5Fp", + "count": "1" + } + ], "name": "Spend Fear", - "img": "icons/magic/death/undead-zombie-grave-green.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Demon_of_Despair_kE4dfhqmIQpNd44e.json b/src/packs/adversaries/adversary_Demon_of_Despair_kE4dfhqmIQpNd44e.json index b1804074..830848c3 100644 --- a/src/packs/adversaries/adversary_Demon_of_Despair_kE4dfhqmIQpNd44e.json +++ b/src/packs/adversaries/adversary_Demon_of_Despair_kE4dfhqmIQpNd44e.json @@ -235,7 +235,51 @@ }, "_id": "2ESeh4tPhr6DI5ty", "img": "icons/magic/death/skull-horned-worn-fire-blue.webp", - "effects": [], + "effects": [ + { + "name": "Depths Of Despair", + "type": "base", + "system": { + "rangeDependence": { + "enabled": false, + "type": "withinRange", + "target": "hostile", + "range": "melee" + } + }, + "_id": "nofxm1vGZ2TmceA2", + "img": "icons/magic/death/skull-horned-worn-fire-blue.webp", + "changes": [ + { + "key": "system.rules.attack.damage.hpDamageMultiplier", + "mode": 5, + "value": "2", + "priority": null + } + ], + "disabled": true, + "duration": { + "startTime": null, + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null + }, + "description": "

The Demon of Despair deals double damage to PCs with 0 Hope.

", + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null + }, + "_key": "!actors.items.effects!kE4dfhqmIQpNd44e.2ESeh4tPhr6DI5ty.nofxm1vGZ2TmceA2" + } + ], "folder": null, "sort": 0, "ownership": { @@ -312,7 +356,14 @@ "range": "melee" } }, - "changes": [], + "changes": [ + { + "key": "system.rules.dualityRoll.defaultHopeDice", + "mode": 5, + "value": "d8", + "priority": null + } + ], "disabled": false, "duration": { "startTime": null, @@ -323,7 +374,7 @@ "startRound": null, "startTurn": null }, - "description": "

All targets aff ected replace their Hope Die with a d8 until they roll a success with Hope or their next rest.

", + "description": "

All targets affected replace their Hope Die with a d8 until they roll a success with Hope or their next rest.

", "tint": "#ffffff", "statuses": [], "sort": 0, diff --git a/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json b/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json index 9e838d6d..2341ee8a 100644 --- a/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json +++ b/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json @@ -256,34 +256,45 @@ "description": "

Spend a Fear to boil the blood of all PCs within Far range. They use a d20 as their Fear Die until the end of the scene.

@Template[type:emanation|range:f]

", "resource": null, "actions": { - "V142qYppCGJn8OiN": { + "jKvzbQT0vp66DDOH": { "type": "effect", - "_id": "V142qYppCGJn8OiN", + "_id": "jKvzbQT0vp66DDOH", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "fear", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, "max": "", - "recovery": null + "recovery": null, + "consumeOnSuccess": false }, - "effects": [], + "effects": [ + { + "_id": "gFeHLGgeRoDdd3VG", + "onSave": false + } + ], "target": { - "type": "self", + "type": "hostile", "amount": null }, "name": "Spend Fear", - "img": "icons/skills/melee/maneuver-greatsword-yellow.webp", - "range": "" + "range": "far" } }, "originItemType": null, @@ -292,7 +303,51 @@ }, "_id": "a33PW8UkziliowlR", "img": "icons/skills/melee/maneuver-greatsword-yellow.webp", - "effects": [], + "effects": [ + { + "name": "Battle Lust", + "img": "icons/skills/melee/maneuver-greatsword-yellow.webp", + "origin": "Compendium.daggerheart.adversaries.Actor.5lphJAgzoqZI3VoG.Item.a33PW8UkziliowlR", + "transfer": false, + "_id": "gFeHLGgeRoDdd3VG", + "type": "base", + "system": { + "rangeDependence": { + "enabled": false, + "type": "withinRange", + "target": "hostile", + "range": "melee" + } + }, + "changes": [ + { + "key": "system.rules.dualityRoll.defaultFearDice", + "mode": 5, + "value": "d20", + "priority": null + } + ], + "disabled": false, + "duration": { + "startTime": null, + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null + }, + "description": "

You use a d20 as your Fear Die until the end of the scene.

", + "tint": "#ffffff", + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null + }, + "_key": "!actors.items.effects!5lphJAgzoqZI3VoG.a33PW8UkziliowlR.gFeHLGgeRoDdd3VG" + } + ], "folder": null, "sort": 0, "ownership": { @@ -457,11 +512,12 @@ "img": "icons/creatures/unholy/demon-fire-horned-clawed.webp", "range": "" }, - "7G6uWlFEeOLsJIWY": { - "type": "effect", - "_id": "7G6uWlFEeOLsJIWY", + "FlE6i0tbKEguF9wz": { + "type": "summon", + "_id": "FlE6i0tbKEguF9wz", "systemPath": "actions", - "description": "

Summon [[/r 1d4]]@UUID[Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb]{Minor Demons}, who appear at Close range.

", + "baseAction": false, + "description": "", "chatDisplay": true, "originItem": { "type": "itemCollection" @@ -474,13 +530,13 @@ "recovery": null, "consumeOnSuccess": false }, - "effects": [], - "target": { - "type": "any", - "amount": null - }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb", + "count": "1d4" + } + ], "name": "Summon", - "img": "icons/creatures/unholy/demon-fire-horned-clawed.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Dryad_wR7cFKrHvRzbzhBT.json b/src/packs/adversaries/adversary_Dryad_wR7cFKrHvRzbzhBT.json index f0a5d81c..ca9ce647 100644 --- a/src/packs/adversaries/adversary_Dryad_wR7cFKrHvRzbzhBT.json +++ b/src/packs/adversaries/adversary_Dryad_wR7cFKrHvRzbzhBT.json @@ -363,33 +363,31 @@ "description": "

Spend a Fear to grow three @UUID[Compendium.daggerheart.adversaries.Actor.o63nS0k3wHu6EgKP]{Treant Sapling Minions}, who appear at Close range and immediately take the spotlight.

", "resource": null, "actions": { - "84Q2b0zIY9c7Yhho": { - "type": "effect", - "_id": "84Q2b0zIY9c7Yhho", + "R84DdS0OIx2cUt1w": { + "type": "summon", + "_id": "R84DdS0OIx2cUt1w", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", - "cost": [ - { - "scalable": false, - "key": "fear", - "value": 1, - "step": null - } - ], + "cost": [], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "self", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.o63nS0k3wHu6EgKP", + "count": "3" + } + ], "name": "Spend Fear", - "img": "icons/magic/unholy/orb-hands-pink.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Failed_Experiment_ChwwVqowFw8hJQwT.json b/src/packs/adversaries/adversary_Failed_Experiment_ChwwVqowFw8hJQwT.json index 39800002..408d5102 100644 --- a/src/packs/adversaries/adversary_Failed_Experiment_ChwwVqowFw8hJQwT.json +++ b/src/packs/adversaries/adversary_Failed_Experiment_ChwwVqowFw8hJQwT.json @@ -304,7 +304,51 @@ }, "_id": "1fE6xo8yIOmZkGNE", "img": "icons/skills/melee/strike-slashes-orange.webp", - "effects": [], + "effects": [ + { + "name": "Overwhelm", + "type": "base", + "system": { + "rangeDependence": { + "enabled": false, + "type": "withinRange", + "target": "hostile", + "range": "melee" + } + }, + "_id": "eGB9G0ljYCcdGbOx", + "img": "icons/skills/melee/strike-slashes-orange.webp", + "changes": [ + { + "key": "system.rules.attack.damage.hpDamageMultiplier", + "mode": 5, + "value": "2", + "priority": null + } + ], + "disabled": true, + "duration": { + "startTime": null, + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null + }, + "description": "

When a target the Failed Experiment attacks has other adversaries within Very Close range, the Failed Experiment deals double damage.

", + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null + }, + "_key": "!actors.items.effects!ChwwVqowFw8hJQwT.1fE6xo8yIOmZkGNE.eGB9G0ljYCcdGbOx" + } + ], "folder": null, "sort": 0, "ownership": { diff --git a/src/packs/adversaries/adversary_Green_Ooze_SHXedd9zZPVfUgUa.json b/src/packs/adversaries/adversary_Green_Ooze_SHXedd9zZPVfUgUa.json index c7446a11..b03b5495 100644 --- a/src/packs/adversaries/adversary_Green_Ooze_SHXedd9zZPVfUgUa.json +++ b/src/packs/adversaries/adversary_Green_Ooze_SHXedd9zZPVfUgUa.json @@ -510,34 +510,41 @@ "description": "

When the @Lookup[@name] has 3 or more HP marked, you can spend a Fear 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.

", "resource": null, "actions": { - "s5mLw6DRGd76MLcC": { - "type": "effect", - "_id": "s5mLw6DRGd76MLcC", + "J8U7dw3cDSsEirr5": { + "type": "summon", + "_id": "J8U7dw3cDSsEirr5", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "fear", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "self", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.aLkLFuVoKz2NLoBK", + "count": "2" + } + ], "name": "Spend Fear", - "img": "icons/creatures/slimes/slime-movement-pseudopods-green.webp", - "range": "" + "range": "self" } }, "originItemType": null, diff --git a/src/packs/adversaries/adversary_Hallowed_Archer_kabueAo6BALApWqp.json b/src/packs/adversaries/adversary_Hallowed_Archer_kabueAo6BALApWqp.json index 0abf1661..8cce1b94 100644 --- a/src/packs/adversaries/adversary_Hallowed_Archer_kabueAo6BALApWqp.json +++ b/src/packs/adversaries/adversary_Hallowed_Archer_kabueAo6BALApWqp.json @@ -229,7 +229,51 @@ }, "_id": "FGJTAeL38zTVd4fA", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", - "effects": [], + "effects": [ + { + "name": "Punish the Guilty", + "type": "base", + "system": { + "rangeDependence": { + "enabled": false, + "type": "withinRange", + "target": "hostile", + "range": "melee" + } + }, + "_id": "ID85zoIa5GfhNMti", + "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", + "changes": [ + { + "key": "system.rules.attack.damage.hpDamageMultiplier", + "mode": 5, + "value": "2", + "priority": null + } + ], + "disabled": true, + "duration": { + "startTime": null, + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null + }, + "description": "

The Hallowed Archer deals double damage to targets marked Guilty by a High Seraph.

", + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null + }, + "_key": "!actors.items.effects!kabueAo6BALApWqp.FGJTAeL38zTVd4fA.ID85zoIa5GfhNMti" + } + ], "folder": null, "sort": 0, "ownership": { diff --git a/src/packs/adversaries/adversary_Head_Vampire_i2UNbRvgyoSs07M6.json b/src/packs/adversaries/adversary_Head_Vampire_i2UNbRvgyoSs07M6.json index 9e948594..d5891359 100644 --- a/src/packs/adversaries/adversary_Head_Vampire_i2UNbRvgyoSs07M6.json +++ b/src/packs/adversaries/adversary_Head_Vampire_i2UNbRvgyoSs07M6.json @@ -474,33 +474,31 @@ "description": "

Spend 2 Fear to summon [[/r 1d4]] @UUID[Compendium.daggerheart.adversaries.Actor.WWyUp6Mxl1S3KYUG]{Vampires}, who appear at Far range and immediately take the spotlight.

", "resource": null, "actions": { - "5Q6RMUTiauKw0tDj": { - "type": "effect", - "_id": "5Q6RMUTiauKw0tDj", + "jGFOnU6PNdWU6iF4": { + "type": "summon", + "_id": "jGFOnU6PNdWU6iF4", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", - "cost": [ - { - "scalable": false, - "key": "fear", - "value": 2, - "step": null - } - ], + "cost": [], "uses": { "value": null, "max": "", - "recovery": null + "recovery": null, + "consumeOnSuccess": false }, - "effects": [], - "target": { - "type": "any", - "amount": null - }, - "name": "Summon Vampires", - "img": "icons/creatures/mammals/bat-giant-tattered-purple.webp", + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.WWyUp6Mxl1S3KYUG", + "count": "1d4" + } + ], + "name": "Spend Fear", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json b/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json index 6f64f883..3bb8ae96 100644 --- a/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json +++ b/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json @@ -479,33 +479,31 @@ "description": "

When the @Lookup[@name] has 4 or more HP marked, you can spend a Fear to split them into two @UUID[Compendium.daggerheart.adversaries.Actor.SHXedd9zZPVfUgUa]{Green Oozes}(with no marked HP or Stress). Immediately spotlight both of them.

", "resource": null, "actions": { - "iQsYAqpUFvJslRDr": { - "type": "effect", - "_id": "iQsYAqpUFvJslRDr", + "aeRdkiRsDNagTKhp": { + "type": "summon", + "_id": "aeRdkiRsDNagTKhp", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", - "cost": [ - { - "scalable": false, - "key": "fear", - "value": 1, - "step": null - } - ], + "cost": [], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "any", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.SHXedd9zZPVfUgUa", + "count": "2" + } + ], "name": "Spend Fear", - "img": "icons/creatures/slimes/slime-movement-pseudopods-green.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Jagged_Knife_Kneebreaker_CBKixLH3yhivZZuL.json b/src/packs/adversaries/adversary_Jagged_Knife_Kneebreaker_CBKixLH3yhivZZuL.json index fc644604..c38260e9 100644 --- a/src/packs/adversaries/adversary_Jagged_Knife_Kneebreaker_CBKixLH3yhivZZuL.json +++ b/src/packs/adversaries/adversary_Jagged_Knife_Kneebreaker_CBKixLH3yhivZZuL.json @@ -336,7 +336,14 @@ "range": "melee" } }, - "changes": [], + "changes": [ + { + "key": "system.rules.attack.damage.hpDamageTakenMultiplier", + "mode": 5, + "value": "2", + "priority": null + } + ], "disabled": false, "duration": { "startTime": null, @@ -350,8 +357,8 @@ "description": "", "tint": "#ffffff", "statuses": [ - "restrained", - "vulnerable" + "vulnerable", + "restrained" ], "sort": 0, "flags": {}, diff --git a/src/packs/adversaries/adversary_Jagged_Knife_Lieutenant_aTljstqteGoLpCBq.json b/src/packs/adversaries/adversary_Jagged_Knife_Lieutenant_aTljstqteGoLpCBq.json index 165bb160..c139d76f 100644 --- a/src/packs/adversaries/adversary_Jagged_Knife_Lieutenant_aTljstqteGoLpCBq.json +++ b/src/packs/adversaries/adversary_Jagged_Knife_Lieutenant_aTljstqteGoLpCBq.json @@ -287,7 +287,35 @@ "system": { "description": "

Summon three @Compendium[daggerheart.adversaries.Actor.C0OMQqV7pN6t7ouR], who appear at Far range.

", "resource": null, - "actions": {}, + "actions": { + "MCTBsw9lusUdubj0": { + "type": "summon", + "_id": "MCTBsw9lusUdubj0", + "systemPath": "actions", + "baseAction": false, + "description": "", + "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, + "actionType": "action", + "cost": [], + "uses": { + "value": null, + "max": "", + "recovery": null, + "consumeOnSuccess": false + }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.C0OMQqV7pN6t7ouR", + "count": "3" + } + ], + "name": "Summon", + "range": "" + } + }, "originItemType": null, "subType": null, "originId": null, diff --git a/src/packs/adversaries/adversary_Petty_Noble_wycLpvebWdUqRhpP.json b/src/packs/adversaries/adversary_Petty_Noble_wycLpvebWdUqRhpP.json index 4ac7e746..db284f40 100644 --- a/src/packs/adversaries/adversary_Petty_Noble_wycLpvebWdUqRhpP.json +++ b/src/packs/adversaries/adversary_Petty_Noble_wycLpvebWdUqRhpP.json @@ -258,57 +258,40 @@ "description": "

Once per scene, mark a Stress to summon 1d4 @UUID[Compendium.daggerheart.adversaries.Actor.B4LZcGuBAHzyVdzy]{Bladed Guards}, who appear at Far range to enforce the @Lookup[@name]’s will.

", "resource": null, "actions": { - "cUKwhq1imsTVru8D": { - "type": "attack", - "_id": "cUKwhq1imsTVru8D", + "tioTtYfIGFIXRITN": { + "type": "summon", + "_id": "tioTtYfIGFIXRITN", "systemPath": "actions", - "description": "

Once per scene, mark a Stress to summon 1d4 @UUID[Compendium.daggerheart.adversaries.Actor.B4LZcGuBAHzyVdzy]{Bladed Guards}, who appear at Far range to enforce the Noble’s will.

", + "baseAction": false, + "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "stress", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, - "max": "", - "recovery": null - }, - "damage": { - "parts": [], - "includeBase": false - }, - "target": { - "type": "any", - "amount": null - }, - "effects": [], - "roll": { - "type": "diceSet", - "trait": null, - "difficulty": null, - "bonus": null, - "advState": "neutral", - "diceRolling": { - "multiplier": "prof", - "flatMultiplier": 1, - "dice": "d4", - "compare": null, - "treshold": null - }, - "useDefault": false - }, - "save": { - "trait": null, - "difficulty": null, - "damageMod": "none" + "max": "1", + "recovery": "scene", + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.B4LZcGuBAHzyVdzy", + "count": "1d4" + } + ], "name": "Summon Guards", - "img": "icons/environment/people/infantry-armored.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Pirate_Captain_OROJbjsqagVh7ECV.json b/src/packs/adversaries/adversary_Pirate_Captain_OROJbjsqagVh7ECV.json index 409d7698..5b00ec60 100644 --- a/src/packs/adversaries/adversary_Pirate_Captain_OROJbjsqagVh7ECV.json +++ b/src/packs/adversaries/adversary_Pirate_Captain_OROJbjsqagVh7ECV.json @@ -313,36 +313,43 @@ "_id": "WGEGO0DSOs5cF0EL", "img": "icons/environment/people/charge.webp", "system": { - "description": "

Once per scene, mark a Stress to summon a Pirate Raiders Horde, which appears at Far range.

", + "description": "

Once per scene, mark a Stress to summon a @UUID[Compendium.daggerheart.adversaries.Actor.5YgEajn0wa4i85kC]{Pirate Raider Horde}, which appears at Far range.

", "resource": null, "actions": { - "NlgIp0KrmZoS27Xy": { - "type": "effect", - "_id": "NlgIp0KrmZoS27Xy", + "nuYk5WeLLpIKa69q": { + "type": "summon", + "_id": "nuYk5WeLLpIKa69q", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "stress", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "any", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.5YgEajn0wa4i85kC", + "count": "1" + } + ], "name": "Mark Stress", - "img": "icons/environment/people/charge.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Red_Ooze_9rVlbJVrDNn1x7PS.json b/src/packs/adversaries/adversary_Red_Ooze_9rVlbJVrDNn1x7PS.json index 320b71af..2c10ae3f 100644 --- a/src/packs/adversaries/adversary_Red_Ooze_9rVlbJVrDNn1x7PS.json +++ b/src/packs/adversaries/adversary_Red_Ooze_9rVlbJVrDNn1x7PS.json @@ -454,33 +454,40 @@ "description": "

When the @Lookup[@name] has 3 or more HP marked, you can spend a Fear 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.

", "resource": null, "actions": { - "dw6Juw8mriH7sg0e": { - "type": "effect", - "_id": "dw6Juw8mriH7sg0e", + "BMEr77hDxaQyYBna": { + "type": "summon", + "_id": "BMEr77hDxaQyYBna", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "fear", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "any", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.1fkLQXVtmILqfJ44", + "count": "2" + } + ], "name": "Spend Fear", - "img": "icons/creatures/slimes/slime-movement-splashing-red.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Secret_Keeper_sLAccjvCWfeedbpI.json b/src/packs/adversaries/adversary_Secret_Keeper_sLAccjvCWfeedbpI.json index 0c8757c5..d17c3f86 100644 --- a/src/packs/adversaries/adversary_Secret_Keeper_sLAccjvCWfeedbpI.json +++ b/src/packs/adversaries/adversary_Secret_Keeper_sLAccjvCWfeedbpI.json @@ -416,28 +416,6 @@ "description": "

Countdown (6). 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.

", "resource": null, "actions": { - "0rixG6jLRynAYNqA": { - "type": "effect", - "_id": "0rixG6jLRynAYNqA", - "systemPath": "actions", - "description": "

Summon a @UUID[Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb]{Minor Demon} who appears at Close range.

", - "chatDisplay": true, - "actionType": "action", - "cost": [], - "uses": { - "value": null, - "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "any", - "amount": null - }, - "name": "Summon", - "img": "icons/magic/unholy/silhouette-light-fire-blue.webp", - "range": "close" - }, "ZVXHY2fpomoKV7jG": { "type": "countdown", "_id": "ZVXHY2fpomoKV7jG", @@ -474,6 +452,33 @@ "name": "Start Countdown", "img": "icons/magic/unholy/silhouette-light-fire-blue.webp", "range": "" + }, + "YReYG6DrWp4QGSij": { + "type": "summon", + "_id": "YReYG6DrWp4QGSij", + "systemPath": "actions", + "baseAction": false, + "description": "", + "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, + "actionType": "action", + "cost": [], + "uses": { + "value": null, + "max": "", + "recovery": null, + "consumeOnSuccess": false + }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb", + "count": "1" + } + ], + "name": "Summon", + "range": "" } }, "originItemType": null, @@ -502,33 +507,31 @@ "description": "

Once per scene, when the @Lookup[@name] marks 2 or more HP, you can mark a Stress to summon a @UUID[Compendium.daggerheart.adversaries.Actor.NoRZ1PqB8N5wcIw0]{Demonic Hound Pack}, which appears at Close range and is immediately spotlighted.

", "resource": null, "actions": { - "JBuQUJhif2A7IlJd": { - "type": "effect", - "_id": "JBuQUJhif2A7IlJd", + "tfmY6HYkkY27NBaF": { + "type": "summon", + "_id": "tfmY6HYkkY27NBaF", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", - "cost": [ - { - "scalable": false, - "key": "stress", - "value": 1, - "step": null - } - ], + "cost": [], "uses": { "value": null, - "max": "1", - "recovery": "scene" - }, - "effects": [], - "target": { - "type": "self", - "amount": null + "max": "", + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.NoRZ1PqB8N5wcIw0", + "count": "1" + } + ], "name": "Mark Stress", - "img": "icons/creatures/unholy/demon-fire-horned-clawed.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Skeleton_Archer_7X5q7a6ueeHs5oA9.json b/src/packs/adversaries/adversary_Skeleton_Archer_7X5q7a6ueeHs5oA9.json index e5381f6f..9d837ac0 100644 --- a/src/packs/adversaries/adversary_Skeleton_Archer_7X5q7a6ueeHs5oA9.json +++ b/src/packs/adversaries/adversary_Skeleton_Archer_7X5q7a6ueeHs5oA9.json @@ -230,7 +230,51 @@ "subType": null, "originId": null }, - "effects": [], + "effects": [ + { + "name": "Opportunist", + "type": "base", + "system": { + "rangeDependence": { + "enabled": false, + "type": "withinRange", + "target": "hostile", + "range": "melee" + } + }, + "_id": "O03vYbyNLO3YPZGo", + "img": "icons/skills/targeting/crosshair-triple-strike-orange.webp", + "changes": [ + { + "key": "system.rules.attack.damage.hpDamageMultiplier", + "mode": 5, + "value": "2", + "priority": null + } + ], + "disabled": true, + "duration": { + "startTime": null, + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null + }, + "description": "

When two or more adversaries are within Very Close range of a creature, all damage the Skeleton Archer deals to that creature is doubled.

", + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null + }, + "_key": "!actors.items.effects!7X5q7a6ueeHs5oA9.6mL2FQ9pQdfoDNzG.O03vYbyNLO3YPZGo" + } + ], "folder": null, "sort": 0, "ownership": { diff --git a/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json b/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json index a6e5ca17..0f1ba28f 100644 --- a/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json +++ b/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json @@ -340,7 +340,35 @@ "system": { "description": "

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 Horde’s HP is equal to the number of Minions combined.

", "resource": null, - "actions": {}, + "actions": { + "g1OQ5xlMHFWsoktd": { + "type": "summon", + "_id": "g1OQ5xlMHFWsoktd", + "systemPath": "actions", + "baseAction": false, + "description": "", + "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, + "actionType": "action", + "cost": [], + "uses": { + "value": null, + "max": "", + "recovery": null, + "consumeOnSuccess": false + }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.PKSXFuaIHUCoH63A", + "count": "1" + } + ], + "name": "Summon", + "range": "" + } + }, "originItemType": null, "subType": null, "originId": null, diff --git a/src/packs/communities/feature_Know_the_Tide_07x6Qe6qMzDw2xN4.json b/src/packs/communities/feature_Know_the_Tide_07x6Qe6qMzDw2xN4.json index 069fe6ba..41f11a74 100644 --- a/src/packs/communities/feature_Know_the_Tide_07x6Qe6qMzDw2xN4.json +++ b/src/packs/communities/feature_Know_the_Tide_07x6Qe6qMzDw2xN4.json @@ -9,13 +9,47 @@ "resource": { "type": "simple", "value": 0, - "max": "", - "icon": "", - "recovery": null, + "max": "@system.levelData.level.current", + "icon": "fa-solid fa-water", + "recovery": "session", "diceStates": {}, "dieFaces": "d4" }, - "actions": {}, + "actions": { + "tFlus34KotJjHfTe": { + "type": "effect", + "_id": "tFlus34KotJjHfTe", + "systemPath": "actions", + "baseAction": false, + "description": "", + "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, + "actionType": "action", + "triggers": [ + { + "trigger": "fearRoll", + "triggeringActorType": "self", + "command": "const { max, value } = this.item.system.resource;\nconst maxValue = actor.system.levelData.level.current;\nconst afterUpdate = value+1;\nif (afterUpdate > maxValue) return;\n\nui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.knowTheTide'));\nreturn { updates: [{\n key: 'resource',\n itemId: this.item.id,\n target: this.item,\n value: 1,\n}]};" + } + ], + "cost": [], + "uses": { + "value": null, + "max": "", + "recovery": null, + "consumeOnSuccess": false + }, + "effects": [], + "target": { + "type": "any", + "amount": null + }, + "name": "Know The Tide", + "range": "" + } + }, "originItemType": null, "subType": null, "originId": null, diff --git a/src/packs/domains/domainCard_Arcana_Touched_5PvMQKCjrgSxzstn.json b/src/packs/domains/domainCard_Arcana_Touched_5PvMQKCjrgSxzstn.json index 80d9797f..556d5074 100644 --- a/src/packs/domains/domainCard_Arcana_Touched_5PvMQKCjrgSxzstn.json +++ b/src/packs/domains/domainCard_Arcana_Touched_5PvMQKCjrgSxzstn.json @@ -54,7 +54,8 @@ "source": "Daggerheart SRD", "page": 120, "artist": "" - } + }, + "domainTouched": 4 }, "flags": {}, "_id": "5PvMQKCjrgSxzstn", diff --git a/src/packs/domains/domainCard_Blade_Touched_Gb5bqpFSBiuBxUix.json b/src/packs/domains/domainCard_Blade_Touched_Gb5bqpFSBiuBxUix.json index d2d5dafc..ebb95570 100644 --- a/src/packs/domains/domainCard_Blade_Touched_Gb5bqpFSBiuBxUix.json +++ b/src/packs/domains/domainCard_Blade_Touched_Gb5bqpFSBiuBxUix.json @@ -13,7 +13,8 @@ "source": "Daggerheart SRD", "page": 121, "artist": "" - } + }, + "domainTouched": 4 }, "flags": {}, "_id": "Gb5bqpFSBiuBxUix", diff --git a/src/packs/domains/domainCard_Bold_Presence_tdsL00yTSLNgZWs6.json b/src/packs/domains/domainCard_Bold_Presence_tdsL00yTSLNgZWs6.json index 02698989..08110cca 100644 --- a/src/packs/domains/domainCard_Bold_Presence_tdsL00yTSLNgZWs6.json +++ b/src/packs/domains/domainCard_Bold_Presence_tdsL00yTSLNgZWs6.json @@ -81,7 +81,7 @@ "name": "Bold Presence", "img": "icons/magic/holy/barrier-shield-winged-blue.webp", "origin": "Compendium.daggerheart.domains.Item.tdsL00yTSLNgZWs6", - "transfer": false, + "transfer": true, "_id": "2XEYhuAcRGTtqvED", "type": "base", "system": { diff --git a/src/packs/domains/domainCard_Bone_Touched_ON5bvnoQBy0SYc9Y.json b/src/packs/domains/domainCard_Bone_Touched_ON5bvnoQBy0SYc9Y.json index 770ddd63..8880bb07 100644 --- a/src/packs/domains/domainCard_Bone_Touched_ON5bvnoQBy0SYc9Y.json +++ b/src/packs/domains/domainCard_Bone_Touched_ON5bvnoQBy0SYc9Y.json @@ -46,7 +46,8 @@ "source": "Daggerheart SRD", "page": 123, "artist": "" - } + }, + "domainTouched": 4 }, "flags": {}, "_id": "ON5bvnoQBy0SYc9Y", diff --git a/src/packs/domains/domainCard_Codex_Touched_7Pu83ABdMukTxu3e.json b/src/packs/domains/domainCard_Codex_Touched_7Pu83ABdMukTxu3e.json index 1e2d5de3..6443ed6a 100644 --- a/src/packs/domains/domainCard_Codex_Touched_7Pu83ABdMukTxu3e.json +++ b/src/packs/domains/domainCard_Codex_Touched_7Pu83ABdMukTxu3e.json @@ -71,7 +71,8 @@ "source": "Daggerheart SRD", "page": 125, "artist": "" - } + }, + "domainTouched": 4 }, "flags": {}, "_id": "7Pu83ABdMukTxu3e", diff --git a/src/packs/domains/domainCard_Ferocity_jSQsSP61CX4MhSN7.json b/src/packs/domains/domainCard_Ferocity_jSQsSP61CX4MhSN7.json index 78593c62..94f0912f 100644 --- a/src/packs/domains/domainCard_Ferocity_jSQsSP61CX4MhSN7.json +++ b/src/packs/domains/domainCard_Ferocity_jSQsSP61CX4MhSN7.json @@ -41,13 +41,7 @@ "name": "Spend Hope", "img": "icons/skills/melee/maneuver-sword-katana-yellow.webp", "range": "", - "triggers": [ - { - "trigger": "postDamageReduction", - "triggeringActorType": "other", - "command": "/* Check if sufficient hope */\nif (this.actor.system.resources.hope.value < 2) return;\n\n/* Check if hit point damage was dealt */\nconst hpDamage = damageUpdates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);\nif (hpDamage.value < 0) return;\n\n/* Dialog to give player choice */\nconst confirmed = await foundry.applications.api.DialogV2.confirm({\n window: { title: this.item?.name ?? '' },\n content: game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.ferocityContent', { bonus: hpDamage.value }),\n});\n\nif (!confirmed) return;\n\n/* Create the effect */\nthis.actor.createEmbeddedDocuments('ActiveEffect', [{\n name: this.item.name,\n img: 'icons/skills/melee/maneuver-sword-katana-yellow.webp',\n description: game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.ferocityEffectDescription', { bonus: hpDamage.value }),\n changes: [{ key: 'system.evasion', mode: 2, value: hpDamage.value }]\n}]);\n\n/* Update hope */\nreturn { updates: [{ \n originActor: this.actor, \n updates: [{\n key: CONFIG.DH.GENERAL.healingTypes.hope.id,\n value: -2,\n total: 2\n }] \n}]}" - } - ] + "triggers": [] } }, "attribution": { diff --git a/src/packs/domains/domainCard_Get_Back_Up_BFWN2cObMdlk9uVz.json b/src/packs/domains/domainCard_Get_Back_Up_BFWN2cObMdlk9uVz.json index ab74e805..571a3fb4 100644 --- a/src/packs/domains/domainCard_Get_Back_Up_BFWN2cObMdlk9uVz.json +++ b/src/packs/domains/domainCard_Get_Back_Up_BFWN2cObMdlk9uVz.json @@ -18,7 +18,7 @@ }, "flags": {}, "_id": "BFWN2cObMdlk9uVz", - "sort": 3400000, + "sort": 3500000, "effects": [ { "name": "Get Back Up", diff --git a/src/packs/domains/domainCard_Grace_Touched_KAuNb51AwhD8KEXk.json b/src/packs/domains/domainCard_Grace_Touched_KAuNb51AwhD8KEXk.json index b87ea24d..346a81f2 100644 --- a/src/packs/domains/domainCard_Grace_Touched_KAuNb51AwhD8KEXk.json +++ b/src/packs/domains/domainCard_Grace_Touched_KAuNb51AwhD8KEXk.json @@ -96,7 +96,8 @@ "source": "Daggerheart SRD", "page": 127, "artist": "" - } + }, + "domainTouched": 4 }, "flags": {}, "_id": "KAuNb51AwhD8KEXk", diff --git a/src/packs/domains/domainCard_Midnight_Touched_uSyGKVxOJcnp28po.json b/src/packs/domains/domainCard_Midnight_Touched_uSyGKVxOJcnp28po.json index 3370c30e..10c42418 100644 --- a/src/packs/domains/domainCard_Midnight_Touched_uSyGKVxOJcnp28po.json +++ b/src/packs/domains/domainCard_Midnight_Touched_uSyGKVxOJcnp28po.json @@ -111,7 +111,8 @@ "source": "Daggerheart SRD", "page": 129, "artist": "" - } + }, + "domainTouched": 4 }, "flags": {}, "_id": "uSyGKVxOJcnp28po", diff --git a/src/packs/domains/domainCard_Notorious_IqxzvvjZiYbgx21A.json b/src/packs/domains/domainCard_Notorious_IqxzvvjZiYbgx21A.json index dfb581e7..2e5f5ffd 100644 --- a/src/packs/domains/domainCard_Notorious_IqxzvvjZiYbgx21A.json +++ b/src/packs/domains/domainCard_Notorious_IqxzvvjZiYbgx21A.json @@ -44,7 +44,8 @@ "source": "Daggerheart SRD", "page": 127, "artist": "" - } + }, + "loadoutIgnore": true }, "flags": {}, "_id": "IqxzvvjZiYbgx21A", diff --git a/src/packs/domains/domainCard_Sage_Touched_VOSFaQHZbmhMyXwi.json b/src/packs/domains/domainCard_Sage_Touched_VOSFaQHZbmhMyXwi.json index dc9ac3d3..432ff638 100644 --- a/src/packs/domains/domainCard_Sage_Touched_VOSFaQHZbmhMyXwi.json +++ b/src/packs/domains/domainCard_Sage_Touched_VOSFaQHZbmhMyXwi.json @@ -94,7 +94,8 @@ "source": "Daggerheart SRD", "page": 131, "artist": "" - } + }, + "domainTouched": 4 }, "flags": {}, "_id": "VOSFaQHZbmhMyXwi", diff --git a/src/packs/domains/domainCard_Salvation_Beam_4uAFGp3LxiC07woC.json b/src/packs/domains/domainCard_Salvation_Beam_4uAFGp3LxiC07woC.json index d637f611..c7aeb02f 100644 --- a/src/packs/domains/domainCard_Salvation_Beam_4uAFGp3LxiC07woC.json +++ b/src/packs/domains/domainCard_Salvation_Beam_4uAFGp3LxiC07woC.json @@ -95,7 +95,7 @@ }, "flags": {}, "_id": "4uAFGp3LxiC07woC", - "sort": 3400000, + "sort": 3500000, "effects": [], "ownership": { "default": 0 diff --git a/src/packs/domains/domainCard_Splendor_Touched_JT5dM3gVL6chDBYU.json b/src/packs/domains/domainCard_Splendor_Touched_JT5dM3gVL6chDBYU.json index 45d0dc96..6b530289 100644 --- a/src/packs/domains/domainCard_Splendor_Touched_JT5dM3gVL6chDBYU.json +++ b/src/packs/domains/domainCard_Splendor_Touched_JT5dM3gVL6chDBYU.json @@ -13,7 +13,8 @@ "source": "Daggerheart SRD", "page": 133, "artist": "" - } + }, + "domainTouched": 4 }, "flags": {}, "_id": "JT5dM3gVL6chDBYU", diff --git a/src/packs/domains/domainCard_Valor_Touched_k1AtYd3lSchIymBr.json b/src/packs/domains/domainCard_Valor_Touched_k1AtYd3lSchIymBr.json index 99546d6f..20fe18ea 100644 --- a/src/packs/domains/domainCard_Valor_Touched_k1AtYd3lSchIymBr.json +++ b/src/packs/domains/domainCard_Valor_Touched_k1AtYd3lSchIymBr.json @@ -82,7 +82,8 @@ "source": "Daggerheart SRD", "page": 134, "artist": "" - } + }, + "domainTouched": 4 }, "flags": {}, "_id": "k1AtYd3lSchIymBr", diff --git a/src/packs/domains/domainCard_Vitality_sWUlSPOJEaXyQLCj.json b/src/packs/domains/domainCard_Vitality_sWUlSPOJEaXyQLCj.json index 729aa251..ec47c9f9 100644 --- a/src/packs/domains/domainCard_Vitality_sWUlSPOJEaXyQLCj.json +++ b/src/packs/domains/domainCard_Vitality_sWUlSPOJEaXyQLCj.json @@ -51,7 +51,8 @@ "source": "Daggerheart SRD", "page": 121, "artist": "" - } + }, + "vaultActive": true }, "flags": {}, "_id": "sWUlSPOJEaXyQLCj", diff --git a/src/packs/domains/folders_Level_10_7pKKYgRQAKlQAksV.json b/src/packs/domains/folders_Level_10_7pKKYgRQAKlQAksV.json index 613aa993..126323da 100644 --- a/src/packs/domains/folders_Level_10_7pKKYgRQAKlQAksV.json +++ b/src/packs/domains/folders_Level_10_7pKKYgRQAKlQAksV.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "7pKKYgRQAKlQAksV", "description": "", - "sort": 1000000, + "sort": 950000, "flags": {}, "_key": "!folders!7pKKYgRQAKlQAksV" } diff --git a/src/packs/domains/folders_Level_1_9Xc6KzNyjDtTGZkp.json b/src/packs/domains/folders_Level_1_9Xc6KzNyjDtTGZkp.json index 095ff6fb..2d9c78f9 100644 --- a/src/packs/domains/folders_Level_1_9Xc6KzNyjDtTGZkp.json +++ b/src/packs/domains/folders_Level_1_9Xc6KzNyjDtTGZkp.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "9Xc6KzNyjDtTGZkp", "description": "", - "sort": 100000, + "sort": 700000, "flags": {}, "_key": "!folders!9Xc6KzNyjDtTGZkp" } diff --git a/src/packs/domains/folders_Level_2_o7t2fsAmRxKLoHrO.json b/src/packs/domains/folders_Level_2_o7t2fsAmRxKLoHrO.json index b242e121..68cc5f04 100644 --- a/src/packs/domains/folders_Level_2_o7t2fsAmRxKLoHrO.json +++ b/src/packs/domains/folders_Level_2_o7t2fsAmRxKLoHrO.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "o7t2fsAmRxKLoHrO", "description": "", - "sort": 200000, + "sort": 800000, "flags": {}, "_key": "!folders!o7t2fsAmRxKLoHrO" } diff --git a/src/packs/domains/folders_Level_3_wWL9mV6i2EGX5xHS.json b/src/packs/domains/folders_Level_3_wWL9mV6i2EGX5xHS.json index 3a4b0055..e04c6f09 100644 --- a/src/packs/domains/folders_Level_3_wWL9mV6i2EGX5xHS.json +++ b/src/packs/domains/folders_Level_3_wWL9mV6i2EGX5xHS.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "wWL9mV6i2EGX5xHS", "description": "", - "sort": 300000, + "sort": 850000, "flags": {}, "_key": "!folders!wWL9mV6i2EGX5xHS" } diff --git a/src/packs/domains/folders_Level_4_yalAnCU3SndrYImF.json b/src/packs/domains/folders_Level_4_yalAnCU3SndrYImF.json index ab0ba963..2b70a495 100644 --- a/src/packs/domains/folders_Level_4_yalAnCU3SndrYImF.json +++ b/src/packs/domains/folders_Level_4_yalAnCU3SndrYImF.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "yalAnCU3SndrYImF", "description": "", - "sort": 400000, + "sort": 900000, "flags": {}, "_key": "!folders!yalAnCU3SndrYImF" } diff --git a/src/packs/domains/folders_Level_5_Emnx4o1DWGTVKoAg.json b/src/packs/domains/folders_Level_5_Emnx4o1DWGTVKoAg.json index 0a821a2d..5bde56f3 100644 --- a/src/packs/domains/folders_Level_5_Emnx4o1DWGTVKoAg.json +++ b/src/packs/domains/folders_Level_5_Emnx4o1DWGTVKoAg.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "Emnx4o1DWGTVKoAg", "description": "", - "sort": 500000, + "sort": 901563, "flags": {}, "_key": "!folders!Emnx4o1DWGTVKoAg" } diff --git a/src/packs/domains/folders_Level_6_EiP5dLozOFZKIeWN.json b/src/packs/domains/folders_Level_6_EiP5dLozOFZKIeWN.json index 5a58c052..e20ae6b5 100644 --- a/src/packs/domains/folders_Level_6_EiP5dLozOFZKIeWN.json +++ b/src/packs/domains/folders_Level_6_EiP5dLozOFZKIeWN.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "EiP5dLozOFZKIeWN", "description": "", - "sort": 600000, + "sort": 903125, "flags": {}, "_key": "!folders!EiP5dLozOFZKIeWN" } diff --git a/src/packs/domains/folders_Level_7_HAGbPLHwm0UozDeG.json b/src/packs/domains/folders_Level_7_HAGbPLHwm0UozDeG.json index 233e3756..e53c0e2a 100644 --- a/src/packs/domains/folders_Level_7_HAGbPLHwm0UozDeG.json +++ b/src/packs/domains/folders_Level_7_HAGbPLHwm0UozDeG.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "HAGbPLHwm0UozDeG", "description": "", - "sort": 700000, + "sort": 906250, "flags": {}, "_key": "!folders!HAGbPLHwm0UozDeG" } diff --git a/src/packs/domains/folders_Level_8_me7ywrVh38j6T8Sm.json b/src/packs/domains/folders_Level_8_me7ywrVh38j6T8Sm.json index 2b125f0d..9a0ad8d9 100644 --- a/src/packs/domains/folders_Level_8_me7ywrVh38j6T8Sm.json +++ b/src/packs/domains/folders_Level_8_me7ywrVh38j6T8Sm.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "me7ywrVh38j6T8Sm", "description": "", - "sort": 800000, + "sort": 912500, "flags": {}, "_key": "!folders!me7ywrVh38j6T8Sm" } diff --git a/src/packs/domains/folders_Level_9_QYdeGsmVYIF34kZR.json b/src/packs/domains/folders_Level_9_QYdeGsmVYIF34kZR.json index c7984fb9..3547b169 100644 --- a/src/packs/domains/folders_Level_9_QYdeGsmVYIF34kZR.json +++ b/src/packs/domains/folders_Level_9_QYdeGsmVYIF34kZR.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "QYdeGsmVYIF34kZR", "description": "", - "sort": 900000, + "sort": 925000, "flags": {}, "_key": "!folders!QYdeGsmVYIF34kZR" } diff --git a/src/packs/environments/environment_Burning_Heart_of_the_Woods_oY69NN4rYxoRE4hl.json b/src/packs/environments/environment_Burning_Heart_of_the_Woods_oY69NN4rYxoRE4hl.json index dc42fb07..ea4f1951 100644 --- a/src/packs/environments/environment_Burning_Heart_of_the_Woods_oY69NN4rYxoRE4hl.json +++ b/src/packs/environments/environment_Burning_Heart_of_the_Woods_oY69NN4rYxoRE4hl.json @@ -314,7 +314,7 @@ "name": "Charcoal Constructs", "type": "feature", "system": { - "description": "

Warped animals wreathed in indigo f l ame trample through a point of your choice. All targets within Close range of that point must make an Agility Reaction Roll. Targets who fail take 3d12+3 physical damage. Targets who succeed take half damage instead.

@Template[type:emanation|range:c]

Are these real animals consumed by the fl ame or merely constructs of the corrupting magic?

", + "description": "

Warped animals wreathed in indigo flame trample through a point of your choice. All targets within Close range of that point must make an Agility Reaction Roll. Targets who fail take 3d12+3 physical damage. Targets who succeed take half damage instead.

@Template[type:emanation|range:c]

Are these real animals consumed by the fl ame or merely constructs of the corrupting magic?

", "resource": null, "actions": { "gbXIaKr8em134IZC": { diff --git a/src/packs/environments/environment_Chaos_Realm_2Z1mKc65LxNk2PqR.json b/src/packs/environments/environment_Chaos_Realm_2Z1mKc65LxNk2PqR.json index 77781de0..361b15bc 100644 --- a/src/packs/environments/environment_Chaos_Realm_2Z1mKc65LxNk2PqR.json +++ b/src/packs/environments/environment_Chaos_Realm_2Z1mKc65LxNk2PqR.json @@ -467,33 +467,48 @@ "description": "

Spend a Fear to summon an @UUID[Compendium.daggerheart.adversaries.Actor.A0SeeDzwjvqOsyof]{Outer Realms Abomination}, an@UUID[Compendium.daggerheart.adversaries.Actor.ms6nuOl3NFkhPj1k]{Outer Realms Corrupter}, and [[/r 2d6]] @UUID[Compendium.daggerheart.adversaries.Actor.moJhHgKqTKPS2WYS]{Outer Realms Thrall}, who appear at Close range of a chosen PC in defiance of logic and causality. Immediately spotlight one of these adversaries, and you can spend an additional Fear to automatically succeed on that adversary’s standard attack.

What halfconsumed remnants of the shattered world do these monstrosities cast aside in pursuit of living flesh? What jagged refl ections of former personhood do you catch between moments of unquestioning malice?

", "resource": null, "actions": { - "5a8ESNroEQHAm7rO": { - "type": "effect", - "_id": "5a8ESNroEQHAm7rO", + "KCzdCu2KhAx9KyhT": { + "type": "summon", + "_id": "KCzdCu2KhAx9KyhT", "systemPath": "actions", - "description": "

Spend a Fear to summon an @UUID[Compendium.daggerheart.adversaries.Actor.A0SeeDzwjvqOsyof]{Outer Realms Abomination}, an@UUID[Compendium.daggerheart.adversaries.Actor.ms6nuOl3NFkhPj1k]{Outer Realms Corrupter}, and [[/r 2d6]] @UUID[Compendium.daggerheart.adversaries.Actor.moJhHgKqTKPS2WYS]{Outer Realms Thrall}, who appear at Close range of a chosen PC in defiance of logic and causality. Immediately spotlight one of these adversaries, and you can spend an additional Fear to automatically succeed on that adversary’s standard attack.

What halfconsumed remnants of the shattered world do these monstrosities cast aside in pursuit of living flesh? What jagged refl ections of former personhood do you catch between moments of unquestioning malice?

", + "baseAction": false, + "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "fear", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "any", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.A0SeeDzwjvqOsyof", + "count": "1" + }, + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.ms6nuOl3NFkhPj1k", + "count": "1" + }, + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.moJhHgKqTKPS2WYS", + "count": "2d6" + } + ], "name": "Spend Fear", - "img": "icons/creatures/unholy/demons-horned-glowing-pink.webp", "range": "" } }, diff --git a/src/packs/environments/environment_Cult_Ritual_QAXXiOKBDmCTauHD.json b/src/packs/environments/environment_Cult_Ritual_QAXXiOKBDmCTauHD.json index 705c9585..1295db59 100644 --- a/src/packs/environments/environment_Cult_Ritual_QAXXiOKBDmCTauHD.json +++ b/src/packs/environments/environment_Cult_Ritual_QAXXiOKBDmCTauHD.json @@ -136,13 +136,89 @@ "system": { "description": "

Cultists dedicated this place to the Fallen Gods, and their foul influence seeps into it. Reduce the PCs’ Hope Die to a d10 while in this environment. The desecration can be removed with a Progress Countdown (6).

How do the PCs fist notice that something is wrong about this place? What fears resurface while hope is kept at bay?

", "resource": null, - "actions": {}, + "actions": { + "7W3sWRLzjG3dKcgq": { + "type": "effect", + "_id": "7W3sWRLzjG3dKcgq", + "systemPath": "actions", + "baseAction": false, + "description": "", + "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, + "actionType": "action", + "cost": [], + "uses": { + "value": null, + "max": "", + "recovery": null, + "consumeOnSuccess": false + }, + "effects": [ + { + "_id": "8yNIw8Y7rfMdOqWC", + "onSave": false + } + ], + "target": { + "type": "any", + "amount": null + }, + "name": "Influence", + "range": "" + } + }, "originItemType": null, "originId": null }, "_id": "iiHjguQG2aBn9g8i", "img": "icons/magic/unholy/orb-contained-pink.webp", - "effects": [], + "effects": [ + { + "name": "Desecrated Ground", + "img": "icons/magic/unholy/orb-contained-pink.webp", + "origin": "Compendium.daggerheart.environments.Actor.QAXXiOKBDmCTauHD.Item.iiHjguQG2aBn9g8i", + "transfer": false, + "_id": "8yNIw8Y7rfMdOqWC", + "type": "base", + "system": { + "rangeDependence": { + "enabled": false, + "type": "withinRange", + "target": "hostile", + "range": "melee" + } + }, + "changes": [ + { + "key": "system.rules.dualityRoll.defaultHopeDice", + "mode": 5, + "value": "d10", + "priority": null + } + ], + "disabled": false, + "duration": { + "startTime": null, + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null + }, + "description": "

Your Hope Die is reduced to a d10 while in the Desecrated Grounds.

", + "tint": "#ffffff", + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null + }, + "_key": "!actors.items.effects!QAXXiOKBDmCTauHD.iiHjguQG2aBn9g8i.8yNIw8Y7rfMdOqWC" + } + ], "folder": null, "sort": 0, "ownership": { @@ -343,11 +419,12 @@ "img": "icons/magic/unholy/barrier-fire-pink.webp", "range": "" }, - "suFEnfpOfeVRvnJF": { - "type": "effect", - "_id": "suFEnfpOfeVRvnJF", + "HG7tbEdlYl3yLQnR": { + "type": "summon", + "_id": "HG7tbEdlYl3yLQnR", "systemPath": "actions", - "description": "

Summon a @UUID[Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb]{Minor Demon} within Very Close range of the ritual’s leader.

", + "baseAction": false, + "description": "", "chatDisplay": true, "originItem": { "type": "itemCollection" @@ -360,13 +437,13 @@ "recovery": null, "consumeOnSuccess": false }, - "effects": [], - "target": { - "type": "any", - "amount": null - }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb", + "count": "1" + } + ], "name": "Summon Demon", - "img": "icons/magic/unholy/barrier-fire-pink.webp", "range": "" } }, diff --git a/src/packs/environments/environment_Divine_Usurpation_4DLYez7VbMCFDAuZ.json b/src/packs/environments/environment_Divine_Usurpation_4DLYez7VbMCFDAuZ.json index aacf87e9..d8e9cded 100644 --- a/src/packs/environments/environment_Divine_Usurpation_4DLYez7VbMCFDAuZ.json +++ b/src/packs/environments/environment_Divine_Usurpation_4DLYez7VbMCFDAuZ.json @@ -248,33 +248,31 @@ "description": "

Spend 2 Fear to summon [[/r 1d4+2]] @UUID[Compendium.daggerheart.adversaries.Actor.OsLG2BjaEdTZUJU9]{Fallen Shock Troop} that appear within Close range of the Usurper to assist their divine siege. Immediately spotlight the Shock Troops to use a “Group Attack” action.

Which High Fallen do these troops serve? Which god’s fl esh do they wish to feast upon?

", "resource": null, "actions": { - "qIQTEO5t72xFtKYI": { - "type": "effect", - "_id": "qIQTEO5t72xFtKYI", + "okcqGrI4rdghugUi": { + "type": "summon", + "_id": "okcqGrI4rdghugUi", "systemPath": "actions", - "description": "

Spend 2 Fear to summon [[/r 1d4+2]] @UUID[Compendium.daggerheart.adversaries.Actor.OsLG2BjaEdTZUJU9]{Fallen Shock Troop} that appear within Close range of the Usurper to assist their divine siege. Immediately spotlight the Shock Troops to use a “Group Attack” action.

Which High Fallen do these troops serve? Which god’s fl esh do they wish to feast upon?

", + "baseAction": false, + "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", - "cost": [ - { - "scalable": false, - "key": "fear", - "value": 2, - "step": null - } - ], + "cost": [], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "self", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.OsLG2BjaEdTZUJU9", + "count": "1d4+2" + } + ], "name": "Spend Fear", - "img": "icons/magic/unholy/orb-hands-pink.webp", "range": "" } }, diff --git a/src/packs/environments/environment_Mountain_Pass_acMu9wJrMZZzLSTJ.json b/src/packs/environments/environment_Mountain_Pass_acMu9wJrMZZzLSTJ.json index 8e7cf1c8..9ba6a918 100644 --- a/src/packs/environments/environment_Mountain_Pass_acMu9wJrMZZzLSTJ.json +++ b/src/packs/environments/environment_Mountain_Pass_acMu9wJrMZZzLSTJ.json @@ -246,7 +246,35 @@ "system": { "description": "

When the PCs enter the raptors’ hunting grounds, two @UUID[Compendium.daggerheart.adversaries.Actor.OMQ0v6PE8s1mSU0K]{Giant Eagles} appear at Very Far range of a chosen PC, identifying the PCs as likely prey.

How long has it been since the eagles last found prey? Do they have eggs in their nest or unfl edged young?

", "resource": null, - "actions": {}, + "actions": { + "88MyOC3IRcct6VLk": { + "type": "summon", + "_id": "88MyOC3IRcct6VLk", + "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.OMQ0v6PE8s1mSU0K", + "count": "2" + } + ], + "name": "Summon", + "range": "" + } + }, "originItemType": null, "originId": null, "featureForm": "reaction" diff --git a/src/packs/environments/environment_Raging_River_t4cdqTfzcqP3H1vJ.json b/src/packs/environments/environment_Raging_River_t4cdqTfzcqP3H1vJ.json index 5c973fa6..6c34c296 100644 --- a/src/packs/environments/environment_Raging_River_t4cdqTfzcqP3H1vJ.json +++ b/src/packs/environments/environment_Raging_River_t4cdqTfzcqP3H1vJ.json @@ -360,33 +360,40 @@ "description": "

Spend a Fear to summon a @UUID[Compendium.daggerheart.adversaries.Actor.8KWVLWXFhlY2kYx0]{Glass Snake} within Close range of a chosen PC. The Snake appears in or near the river and immediately takes the spotlight to use their “Spinning Serpent” action.

What treasures does the beast have in their burrow? What travelers have already fallen victim to this predator?

", "resource": null, "actions": { - "Mnp0Yzc7EPVXm8So": { - "type": "effect", - "_id": "Mnp0Yzc7EPVXm8So", + "uY9HMKE4Q5g7bRKg": { + "type": "summon", + "_id": "uY9HMKE4Q5g7bRKg", "systemPath": "actions", - "description": "

Spend a Fear to summon a @UUID[Compendium.daggerheart.adversaries.Actor.8KWVLWXFhlY2kYx0]{Glass Snake} within Close range of a chosen PC. The Snake appears in or near the river and immediately takes the spotlight to use their “Spinning Serpent” action.

What treasures does the beast have in their burrow? What travelers have already fallen victim to this predator?

", + "baseAction": false, + "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "fear", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "self", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.8KWVLWXFhlY2kYx0", + "count": "1" + } + ], "name": "Spend Fear", - "img": "icons/creatures/reptiles/snake-fangs-bite-green-yellow.webp", "range": "" } }, diff --git a/src/packs/items/weapons/weapon_Advanced_Greatstaff_4UzxqfkwF8gDSdu7.json b/src/packs/items/weapons/weapon_Advanced_Greatstaff_4UzxqfkwF8gDSdu7.json index 6ce54823..c66354c2 100644 --- a/src/packs/items/weapons/weapon_Advanced_Greatstaff_4UzxqfkwF8gDSdu7.json +++ b/src/packs/items/weapons/weapon_Advanced_Greatstaff_4UzxqfkwF8gDSdu7.json @@ -113,18 +113,7 @@ "name": "Powerful", "description": "On a successful attack, roll an additional damage die and discard the lowest result.", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", - "changes": [ - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" - } - ], + "changes": [], "_id": "sGVVxSM68Fmr1sSM", "type": "base", "system": {}, diff --git a/src/packs/items/weapons/weapon_Advanced_Greatsword_MAC6YWTo4lzSotQc.json b/src/packs/items/weapons/weapon_Advanced_Greatsword_MAC6YWTo4lzSotQc.json index fe3fff0e..71226630 100644 --- a/src/packs/items/weapons/weapon_Advanced_Greatsword_MAC6YWTo4lzSotQc.json +++ b/src/packs/items/weapons/weapon_Advanced_Greatsword_MAC6YWTo4lzSotQc.json @@ -118,16 +118,6 @@ "key": "system.evasion", "mode": 2, "value": "-1" - }, - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" } ], "_id": "E0PjC15OP55vIype", diff --git a/src/packs/items/weapons/weapon_Double_Flail_xm1yU7k58fMgXxRR.json b/src/packs/items/weapons/weapon_Double_Flail_xm1yU7k58fMgXxRR.json index 2e00f9c1..a118b399 100644 --- a/src/packs/items/weapons/weapon_Double_Flail_xm1yU7k58fMgXxRR.json +++ b/src/packs/items/weapons/weapon_Double_Flail_xm1yU7k58fMgXxRR.json @@ -113,18 +113,7 @@ "name": "Powerful", "description": "On a successful attack, roll an additional damage die and discard the lowest result.", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", - "changes": [ - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" - } - ], + "changes": [], "_id": "DCie5eR1dZH2Qvln", "type": "base", "system": {}, diff --git a/src/packs/items/weapons/weapon_Elder_Bow_JdWcn9W1edhAEInL.json b/src/packs/items/weapons/weapon_Elder_Bow_JdWcn9W1edhAEInL.json index b6437781..35659402 100644 --- a/src/packs/items/weapons/weapon_Elder_Bow_JdWcn9W1edhAEInL.json +++ b/src/packs/items/weapons/weapon_Elder_Bow_JdWcn9W1edhAEInL.json @@ -113,18 +113,7 @@ "name": "Powerful", "description": "On a successful attack, roll an additional damage die and discard the lowest result.", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", - "changes": [ - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" - } - ], + "changes": [], "_id": "sZ1XotFlGdkPPDG4", "type": "base", "system": {}, diff --git a/src/packs/items/weapons/weapon_Floating_Bladeshards_3vti3xfo0wJND7ew.json b/src/packs/items/weapons/weapon_Floating_Bladeshards_3vti3xfo0wJND7ew.json index fa7b7d45..232f26e9 100644 --- a/src/packs/items/weapons/weapon_Floating_Bladeshards_3vti3xfo0wJND7ew.json +++ b/src/packs/items/weapons/weapon_Floating_Bladeshards_3vti3xfo0wJND7ew.json @@ -113,18 +113,7 @@ "name": "Powerful", "description": "On a successful attack, roll an additional damage die and discard the lowest result.", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", - "changes": [ - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" - } - ], + "changes": [], "_id": "T831j6kZiMnpMNmv", "type": "base", "system": {}, diff --git a/src/packs/items/weapons/weapon_Gilded_Falchion_VwcOgqnzjf9LBj2S.json b/src/packs/items/weapons/weapon_Gilded_Falchion_VwcOgqnzjf9LBj2S.json index 551dcf56..ee8afebc 100644 --- a/src/packs/items/weapons/weapon_Gilded_Falchion_VwcOgqnzjf9LBj2S.json +++ b/src/packs/items/weapons/weapon_Gilded_Falchion_VwcOgqnzjf9LBj2S.json @@ -113,18 +113,7 @@ "name": "Powerful", "description": "On a successful attack, roll an additional damage die and discard the lowest result.", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", - "changes": [ - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" - } - ], + "changes": [], "_id": "ir4iKLIQ4CH1Qckn", "type": "base", "system": {}, diff --git a/src/packs/items/weapons/weapon_Greatbow_MXBpbqQsZFln4rZk.json b/src/packs/items/weapons/weapon_Greatbow_MXBpbqQsZFln4rZk.json index f97e5432..f56e77c7 100644 --- a/src/packs/items/weapons/weapon_Greatbow_MXBpbqQsZFln4rZk.json +++ b/src/packs/items/weapons/weapon_Greatbow_MXBpbqQsZFln4rZk.json @@ -113,18 +113,7 @@ "name": "Powerful", "description": "On a successful attack, roll an additional damage die and discard the lowest result.", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", - "changes": [ - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" - } - ], + "changes": [], "_id": "K4VgrDjVj1U1m9Ie", "type": "base", "system": {}, diff --git a/src/packs/items/weapons/weapon_Greatstaff_Yk8pTEmyLLi4095S.json b/src/packs/items/weapons/weapon_Greatstaff_Yk8pTEmyLLi4095S.json index 0fbfc2b4..66c12e5e 100644 --- a/src/packs/items/weapons/weapon_Greatstaff_Yk8pTEmyLLi4095S.json +++ b/src/packs/items/weapons/weapon_Greatstaff_Yk8pTEmyLLi4095S.json @@ -113,18 +113,7 @@ "name": "Powerful", "description": "On a successful attack, roll an additional damage die and discard the lowest result.", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", - "changes": [ - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" - } - ], + "changes": [], "_id": "904orawScurM9GjG", "type": "base", "system": {}, diff --git a/src/packs/items/weapons/weapon_Greatsword_70ysaFJDREwTgvZa.json b/src/packs/items/weapons/weapon_Greatsword_70ysaFJDREwTgvZa.json index 4707e397..f60e438d 100644 --- a/src/packs/items/weapons/weapon_Greatsword_70ysaFJDREwTgvZa.json +++ b/src/packs/items/weapons/weapon_Greatsword_70ysaFJDREwTgvZa.json @@ -118,16 +118,6 @@ "key": "system.evasion", "mode": 2, "value": "-1" - }, - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" } ], "_id": "cffkpiwGpEGhjiUC", diff --git a/src/packs/items/weapons/weapon_Improved_Greatstaff_LCuTrYXi4lhg6LqW.json b/src/packs/items/weapons/weapon_Improved_Greatstaff_LCuTrYXi4lhg6LqW.json index 5faa0b0e..cf1bdf63 100644 --- a/src/packs/items/weapons/weapon_Improved_Greatstaff_LCuTrYXi4lhg6LqW.json +++ b/src/packs/items/weapons/weapon_Improved_Greatstaff_LCuTrYXi4lhg6LqW.json @@ -113,18 +113,7 @@ "name": "Powerful", "description": "On a successful attack, roll an additional damage die and discard the lowest result.", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", - "changes": [ - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" - } - ], + "changes": [], "_id": "hnayB09P25ZW3gVY", "type": "base", "system": {}, diff --git a/src/packs/items/weapons/weapon_Improved_Greatsword_FPX4ouDrxXiQ5MDf.json b/src/packs/items/weapons/weapon_Improved_Greatsword_FPX4ouDrxXiQ5MDf.json index f8407b13..f71e5ea6 100644 --- a/src/packs/items/weapons/weapon_Improved_Greatsword_FPX4ouDrxXiQ5MDf.json +++ b/src/packs/items/weapons/weapon_Improved_Greatsword_FPX4ouDrxXiQ5MDf.json @@ -118,16 +118,6 @@ "key": "system.evasion", "mode": 2, "value": "-1" - }, - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" } ], "_id": "2nl35v8sPAudiOIb", diff --git a/src/packs/items/weapons/weapon_Legendary_Greatstaff_jDtvEabkHY1GFgfc.json b/src/packs/items/weapons/weapon_Legendary_Greatstaff_jDtvEabkHY1GFgfc.json index 0d317f0d..a5ea82f9 100644 --- a/src/packs/items/weapons/weapon_Legendary_Greatstaff_jDtvEabkHY1GFgfc.json +++ b/src/packs/items/weapons/weapon_Legendary_Greatstaff_jDtvEabkHY1GFgfc.json @@ -113,18 +113,7 @@ "name": "Powerful", "description": "On a successful attack, roll an additional damage die and discard the lowest result.", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", - "changes": [ - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" - } - ], + "changes": [], "_id": "OV1Ly7vX4owBUgLQ", "type": "base", "system": {}, diff --git a/src/packs/items/weapons/weapon_Legendary_Greatsword_zMZ46F9VR7zdTxb9.json b/src/packs/items/weapons/weapon_Legendary_Greatsword_zMZ46F9VR7zdTxb9.json index fb7a2ed3..840e7ec7 100644 --- a/src/packs/items/weapons/weapon_Legendary_Greatsword_zMZ46F9VR7zdTxb9.json +++ b/src/packs/items/weapons/weapon_Legendary_Greatsword_zMZ46F9VR7zdTxb9.json @@ -118,16 +118,6 @@ "key": "system.evasion", "mode": 2, "value": "-1" - }, - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" } ], "_id": "oRCiXSElN5xufUfn", diff --git a/src/packs/items/weapons/weapon_Mage_Orb_XKBmBUEoGLdLcuqQ.json b/src/packs/items/weapons/weapon_Mage_Orb_XKBmBUEoGLdLcuqQ.json index 8d3fd741..3b5983f5 100644 --- a/src/packs/items/weapons/weapon_Mage_Orb_XKBmBUEoGLdLcuqQ.json +++ b/src/packs/items/weapons/weapon_Mage_Orb_XKBmBUEoGLdLcuqQ.json @@ -113,18 +113,7 @@ "name": "Powerful", "description": "On a successful attack, roll an additional damage die and discard the lowest result.", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", - "changes": [ - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" - } - ], + "changes": [], "_id": "2J6vzNUel78JFypp", "type": "base", "system": {}, diff --git a/src/packs/rolltables/tables_Consumables_tF04P02yVN1YDVel.json b/src/packs/rolltables/tables_Consumables_tF04P02yVN1YDVel.json index c2413ec3..c3f5ffdc 100644 --- a/src/packs/rolltables/tables_Consumables_tF04P02yVN1YDVel.json +++ b/src/packs/rolltables/tables_Consumables_tF04P02yVN1YDVel.json @@ -1,7 +1,7 @@ { "name": "Consumables", "img": "icons/consumables/potions/bottle-corked-red.webp", - "description": "

To generate a random consumable, choose a rarity, roll the designated dice, and match the total to the item in the table:

", + "description": "", "results": [ { "type": "document", @@ -1511,8 +1511,27 @@ "default": 0, "Bgvu4A6AMkRFOTGR": 3 }, - "flags": {}, - "formula": "1d60", + "flags": { + "daggerheart": { + "activeAltFormula": "", + "formulaName": "Common", + "altFormula": { + "uoUn5fRTUkyg6U2G": { + "name": "Uncommon", + "formula": "3d12" + }, + "FGxM2yoxUUUd9Eov": { + "name": "Rare", + "formula": "4d12" + }, + "HZ2hRBxu0k8IW0jC": { + "name": "Legendary", + "formula": "5d12" + } + } + } + }, + "formula": "2d12", "_id": "tF04P02yVN1YDVel", "sort": 300000, "_key": "!tables!tF04P02yVN1YDVel" diff --git a/src/packs/rolltables/tables_Loot_S61Shlt2I5CbLRjz.json b/src/packs/rolltables/tables_Loot_S61Shlt2I5CbLRjz.json index 9517eadd..2151ae81 100644 --- a/src/packs/rolltables/tables_Loot_S61Shlt2I5CbLRjz.json +++ b/src/packs/rolltables/tables_Loot_S61Shlt2I5CbLRjz.json @@ -1,7 +1,7 @@ { "name": "Loot", "img": "icons/commodities/treasure/brooch-gold-ruby.webp", - "description": "

To generate a random item, choose a rarity, roll the designated dice, and match the total to the item in the table:

", + "description": "", "results": [ { "type": "document", @@ -1511,8 +1511,27 @@ "default": 0, "Bgvu4A6AMkRFOTGR": 3 }, - "flags": {}, - "formula": "1d60", + "flags": { + "daggerheart": { + "activeAltFormula": "", + "formulaName": "Common", + "altFormula": { + "hJJtajaMk14bYM4X": { + "name": "Uncommon", + "formula": "3d12" + }, + "yDVeXdKpG7LzjHWa": { + "name": "Rare", + "formula": "4d12" + }, + "qPHNIuUgWAHauI6V": { + "name": "Legendary", + "formula": "5d12" + } + } + } + }, + "formula": "2d12", "_id": "S61Shlt2I5CbLRjz", "sort": 200000, "_key": "!tables!S61Shlt2I5CbLRjz" diff --git a/src/packs/rolltables/tables_Table_of_Random_Objectives_I5L1dlgxXTNrCCkL.json b/src/packs/rolltables/tables_Random_Objectives_I5L1dlgxXTNrCCkL.json similarity index 95% rename from src/packs/rolltables/tables_Table_of_Random_Objectives_I5L1dlgxXTNrCCkL.json rename to src/packs/rolltables/tables_Random_Objectives_I5L1dlgxXTNrCCkL.json index b10127e7..9165f9d7 100644 --- a/src/packs/rolltables/tables_Table_of_Random_Objectives_I5L1dlgxXTNrCCkL.json +++ b/src/packs/rolltables/tables_Random_Objectives_I5L1dlgxXTNrCCkL.json @@ -1,7 +1,7 @@ { - "name": "Table of Random Objectives", + "name": "Random Objectives", "img": "icons/sundries/documents/document-torn-diagram-tan.webp", - "description": "

Layering Goals Other than Attrition into Combat

", + "description": "", "results": [ { "type": "text", @@ -311,7 +311,20 @@ "default": 0, "Bgvu4A6AMkRFOTGR": 3 }, - "flags": {}, + "flags": { + "daggerheart": { + "formulaName": "Roll Formula", + "altFormula": {}, + "activeAltFormula": null, + "flags": { + "daggerheart": { + "formulaName": "Roll Formula", + "altFormula": {}, + "activeAltFormula": null + } + } + } + }, "formula": "1d12", "_id": "I5L1dlgxXTNrCCkL", "sort": 400000, diff --git a/src/packs/subclasses/feature_Advanced_Training_uGcs785h94RMtueH.json b/src/packs/subclasses/feature_Advanced_Training_uGcs785h94RMtueH.json index 8f9c63f1..16a5cd79 100644 --- a/src/packs/subclasses/feature_Advanced_Training_uGcs785h94RMtueH.json +++ b/src/packs/subclasses/feature_Advanced_Training_uGcs785h94RMtueH.json @@ -16,7 +16,51 @@ "artist": "" } }, - "effects": [], + "effects": [ + { + "name": "Advanced Training", + "type": "base", + "system": { + "rangeDependence": { + "enabled": false, + "type": "withinRange", + "target": "hostile", + "range": "melee" + } + }, + "_id": "bKOuMxhB2Jth3j2T", + "img": "icons/creatures/mammals/wolf-howl-moon-gray.webp", + "changes": [ + { + "key": "system.companionData.levelupChoices", + "mode": 2, + "value": "2", + "priority": null + } + ], + "disabled": false, + "duration": { + "startTime": null, + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null + }, + "description": "

Choose two additional level-up options for your companion.

", + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null + }, + "_key": "!items.effects!uGcs785h94RMtueH.bKOuMxhB2Jth3j2T" + } + ], "sort": 0, "ownership": { "default": 0, diff --git a/src/packs/subclasses/feature_Expert_Training_iCXtOWBKv1FdKdWz.json b/src/packs/subclasses/feature_Expert_Training_iCXtOWBKv1FdKdWz.json index e5850da6..06819dc9 100644 --- a/src/packs/subclasses/feature_Expert_Training_iCXtOWBKv1FdKdWz.json +++ b/src/packs/subclasses/feature_Expert_Training_iCXtOWBKv1FdKdWz.json @@ -16,7 +16,51 @@ "artist": "" } }, - "effects": [], + "effects": [ + { + "name": "Expert Training", + "type": "base", + "system": { + "rangeDependence": { + "enabled": false, + "type": "withinRange", + "target": "hostile", + "range": "melee" + } + }, + "_id": "rknTONvaUDZ2Yz1W", + "img": "icons/creatures/mammals/dog-husky-white-blue.webp", + "changes": [ + { + "key": "system.companionData.levelupChoices", + "mode": 2, + "value": "1", + "priority": null + } + ], + "disabled": false, + "duration": { + "startTime": null, + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null + }, + "description": "

Choose an additional level-up option for your companion.

", + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null + }, + "_key": "!items.effects!iCXtOWBKv1FdKdWz.rknTONvaUDZ2Yz1W" + } + ], "sort": 0, "ownership": { "default": 0, diff --git a/styles/less/dialog/character-reset/sheet.less b/styles/less/dialog/character-reset/sheet.less new file mode 100644 index 00000000..44312a3e --- /dev/null +++ b/styles/less/dialog/character-reset/sheet.less @@ -0,0 +1,27 @@ +.daggerheart.dh-style.dialog.views.character-reset { + .character-reset-container { + display: flex; + flex-direction: column; + gap: 8px; + + legend { + padding: 0 4px; + } + + .character-reset-header { + font-size: var(--font-size-18); + text-align: center; + } + + .reset-data-container { + display: grid; + grid-template-columns: 3fr 2fr; + align-items: center; + gap: 4px; + + label { + font-weight: bold; + } + } + } +} diff --git a/styles/less/dialog/death-move/death-move-container.less b/styles/less/dialog/death-move/death-move-container.less index e5803d95..903704e3 100644 --- a/styles/less/dialog/death-move/death-move-container.less +++ b/styles/less/dialog/death-move/death-move-container.less @@ -1,55 +1,56 @@ -@import '../../utils/spacing.less'; -@import '../../utils/colors.less'; -@import '../../utils/fonts.less'; - -.daggerheart.dh-style.dialog.death-move { - .death-move-container { - display: flex; - flex-direction: column; - gap: 5px; - - .moves-list { - .move-item { - display: flex; - align-items: center; - gap: 5px; - - &:hover { - background-color: light-dark(@soft-shadow, @soft-white-shadow); - cursor: pointer; - } - padding: 5px; - border-radius: 5px; - transition: background-color 0.3s ease-in-out; - - .label { - display: flex; - align-items: center; - gap: 10px; - cursor: pointer; - flex: 1; - i { - text-align: center; - width: 30px; - } - } - - input[type='radio'] { - margin-left: auto; - } - } - } - } - - footer { - margin-top: 8px; - display: flex; - gap: 8px; - - button { - flex: 1; - height: 40px; - font-weight: 600; - } - } -} +@import '../../utils/spacing.less'; +@import '../../utils/colors.less'; +@import '../../utils/fonts.less'; + +.daggerheart.dh-style.dialog.death-move { + .death-move-container { + display: flex; + flex-direction: column; + gap: 5px; + + .moves-list { + .move-item { + display: flex; + align-items: center; + gap: 5px; + padding: 5px; + border-radius: 5px; + transition: background-color 0.3s ease-in-out; + height: 37px; + + &:hover { + background-color: light-dark(@soft-shadow, @soft-white-shadow); + cursor: pointer; + } + + .label { + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; + flex: 1; + i { + text-align: center; + width: 30px; + } + } + + input[type='radio'] { + margin-left: auto; + } + } + } + } + + footer { + margin-top: 8px; + display: flex; + gap: 8px; + + button { + flex: 1; + height: 40px; + font-weight: 600; + } + } +} diff --git a/styles/less/dialog/index.less b/styles/less/dialog/index.less index b5ed8764..733cdd1c 100644 --- a/styles/less/dialog/index.less +++ b/styles/less/dialog/index.less @@ -38,4 +38,8 @@ @import './item-transfer/sheet.less'; -@import './settings/change-currency-icon.less'; \ No newline at end of file +@import './settings/change-currency-icon.less'; + +@import './risk-it-all/sheet.less'; + +@import './character-reset/sheet.less'; diff --git a/styles/less/dialog/risk-it-all/sheet.less b/styles/less/dialog/risk-it-all/sheet.less new file mode 100644 index 00000000..db34a5c1 --- /dev/null +++ b/styles/less/dialog/risk-it-all/sheet.less @@ -0,0 +1,60 @@ +.daggerheart.dialog.dh-style.views.risk-it-all { + .risk-it-all-container { + display: flex; + align-items: center; + flex-direction: column; + gap: 8px; + text-align: center; + + header { + font-weight: bold; + font-size: var(--font-size-20); + } + + .section-label { + font-size: var(--font-size-18); + text-decoration: underline; + } + + .remaining-section { + display: flex; + flex-direction: column; + gap: 2px; + } + + .resource-section { + width: 100%; + display: flex; + gap: 8px; + } + + .final-section { + width: 100%; + display: flex; + flex-direction: column; + gap: 2px; + + .final-section-values-container { + width: 100%; + display: flex; + align-items: center; + justify-content: space-evenly; + + .final-section-value-container { + display: flex; + flex-direction: column; + gap: 2px; + } + } + } + } + + footer { + width: 100%; + display: flex; + + button { + flex: 1; + } + } +} diff --git a/styles/less/global/chat.less b/styles/less/global/chat.less index 69ee369a..b9478ea4 100644 --- a/styles/less/global/chat.less +++ b/styles/less/global/chat.less @@ -2,19 +2,25 @@ @import '../utils/fonts.less'; @import '../utils/mixin.less'; -.theme-light { - .daggerheart.chat-sidebar .chat-log, - #chat-notifications .chat-log { - .chat-message { - background-image: url('../assets/parchments/dh-parchment-light.png'); +.daggerheart.chat-sidebar.theme-light, +#interface.theme-light { + .chat-log .chat-message { + background-image: url('../assets/parchments/dh-parchment-light.png'); - .message-header .message-header-metadata .message-metadata, - .message-header .message-header-main .message-sub-header-container { - color: @dark; - } + .message-header .message-header-metadata .message-metadata, + .message-header .message-header-main .message-sub-header-container { + color: @dark; + } - .message-header .message-header-main .message-sub-header-container h4 { - color: @dark-blue; + .message-header .message-header-main .message-sub-header-container h4 { + color: @dark-blue; + } + + .message-content { + .table-draw { + .table-description { + color: @dark; + } } } } @@ -85,6 +91,7 @@ .message-content { padding-bottom: 8px; + .flavor-text { font-size: var(--font-size-12); line-height: 20px; @@ -92,6 +99,33 @@ text-align: center; display: block; } + + .table-draw { + .table-flavor { + padding-top: 5px; + padding-bottom: 0.5rem; + font-size: var(--font-size-12); + } + + .table-description { + color: @beige; + font-style: italic; + + &.flavor-spaced { + padding-top: 0; + } + } + + .table-results { + .description { + flex-basis: min-content; + + > p:first-of-type { + margin-top: 0; + } + } + } + } } } } diff --git a/styles/less/global/dialog.less b/styles/less/global/dialog.less index 8c532c2b..a3400700 100644 --- a/styles/less/global/dialog.less +++ b/styles/less/global/dialog.less @@ -67,6 +67,35 @@ } } + .dialog-selection-container { + display: flex; + gap: 10px; + flex-wrap: wrap; + + .selection-chip { + display: flex; + align-items: center; + border-radius: 5px; + width: fit-content; + gap: 5px; + cursor: pointer; + padding: 5px; + background: light-dark(@dark-blue-10, @golden-10); + color: light-dark(@dark-blue, @golden); + + .label { + font-style: normal; + font-weight: 400; + font-size: var(--font-size-14); + line-height: 17px; + } + + &.selected { + background: light-dark(@dark-blue-40, @golden-40); + } + } + } + .standard-form { font-family: @font-body; } diff --git a/styles/less/global/enrichment.less b/styles/less/global/enrichment.less index 2ad3975a..8256d60a 100644 --- a/styles/less/global/enrichment.less +++ b/styles/less/global/enrichment.less @@ -1,5 +1,6 @@ .measured-template-button, .enriched-damage-button, +.fate-roll-button, .duality-roll-button { display: inline; diff --git a/styles/less/global/global.less b/styles/less/global/global.less index 6cc63c2a..6c63fe7a 100644 --- a/styles/less/global/global.less +++ b/styles/less/global/global.less @@ -51,3 +51,14 @@ } } } + +/* TODO: Remove me when this issue is resolved https://github.com/foundryvtt/foundryvtt/issues/13734 */ +body.theme-dark, +.themed.theme-dark { + color-scheme: dark; +} + +body.theme-light, +.themed.theme-light { + color-scheme: light; +} diff --git a/styles/less/global/index.less b/styles/less/global/index.less index f51140de..216dc9f4 100644 --- a/styles/less/global/index.less +++ b/styles/less/global/index.less @@ -10,7 +10,6 @@ @import './tab-description.less'; @import './tab-features.less'; @import './tab-effects.less'; -@import './tab-settings.less'; @import './item-header.less'; @import './feature-section.less'; @import './inventory-item.less'; diff --git a/styles/less/global/item-header.less b/styles/less/global/item-header.less index 073762e0..f47ca7dc 100755 --- a/styles/less/global/item-header.less +++ b/styles/less/global/item-header.less @@ -160,7 +160,7 @@ .item-description { display: flex; flex-direction: column; - gap: 10px; + gap: 7px; } h3 { diff --git a/styles/less/global/tab-settings.less b/styles/less/global/tab-settings.less deleted file mode 100644 index 3d5248be..00000000 --- a/styles/less/global/tab-settings.less +++ /dev/null @@ -1,8 +0,0 @@ -@import '../utils/colors.less'; -@import '../utils/fonts.less'; - -.sheet.daggerheart.dh-style { - .tab.settings { - margin-bottom: 36px; - } -} diff --git a/styles/less/hud/token-hud/token-hud.less b/styles/less/hud/token-hud/token-hud.less index 46003975..ea58f673 100644 --- a/styles/less/hud/token-hud/token-hud.less +++ b/styles/less/hud/token-hud/token-hud.less @@ -24,13 +24,13 @@ font-weight: bold; } } + } - .clown-car img { - transition: 0.5s; + .clown-car img { + transition: 0.5s; - &.flipped { - transform: scaleX(-1); - } + &.flipped { + transform: scaleX(-1); } } diff --git a/styles/less/sheets/actions/actions.less b/styles/less/sheets/actions/actions.less index 6796006c..07c99491 100644 --- a/styles/less/sheets/actions/actions.less +++ b/styles/less/sheets/actions/actions.less @@ -1,4 +1,53 @@ .application.daggerheart.dh-style.action-config { + .actor-summon-items { + width: 100%; + display: flex; + flex-direction: column; + gap: 10px; + + .actor-summon-line { + display: flex; + align-items: center; + gap: 5px; + border-radius: 3px; + + .actor-summon-name { + flex: 2; + display: flex; + align-items: center; + gap: 5px; + + img { + height: 40px; + } + } + + .actor-summon-controls { + flex: 1; + display: flex; + align-items: center; + gap: 5px; + + .controls { + display: flex; + gap: 5px; + } + } + } + + .summon-dragger { + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + height: 40px; + margin-top: 10px; + border: 1px dashed light-dark(@dark-blue-50, @beige-50); + border-radius: 3px; + color: light-dark(@dark-blue-50, @beige-50); + } + } + .trigger-data { width: 100%; display: flex; diff --git a/styles/less/sheets/actors/character/header.less b/styles/less/sheets/actors/character/header.less index 93d6c6be..5e8ef002 100644 --- a/styles/less/sheets/actors/character/header.less +++ b/styles/less/sheets/actors/character/header.less @@ -148,7 +148,7 @@ padding: 0 0.375rem; } - button[data-action=viewParty] { + button[data-action='viewParty'] { margin-right: 6px; } } @@ -195,6 +195,11 @@ .hope-value { display: flex; cursor: pointer; + + &.scar { + cursor: initial; + opacity: 0.6; + } } } } diff --git a/styles/less/sheets/actors/companion/header.less b/styles/less/sheets/actors/companion/header.less index 3616a6b3..2a162a25 100644 --- a/styles/less/sheets/actors/companion/header.less +++ b/styles/less/sheets/actors/companion/header.less @@ -207,7 +207,7 @@ .input-section { display: flex; align-items: center; - justify-content: space-between; + gap: 8px; } } diff --git a/styles/less/sheets/index.less b/styles/less/sheets/index.less index 44a6aa4d..1bdb451a 100644 --- a/styles/less/sheets/index.less +++ b/styles/less/sheets/index.less @@ -39,3 +39,6 @@ @import './items/feature.less'; @import './items/heritage.less'; @import './items/item-sheet-shared.less'; + +@import './rollTables/sheet.less'; +@import './actions/actions.less'; diff --git a/styles/less/sheets/rollTables/sheet.less b/styles/less/sheets/rollTables/sheet.less new file mode 100644 index 00000000..a7c05455 --- /dev/null +++ b/styles/less/sheets/rollTables/sheet.less @@ -0,0 +1,29 @@ +.application.sheet.roll-table-sheet { + .formulas-section { + legend { + margin-left: auto; + margin-right: auto; + } + + .formulas-container { + display: grid; + grid-template-columns: 1fr 1fr 40px; + gap: 10px; + text-align: center; + + .formula-button { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + } + } + } + + .roll-table-view-formula-container { + width: fit-content; + display: flex; + align-items: center; + gap: 4px; + } +} diff --git a/styles/less/ui/chat/ability-use.less b/styles/less/ui/chat/ability-use.less index 88302d0d..b590911d 100644 --- a/styles/less/ui/chat/ability-use.less +++ b/styles/less/ui/chat/ability-use.less @@ -2,6 +2,7 @@ @import '../../utils/fonts.less'; @import '../../utils/spacing.less'; +.daggerheart.chat-sidebar.theme-light, #interface.theme-light { .daggerheart.chat.domain-card { .domain-card-move .domain-card-header { diff --git a/styles/less/ui/chat/action.less b/styles/less/ui/chat/action.less index 817b0acd..a3d2f3cc 100644 --- a/styles/less/ui/chat/action.less +++ b/styles/less/ui/chat/action.less @@ -2,6 +2,7 @@ @import '../../utils/fonts.less'; @import '../../utils/spacing.less'; +.daggerheart.chat-sidebar.theme-light, #interface.theme-light { .daggerheart.chat.action { .action-move .action-section { @@ -98,6 +99,61 @@ .description { padding: 8px; + + .summons-header { + font-size: var(--font-size-14); + text-align: center; + display: flex; + align-items: center; + justify-content: center; + + span { + width: 100%; + } + + &:before, + &:after { + content: ' '; + height: 1px; + width: 100%; + } + + &:before { + background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, light-dark(@dark-blue, @golden) 100%); + } + + &:after { + background: linear-gradient(90deg, light-dark(@dark-blue, @golden) 0%, rgba(0, 0, 0, 0) 100%); + } + } + + .summons-container { + display: flex; + flex-direction: column; + gap: 4px; + + .summon-container { + display: flex; + align-items: center; + justify-content: space-between; + + .summon-label-container { + flex: 1; + display: flex; + align-items: center; + gap: 4px; + + img { + height: 32px; + } + + label { + display: flex; + flex-wrap: wrap; + } + } + } + } } .ability-card-footer { diff --git a/styles/less/ui/chat/chat.less b/styles/less/ui/chat/chat.less index 6f0e5e85..cf269cee 100644 --- a/styles/less/ui/chat/chat.less +++ b/styles/less/ui/chat/chat.less @@ -2,9 +2,9 @@ @import '../../utils/fonts.less'; @import '../../utils/spacing.less'; +.daggerheart.chat-sidebar.theme-light, #interface.theme-light { - .daggerheart.chat-sidebar .chat-log, - #chat-notifications .chat-log { + .chat-log { --text-color: @dark-blue; --bg-color: @dark-blue-40; @@ -14,6 +14,7 @@ color: @dark; } + &.fate, &.duality { background-image: url(../assets/parchments/dh-parchment-dark.png); @@ -66,6 +67,7 @@ } } + &.fate, &.critical { --text-color: @chat-purple; --bg-color: @chat-purple-40; @@ -80,7 +82,7 @@ } } - &:not(.duality) { + &:not(.duality .fate) { .font-20 { color: @dark; } @@ -173,6 +175,26 @@ } } + &.fate { + &.hope { + --text-color: @golden; + --bg-color: @golden-40; + .message-header, + .message-content { + background-color: @golden-bg; + } + } + + &.fear { + --text-color: @chat-blue; + --bg-color: @chat-blue-40; + .message-header, + .message-content { + background-color: @chat-blue-bg; + } + } + } + &.duality { &.hope { --text-color: @golden; diff --git a/styles/less/ui/chat/damage-summary.less b/styles/less/ui/chat/damage-summary.less index 3fea45e5..b47cd41f 100644 --- a/styles/less/ui/chat/damage-summary.less +++ b/styles/less/ui/chat/damage-summary.less @@ -1,5 +1,6 @@ @import '../../utils/colors.less'; +.daggerheart.chat-sidebar.theme-light, #interface.theme-light { .daggerheart.chat.damage-summary .token-target-container { &:hover { diff --git a/styles/less/ui/chat/deathmoves.less b/styles/less/ui/chat/deathmoves.less new file mode 100644 index 00000000..4cfe1b13 --- /dev/null +++ b/styles/less/ui/chat/deathmoves.less @@ -0,0 +1,152 @@ +@import '../../utils/colors.less'; +@import '../../utils/fonts.less'; +@import '../../utils/spacing.less'; + +#interface.theme-light { + .daggerheart.chat.death-moves { + .death-moves-list .death-move { + &:hover { + background: @dark-blue-10; + } + + .death-label { + border-bottom: 1px solid @dark-blue; + + .header-label .title { + color: @dark-blue; + } + .header-label .label { + color: @dark; + } + } + + .fa-chevron-down { + color: @dark-blue; + } + } + + .description { + color: @dark; + } + + .result { + color: @dark; + } + + .risk-it-all-button { + color: @dark; + } + } +} + +.daggerheart.chat { + &.death-moves { + display: flex; + flex-direction: column; + align-items: center; + + details[open] { + .fa-chevron-down { + transform: rotate(180deg); + transition: all 0.3s ease; + } + } + + .death-moves-list { + display: flex; + flex-direction: column; + gap: 5px; + width: 100%; + + .fa-chevron-down { + transition: all 0.3s ease; + margin-left: auto; + } + + .death-move { + width: 100%; + + .death-label { + display: flex; + align-items: center; + gap: 5px; + border-bottom: 1px solid @golden; + margin: 0 8px; + padding-bottom: 5px; + width: -webkit-fill-available; + + &:hover { + background: light-dark(@dark-blue-10, @golden-10); + cursor: pointer; + transition: all 0.3s ease; + } + + .death-image { + width: 40px; + height: 40px; + border-radius: 3px; + } + + .header-label { + padding: 8px; + .title { + font-size: var(--font-size-16); + color: @golden; + font-weight: 700; + } + .label { + font-size: var(--font-size-12); + color: @beige; + margin: 0; + } + } + } + + .description { + padding: 8px; + } + } + + .action-use-button-parent { + width: 100%; + + .action-use-target { + display: flex; + align-items: center; + justify-content: space-between; + gap: 4px; + width: 100%; + padding: 4px 8px 10px 40px; + font-size: var(--font-size-12); + + label { + font-weight: bold; + } + + select { + flex: 1; + } + } + } + + .action-use-button { + width: -webkit-fill-available; + margin: 0 8px; + font-weight: 600; + height: 40px; + } + } + + .result { + padding: 8px; + font-weight: bold; + } + + .risk-it-all-button { + width: -webkit-fill-available; + margin: 0 8px; + font-weight: 600; + height: 40px; + } + } +} diff --git a/styles/less/ui/chat/downtime.less b/styles/less/ui/chat/downtime.less index a99bde33..ca0cd090 100644 --- a/styles/less/ui/chat/downtime.less +++ b/styles/less/ui/chat/downtime.less @@ -2,6 +2,7 @@ @import '../../utils/fonts.less'; @import '../../utils/spacing.less'; +.daggerheart.chat-sidebar.theme-light, #interface.theme-light { .daggerheart.chat.downtime { .downtime-moves-list .downtime-move { @@ -103,7 +104,7 @@ width: 100%; .action-use-target { - display:flex; + display: flex; align-items: center; justify-content: space-between; gap: 4px; @@ -127,7 +128,6 @@ font-weight: 600; height: 40px; } - } } } diff --git a/styles/less/ui/chat/effect-summary.less b/styles/less/ui/chat/effect-summary.less index 3d72571d..87d53eeb 100644 --- a/styles/less/ui/chat/effect-summary.less +++ b/styles/less/ui/chat/effect-summary.less @@ -1,5 +1,6 @@ @import '../../utils/colors.less'; +.daggerheart.chat-sidebar.theme-light, #interface.theme-light { .daggerheart.chat.effect-summary { .effect-header, diff --git a/styles/less/ui/chat/group-roll.less b/styles/less/ui/chat/group-roll.less index 02b8e312..9ed87220 100644 --- a/styles/less/ui/chat/group-roll.less +++ b/styles/less/ui/chat/group-roll.less @@ -125,9 +125,9 @@ .group-roll-trait { padding: 2px 8px; - border: 1px solid light-dark(white, white); + border: 1px solid light-dark(@dark-blue, white); border-radius: 6px; - color: light-dark(white, white); + color: light-dark(@dark-blue, white); background: light-dark(@beige-80, @beige-80); } } diff --git a/styles/less/ui/chat/sheet.less b/styles/less/ui/chat/sheet.less index 3d47a9b5..b632db35 100644 --- a/styles/less/ui/chat/sheet.less +++ b/styles/less/ui/chat/sheet.less @@ -1,6 +1,7 @@ @import '../../utils/colors.less'; @import '../../utils/fonts.less'; +.daggerheart.chat-sidebar.theme-light, #interface.theme-light { .chat-message:not(.duality) .message-content { color: @dark; diff --git a/styles/less/ui/index.less b/styles/less/ui/index.less index 25f51d0f..065e43c5 100644 --- a/styles/less/ui/index.less +++ b/styles/less/ui/index.less @@ -6,6 +6,7 @@ @import './chat/effect-summary.less'; @import './chat/group-roll.less'; @import './chat/refresh-message.less'; +@import './chat/deathmoves.less'; @import './chat/sheet.less'; @import './combat-sidebar/combat-sidebar.less'; diff --git a/styles/less/ui/sidebar/daggerheartMenu.less b/styles/less/ui/sidebar/daggerheartMenu.less index 80eda9a1..677214d7 100644 --- a/styles/less/ui/sidebar/daggerheartMenu.less +++ b/styles/less/ui/sidebar/daggerheartMenu.less @@ -5,6 +5,8 @@ display: flex; flex-direction: column; gap: 8px; + overflow: auto; + height: 100%; } h2 { diff --git a/styles/less/ui/sidebar/tabs.less b/styles/less/ui/sidebar/tabs.less index e9de2924..c620ff91 100644 --- a/styles/less/ui/sidebar/tabs.less +++ b/styles/less/ui/sidebar/tabs.less @@ -1,4 +1,4 @@ -.theme-light #interface #ui-right #sidebar { +.theme-light#interface #ui-right #sidebar { menu li button img { filter: @grey-filter; } diff --git a/system.json b/system.json index 5570bdbf..8624bab7 100644 --- a/system.json +++ b/system.json @@ -2,7 +2,7 @@ "id": "daggerheart", "title": "Daggerheart", "description": "An unofficial implementation of the Daggerheart system", - "version": "1.4.6", + "version": "1.6.0", "compatibility": { "minimum": "13.346", "verified": "13.351", @@ -285,6 +285,7 @@ }, "ChatMessage": { "dualityRoll": {}, + "fateRoll": {}, "adversaryRoll": {}, "damageRoll": {}, "abilityUse": {}, diff --git a/templates/actionTypes/summon.hbs b/templates/actionTypes/summon.hbs new file mode 100644 index 00000000..429977d9 --- /dev/null +++ b/templates/actionTypes/summon.hbs @@ -0,0 +1,50 @@ +
+ + {{localize "DAGGERHEART.ACTIONS.TYPES.summon.name"}} + + +
    + {{#each @root.summons as |summon index|}} +
  • +
    + +

    + {{summon.actor.name}} +

    +
    + +
    +
    +
    + +
    +
    + + +
    +
  • + {{/each}} +
    + {{localize "DAGGERHEART.ACTIONS.Settings.summon.dropSummonsHere"}} +
    +
+
\ No newline at end of file diff --git a/templates/dialogs/characterReset.hbs b/templates/dialogs/characterReset.hbs new file mode 100644 index 00000000..298826e5 --- /dev/null +++ b/templates/dialogs/characterReset.hbs @@ -0,0 +1,33 @@ +
+
+
{{localize "DAGGERHEART.APPLICATIONS.CharacterReset.headerTitle"}}
+ +
+ {{localize "DAGGERHEART.APPLICATIONS.CharacterReset.alwaysDeleteSection"}} + +
+ {{#each this.data.delete as | data key|}} +
+ + +
+ {{/each}} +
+
+ +
+ {{localize "DAGGERHEART.APPLICATIONS.CharacterReset.optionalDeleteSection"}} + +
+ {{#each this.data.optional as | data key|}} +
+ + +
+ {{/each}} +
+
+ + +
+
\ No newline at end of file diff --git a/templates/dialogs/dice-roll/damageSelection.hbs b/templates/dialogs/dice-roll/damageSelection.hbs index ba542666..c0dbae62 100644 --- a/templates/dialogs/dice-roll/damageSelection.hbs +++ b/templates/dialogs/dice-roll/damageSelection.hbs @@ -2,6 +2,20 @@

{{title}}

+ + {{#if hasSelectedEffects}} +
+ {{localize "DAGGERHEART.GENERAL.Effect.plural"}} + + {{#each selectedEffects as |effect id|}} +
+ + {{effect.name}} +
+ {{/each}} +
+ {{/if}} + {{#each @root.formula}}
{{localize "DAGGERHEART.GENERAL.formula"}}: {{roll.formula}} diff --git a/templates/dialogs/dice-roll/rollSelection.hbs b/templates/dialogs/dice-roll/rollSelection.hbs index c7a9b0f9..5851a33d 100644 --- a/templates/dialogs/dice-roll/rollSelection.hbs +++ b/templates/dialogs/dice-roll/rollSelection.hbs @@ -68,82 +68,127 @@ {{/if}} {{/if}} {{/if}} + {{#if (eq @root.rollType 'FateRoll')}} + {{#if (eq @root.roll.fateDie 'Hope')}} + +
+ +
+ {{localize "DAGGERHEART.GENERAL.hope"}} + +
+
+ {{/if}} + + {{#if (eq @root.roll.fateDie 'Fear')}} +
+ +
+ {{localize "DAGGERHEART.GENERAL.fear"}} + +
+
+ {{/if}} + + {{/if}}
- {{#if experiences.length}} -
- {{localize "DAGGERHEART.GENERAL.experience.plural"}} - {{#each experiences}} - {{#if name}} -
- - {{name}} +{{value}} -
- {{/if}} - {{/each}} -
- {{/if}} + {{#if (ne @root.rollType 'FateRoll')}} + {{#if hasSelectedEffects}} +
+ {{localize "DAGGERHEART.GENERAL.Effect.plural"}} -
- {{localize "DAGGERHEART.GENERAL.Modifier.plural"}} -
- - -
- {{#unless (eq @root.rollType 'D20Roll')}} + {{#each selectedEffects as |effect id|}} +
+ + {{effect.name}} +
+ {{/each}} +
+ {{/if}} + + {{#if experiences.length}} +
+ {{localize "DAGGERHEART.GENERAL.experience.plural"}} + {{#each experiences}} + {{#if name}} +
+ + {{name}} +{{value}} +
+ {{/if}} + {{/each}} +
+ {{/if}} +
+ {{#if @root.advantage}} + {{localize "DAGGERHEART.GENERAL.Modifier.plural"}}
- - + +
- {{#if abilities}} - {{localize "DAGGERHEART.GENERAL.traitModifier"}} - + {{#times 10}} + + {{/times}} + + + + {{#if abilities}} + {{localize "DAGGERHEART.GENERAL.traitModifier"}} + + {{/if}} + {{/unless}} + {{/if}} + {{#if @root.rallyDie.length}} + {{localize "DAGGERHEART.CLASS.Feature.rallyDice"}} + {{/if}} - {{/unless}} - {{#if @root.rallyDie.length}} - {{localize "DAGGERHEART.CLASS.Feature.rallyDice"}} - - {{/if}} - {{#if (eq @root.rollType 'DualityRoll')}}{{localize "DAGGERHEART.GENERAL.situationalBonus"}}{{/if}} - -
+ {{#if (eq @root.rollType 'DualityRoll')}}{{localize "DAGGERHEART.GENERAL.situationalBonus"}}{{/if}} + +
+ {{/if}} {{/unless}} {{#if (or costs uses)}} {{> 'systems/daggerheart/templates/dialogs/dice-roll/costSelection.hbs'}} {{/if}} - {{localize "DAGGERHEART.GENERAL.formula"}}: {{@root.formula}} + {{#if (ne @root.rollType 'FateRoll')}} + {{localize "DAGGERHEART.GENERAL.formula"}}: {{@root.formula}} + {{/if}}
+
+
+ + +
+ + +
+ +
+
+ + {{this.final.hitPoints.value}}/{{this.final.hitPoints.max}} +
+
+ + {{this.final.stress.value}}/{{this.final.stress.max}} +
+
+
+ +
+ +
+ + \ No newline at end of file diff --git a/templates/hud/tokenHUD.hbs b/templates/hud/tokenHUD.hbs index f079e5d9..1ba29621 100644 --- a/templates/hud/tokenHUD.hbs +++ b/templates/hud/tokenHUD.hbs @@ -11,6 +11,11 @@ + {{#if hasCompanion}} + + {{/if}} {{#if canConfigure}} {{/if}} diff --git a/templates/settings/automation-settings/general.hbs b/templates/settings/automation-settings/general.hbs index d49ef9b8..bd91b2b1 100644 --- a/templates/settings/automation-settings/general.hbs +++ b/templates/settings/automation-settings/general.hbs @@ -18,7 +18,6 @@ {{formGroup settingFields.schema.fields.hordeDamage value=settingFields._source.hordeDamage localize=true}} {{formGroup settingFields.schema.fields.effects.fields.rangeDependent value=settingFields._source.effects.rangeDependent localize=true}} {{formGroup settingFields.schema.fields.levelupAuto value=settingFields._source.levelupAuto localize=true}} - {{formGroup settingFields.schema.fields.playerCanEditSheet value=settingFields._source.playerCanEditSheet localize=true}} {{formGroup settingFields.schema.fields.damageReductionRulesDefault value=settingFields._source.damageReductionRulesDefault localize=true}} {{formGroup settingFields.schema.fields.resourceScrollTexts value=settingFields._source.resourceScrollTexts localize=true}} diff --git a/templates/settings/automation-settings/rules.hbs b/templates/settings/automation-settings/rules.hbs index a12c2999..24f0b262 100644 --- a/templates/settings/automation-settings/rules.hbs +++ b/templates/settings/automation-settings/rules.hbs @@ -9,11 +9,12 @@ {{formGroup settingFields.schema.fields.defeated.fields.enabled value=settingFields._source.defeated.enabled localize=true}} - {{formGroup settingFields.schema.fields.defeated.fields.overlay value=settingFields._source.defeated.overlay localize=true}} + {{formGroup settingFields.schema.fields.defeated.fields.overlay value=settingFields._source.defeated.overlay localize=true}} {{formGroup settingFields.schema.fields.defeated.fields.characterDefault value=settingFields._source.defeated.characterDefault labelAttr="name" localize=true}} {{formGroup settingFields.schema.fields.defeated.fields.adversaryDefault value=settingFields._source.defeated.adversaryDefault labelAttr="name" localize=true}} {{formGroup settingFields.schema.fields.defeated.fields.companionDefault value=settingFields._source.defeated.companionDefault labelAttr="name" localize=true}} + {{formGroup settingFields.schema.fields.defeated.fields.deathMoveIcon value=settingFields._source.defeated.deathMoveIcon localize=true}} {{formGroup settingFields.schema.fields.defeated.fields.deadIcon value=settingFields._source.defeated.deadIcon localize=true}} {{formGroup settingFields.schema.fields.defeated.fields.defeatedIcon value=settingFields._source.defeated.defeatedIcon localize=true}} {{formGroup settingFields.schema.fields.defeated.fields.unconsciousIcon value=settingFields._source.defeated.unconsciousIcon localize=true}} diff --git a/templates/settings/homebrew-settings/downtime.hbs b/templates/settings/homebrew-settings/downtime.hbs index 8612f3d5..25f4c95d 100644 --- a/templates/settings/homebrew-settings/downtime.hbs +++ b/templates/settings/homebrew-settings/downtime.hbs @@ -10,7 +10,7 @@ {{localize "DAGGERHEART.APPLICATIONS.Downtime.longRest.title"}} - +
@@ -31,7 +31,7 @@ {{localize "DAGGERHEART.APPLICATIONS.Downtime.shortRest.title"}} - +
diff --git a/templates/settings/homebrew-settings/itemFeatures.hbs b/templates/settings/homebrew-settings/itemFeatures.hbs index 22c23af6..df3419fa 100644 --- a/templates/settings/homebrew-settings/itemFeatures.hbs +++ b/templates/settings/homebrew-settings/itemFeatures.hbs @@ -8,7 +8,7 @@ {{localize "DAGGERHEART.GENERAL.weaponFeatures"}} - +
@@ -22,7 +22,7 @@ {{localize "DAGGERHEART.GENERAL.armorFeatures"}} - +
diff --git a/templates/settings/homebrew-settings/settings.hbs b/templates/settings/homebrew-settings/settings.hbs index cdcbd461..4b6e7d85 100644 --- a/templates/settings/homebrew-settings/settings.hbs +++ b/templates/settings/homebrew-settings/settings.hbs @@ -8,6 +8,7 @@

{{localize 'DAGGERHEART.SETTINGS.Menu.homebrew.name'}}

{{formGroup settingFields.schema.fields.maxFear value=settingFields._source.maxFear localize=true}} + {{formGroup settingFields.schema.fields.maxHope value=settingFields._source.maxHope localize=true}} {{formGroup settingFields.schema.fields.maxDomains value=settingFields._source.maxDomains localize=true}} {{formGroup settingFields.schema.fields.maxLoadout value=settingFields._source.maxLoadout localize=true}}
diff --git a/templates/sheets-settings/action-settings/base.hbs b/templates/sheets-settings/action-settings/base.hbs index 65010903..95c3cc50 100644 --- a/templates/sheets-settings/action-settings/base.hbs +++ b/templates/sheets-settings/action-settings/base.hbs @@ -11,6 +11,6 @@
{{localize "DAGGERHEART.GENERAL.description"}} - {{formInput fields.description value=source.description enriched=source.description name="description" toggled=true }} + {{formInput fields.description value=source.description enriched=action.description name="description" toggled=true }}
\ No newline at end of file diff --git a/templates/sheets-settings/action-settings/configuration.hbs b/templates/sheets-settings/action-settings/configuration.hbs index 51b2a72b..5bd29e39 100644 --- a/templates/sheets-settings/action-settings/configuration.hbs +++ b/templates/sheets-settings/action-settings/configuration.hbs @@ -2,7 +2,7 @@ class="tab {{this.tabs.config.cssClass}}" data-group="primary" data-tab="config" -> +> {{> 'systems/daggerheart/templates/actionTypes/uses.hbs' fields=fields.uses.fields source=source.uses}} {{> 'systems/daggerheart/templates/actionTypes/cost.hbs' fields=fields.cost.element.fields source=source.cost costOptions=costOptions}} {{> 'systems/daggerheart/templates/actionTypes/range-target.hbs' fields=(object range=fields.range target=fields.target.fields) source=(object target=source.target range=source.range)}} diff --git a/templates/sheets-settings/action-settings/effect.hbs b/templates/sheets-settings/action-settings/effect.hbs index bf2f3aa1..e94f4328 100644 --- a/templates/sheets-settings/action-settings/effect.hbs +++ b/templates/sheets-settings/action-settings/effect.hbs @@ -9,5 +9,6 @@ {{#if fields.macro}}{{> 'systems/daggerheart/templates/actionTypes/macro.hbs' fields=fields.macro source=source.macro}}{{/if}} {{#if fields.effects}}{{> 'systems/daggerheart/templates/actionTypes/effect.hbs' fields=fields.effects.element.fields source=source.effects}}{{/if}} {{#if fields.beastform}}{{> 'systems/daggerheart/templates/actionTypes/beastform.hbs' fields=fields.beastform.fields source=source.beastform}}{{/if}} + {{#if fields.summon}}{{> 'systems/daggerheart/templates/actionTypes/summon.hbs' fields=fields.summon.element.fields source=source.summon}}{{/if}} {{#if fields.countdown}}{{> 'systems/daggerheart/templates/actionTypes/countdown.hbs' fields=fields.countdown.element.fields source=source.countdown}}{{/if}} \ No newline at end of file diff --git a/templates/sheets-settings/action-settings/trigger.hbs b/templates/sheets-settings/action-settings/trigger.hbs index b048461e..9ef97733 100644 --- a/templates/sheets-settings/action-settings/trigger.hbs +++ b/templates/sheets-settings/action-settings/trigger.hbs @@ -30,7 +30,7 @@
- {{formInput @root.fields.triggers.element.fields.command value=trigger.command elementType="code-mirror" name=(concat "triggers." index ".command") aria=(object label=(localize "Test")) }} + {{formInput @root.fields.triggers.element.fields.command value=trigger.command elementType="code-mirror" name=(concat "triggers." index ".command") }}
{{/each}} diff --git a/templates/sheets-settings/character-settings/details.hbs b/templates/sheets-settings/character-settings/details.hbs index 42e17a9b..3f9247e0 100644 --- a/templates/sheets-settings/character-settings/details.hbs +++ b/templates/sheets-settings/character-settings/details.hbs @@ -31,7 +31,7 @@ {{formGroup systemFields.resources.fields.stress.fields.max value=document._source.system.resources.stress.max localize=true}} {{formGroup systemFields.resources.fields.hope.fields.value value=document._source.system.resources.hope.value localize=true}} - {{formGroup systemFields.resources.fields.hope.fields.max value=document._source.system.resources.hope.max localize=true}} + {{formGroup systemFields.scars value=document._source.system.scars localize=true}} {{formGroup systemFields.proficiency value=document._source.system.proficiency localize=true}} diff --git a/templates/sheets/actors/character/header.hbs b/templates/sheets/actors/character/header.hbs index 1459e10b..d2c01f3c 100644 --- a/templates/sheets/actors/character/header.hbs +++ b/templates/sheets/actors/character/header.hbs @@ -76,6 +76,11 @@ {{/if}} {{/times}} + {{#times document.system.scars}} + + + + {{/times}}
{{#if document.system.class.value}}
@@ -123,9 +128,7 @@ {{/each}}
- {{#> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' showSettings=showSettings }} - {{#if ../showSettings}} - - {{/if}} + {{#> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }} + {{/'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}} \ No newline at end of file diff --git a/templates/sheets/actors/character/sidebar.hbs b/templates/sheets/actors/character/sidebar.hbs index 758af1b9..b5cd92ab 100644 --- a/templates/sheets/actors/character/sidebar.hbs +++ b/templates/sheets/actors/character/sidebar.hbs @@ -1,12 +1,12 @@
\ No newline at end of file diff --git a/templates/ui/chat/deathMove.hbs b/templates/ui/chat/deathMove.hbs index 9940376e..7c677fe3 100644 --- a/templates/ui/chat/deathMove.hbs +++ b/templates/ui/chat/deathMove.hbs @@ -1,17 +1,32 @@ -
-
    -
    - - +
    +
      +
      + +

      {{this.title}}

      {{localize 'DAGGERHEART.UI.Chat.deathMove.title'}}
      - +
      {{{this.description}}}
    +
    + {{{this.result}}} +
    + {{#if this.showRiskItAllButton}} +
    + +
    + {{/if}} + +
    +
    \ No newline at end of file diff --git a/templates/ui/chat/parts/roll-part.hbs b/templates/ui/chat/parts/roll-part.hbs index 8e88015f..ea962f9e 100644 --- a/templates/ui/chat/parts/roll-part.hbs +++ b/templates/ui/chat/parts/roll-part.hbs @@ -29,64 +29,86 @@
    - {{#if roll.hope}} -
    - -
    - {{#if roll.hope.rerolled.any}}{{/if}} - {{roll.hope.value}} -
    -
    -
    - -
    - {{#if roll.fear.rerolled.any}}{{/if}} - {{roll.fear.value}} -
    -
    - {{#if roll.advantage.type}} -
    - {{#if (eq roll.advantage.type 1)}} - -
    {{roll.advantage.value}}
    - {{else}} - -
    {{roll.advantage.value}}
    - {{/if}} -
    - {{/if}} - {{#if roll.rally.dice}} -
    - -
    {{roll.rally.value}}
    -
    - {{/if}} - {{#each roll.extra}} - {{#each results}} - {{#unless discarded}} -
    - -
    {{result}}
    -
    - {{/unless}} - {{/each}} - {{/each}} - {{else}} - {{#each roll.dice}} - {{#each results}} -
    -
    - {{result}} -
    + {{#if roll.fate}} + {{#if (eq roll.fate.fateDie "Hope")}} +
    + +
    + {{roll.fate.value}}
    +
    + {{/if}} + {{#if (eq roll.fate.fateDie "Fear")}} +
    + +
    + {{roll.fate.value}} +
    +
    + {{/if}} + {{else}} + {{#if roll.hope}} +
    + +
    + {{#if roll.hope.rerolled.any}}{{/if}} + {{roll.hope.value}} +
    +
    +
    + +
    + {{#if roll.fear.rerolled.any}}{{/if}} + {{roll.fear.value}} +
    +
    + {{#if roll.advantage.type}} +
    + {{#if (eq roll.advantage.type 1)}} + +
    {{roll.advantage.value}}
    + {{else}} + +
    {{roll.advantage.value}}
    + {{/if}} +
    + {{/if}} + {{#if roll.rally.dice}} +
    + +
    {{roll.rally.value}}
    +
    + {{/if}} + {{#each roll.extra}} + {{#each results}} + {{#unless discarded}} +
    + +
    {{result}}
    +
    + {{/unless}} + {{/each}} {{/each}} - {{/each}} + {{else}} + {{#each roll.dice}} + {{#each results}} +
    +
    + {{result}} +
    +
    + {{/each}} + {{/each}} + {{/if}} {{/if}}
    -
    {{roll.formula}}
    + {{#if roll.fate}} + {{else}} +
    {{roll.formula}}
    + {{/if}}
{{/unless}} -
\ No newline at end of file + diff --git a/templates/ui/chat/table-result.hbs b/templates/ui/chat/table-result.hbs new file mode 100644 index 00000000..3eedad1f --- /dev/null +++ b/templates/ui/chat/table-result.hbs @@ -0,0 +1,17 @@ +
+ {{#if flavor}}
{{flavor}}
{{/if}} + + {{#if description}} +
{{{description}}}
+ {{/if}} + {{{rollHTML}}} + +
    + {{#each results as |result|}} +
  • + + {{{result.details}}} +
  • + {{/each}} +
+
diff --git a/templates/ui/itemBrowser/itemContainer.hbs b/templates/ui/itemBrowser/itemContainer.hbs index f6aefa6b..0040a692 100644 --- a/templates/ui/itemBrowser/itemContainer.hbs +++ b/templates/ui/itemBrowser/itemContainer.hbs @@ -10,7 +10,7 @@
- {{{system.description}}} + {{{system.enrichedDescription}}}
{{/each}} \ No newline at end of file