diff --git a/daggerheart.mjs b/daggerheart.mjs index 76205475..61e74f9b 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -60,6 +60,9 @@ CONFIG.Canvas.layers.tokens.layerClass = DhTokenLayer; CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate; +CONFIG.RollTable.documentClass = documents.DhRollTable; +CONFIG.RollTable.resultTemplate = 'systems/daggerheart/templates/ui/chat/table-result.hbs'; + CONFIG.Scene.documentClass = documents.DhScene; CONFIG.Token.documentClass = documents.DhToken; @@ -107,7 +110,7 @@ Hooks.once('init', () => { type: game.i18n.localize(typePath) }); - const { Items, Actors } = foundry.documents.collections; + const { Items, Actors, RollTables } = foundry.documents.collections; Items.unregisterSheet('core', foundry.applications.sheets.ItemSheetV2); Items.registerSheet(SYSTEM.id, applications.sheets.items.Ancestry, { types: ['ancestry'], @@ -192,6 +195,12 @@ Hooks.once('init', () => { label: sheetLabel('TYPES.Actor.party') }); + RollTables.unregisterSheet('core', foundry.applications.sheets.RollTableSheet); + RollTables.registerSheet(SYSTEM.id, applications.sheets.rollTables.RollTableSheet, { + types: ['base'], + makeDefault: true + }); + DocumentSheetConfig.unregisterSheet( CONFIG.ActiveEffect.documentClass, 'core', @@ -300,6 +309,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 = @@ -317,7 +327,8 @@ Hooks.on('chatMessage', (_, message) => { title, label: game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll'), actionType: null, - advantage + advantage, + grantResources }); return false; } diff --git a/lang/en.json b/lang/en.json index 69965b9e..4d6815c3 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", @@ -482,7 +488,9 @@ "tokenHUD": { "genericEffects": "Foundry Effects", "depositPartyTokens": "Deposit Party Tokens", - "retrievePartyTokens": "Retrieve Party Tokens" + "retrievePartyTokens": "Retrieve Party Tokens", + "depositCompanionTokens": "Deposit Companion Token", + "retrieveCompanionTokens": "Retrieve Companion Token" } }, "ImageSelect": { @@ -612,6 +620,7 @@ }, "RerollDialog": { "title": "Reroll", + "damageTitle": "Reroll Damage", "deselectDiceNotification": "Deselect one of the selected dice first", "acceptCurrentRolls": "Accept Current Rolls" }, @@ -973,6 +982,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" @@ -2210,6 +2223,7 @@ "single": "Player", "plurial": "Players" }, + "portrait": "Portrait", "proficiency": "Proficiency", "quantity": "Quantity", "range": "Range", @@ -2369,6 +2383,12 @@ "secondaryWeapon": "Secondary Weapon" } }, + "ROLLTABLES": { + "FIELDS": { + "formulaName": { "label": "Formula Name" } + }, + "formula": "Formula" + }, "SETTINGS": { "Appearance": { "FIELDS": { @@ -2435,7 +2455,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", @@ -2520,6 +2544,7 @@ "itemFeatures": "Item Features", "nrChoices": "# Moves Per Rest", "resetMovesTitle": "Reset {type} Downtime Moves", + "resetItemFeaturesTitle": "Reset {type}", "resetMovesText": "Are you sure you want to reset?", "FIELDS": { "maxFear": { "label": "Max Fear" }, @@ -2868,7 +2893,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/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..0836af9c --- /dev/null +++ b/module/applications/dialogs/characterResetDialog.mjs @@ -0,0 +1,105 @@ +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; + +export default class CharacterResetDialog extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(actor, options = {}) { + super(options); + + this.actor = actor; + this.data = { + delete: { + class: { keep: false, label: 'TYPES.Item.class' }, + subclass: { keep: false, label: 'TYPES.Item.subclass' }, + ancestry: { keep: false, label: 'TYPES.Item.ancestry' }, + community: { keep: false, label: 'TYPES.Item.community' } + }, + optional: { + portrait: { keep: true, label: 'DAGGERHEART.GENERAL.portrait' }, + name: { keep: true, label: 'Name' }, + biography: { keep: true, label: 'DAGGERHEART.GENERAL.Tabs.biography' }, + inventory: { keep: true, label: 'DAGGERHEART.GENERAL.inventory' } + } + }; + } + + static DEFAULT_OPTIONS = { + tag: 'form', + classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'character-reset'], + window: { + icon: 'fa-solid fa-arrow-rotate-left', + title: 'DAGGERHEART.APPLICATIONS.CharacterReset.title' + }, + actions: { + finishSelection: this.#finishSelection + }, + form: { + handler: this.updateData, + submitOnChange: true, + submitOnClose: false + } + }; + + /** @override */ + static PARTS = { + resourceDice: { + id: 'resourceDice', + template: 'systems/daggerheart/templates/dialogs/characterReset.hbs' + } + }; + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.data = this.data; + + return context; + } + + static async updateData(event, _, formData) { + const { data } = foundry.utils.expandObject(formData.object); + + this.data = foundry.utils.mergeObject(this.data, data); + this.render(); + } + + static getUpdateData() { + const update = {}; + if (!this.data.optional.portrait) update.if(!this.data.optional.biography); + + if (!this.data.optional.inventory) return update; + } + + static async #finishSelection() { + const update = {}; + if (!this.data.optional.name.keep) { + const defaultName = game.system.api.documents.DhpActor.defaultName({ type: 'character' }); + foundry.utils.setProperty(update, 'name', defaultName); + foundry.utils.setProperty(update, 'prototypeToken.name', defaultName); + } + + if (!this.data.optional.portrait.keep) { + foundry.utils.setProperty(update, 'img', this.actor.schema.fields.img.initial(this.actor)); + foundry.utils.setProperty(update, 'prototypeToken.==texture', {}); + foundry.utils.setProperty(update, 'prototypeToken.==ring', {}); + } + + if (this.data.optional.biography.keep) + foundry.utils.setProperty(update, 'system.biography', this.actor.system.biography); + + if (this.data.optional.inventory.keep) foundry.utils.setProperty(update, 'system.gold', this.actor.system.gold); + + const { system, ...rest } = update; + await this.actor.update({ + ...rest, + '==system': system ?? {} + }); + + const inventoryItemTypes = ['weapon', 'armor', 'consumable', 'loot']; + await this.actor.deleteEmbeddedDocuments( + 'Item', + this.actor.items + .filter(x => !inventoryItemTypes.includes(x.type) || !this.data.optional.inventory.keep) + .map(x => x.id) + ); + + this.close(); + } +} diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index 6f320152..4a4b1556 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -109,11 +109,17 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio context.roll = this.roll; context.rollType = this.roll?.constructor.name; context.rallyDie = this.roll.rallyChoices; - const experiences = this.config.data?.system?.experiences || {}; + + const actorExperiences = this.config.data?.system?.experiences || {}; + const companionExperiences = this.config.roll.companionRoll + ? (this.config.data?.companion?.system.experiences ?? {}) + : null; + const experiences = companionExperiences ?? actorExperiences; context.experiences = Object.keys(experiences).map(id => ({ id, ...experiences[id] })); + context.selectedExperiences = this.config.experiences; context.advantage = this.config.roll?.advantage; context.disadvantage = this.config.roll?.disadvantage; diff --git a/module/applications/dialogs/deathMove.mjs b/module/applications/dialogs/deathMove.mjs index 01df6057..3eadede6 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() { @@ -84,6 +85,7 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV label: game.i18n.localize('DAGGERHEART.GENERAL.dualityDice'), actionType: null, advantage: null, + grantResources: false, customConfig: { skips: { resources: true, reaction: true } } }); @@ -118,6 +120,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 +144,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/applications/hud/tokenHUD.mjs b/module/applications/hud/tokenHUD.mjs index 87c3e88e..77caaaff 100644 --- a/module/applications/hud/tokenHUD.mjs +++ b/module/applications/hud/tokenHUD.mjs @@ -5,7 +5,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { classes: ['daggerheart'], actions: { combat: DHTokenHUD.#onToggleCombat, - togglePartyTokens: DHTokenHUD.#togglePartyTokens + togglePartyTokens: DHTokenHUD.#togglePartyTokens, + toggleCompanions: DHTokenHUD.#toggleCompanions } }; @@ -26,7 +27,7 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { context.partyOnCanvas = this.actor.type === 'party' && this.actor.system.partyMembers.some(member => member.getActiveTokens().length > 0); - context.icons.toggleParty = 'systems/daggerheart/assets/icons/arrow-dunk.png'; + context.icons.toggleClowncar = 'systems/daggerheart/assets/icons/arrow-dunk.png'; context.actorType = this.actor.type; context.usesEffects = this.actor.type !== 'party'; context.canToggleCombat = DHTokenHUD.#nonCombatTypes.includes(this.actor.type) @@ -56,6 +57,9 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { }, {}) : null; + context.hasCompanion = this.actor.system.companion; + context.companionOnCanvas = context.hasCompanion && this.actor.system.companion.getActiveTokens().length > 0; + return context; } @@ -101,8 +105,24 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { : 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.depositPartyTokens' ); + await this.toggleClowncar(this.actor.system.partyMembers); + } + + static async #toggleCompanions(_, button) { + const icon = button.querySelector('img'); + icon.classList.toggle('flipped'); + button.dataset.tooltip = game.i18n.localize( + icon.classList.contains('flipped') + ? 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.retrieveCompanionTokens' + : 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.depositCompanionTokens' + ); + + await this.toggleClowncar([this.actor.system.companion]); + } + + async toggleClowncar(actors) { const animationDuration = 500; - const activeTokens = this.actor.system.partyMembers.flatMap(member => member.getActiveTokens()); + const activeTokens = actors.flatMap(member => member.getActiveTokens()); const { x: actorX, y: actorY } = this.document; if (activeTokens.length > 0) { for (let token of activeTokens) { @@ -114,14 +134,15 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { } } else { const activeScene = game.scenes.find(x => x.id === game.user.viewedScene); - const partyTokenData = []; - for (let member of this.actor.system.partyMembers) { + const tokenData = []; + for (let member of actors) { const data = await member.getTokenDocument(); - partyTokenData.push(data.toObject()); + tokenData.push(data.toObject()); } + const newTokens = await activeScene.createEmbeddedDocuments( 'Token', - partyTokenData.map(tokenData => ({ + tokenData.map(tokenData => ({ ...tokenData, alpha: 0, x: actorX, diff --git a/module/applications/scene/sceneConfigSettings.mjs b/module/applications/scene/sceneConfigSettings.mjs index 8a58db5c..98e18f09 100644 --- a/module/applications/scene/sceneConfigSettings.mjs +++ b/module/applications/scene/sceneConfigSettings.mjs @@ -65,8 +65,15 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const item = await foundry.utils.fromUuid(data.uuid); if (item instanceof game.system.api.documents.DhpActor && item.type === 'environment') { + let sceneUuid = data.uuid; + if (item.pack) { + const inWorldActor = await game.system.api.documents.DhpActor.create([item.toObject()]); + if (!inWorldActor.length) return; + sceneUuid = inWorldActor[0].uuid; + } + await this.daggerheartFlag.updateSource({ - sceneEnvironments: [...this.daggerheartFlag.sceneEnvironments, data.uuid] + sceneEnvironments: [...this.daggerheartFlag.sceneEnvironments, sceneUuid] }); this.render({ internalRefresh: true }); } @@ -97,6 +104,10 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S /** @override */ async _processSubmitData(event, form, submitData, options) { submitData.flags.daggerheart = this.daggerheartFlag.toObject(); + submitData.flags.daggerheart.sceneEnvironments = submitData.flags.daggerheart.sceneEnvironments.filter(x => + foundry.utils.fromUuidSync(x) + ); + for (const key of Object.keys(this.document._source.flags.daggerheart?.sceneEnvironments ?? {})) { if (!submitData.flags.daggerheart.sceneEnvironments[key]) { submitData.flags.daggerheart.sceneEnvironments[`-=${key}`] = null; diff --git a/module/applications/settings/homebrewSettings.mjs b/module/applications/settings/homebrewSettings.mjs index 3c4486c1..6e2e665d 100644 --- a/module/applications/settings/homebrewSettings.mjs +++ b/module/applications/settings/homebrewSettings.mjs @@ -36,7 +36,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli addItem: this.addItem, editItem: this.editItem, removeItem: this.removeItem, - resetMoves: this.resetMoves, + resetDowntimeMoves: this.resetDowntimeMoves, + resetItemFeatures: this.resetItemFeatures, addDomain: this.addDomain, toggleSelectedDomain: this.toggleSelectedDomain, deleteDomain: this.deleteDomain, @@ -232,7 +233,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli this.render(); } - static async resetMoves(_, target) { + static async resetDowntimeMoves(_, target) { const confirmed = await foundry.applications.api.DialogV2.confirm({ window: { title: game.i18n.format('DAGGERHEART.SETTINGS.Homebrew.resetMovesTitle', { @@ -266,7 +267,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli ...move, name: game.i18n.localize(move.name), description: game.i18n.localize(move.description), - actions: move.actions.reduce((acc, key) => { + actions: Object.keys(move.actions).reduce((acc, key) => { const action = move.actions[key]; acc[key] = { ...action, @@ -293,6 +294,31 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli this.render(); } + static async resetItemFeatures(_, target) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.format('DAGGERHEART.SETTINGS.Homebrew.resetItemFeaturesTitle', { + type: game.i18n.localize(`DAGGERHEART.GENERAL.${target.dataset.type}`) + }) + }, + content: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.resetMovesText') + }); + + if (!confirmed) return; + + await this.settings.updateSource({ + [`itemFeatures.${target.dataset.type}`]: Object.keys( + this.settings.itemFeatures[target.dataset.type] + ).reduce((acc, key) => { + acc[`-=${key}`] = null; + + return acc; + }, {}) + }); + + this.render(); + } + static async addDomain(event) { event.preventDefault(); const content = new foundry.data.fields.StringField({ diff --git a/module/applications/sheets-configs/action-base-config.mjs b/module/applications/sheets-configs/action-base-config.mjs index 7051ad2b..42252362 100644 --- a/module/applications/sheets-configs/action-base-config.mjs +++ b/module/applications/sheets-configs/action-base-config.mjs @@ -125,6 +125,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) async _prepareContext(_options) { const context = await super._prepareContext(_options, 'action'); context.source = this.action.toObject(true); + context.action = this.action; context.summons = []; for (const summon of context.source.summon ?? []) { diff --git a/module/applications/sheets/_module.mjs b/module/applications/sheets/_module.mjs index c503e054..390267d5 100644 --- a/module/applications/sheets/_module.mjs +++ b/module/applications/sheets/_module.mjs @@ -1,3 +1,4 @@ export * as actors from './actors/_module.mjs'; export * as api from './api/_modules.mjs'; export * as items from './items/_module.mjs'; +export * as rollTables from './rollTables/_module.mjs'; diff --git a/module/applications/sheets/actors/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/module/applications/sheets/actors/companion.mjs b/module/applications/sheets/actors/companion.mjs index 6f3bbf16..b30b9c07 100644 --- a/module/applications/sheets/actors/companion.mjs +++ b/module/applications/sheets/actors/companion.mjs @@ -62,10 +62,10 @@ export default class DhCompanionSheet extends DHBaseActorSheet { title: `${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}: ${this.actor.name}`, headerTitle: `Companion ${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}`, roll: { - trait: partner.system.spellcastModifierTrait?.key + trait: partner.system.spellcastModifierTrait?.key, + companionRoll: true }, - hasRoll: true, - data: partner.getRollData() + hasRoll: true }; const result = await partner.diceRoll(config); diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index b590de86..3c0444eb 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -600,7 +600,7 @@ export default function DHApplicationMixin(Base) { { relativeTo: isAction ? doc.parent : doc, rollData: doc.getRollData?.(), - secrets: isAction ? doc.parent.isOwner : doc.isOwner + secrets: isAction ? doc.parent.parent.isOwner : doc.isOwner } ); } diff --git a/module/applications/sheets/rollTables/_module.mjs b/module/applications/sheets/rollTables/_module.mjs new file mode 100644 index 00000000..73067b64 --- /dev/null +++ b/module/applications/sheets/rollTables/_module.mjs @@ -0,0 +1 @@ +export { default as RollTableSheet } from './rollTable.mjs'; diff --git a/module/applications/sheets/rollTables/rollTable.mjs b/module/applications/sheets/rollTables/rollTable.mjs new file mode 100644 index 00000000..9ead6814 --- /dev/null +++ b/module/applications/sheets/rollTables/rollTable.mjs @@ -0,0 +1,191 @@ +export default class DhRollTableSheet extends foundry.applications.sheets.RollTableSheet { + static DEFAULT_OPTIONS = { + ...super.DEFAULT_OPTIONS, + actions: { + changeMode: DhRollTableSheet.#onChangeMode, + drawResult: DhRollTableSheet.#onDrawResult, + resetResults: DhRollTableSheet.#onResetResults, + addFormula: DhRollTableSheet.#addFormula, + removeFormula: DhRollTableSheet.#removeFormula + } + }; + + static buildParts() { + const { footer, header, sheet, results, ...parts } = super.PARTS; + return { + sheet: { + ...sheet, + template: 'systems/daggerheart/templates/sheets/rollTable/sheet.hbs' + }, + header: { template: 'systems/daggerheart/templates/sheets/rollTable/header.hbs' }, + ...parts, + results: { + template: 'systems/daggerheart/templates/sheets/rollTable/results.hbs', + templates: ['templates/sheets/roll-table/result-details.hbs'], + scrollable: ['table[data-results] tbody'] + }, + summary: { template: 'systems/daggerheart/templates/sheets/rollTable/summary.hbs' }, + footer + }; + } + + static PARTS = DhRollTableSheet.buildParts(); + + async _preRender(context, options) { + await super._preRender(context, options); + + if (!options.internalRefresh) + this.daggerheartFlag = new game.system.api.data.DhRollTable(this.document.flags.daggerheart); + } + + /* root PART has a blank element on _attachPartListeners, so it cannot be used to set the eventListeners for the view mode */ + async _onRender(context, options) { + super._onRender(context, options); + + for (const element of this.element.querySelectorAll('.system-update-field')) + element.addEventListener('change', this.updateSystemField.bind(this)); + } + + async _preparePartContext(partId, context, options) { + context = await super._preparePartContext(partId, context, options); + + switch (partId) { + case 'sheet': + context.altFormula = this.daggerheartFlag.altFormula; + context.usesAltFormula = Object.keys(this.daggerheartFlag.altFormula).length > 0; + context.altFormulaOptions = { + '': { name: this.daggerheartFlag.formulaName }, + ...this.daggerheartFlag.altFormula + }; + context.activeAltFormula = this.daggerheartFlag.activeAltFormula; + context.selectedFormula = this.daggerheartFlag.getActiveFormula(this.document.formula); + context.results = this.getExtendedResults(context.results); + break; + case 'header': + context.altFormula = this.daggerheartFlag.altFormula; + context.usesAltFormula = Object.keys(this.daggerheartFlag.altFormula).length > 0; + context.altFormulaOptions = { + '': { name: this.daggerheartFlag.formulaName }, + ...this.daggerheartFlag.altFormula + }; + context.activeAltFormula = this.daggerheartFlag.activeAltFormula; + break; + case 'summary': + context.systemFields = this.daggerheartFlag.schema.fields; + context.altFormula = this.daggerheartFlag.altFormula; + context.formulaName = this.daggerheartFlag.formulaName; + break; + case 'results': + context.results = this.getExtendedResults(context.results); + break; + } + + return context; + } + + getExtendedResults(results) { + const bodyDarkMode = document.body.classList.contains('theme-dark'); + const elementLightMode = this.element.classList.contains('theme-light'); + const elementDarkMode = this.element.classList.contains('theme-dark'); + const isDarkMode = elementDarkMode || (!elementLightMode && bodyDarkMode); + + return results.map(x => ({ + ...x, + displayImg: isDarkMode && x.img === 'icons/svg/d20-black.svg' ? 'icons/svg/d20.svg' : x.img + })); + } + + /* -------------------------------------------- */ + /* Flag SystemData update methods */ + /* -------------------------------------------- */ + + async updateSystemField(event) { + const { dataset, value } = event.target; + await this.daggerheartFlag.updateSource({ [dataset.path]: value }); + this.render({ internalRefresh: true }); + } + + getSystemFlagUpdate() { + const deleteUpdate = Object.keys(this.document._source.flags.daggerheart?.altFormula ?? {}).reduce( + (acc, formulaKey) => { + if (!this.daggerheartFlag.altFormula[formulaKey]) acc.altFormula[`-=${formulaKey}`] = null; + + return acc; + }, + { altFormula: {} } + ); + + return { ['flags.daggerheart']: foundry.utils.mergeObject(this.daggerheartFlag.toObject(), deleteUpdate) }; + } + + static async #addFormula() { + await this.daggerheartFlag.updateSource({ + [`altFormula.${foundry.utils.randomID()}`]: game.system.api.data.DhRollTable.getDefaultFormula() + }); + this.render({ internalRefresh: true }); + } + + static async #removeFormula(_event, target) { + await this.daggerheartFlag.updateSource({ + [`altFormula.-=${target.dataset.key}`]: null + }); + this.render({ internalRefresh: true }); + } + + /* -------------------------------------------- */ + /* Extended RollTable methods */ + /* -------------------------------------------- */ + + /** + * Alternate between view and edit modes. + * @this {RollTableSheet} + * @type {ApplicationClickAction} + */ + static async #onChangeMode() { + this.mode = this.isEditMode ? 'view' : 'edit'; + await this.document.update(this.getSystemFlagUpdate()); + await this.render({ internalRefresh: true }); + } + + /** @inheritdoc */ + async _processSubmitData(event, form, submitData, options) { + /* RollTable sends an empty dummy event when swapping from view/edit first time */ + if (Object.keys(submitData).length) { + if (!submitData.flags) submitData.flags = { daggerheart: {} }; + submitData.flags.daggerheart = this.getSystemFlagUpdate(); + } + + super._processSubmitData(event, form, submitData, options); + } + + /** @inheritdoc */ + static async #onResetResults() { + await this.document.update(this.getSystemFlagUpdate()); + await this.document.resetResults(); + } + + /** + * Roll and draw a TableResult. + * @this {RollTableSheet} + * @type {ApplicationClickAction} + */ + static async #onDrawResult(_event, button) { + if (this.form) await this.submit({ operation: { render: false } }); + button.disabled = true; + const table = this.document; + + await this.document.update(this.getSystemFlagUpdate()); + + /* Sending in the currently selectd activeFormula to table.roll to use as the formula */ + const selectedFormula = this.daggerheartFlag.getActiveFormula(this.document.formula); + const tableRoll = await table.roll({ selectedFormula }); + const draws = table.getResultsForRoll(tableRoll.roll.total); + if (draws.length > 0) { + if (game.settings.get('core', 'animateRollTable')) await this._animateRoll(draws); + await table.draw(tableRoll); + } + + // Reenable the button if drawing with replacement since the draw won't trigger a sheet re-render + if (table.replacement) button.disabled = false; + } +} diff --git a/module/applications/ui/combatTracker.mjs b/module/applications/ui/combatTracker.mjs index 288ba8ad..fc47f085 100644 --- a/module/applications/ui/combatTracker.mjs +++ b/module/applications/ui/combatTracker.mjs @@ -42,8 +42,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C this.combats .find(x => x.active) ?.system?.extendedBattleToggles?.reduce((acc, toggle) => (acc ?? 0) + toggle.category, null) ?? null; - const maxBP = CONFIG.DH.ENCOUNTER.BaseBPPerEncounter(context.characters.length) + modifierBP; - const currentBP = AdversaryBPPerEncounter(context.adversaries, context.characters); + const maxBP = CONFIG.DH.ENCOUNTER.BaseBPPerEncounter(context.allCharacters.length) + modifierBP; + const currentBP = AdversaryBPPerEncounter(context.adversaries, context.allCharacters); Object.assign(context, { fear: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), @@ -73,9 +73,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C Object.assign(context, { actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens, adversaries, - characters: characters - ?.filter(x => !x.isNPC) - .filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0), + allCharacters: characters, + characters: characters.filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0), spotlightRequests }); } diff --git a/module/applications/ui/effectsDisplay.mjs b/module/applications/ui/effectsDisplay.mjs index 0875e783..8c0c939c 100644 --- a/module/applications/ui/effectsDisplay.mjs +++ b/module/applications/ui/effectsDisplay.mjs @@ -76,6 +76,8 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica }; toggleHidden(token, focused) { + if (!this.element) return; + const effects = DhEffectsDisplay.getTokenEffects(focused ? token : null); this.element.hidden = effects.length === 0; diff --git a/module/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/_module.mjs b/module/data/_module.mjs index 7ad20808..f7e25a4e 100644 --- a/module/data/_module.mjs +++ b/module/data/_module.mjs @@ -1,6 +1,7 @@ export { default as DhCombat } from './combat.mjs'; export { default as DhCombatant } from './combatant.mjs'; export { default as DhTagTeamRoll } from './tagTeamRoll.mjs'; +export { default as DhRollTable } from './rollTable.mjs'; export { default as RegisteredTriggers } from './registeredTriggers.mjs'; export * as countdowns from './countdowns.mjs'; diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index 4e699f79..115e6463 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -241,6 +241,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel hasHealing: this.hasHealing, hasEffect: this.hasEffect, hasSave: this.hasSave, + onSave: this.save?.damageMod, isDirect: !!this.damage?.direct, selectedRollMode: game.settings.get('core', 'rollMode'), data: this.getRollData(), diff --git a/module/data/actor/base.mjs b/module/data/actor/base.mjs index b90361e2..08308eab 100644 --- a/module/data/actor/base.mjs +++ b/module/data/actor/base.mjs @@ -27,7 +27,7 @@ const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) => }); /* Common rules applying to Characters and Adversaries */ -export const commonActorRules = (extendedData = { damageReduction: {} }) => ({ +export const commonActorRules = (extendedData = { damageReduction: {}, attack: { damage: {} } }) => ({ conditionImmunities: new fields.SchemaField({ hidden: new fields.BooleanField({ initial: false }), restrained: new fields.BooleanField({ initial: false }), @@ -41,7 +41,23 @@ export const commonActorRules = (extendedData = { damageReduction: {} }) => ({ magical: new fields.NumberField({ initial: 0, min: 0 }), physical: new fields.NumberField({ initial: 0, min: 0 }) }), - ...extendedData.damageReduction + ...(extendedData.damageReduction ?? {}) + }), + attack: new fields.SchemaField({ + ...extendedData.attack, + damage: new fields.SchemaField({ + hpDamageMultiplier: new fields.NumberField({ + required: true, + nullable: false, + initial: 1 + }), + hpDamageTakenMultiplier: new fields.NumberField({ + required: true, + nullable: false, + initial: 1 + }), + ...(extendedData.attack?.damage ?? {}) + }) }) }); diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 897c4790..e8da2e10 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -253,35 +253,35 @@ export default class DhCharacter extends BaseDataActor { hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint' }), disabledArmor: new fields.BooleanField({ intial: false }) + }, + attack: { + damage: { + diceIndex: new fields.NumberField({ + integer: true, + min: 0, + max: 5, + initial: 0, + label: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.label', + hint: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.hint' + }), + bonus: new fields.NumberField({ + required: true, + initial: 0, + min: 0, + label: 'DAGGERHEART.GENERAL.Rules.attack.damage.bonus.label' + }) + }, + roll: new fields.SchemaField({ + trait: new fields.StringField({ + required: true, + choices: CONFIG.DH.ACTOR.abilities, + nullable: true, + initial: null, + label: 'DAGGERHEART.GENERAL.Rules.attack.roll.trait.label' + }) + }) } }), - attack: new fields.SchemaField({ - damage: new fields.SchemaField({ - diceIndex: new fields.NumberField({ - integer: true, - min: 0, - max: 5, - initial: 0, - label: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.label', - hint: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.hint' - }), - bonus: new fields.NumberField({ - required: true, - initial: 0, - min: 0, - label: 'DAGGERHEART.GENERAL.Rules.attack.damage.bonus.label' - }) - }), - roll: new fields.SchemaField({ - trait: new fields.StringField({ - required: true, - choices: CONFIG.DH.ACTOR.abilities, - nullable: true, - initial: null, - label: 'DAGGERHEART.GENERAL.Rules.attack.roll.trait.label' - }) - }) - }), dualityRoll: new fields.SchemaField({ defaultHopeDice: new fields.NumberField({ nullable: false, @@ -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() { @@ -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() { @@ -671,6 +682,8 @@ export default class DhCharacter extends BaseDataActor { } } } + + this.companion.system.attack.roll.bonus = this.traits.instinct.value; } this.resources.hope.value = Math.min(baseHope, this.resources.hope.max); diff --git a/module/data/fields/action/damageField.mjs b/module/data/fields/action/damageField.mjs index bb81c702..ef91c64e 100644 --- a/module/data/fields/action/damageField.mjs +++ b/module/data/fields/action/damageField.mjs @@ -105,12 +105,22 @@ export default class DamageField extends fields.SchemaField { damagePromises.push( actor.takeHealing(config.damage).then(updates => targetDamage.push({ token, updates })) ); - else + else { + const configDamage = foundry.utils.deepClone(config.damage); + const hpDamageMultiplier = config.actionActor?.system.rules.attack.damage.hpDamageMultiplier ?? 1; + const hpDamageTakenMultiplier = actor.system.rules.attack.damage.hpDamageTakenMultiplier; + if (configDamage.hitPoints) { + for (const part of configDamage.hitPoints.parts) { + part.total = Math.ceil(part.total * hpDamageMultiplier * hpDamageTakenMultiplier); + } + } + damagePromises.push( actor - .takeDamage(config.damage, config.isDirect) + .takeDamage(configDamage, config.isDirect) .then(updates => targetDamage.push({ token, updates })) ); + } } Promise.all(damagePromises).then(async _ => { diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index 2399b7db..84f39103 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -147,7 +147,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { return await foundry.applications.ux.TextEditor.implementation.enrichHTML(fullDescription, { relativeTo: this, rollData: this.getRollData(), - secrets: this.isOwner + secrets: this.parent.isOwner }); } diff --git a/module/data/registeredTriggers.mjs b/module/data/registeredTriggers.mjs index 8a100585..ee4f3b49 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; @@ -71,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); @@ -83,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; @@ -107,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/data/rollTable.mjs b/module/data/rollTable.mjs new file mode 100644 index 00000000..78f7e6dd --- /dev/null +++ b/module/data/rollTable.mjs @@ -0,0 +1,38 @@ +import FormulaField from './fields/formulaField.mjs'; + +//Extra definitions for RollTable +export default class DhRollTable extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + formulaName: new fields.StringField({ + required: true, + nullable: false, + initial: 'Roll Formula', + label: 'DAGGERHEART.ROLLTABLES.FIELDS.formulaName.label' + }), + altFormula: new fields.TypedObjectField( + new fields.SchemaField({ + name: new fields.StringField({ + required: true, + nullable: false, + initial: 'Roll Formula', + label: 'DAGGERHEART.ROLLTABLES.FIELDS.formulaName.label' + }), + formula: new FormulaField({ label: 'Formula Roll', initial: '1d20' }) + }) + ), + activeAltFormula: new fields.StringField({ nullable: true, initial: null }) + }; + } + + getActiveFormula(baseFormula) { + return this.activeAltFormula ? (this.altFormula[this.activeAltFormula]?.formula ?? baseFormula) : baseFormula; + } + + static getDefaultFormula = () => ({ + name: game.i18n.localize('Roll Formula'), + formula: '1d20' + }); +} diff --git a/module/data/settings/Automation.mjs b/module/data/settings/Automation.mjs index 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/dice/d20Roll.mjs b/module/dice/d20Roll.mjs index 3ddd8027..f117ff65 100644 --- a/module/dice/d20Roll.mjs +++ b/module/dice/d20Roll.mjs @@ -99,11 +99,14 @@ export default class D20Roll extends DHRoll { this.options.roll.modifiers = this.applyBaseBonus(); + const actorExperiences = this.options.roll.companionRoll + ? (this.options.data?.companion?.system.experiences ?? {}) + : (this.options.data.system?.experiences ?? {}); this.options.experiences?.forEach(m => { - if (this.options.data.system?.experiences?.[m]) + if (actorExperiences[m]) this.options.roll.modifiers.push({ - label: this.options.data.system.experiences[m].name, - value: this.options.data.system.experiences[m].value + label: actorExperiences[m].name, + value: actorExperiences[m].value }); }); diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index a5ac5091..1977c7ea 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -3,7 +3,7 @@ import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; export default class DHRoll extends Roll { baseTerms = []; constructor(formula, data = {}, options = {}) { - super(formula, data, options); + super(formula, data, foundry.utils.mergeObject(options, { roll: [] }, { overwrite: false })); options.bonusEffects = this.bonusEffectBuilder(); if (!this.data || !Object.keys(this.data).length) this.data = options.data; } diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index 0edbe5ad..e65d0ff5 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -274,7 +274,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/documents/_module.mjs b/module/documents/_module.mjs index 8073cfe1..b9cfd3f2 100644 --- a/module/documents/_module.mjs +++ b/module/documents/_module.mjs @@ -4,6 +4,7 @@ export { default as DhpCombat } from './combat.mjs'; export { default as DHCombatant } from './combatant.mjs'; export { default as DhActiveEffect } from './activeEffect.mjs'; export { default as DhChatMessage } from './chatMessage.mjs'; +export { default as DhRollTable } from './rollTable.mjs'; export { default as DhScene } from './scene.mjs'; export { default as DhToken } from './token.mjs'; export { default as DhTooltipManager } from './tooltipManager.mjs'; diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 1616c496..e8bea0bf 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -612,7 +612,7 @@ export default class DhpActor extends Actor { if (!updates.length) return; const hpDamage = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id); - if (hpDamage) { + if (hpDamage?.value) { hpDamage.value = this.convertDamageToThreshold(hpDamage.value); if ( this.type === 'character' && @@ -854,8 +854,8 @@ export default class DhpActor extends Actor { async toggleDefeated(defeatedState) { const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).defeated; - const { unconscious, defeated, dead } = CONFIG.DH.GENERAL.conditions(); - const defeatedConditions = new Set([unconscious.id, defeated.id, dead.id]); + const { deathMove, unconscious, defeated, dead } = CONFIG.DH.GENERAL.conditions(); + const defeatedConditions = new Set([deathMove.id, unconscious.id, defeated.id, dead.id]); if (!defeatedState) { for (let defeatedId of defeatedConditions) { await this.toggleStatusEffect(defeatedId, { overlay: settings.overlay, active: defeatedState }); @@ -869,6 +869,18 @@ export default class DhpActor extends Actor { } } + async setDeathMoveDefeated(defeatedIconId) { + const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).defeated; + const actorDefault = settings[`${this.type}Default`]; + if (!settings.enabled || !settings.enabled || !actorDefault || actorDefault === defeatedIconId) return; + + for (let defeatedId of Object.keys(CONFIG.DH.GENERAL.defeatedConditionChoices)) { + await this.toggleStatusEffect(defeatedId, { overlay: settings.overlay, active: false }); + } + + if (defeatedIconId) await this.toggleStatusEffect(defeatedIconId, { overlay: settings.overlay, active: true }); + } + queueScrollText(scrollingTextData) { this.#scrollTextQueue.push(...scrollingTextData.map(data => () => createScrollText(this, data))); if (!this.#scrollTextInterval) { diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index e03c3cf0..1d2c6c41 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -188,7 +188,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { config = foundry.utils.deepClone(this.system); config.event = event; - if (this.system.onSave) { + if (config.hasSave) { const pendingingSaves = targets.filter(t => t.saved.success === null); if (pendingingSaves.length) { const confirm = await foundry.applications.api.DialogV2.confirm({ diff --git a/module/documents/rollTable.mjs b/module/documents/rollTable.mjs new file mode 100644 index 00000000..50b8fe63 --- /dev/null +++ b/module/documents/rollTable.mjs @@ -0,0 +1,122 @@ +export default class DhRollTable extends foundry.documents.RollTable { + async roll({ selectedFormula, roll, recursive = true, _depth = 0 } = {}) { + // Prevent excessive recursion + if (_depth > 5) { + throw new Error(`Maximum recursion depth exceeded when attempting to draw from RollTable ${this.id}`); + } + + const formula = selectedFormula ?? this.formula; + + // If there is no formula, automatically calculate an even distribution + if (!this.formula) { + await this.normalize(); + } + + // Reference the provided roll formula + roll = roll instanceof Roll ? roll : Roll.create(formula); + let results = []; + + // Ensure that at least one non-drawn result remains + const available = this.results.filter(r => !r.drawn); + if (!available.length) { + ui.notifications.warn(game.i18n.localize('TABLE.NoAvailableResults')); + return { roll, results }; + } + + // Ensure that results are available within the minimum/maximum range + const minRoll = (await roll.reroll({ minimize: true })).total; + const maxRoll = (await roll.reroll({ maximize: true })).total; + const availableRange = available.reduce( + (range, result) => { + const r = result.range; + if (!range[0] || r[0] < range[0]) range[0] = r[0]; + if (!range[1] || r[1] > range[1]) range[1] = r[1]; + return range; + }, + [null, null] + ); + if (availableRange[0] > maxRoll || availableRange[1] < minRoll) { + ui.notifications.warn('No results can possibly be drawn from this table and formula.'); + return { roll, results }; + } + + // Continue rolling until one or more results are recovered + let iter = 0; + while (!results.length) { + if (iter >= 10000) { + ui.notifications.error( + `Failed to draw an available entry from Table ${this.name}, maximum iteration reached` + ); + break; + } + roll = await roll.reroll(); + results = this.getResultsForRoll(roll.total); + iter++; + } + + // Draw results recursively from any inner Roll Tables + if (recursive) { + const inner = []; + for (const result of results) { + const { type, documentUuid } = result; + const documentName = foundry.utils.parseUuid(documentUuid)?.type; + if (type === 'document' && documentName === 'RollTable') { + const innerTable = await fromUuid(documentUuid); + if (innerTable) { + const innerRoll = await innerTable.roll({ _depth: _depth + 1 }); + inner.push(...innerRoll.results); + } + } else inner.push(result); + } + results = inner; + } + + // Return the Roll and the results + return { roll, results }; + } + + async toMessage(results, { roll, messageData = {}, messageOptions = {} } = {}) { + messageOptions.rollMode ??= game.settings.get('core', 'rollMode'); + + // Construct chat data + messageData = foundry.utils.mergeObject( + { + author: game.user.id, + speaker: foundry.documents.ChatMessage.implementation.getSpeaker(), + rolls: [], + sound: roll ? CONFIG.sounds.dice : null, + flags: { 'core.RollTable': this.id } + }, + messageData + ); + if (roll) messageData.rolls.push(roll); + + // Render the chat card which combines the dice roll with the drawn results + const detailsPromises = await Promise.allSettled(results.map(r => r.getHTML())); + const flavorKey = `TABLE.DrawFlavor${results.length > 1 ? 'Plural' : ''}`; + const flavor = game.i18n.format(flavorKey, { + number: results.length, + name: foundry.utils.escapeHTML(this.name) + }); + messageData.content = await foundry.applications.handlebars.renderTemplate(CONFIG.RollTable.resultTemplate, { + description: await TextEditor.implementation.enrichHTML(this.description, { + documents: true, + secrets: this.isOwner + }), + flavor: flavor, + results: results.map((result, i) => { + const r = result.toObject(false); + r.details = detailsPromises[i].value ?? ''; + const useTableIcon = + result.icon === CONFIG.RollTable.resultIcon && this.img !== this.constructor.DEFAULT_ICON; + r.icon = useTableIcon ? this.img : result.icon; + return r; + }), + rollHTML: this.displayRoll && roll ? await roll.render() : null, + table: this + }); + + // Create the chat message + return foundry.documents.ChatMessage.implementation.create(messageData, messageOptions); + } +} diff --git a/module/documents/scene.mjs b/module/documents/scene.mjs index 7f880b1d..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); diff --git a/module/enrichers/DualityRollEnricher.mjs b/module/enrichers/DualityRollEnricher.mjs index f6f022f9..91149fd8 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, customConfig }, + { reaction, traitValue, target, difficulty, title, label, advantage, grantResources, customConfig }, event ) => { const config = { @@ -93,13 +96,18 @@ export const enrichedDualityRoll = async ( advantage, type: reaction ? 'reaction' : null }, + skips: { + resources: !grantResources, + triggers: !grantResources + }, type: 'trait', hasRoll: true, ...(customConfig ?? {}) }; 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: {} }; diff --git a/module/systemRegistration/migrations.mjs b/module/systemRegistration/migrations.mjs index b3116459..086647bf 100644 --- a/module/systemRegistration/migrations.mjs +++ b/module/systemRegistration/migrations.mjs @@ -210,6 +210,22 @@ export async function runMigrations() { lastMigrationVersion = '1.2.7'; } + + if (foundry.utils.isNewerVersion('1.5.5', lastMigrationVersion)) { + for (const scene of game.scenes) { + if (!scene.flags.daggerheart) continue; + const systemData = new game.system.api.data.scenes.DHScene(scene.flags.daggerheart); + const sceneEnvironments = systemData.sceneEnvironments; + + const newEnvironments = sceneEnvironments.filter(x => !x?.pack); + if (newEnvironments.length !== sceneEnvironments.length) + await scene.update({ 'flags.daggerheart.sceneEnvironments': newEnvironments }); + } + + ui.nav.render(true); + + lastMigrationVersion = '1.5.5'; + } //#endregion await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion, lastMigrationVersion); diff --git a/src/packs/adversaries/adversary_Demon_of_Despair_kE4dfhqmIQpNd44e.json b/src/packs/adversaries/adversary_Demon_of_Despair_kE4dfhqmIQpNd44e.json index 188b2687..830848c3 100644 --- a/src/packs/adversaries/adversary_Demon_of_Despair_kE4dfhqmIQpNd44e.json +++ b/src/packs/adversaries/adversary_Demon_of_Despair_kE4dfhqmIQpNd44e.json @@ -235,7 +235,51 @@ }, "_id": "2ESeh4tPhr6DI5ty", "img": "icons/magic/death/skull-horned-worn-fire-blue.webp", - "effects": [], + "effects": [ + { + "name": "Depths Of Despair", + "type": "base", + "system": { + "rangeDependence": { + "enabled": false, + "type": "withinRange", + "target": "hostile", + "range": "melee" + } + }, + "_id": "nofxm1vGZ2TmceA2", + "img": "icons/magic/death/skull-horned-worn-fire-blue.webp", + "changes": [ + { + "key": "system.rules.attack.damage.hpDamageMultiplier", + "mode": 5, + "value": "2", + "priority": null + } + ], + "disabled": true, + "duration": { + "startTime": null, + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null + }, + "description": "
The Demon of Despair deals double damage to PCs with 0 Hope.
", + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null + }, + "_key": "!actors.items.effects!kE4dfhqmIQpNd44e.2ESeh4tPhr6DI5ty.nofxm1vGZ2TmceA2" + } + ], "folder": null, "sort": 0, "ownership": { diff --git a/src/packs/adversaries/adversary_Failed_Experiment_ChwwVqowFw8hJQwT.json b/src/packs/adversaries/adversary_Failed_Experiment_ChwwVqowFw8hJQwT.json index 39800002..408d5102 100644 --- a/src/packs/adversaries/adversary_Failed_Experiment_ChwwVqowFw8hJQwT.json +++ b/src/packs/adversaries/adversary_Failed_Experiment_ChwwVqowFw8hJQwT.json @@ -304,7 +304,51 @@ }, "_id": "1fE6xo8yIOmZkGNE", "img": "icons/skills/melee/strike-slashes-orange.webp", - "effects": [], + "effects": [ + { + "name": "Overwhelm", + "type": "base", + "system": { + "rangeDependence": { + "enabled": false, + "type": "withinRange", + "target": "hostile", + "range": "melee" + } + }, + "_id": "eGB9G0ljYCcdGbOx", + "img": "icons/skills/melee/strike-slashes-orange.webp", + "changes": [ + { + "key": "system.rules.attack.damage.hpDamageMultiplier", + "mode": 5, + "value": "2", + "priority": null + } + ], + "disabled": true, + "duration": { + "startTime": null, + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null + }, + "description": "When a target the Failed Experiment attacks has other adversaries within Very Close range, the Failed Experiment deals double damage.
", + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null + }, + "_key": "!actors.items.effects!ChwwVqowFw8hJQwT.1fE6xo8yIOmZkGNE.eGB9G0ljYCcdGbOx" + } + ], "folder": null, "sort": 0, "ownership": { diff --git a/src/packs/adversaries/adversary_Hallowed_Archer_kabueAo6BALApWqp.json b/src/packs/adversaries/adversary_Hallowed_Archer_kabueAo6BALApWqp.json index 0abf1661..8cce1b94 100644 --- a/src/packs/adversaries/adversary_Hallowed_Archer_kabueAo6BALApWqp.json +++ b/src/packs/adversaries/adversary_Hallowed_Archer_kabueAo6BALApWqp.json @@ -229,7 +229,51 @@ }, "_id": "FGJTAeL38zTVd4fA", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", - "effects": [], + "effects": [ + { + "name": "Punish the Guilty", + "type": "base", + "system": { + "rangeDependence": { + "enabled": false, + "type": "withinRange", + "target": "hostile", + "range": "melee" + } + }, + "_id": "ID85zoIa5GfhNMti", + "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", + "changes": [ + { + "key": "system.rules.attack.damage.hpDamageMultiplier", + "mode": 5, + "value": "2", + "priority": null + } + ], + "disabled": true, + "duration": { + "startTime": null, + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null + }, + "description": "The Hallowed Archer deals double damage to targets marked Guilty by a High Seraph.
", + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null + }, + "_key": "!actors.items.effects!kabueAo6BALApWqp.FGJTAeL38zTVd4fA.ID85zoIa5GfhNMti" + } + ], "folder": null, "sort": 0, "ownership": { diff --git a/src/packs/adversaries/adversary_Jagged_Knife_Kneebreaker_CBKixLH3yhivZZuL.json b/src/packs/adversaries/adversary_Jagged_Knife_Kneebreaker_CBKixLH3yhivZZuL.json index fc644604..c38260e9 100644 --- a/src/packs/adversaries/adversary_Jagged_Knife_Kneebreaker_CBKixLH3yhivZZuL.json +++ b/src/packs/adversaries/adversary_Jagged_Knife_Kneebreaker_CBKixLH3yhivZZuL.json @@ -336,7 +336,14 @@ "range": "melee" } }, - "changes": [], + "changes": [ + { + "key": "system.rules.attack.damage.hpDamageTakenMultiplier", + "mode": 5, + "value": "2", + "priority": null + } + ], "disabled": false, "duration": { "startTime": null, @@ -350,8 +357,8 @@ "description": "", "tint": "#ffffff", "statuses": [ - "restrained", - "vulnerable" + "vulnerable", + "restrained" ], "sort": 0, "flags": {}, diff --git a/src/packs/adversaries/adversary_Skeleton_Archer_7X5q7a6ueeHs5oA9.json b/src/packs/adversaries/adversary_Skeleton_Archer_7X5q7a6ueeHs5oA9.json index e5381f6f..9d837ac0 100644 --- a/src/packs/adversaries/adversary_Skeleton_Archer_7X5q7a6ueeHs5oA9.json +++ b/src/packs/adversaries/adversary_Skeleton_Archer_7X5q7a6ueeHs5oA9.json @@ -230,7 +230,51 @@ "subType": null, "originId": null }, - "effects": [], + "effects": [ + { + "name": "Opportunist", + "type": "base", + "system": { + "rangeDependence": { + "enabled": false, + "type": "withinRange", + "target": "hostile", + "range": "melee" + } + }, + "_id": "O03vYbyNLO3YPZGo", + "img": "icons/skills/targeting/crosshair-triple-strike-orange.webp", + "changes": [ + { + "key": "system.rules.attack.damage.hpDamageMultiplier", + "mode": 5, + "value": "2", + "priority": null + } + ], + "disabled": true, + "duration": { + "startTime": null, + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null + }, + "description": "When two or more adversaries are within Very Close range of a creature, all damage the Skeleton Archer deals to that creature is doubled.
", + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null + }, + "_key": "!actors.items.effects!7X5q7a6ueeHs5oA9.6mL2FQ9pQdfoDNzG.O03vYbyNLO3YPZGo" + } + ], "folder": null, "sort": 0, "ownership": { diff --git a/src/packs/communities/feature_Know_the_Tide_07x6Qe6qMzDw2xN4.json b/src/packs/communities/feature_Know_the_Tide_07x6Qe6qMzDw2xN4.json index 069fe6ba..41f11a74 100644 --- a/src/packs/communities/feature_Know_the_Tide_07x6Qe6qMzDw2xN4.json +++ b/src/packs/communities/feature_Know_the_Tide_07x6Qe6qMzDw2xN4.json @@ -9,13 +9,47 @@ "resource": { "type": "simple", "value": 0, - "max": "", - "icon": "", - "recovery": null, + "max": "@system.levelData.level.current", + "icon": "fa-solid fa-water", + "recovery": "session", "diceStates": {}, "dieFaces": "d4" }, - "actions": {}, + "actions": { + "tFlus34KotJjHfTe": { + "type": "effect", + "_id": "tFlus34KotJjHfTe", + "systemPath": "actions", + "baseAction": false, + "description": "", + "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, + "actionType": "action", + "triggers": [ + { + "trigger": "fearRoll", + "triggeringActorType": "self", + "command": "const { max, value } = this.item.system.resource;\nconst maxValue = actor.system.levelData.level.current;\nconst afterUpdate = value+1;\nif (afterUpdate > maxValue) return;\n\nui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.knowTheTide'));\nreturn { updates: [{\n key: 'resource',\n itemId: this.item.id,\n target: this.item,\n value: 1,\n}]};" + } + ], + "cost": [], + "uses": { + "value": null, + "max": "", + "recovery": null, + "consumeOnSuccess": false + }, + "effects": [], + "target": { + "type": "any", + "amount": null + }, + "name": "Know The Tide", + "range": "" + } + }, "originItemType": null, "subType": null, "originId": null, diff --git a/src/packs/rolltables/tables_Consumables_tF04P02yVN1YDVel.json b/src/packs/rolltables/tables_Consumables_tF04P02yVN1YDVel.json index c2413ec3..c3f5ffdc 100644 --- a/src/packs/rolltables/tables_Consumables_tF04P02yVN1YDVel.json +++ b/src/packs/rolltables/tables_Consumables_tF04P02yVN1YDVel.json @@ -1,7 +1,7 @@ { "name": "Consumables", "img": "icons/consumables/potions/bottle-corked-red.webp", - "description": "To generate a random consumable, choose a rarity, roll the designated dice, and match the total to the item in the table:
Common: 1d12 or 2d12
Uncommon: 2d12 or 3d12
Rare: 3d12 or 4d12
Legendary: 4d12 or 5d12
To generate a random item, choose a rarity, roll the designated dice, and match the total to the item in the table:
Common: 1d12 or 2d12
Uncommon: 2d12 or 3d12
Rare: 3d12 or 4d12
Legendary: 4d12 or 5d12
Layering Goals Other than Attrition into Combat
", + "description": "", "results": [ { "type": "text", @@ -311,7 +311,20 @@ "default": 0, "Bgvu4A6AMkRFOTGR": 3 }, - "flags": {}, + "flags": { + "daggerheart": { + "formulaName": "Roll Formula", + "altFormula": {}, + "activeAltFormula": null, + "flags": { + "daggerheart": { + "formulaName": "Roll Formula", + "altFormula": {}, + "activeAltFormula": null + } + } + } + }, "formula": "1d12", "_id": "I5L1dlgxXTNrCCkL", "sort": 400000, diff --git a/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/styles/less/global/chat.less b/styles/less/global/chat.less index dc671e44..b9478ea4 100644 --- a/styles/less/global/chat.less +++ b/styles/less/global/chat.less @@ -15,6 +15,14 @@ .message-header .message-header-main .message-sub-header-container h4 { color: @dark-blue; } + + .message-content { + .table-draw { + .table-description { + color: @dark; + } + } + } } } @@ -83,6 +91,7 @@ .message-content { padding-bottom: 8px; + .flavor-text { font-size: var(--font-size-12); line-height: 20px; @@ -90,6 +99,33 @@ text-align: center; display: block; } + + .table-draw { + .table-flavor { + padding-top: 5px; + padding-bottom: 0.5rem; + font-size: var(--font-size-12); + } + + .table-description { + color: @beige; + font-style: italic; + + &.flavor-spaced { + padding-top: 0; + } + } + + .table-results { + .description { + flex-basis: min-content; + + > p:first-of-type { + margin-top: 0; + } + } + } + } } } } diff --git a/styles/less/hud/token-hud/token-hud.less b/styles/less/hud/token-hud/token-hud.less index 46003975..ea58f673 100644 --- a/styles/less/hud/token-hud/token-hud.less +++ b/styles/less/hud/token-hud/token-hud.less @@ -24,13 +24,13 @@ font-weight: bold; } } + } - .clown-car img { - transition: 0.5s; + .clown-car img { + transition: 0.5s; - &.flipped { - transform: scaleX(-1); - } + &.flipped { + transform: scaleX(-1); } } diff --git a/styles/less/sheets/index.less b/styles/less/sheets/index.less index 216cda33..1bdb451a 100644 --- a/styles/less/sheets/index.less +++ b/styles/less/sheets/index.less @@ -40,4 +40,5 @@ @import './items/heritage.less'; @import './items/item-sheet-shared.less'; +@import './rollTables/sheet.less'; @import './actions/actions.less'; diff --git a/styles/less/sheets/rollTables/sheet.less b/styles/less/sheets/rollTables/sheet.less new file mode 100644 index 00000000..a7c05455 --- /dev/null +++ b/styles/less/sheets/rollTables/sheet.less @@ -0,0 +1,29 @@ +.application.sheet.roll-table-sheet { + .formulas-section { + legend { + margin-left: auto; + margin-right: auto; + } + + .formulas-container { + display: grid; + grid-template-columns: 1fr 1fr 40px; + gap: 10px; + text-align: center; + + .formula-button { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + } + } + } + + .roll-table-view-formula-container { + width: fit-content; + display: flex; + align-items: center; + gap: 4px; + } +} diff --git a/system.json b/system.json index 8e4d19d9..fc7d0139 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.5.4", + "version": "1.5.5", "compatibility": { "minimum": "13.346", "verified": "13.351", 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 @@ +