From 0aabcec340eef7053353808721a42183eab600a6 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Tue, 19 Aug 2025 18:56:30 +0200 Subject: [PATCH 01/89] Raised version --- system.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system.json b/system.json index d954c6d5..e6b7650f 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.0.5", + "version": "1.0.6", "compatibility": { "minimum": "13", "verified": "13.347", From 98cf6fa6de1d2bb61f0a9b9c4a4d2df385120b70 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:13:26 +0100 Subject: [PATCH 02/89] [Feature] Character Creation Confirmations (#1533) * Added confirmation on ignoring character setup. Added reset option to character sheet. * Removed the system setting for playerCanEdit. It's always available now. --- lang/en.json | 11 ++-- .../applications/sheets/actors/character.mjs | 51 ++++++++++++++++--- module/data/settings/Automation.mjs | 5 -- .../settings/automation-settings/general.hbs | 1 - templates/sheets/actors/character/header.hbs | 6 +-- 5 files changed, 52 insertions(+), 22 deletions(-) diff --git a/lang/en.json b/lang/en.json index 8e64ab7d..870e1b2b 100755 --- a/lang/en.json +++ b/lang/en.json @@ -237,10 +237,13 @@ "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" + "cancelBeastform": "Cancel Beastform", + "resetCharacterConfirmationTitle": "Reset Character", + "resetCharacterConfirmationContent": "You are reseting all character data except name and portrait. Are you sure?" }, "Companion": { "FIELDS": { @@ -314,6 +317,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.", @@ -2450,10 +2455,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", diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index e11fee05..d691c129 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.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' } ] }, @@ -220,13 +226,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; @@ -666,6 +665,32 @@ 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() { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.localize('DAGGERHEART.ACTORS.Character.resetCharacterConfirmationTitle') + }, + content: game.i18n.localize('DAGGERHEART.ACTORS.Character.resetCharacterConfirmationContent') + }); + + if (!confirmed) return; + + await this.document.update({ + '==system': {} + }); + await this.document.deleteEmbeddedDocuments( + 'Item', + this.document.items.map(x => x.id) + ); + await this.document.deleteEmbeddedDocuments( + 'ActiveEffect', + this.document.effects.map(x => x.id) + ); + } + /** * Opens the Death Move interface for the character. * @type {ApplicationClickAction} @@ -956,6 +981,18 @@ export default class CharacterSheet extends DHBaseActorSheet { } async _onDropItem(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/data/settings/Automation.mjs b/module/data/settings/Automation.mjs index 3376b153..bff0bae9 100644 --- a/module/data/settings/Automation.mjs +++ b/module/data/settings/Automation.mjs @@ -55,11 +55,6 @@ 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, 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/sheets/actors/character/header.hbs b/templates/sheets/actors/character/header.hbs index 1459e10b..87319dbb 100644 --- a/templates/sheets/actors/character/header.hbs +++ b/templates/sheets/actors/character/header.hbs @@ -123,9 +123,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 From 9d75157e1711fa921fef0c2e2dcf8fcc0e39380c Mon Sep 17 00:00:00 2001 From: Chris Ryan <73275196+chrisryan10@users.noreply.github.com> Date: Sun, 18 Jan 2026 00:11:50 +1000 Subject: [PATCH 03/89] [Feature] Death moves and Fate rolls (#1463) * Update the death move descriptions * Renamed to DhDeathMove * Partial Fate Roll creation and Fate Roll Enricher (/fr) * Hide stuff not required for fate roll * Hide formula display; code removal; start to add Fear die as a choice for Fate roll * Fix chat message display; start moving towards supporting Hope and Fear for Fate roll * /fr now supports type=X, where X is Hope or Fear, if not supplied, defaults to Hope * Fixed DSN rolling; removed console messages; chat message clean up * Add localisation entry * Trying to sort out the button for the fate roll * Style the fate message based on Hope/Fear colors. * Partial improvement on the fate template buttons - chat display is correct, but the roll dialog is wrong * Fixed enricher button; localization fixes; debug cleanup * Error checking for the fate type parsing in all potential problem locations * Added localization for the fate type parsing error * Start on Avoid Death death move * debug stuff * More death moves setup/testing * Avoid fate scars update in place, with scars migrating to an integer value. * Remove some debug code; add Blaze Of Glory shell * Start on Guaranteed Critical for Blaze of Glory * Partial implementation of Blaze of Glory * Dice/critical checks/tests * Moved detection of guaranteed critical to before the roll dialog is created, so it can be skipped; removed debug code * Remove debug * Update Blaze of Glory effect description * Risk It All - critical roll - clear all stress and HP * Auto remove all marked stress and HP for Risk It All, if Hope value rolled covers it. * Display the Death Move description in chat expanded if the appropriate config setting is on * Made the Blaze of Glory ActiveEffect image use configured version * Update the current Hope value if the scar value change affects it * Scars management in the Character details editor * Separate less file for the Death Moves instead of reusing Downtime * Added result messages to the Death Move chat output and removed debug statements * Some localization, style and smaller changes * Fixed RiskItAll resource handling method * Risk It All success chat message start * [Add] Hope/Scar Interplay (#1531) * Migrated character.maxHope to homebrew settings * Added a visual for scars * . * . * Pass the hope value in the button data; skeleton risk it all dialog to fill out. * Start on risk it dialog * More dialog stuff * Remove non-existent field * Dialog templating and logic * . * Ensure effect is Applied to Actor (#1547) Co-authored-by: Chris Ryan * [Fix] 1548 - Standalone Item Add Actions (#1549) * Fixed so that items not on an actor don't error out on creating actions * Fixed deletion of items error * Raised version * Fix the sliders to do the correct maximums * Pass the actor id through the button; fix /dr and /fr flavor text * Remove debug message --------- Co-authored-by: Chris Ryan Co-authored-by: WBHarry Co-authored-by: WBHarry <89362246+WBHarry@users.noreply.github.com> --- daggerheart.mjs | 40 +++- lang/en.json | 35 +++- module/applications/dialogs/_module.mjs | 1 + module/applications/dialogs/d20RollDialog.mjs | 2 +- module/applications/dialogs/deathMove.mjs | 148 +++++++++++++- .../applications/dialogs/riskItAllDialog.mjs | 94 +++++++++ .../applications/sheets/actors/character.mjs | 9 +- module/applications/ui/chatLog.mjs | 17 +- module/data/action/baseAction.mjs | 6 +- module/data/actor/character.mjs | 44 ++++- module/data/chat-message/_modules.mjs | 1 + module/data/settings/Homebrew.mjs | 7 + module/dice/_module.mjs | 1 + module/dice/dualityRoll.mjs | 47 +++-- module/dice/fateRoll.mjs | 85 +++++++++ module/documents/actor.mjs | 14 +- module/documents/chatMessage.mjs | 9 + module/enrichers/DualityRollEnricher.mjs | 8 +- module/enrichers/FateRollEnricher.mjs | 80 ++++++++ module/enrichers/_module.mjs | 11 +- module/helpers/utils.mjs | 22 ++- .../death-move/death-move-container.less | 111 +++++------ styles/less/dialog/index.less | 4 +- styles/less/dialog/risk-it-all/sheet.less | 60 ++++++ styles/less/global/enrichment.less | 1 + .../less/sheets/actors/character/header.less | 5 + styles/less/ui/chat/chat.less | 26 ++- styles/less/ui/chat/deathmoves.less | 152 +++++++++++++++ styles/less/ui/index.less | 1 + system.json | 1 + templates/dialogs/dice-roll/rollSelection.hbs | 180 +++++++++++------- templates/dialogs/riskItAllDialog.hbs | 39 ++++ .../settings/homebrew-settings/settings.hbs | 1 + .../action-settings/trigger.hbs | 2 +- .../character-settings/details.hbs | 2 +- templates/sheets/actors/character/header.hbs | 5 + templates/ui/chat/deathMove.hbs | 27 ++- templates/ui/chat/parts/roll-part.hbs | 126 +++++++----- 38 files changed, 1166 insertions(+), 258 deletions(-) create mode 100644 module/applications/dialogs/riskItAllDialog.mjs create mode 100644 module/dice/fateRoll.mjs create mode 100644 module/enrichers/FateRollEnricher.mjs create mode 100644 styles/less/dialog/risk-it-all/sheet.less create mode 100644 styles/less/ui/chat/deathmoves.less create mode 100644 templates/dialogs/riskItAllDialog.hbs diff --git a/daggerheart.mjs b/daggerheart.mjs index 3abcd210..23f153dd 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -8,8 +8,9 @@ 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, @@ -24,12 +25,13 @@ 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; @@ -298,8 +300,8 @@ Hooks.on('chatMessage', (_, message) => { const difficulty = rollCommand.difficulty; const target = getCommandTarget({ allowNull: true }); - const title = traitValue - ? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { + 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'); @@ -316,6 +318,34 @@ Hooks.on('chatMessage', (_, message) => { }); 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; + } }); const updateActorsRangeDependentEffects = async token => { diff --git a/lang/en.json b/lang/en.json index 870e1b2b..69965b9e 100755 --- a/lang/en.json +++ b/lang/en.json @@ -619,6 +619,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", @@ -1017,15 +1024,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": { @@ -2066,6 +2073,7 @@ "description": "Description", "main": "Data", "information": "Information", + "itemFeatures": "Item Features", "notes": "Notes", "inventory": "Inventory", "loadout": "Loadout", @@ -2141,6 +2149,7 @@ "dropActorsHere": "Drop Actors here", "dropFeaturesHere": "Drop Features here", "duality": "Duality", + "dualityDice": "Duality Dice", "dualityRoll": "Duality Roll", "enabled": "Enabled", "evasion": "Evasion", @@ -2150,11 +2159,14 @@ "plural": "Experiences" }, "failure": "Failure", + "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": { @@ -2214,6 +2226,7 @@ "rollWith": "{roll} Roll", "save": "Save", "scalable": "Scalable", + "scars": "Scars", "situationalBonus": "Situational Bonus", "spent": "Spent", "step": "Step", @@ -2510,6 +2523,7 @@ "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", @@ -2657,7 +2671,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" @@ -2779,7 +2802,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", diff --git a/module/applications/dialogs/_module.mjs b/module/applications/dialogs/_module.mjs index 92038c41..d43045e6 100644 --- a/module/applications/dialogs/_module.mjs +++ b/module/applications/dialogs/_module.mjs @@ -14,3 +14,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/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index 441842dc..6f320152 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -123,7 +123,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; } diff --git a/module/applications/dialogs/deathMove.mjs b/module/applications/dialogs/deathMove.mjs index d0686d2b..01df6057 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,107 @@ 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; + + if (config.roll.fate.value <= this.actor.system.levelData.level.current) { + // apply scarring - for now directly apply - later add a button. + const newScarAmount = this.actor.system.scars + 1; + + await this.actor.update({ + system: { + scars: newScarAmount + } + }); + + if (newScarAmount >= this.actor.system.resources.hope.max) { + return game.i18n.format('DAGGERHEART.UI.Chat.deathMove.journeysEnd', { scars: newScarAmount }); + } + + return game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.gainScar'); + } + + return game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.avoidScar'); + } + + 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, + 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) { + 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' + } + ] + } + ]); + + 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 +152,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 +204,5 @@ export default class DhpDeathMove extends HandlebarsApplicationMixin(Application }; cls.create(msg); - - this.close(); } } 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/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index d691c129..5c6bac3a 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'; @@ -696,7 +696,7 @@ export default class CharacterSheet extends DHBaseActorSheet { * @type {ApplicationClickAction} */ static async #makeDeathMove() { - await new DhpDeathMove(this.document).render({ force: true }); + await new DhDeathMove(this.document).render({ force: true }); } /** @@ -753,9 +753,8 @@ export default class CharacterSheet extends DHBaseActorSheet { 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(); } diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index c4a313fa..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() { @@ -94,15 +97,17 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo /** 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"); + 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") + document + .querySelector('#chat-notifications .chat-log') + ?.classList.remove('themed', 'theme-light', 'theme-dark'); } async onRollSimple(event, message) { @@ -383,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/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index b5f95aff..4e699f79 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -376,14 +376,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/actor/character.mjs b/module/data/actor/character.mjs index f6ab7e3a..a7f99ca8 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -35,7 +35,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'), @@ -78,12 +85,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(), @@ -301,6 +303,9 @@ export default class DhCharacter extends BaseDataActor { runeWard: new fields.BooleanField({ initial: false }), burden: new fields.SchemaField({ ignore: new fields.BooleanField() + }), + roll: new fields.SchemaField({ + guaranteedCritical: new fields.BooleanField() }) }) }; @@ -642,7 +647,9 @@ 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; } @@ -699,6 +706,20 @@ 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 + }; + } + } } async _preDelete() { @@ -714,4 +735,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/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/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/dualityRoll.mjs b/module/dice/dualityRoll.mjs index aaca7400..0edbe5ad 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); } @@ -178,6 +176,21 @@ 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}`]); @@ -223,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) @@ -231,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) @@ -243,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 }; 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/actor.mjs b/module/documents/actor.mjs index 27c310ae..cec1a24d 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -764,16 +764,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 ); } @@ -782,7 +790,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 diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index 2f23cc1a..e03c3cf0 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, diff --git a/module/enrichers/DualityRollEnricher.mjs b/module/enrichers/DualityRollEnricher.mjs index 536847f7..f6f022f9 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); @@ -80,7 +80,7 @@ export const renderDualityButton = async event => { }; export const enrichedDualityRoll = async ( - { reaction, traitValue, target, difficulty, title, label, advantage }, + { reaction, traitValue, target, difficulty, title, label, advantage, customConfig }, event ) => { const config = { @@ -94,7 +94,8 @@ export const enrichedDualityRoll = async ( type: reaction ? 'reaction' : null }, type: 'trait', - hasRoll: true + hasRoll: true, + ...(customConfig ?? {}) }; if (target) { @@ -105,4 +106,5 @@ export const enrichedDualityRoll = async ( 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/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..01a3f954 100644 --- a/styles/less/dialog/index.less +++ b/styles/less/dialog/index.less @@ -38,4 +38,6 @@ @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'; 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/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/sheets/actors/character/header.less b/styles/less/sheets/actors/character/header.less index 593f1b73..5e8ef002 100644 --- a/styles/less/sheets/actors/character/header.less +++ b/styles/less/sheets/actors/character/header.less @@ -195,6 +195,11 @@ .hope-value { display: flex; cursor: pointer; + + &.scar { + cursor: initial; + opacity: 0.6; + } } } } diff --git a/styles/less/ui/chat/chat.less b/styles/less/ui/chat/chat.less index 57e9fd57..1b1e3c1c 100644 --- a/styles/less/ui/chat/chat.less +++ b/styles/less/ui/chat/chat.less @@ -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,28 @@ } } + &.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/deathmoves.less b/styles/less/ui/chat/deathmoves.less new file mode 100644 index 00000000..175b8753 --- /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/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/system.json b/system.json index 2c68f785..8e4d19d9 100644 --- a/system.json +++ b/system.json @@ -285,6 +285,7 @@ }, "ChatMessage": { "dualityRoll": {}, + "fateRoll": {}, "adversaryRoll": {}, "damageRoll": {}, "abilityUse": {}, diff --git a/templates/dialogs/dice-roll/rollSelection.hbs b/templates/dialogs/dice-roll/rollSelection.hbs index e60f4683..5851a33d 100644 --- a/templates/dialogs/dice-roll/rollSelection.hbs +++ b/templates/dialogs/dice-roll/rollSelection.hbs @@ -68,95 +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 hasSelectedEffects}} -
- {{localize "DAGGERHEART.GENERAL.Effect.plural"}} + {{#if (ne @root.rollType 'FateRoll')}} + {{#if hasSelectedEffects}} +
+ {{localize "DAGGERHEART.GENERAL.Effect.plural"}} - {{#each selectedEffects as |effect id|}} -
- - {{effect.name}} -
- {{/each}} -
- {{/if}} - - {{#if experiences.length}} -
- {{localize "DAGGERHEART.GENERAL.experience.plural"}} - {{#each experiences}} - {{#if name}} -
- - {{name}} +{{value}} + {{#each selectedEffects as |effect id|}} +
+ + {{effect.name}}
- {{/if}} - {{/each}} -
- {{/if}} + {{/each}} +
+ {{/if}} -
- {{localize "DAGGERHEART.GENERAL.Modifier.plural"}} -
- - -
- {{#unless (eq @root.rollType 'D20Roll')}} + {{#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/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/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 87319dbb..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}}
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 + From 77bac647a86f886e811bd71126cc51289be75d33 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:31:54 +0100 Subject: [PATCH 04/89] Fixed typo in levelupViewMode domain card option (#1558) --- module/applications/levelup/levelupViewMode.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 => { From cc998bffa7ca04bdf203f6969ebde17cf8c751cb Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Tue, 20 Jan 2026 11:03:52 +0100 Subject: [PATCH 05/89] [Feature] DeathMove Condition Improvement (#1562) * Added DeathMove condition and automated changing to the correct condition depending on the result of death moves * . * Update module/data/settings/Automation.mjs Co-authored-by: Chris Ryan <73275196+chrisryan10@users.noreply.github.com> * Update lang/en.json Co-authored-by: Chris Ryan <73275196+chrisryan10@users.noreply.github.com> * Fixed DefeatedCondition localizations --------- Co-authored-by: Chris Ryan <73275196+chrisryan10@users.noreply.github.com> --- lang/en.json | 10 +++++++++- module/applications/dialogs/deathMove.mjs | 11 +++++++---- module/config/generalConfig.mjs | 6 +++++- module/data/actor/character.mjs | 13 ++++++++++++- module/data/settings/Automation.mjs | 16 +++++++++++----- module/documents/actor.mjs | 16 ++++++++++++++-- templates/settings/automation-settings/rules.hbs | 3 ++- 7 files changed, 60 insertions(+), 15 deletions(-) diff --git a/lang/en.json b/lang/en.json index 69965b9e..dda78410 100755 --- a/lang/en.json +++ b/lang/en.json @@ -973,6 +973,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" @@ -2435,7 +2439,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", diff --git a/module/applications/dialogs/deathMove.mjs b/module/applications/dialogs/deathMove.mjs index 01df6057..d1b9379b 100644 --- a/module/applications/dialogs/deathMove.mjs +++ b/module/applications/dialogs/deathMove.mjs @@ -54,10 +54,9 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV 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) { - // apply scarring - for now directly apply - later add a button. const newScarAmount = this.actor.system.scars + 1; - await this.actor.update({ system: { scars: newScarAmount @@ -65,13 +64,15 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV }); 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 }); } - return game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.gainScar'); + returnMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.gainScar'); } - return game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.avoidScar'); + await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.unconscious.id); + return returnMessage; } async handleRiskItAll() { @@ -118,6 +119,7 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV } 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'); } @@ -141,6 +143,7 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV } ]); + await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id); return game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.blazeOfGlory'); } diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 37894644..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' diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index a7f99ca8..12396384 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -549,7 +549,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() { diff --git a/module/data/settings/Automation.mjs b/module/data/settings/Automation.mjs index bff0bae9..436f0eb7 100644 --- a/module/data/settings/Automation.mjs +++ b/module/data/settings/Automation.mjs @@ -58,7 +58,7 @@ export default class DhAutomation extends foundry.abstract.DataModel { 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({ @@ -69,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({ @@ -84,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/documents/actor.mjs b/module/documents/actor.mjs index cec1a24d..d76d7447 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -849,8 +849,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 }); @@ -864,6 +864,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/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}} From 3725fc29ef8c8d23f364c03d6d070a4e13415614 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Wed, 21 Jan 2026 02:52:07 +0100 Subject: [PATCH 06/89] [Feature] Seaborne Improvement (#1553) * Added a max to KnowTheTide. Added a onFear trigger to increase the resource * . * Added a notification message when KnowTheTide gains a token --- lang/en.json | 3 +- module/data/registeredTriggers.mjs | 1 + ...eature_Know_the_Tide_07x6Qe6qMzDw2xN4.json | 42 +++++++++++++++++-- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/lang/en.json b/lang/en.json index dda78410..37cd627e 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2876,7 +2876,8 @@ "documentIsMissing": "The {documentType} is missing from the world.", "tokenActorMissing": "{name} is missing an Actor", "tokenActorsMissing": "[{names}] missing Actors", - "domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used" + "domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used", + "knowTheTide": "Know The Tide gained a token" }, "Sidebar": { "actorDirectory": { diff --git a/module/data/registeredTriggers.mjs b/module/data/registeredTriggers.mjs index 8a100585..3fd9f82c 100644 --- a/module/data/registeredTriggers.mjs +++ b/module/data/registeredTriggers.mjs @@ -20,6 +20,7 @@ export default class RegisteredTriggers extends Map { } registerItemTriggers(item, registerOverride) { + if (!item.actor || !item._stats.createdTime) return; for (const action of item.system.actions ?? []) { if (!action.actor) continue; 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, From 2aba7cf9213e41e970c31e4b3c0305c3217b0d17 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Wed, 21 Jan 2026 02:56:47 +0100 Subject: [PATCH 07/89] Changed to use a dialog to choose which parts are kept when reseting (#1557) --- lang/en.json | 7 ++ module/applications/dialogs/_module.mjs | 1 + .../dialogs/characterResetDialog.mjs | 105 ++++++++++++++++++ .../applications/sheets/actors/character.mjs | 26 +---- styles/less/dialog/character-reset/sheet.less | 27 +++++ styles/less/dialog/index.less | 2 + templates/dialogs/characterReset.hbs | 33 ++++++ 7 files changed, 179 insertions(+), 22 deletions(-) create mode 100644 module/applications/dialogs/characterResetDialog.mjs create mode 100644 styles/less/dialog/character-reset/sheet.less create mode 100644 templates/dialogs/characterReset.hbs diff --git a/lang/en.json b/lang/en.json index 37cd627e..720a08c5 100755 --- a/lang/en.json +++ b/lang/en.json @@ -330,6 +330,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", @@ -2214,6 +2220,7 @@ "single": "Player", "plurial": "Players" }, + "portrait": "Portrait", "proficiency": "Proficiency", "quantity": "Quantity", "range": "Range", diff --git a/module/applications/dialogs/_module.mjs b/module/applications/dialogs/_module.mjs index d43045e6..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'; diff --git a/module/applications/dialogs/characterResetDialog.mjs b/module/applications/dialogs/characterResetDialog.mjs new file mode 100644 index 00000000..1f3f3d5a --- /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: false, 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/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 5c6bac3a..4ecaeb06 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -669,26 +669,7 @@ export default class CharacterSheet extends DHBaseActorSheet { * Resets the character data and removes all embedded documents. */ static async #resetCharacter() { - const confirmed = await foundry.applications.api.DialogV2.confirm({ - window: { - title: game.i18n.localize('DAGGERHEART.ACTORS.Character.resetCharacterConfirmationTitle') - }, - content: game.i18n.localize('DAGGERHEART.ACTORS.Character.resetCharacterConfirmationContent') - }); - - if (!confirmed) return; - - await this.document.update({ - '==system': {} - }); - await this.document.deleteEmbeddedDocuments( - 'Item', - this.document.items.map(x => x.id) - ); - await this.document.deleteEmbeddedDocuments( - 'ActiveEffect', - this.document.effects.map(x => x.id) - ); + new game.system.api.applications.dialogs.CharacterResetDialog(this.document).render({ force: true }); } /** @@ -753,8 +734,9 @@ export default class CharacterSheet extends DHBaseActorSheet { 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(); } 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/index.less b/styles/less/dialog/index.less index 01a3f954..733cdd1c 100644 --- a/styles/less/dialog/index.less +++ b/styles/less/dialog/index.less @@ -41,3 +41,5 @@ @import './settings/change-currency-icon.less'; @import './risk-it-all/sheet.less'; + +@import './character-reset/sheet.less'; 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 From c90875fa7cb14553fd223362433daf186af3c11c Mon Sep 17 00:00:00 2001 From: WBHarry Date: Wed, 21 Jan 2026 14:05:39 +0100 Subject: [PATCH 08/89] Changed ResetDialog to have all optional sections initially kept --- module/applications/dialogs/characterResetDialog.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/applications/dialogs/characterResetDialog.mjs b/module/applications/dialogs/characterResetDialog.mjs index 1f3f3d5a..0836af9c 100644 --- a/module/applications/dialogs/characterResetDialog.mjs +++ b/module/applications/dialogs/characterResetDialog.mjs @@ -16,7 +16,7 @@ export default class CharacterResetDialog extends HandlebarsApplicationMixin(App portrait: { keep: true, label: 'DAGGERHEART.GENERAL.portrait' }, name: { keep: true, label: 'Name' }, biography: { keep: true, label: 'DAGGERHEART.GENERAL.Tabs.biography' }, - inventory: { keep: false, label: 'DAGGERHEART.GENERAL.inventory' } + inventory: { keep: true, label: 'DAGGERHEART.GENERAL.inventory' } } }; } From cbd268ea1f51ab59a1cede31fb28c6f91d3c112c Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sat, 24 Jan 2026 11:10:30 +0100 Subject: [PATCH 09/89] [Feature] DR Command Resources (#1572) * Dr chatcommand and buttons now grant resources via automation by default. Optionally turned off via parameter noResources=true * . --- daggerheart.mjs | 4 +++- module/dice/dualityRoll.mjs | 2 +- module/enrichers/DualityRollEnricher.mjs | 16 ++++++++++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/daggerheart.mjs b/daggerheart.mjs index 3abcd210..c3a5c348 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -296,6 +296,7 @@ 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 @@ -312,7 +313,8 @@ Hooks.on('chatMessage', (_, message) => { title, label: game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll'), actionType: null, - advantage + advantage, + grantResources }); return false; } diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index aaca7400..30b569ac 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -261,7 +261,7 @@ export default class DualityRoll extends D20Roll { } static async handleTriggers(roll, config) { - if (!config.source?.actor) return; + if (!config.source?.actor || config.skips?.triggers) return; const updates = []; const dualityUpdates = await game.system.registeredTriggers.runTrigger( diff --git a/module/enrichers/DualityRollEnricher.mjs b/module/enrichers/DualityRollEnricher.mjs index 536847f7..67728a37 100644 --- a/module/enrichers/DualityRollEnricher.mjs +++ b/module/enrichers/DualityRollEnricher.mjs @@ -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,14 +75,15 @@ 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 }, event ) => { const config = { @@ -93,12 +96,17 @@ export const enrichedDualityRoll = async ( advantage, type: reaction ? 'reaction' : null }, + skips: { + resources: !grantResources, + triggers: !grantResources + }, type: 'trait', hasRoll: true }; if (target) { - await target.diceRoll(config); + const result = await target.diceRoll(config); + result.resourceUpdates.updateResources(); } else { // For no target, call DualityRoll directly with basic data config.data = { experiences: {}, traits: {}, rules: {} }; From bdb89973248dc0001814a3e393697c850c528446 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sat, 24 Jan 2026 11:15:07 +0100 Subject: [PATCH 10/89] [Feature] 1543 - SceneEnvironment Trigger Registration (#1564) * Added trigger Registration/Unregistration for scene environments * Fixed so that saving throw damage mitigation works again (#1555) * . (#1563) * Fixed DowntimeMoves and ItemFeatures reset functions (#1568) * Fixed an error where a player having their token initially selected caused an error in effectsDisplay.mjs (#1569) * [Fix] Damage Rerolls (#1566) * Fixed so that damage rerolls work again * Set default data for a roll instead and fix title (#1570) * Set default data for a roll instead and fix title * Ensure same options object is used --------- Co-authored-by: Carlos Fernandez * Fixed when users drag in compendium environments to the sceneEnvironments (#1573) * . --------- Co-authored-by: Carlos Fernandez --- module/data/registeredTriggers.mjs | 34 ++++++++++++++++++++---------- module/documents/scene.mjs | 21 ++++++++++++++++++ 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/module/data/registeredTriggers.mjs b/module/data/registeredTriggers.mjs index 3fd9f82c..ee4f3b49 100644 --- a/module/data/registeredTriggers.mjs +++ b/module/data/registeredTriggers.mjs @@ -72,10 +72,21 @@ export default class RegisteredTriggers extends Map { } } + 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); @@ -84,14 +95,17 @@ export default class RegisteredTriggers extends Map { } } + 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) { - /* TODO: Finish sceneEnvironment registration and unreg */ - // const systemData = new game.system.api.data.scenes.DHScene(scene.flags.daggerheart); - // for (const environment of systemData.sceneEnvironments) { - // for (const feature of environment.system.features) { - // if(feature) this.registerItemTriggers(feature, true); - // } - // } + this.registerSceneEnvironmentTriggers(scene.flags.daggerheart); for (const actor of scene.tokens.filter(x => x.actor).map(x => x.actor)) { if (actor.prototypeToken.actorLink) continue; @@ -108,13 +122,11 @@ export default class RegisteredTriggers extends Map { if (!triggerSettings.enabled) return updates; const dualityTrigger = this.get(trigger); - if (dualityTrigger) { - const tokenBoundActors = ['adversary', 'environment']; - const triggerActors = ['character', ...tokenBoundActors]; + 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; - if (tokenBoundActors.includes(actor.type) && !actor.getActiveTokens().length) continue; const triggerData = CONFIG.DH.TRIGGER.triggers[trigger]; if (triggerData.usesActor && triggeringActorType !== 'any') { diff --git a/module/documents/scene.mjs b/module/documents/scene.mjs index 7f880b1d..9e2a3f5b 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); From cb998860d91cad3fa4d295677b78606ca156116c Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:19:16 +0100 Subject: [PATCH 11/89] Spellcastmodifiers were not being sorted correctly for use (#1578) --- module/data/actor/character.mjs | 2 +- templates/sheets/actors/character/sidebar.hbs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 12396384..47660da4 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -368,7 +368,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() { diff --git a/templates/sheets/actors/character/sidebar.hbs b/templates/sheets/actors/character/sidebar.hbs index 0db2bf42..24e68e02 100644 --- a/templates/sheets/actors/character/sidebar.hbs +++ b/templates/sheets/actors/character/sidebar.hbs @@ -1,12 +1,12 @@