diff --git a/daggerheart.mjs b/daggerheart.mjs index 55a7d0bf..37efbef6 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -61,8 +61,10 @@ CONFIG.Token.hudClass = applications.hud.DHTokenHUD; CONFIG.ui.combat = applications.ui.DhCombatTracker; CONFIG.ui.chat = applications.ui.DhChatLog; +CONFIG.ui.effectsDisplay = applications.ui.DhEffectsDisplay; CONFIG.ui.hotbar = applications.ui.DhHotbar; CONFIG.ui.sidebar = applications.sidebar.DhSidebar; +CONFIG.ui.actors = applications.sidebar.DhActorDirectory; CONFIG.ui.daggerheartMenu = applications.sidebar.DaggerheartMenu; CONFIG.ui.resources = applications.ui.DhFearTracker; CONFIG.ui.countdowns = applications.ui.DhCountdowns; @@ -167,6 +169,9 @@ Hooks.on('ready', async () => { ui.countdowns.render({ force: true }); } + ui.effectsDisplay = new CONFIG.ui.effectsDisplay(); + ui.effectsDisplay.render({ force: true }); + if (!(ui.compendiumBrowser instanceof applications.ui.ItemBrowser)) ui.compendiumBrowser = new applications.ui.ItemBrowser(); diff --git a/lang/en.json b/lang/en.json index c99cf652..ce4140f0 100755 --- a/lang/en.json +++ b/lang/en.json @@ -1221,7 +1221,7 @@ }, "burning": { "name": "Burning", - "description": "When you roll the maximum value on a damage die, roll an additional damage die.", + "description": "When you roll a 6 on a damage die, the target must mark a Stress.", "actions": { "burn": { "name": "Burn", @@ -2032,6 +2032,7 @@ "basics": "Basics", "bonus": "Bonus", "burden": "Burden", + "condition": "Condition", "continue": "Continue", "criticalSuccess": "Critical Success", "criticalShort": "Critical", @@ -2585,6 +2586,10 @@ "decreasingLoop": "Decreasing Looping", "increasingLoop": "Increasing Looping" }, + "EffectsDisplay": { + "removeThing": "[Right Click] Remove {thing}", + "appliedBy": "Applied By: {by}" + }, "ItemBrowser": { "title": "Daggerheart Compendium Browser", "hint": "Select a Folder in sidebar to start browsing through the compendium", @@ -2702,6 +2707,12 @@ "documentIsMissing": "The {documentType} is missing from the world." }, "Sidebar": { + "actorDirectory": { + "tier": "Tier {tier} {type}", + "character": "Level {level} Character", + "companion": "Level {level} - {partner}", + "companionNoPartner": "No Partner" + }, "daggerheartMenu": { "title": "Daggerheart Menu", "startSession": "Start Session", diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index 2534a2b8..f46125d8 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -197,7 +197,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio this.config.actionType = this.reactionOverride ? CONFIG.DH.ITEM.actionTypes.reaction.id : this.config.actionType === CONFIG.DH.ITEM.actionTypes.reaction.id - ? null + ? CONFIG.DH.ITEM.actionTypes.action.id : this.config.actionType; this.render(); } diff --git a/module/applications/hud/tokenHUD.mjs b/module/applications/hud/tokenHUD.mjs index 5fa40a4c..f90c26be 100644 --- a/module/applications/hud/tokenHUD.mjs +++ b/module/applications/hud/tokenHUD.mjs @@ -21,7 +21,6 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { async _prepareContext(options) { const context = await super._prepareContext(options); - context.partyOnCanvas = this.actor.type === 'party' && this.actor.system.partyMembers.some(member => member.getActiveTokens().length > 0); @@ -31,6 +30,7 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { context.canToggleCombat = DHTokenHUD.#nonCombatTypes.includes(this.actor.type) ? false : context.canToggleCombat; + context.systemStatusEffects = Object.keys(context.statusEffects).reduce((acc, key) => { const effect = context.statusEffects[key]; if (effect.systemEffect) { @@ -193,16 +193,18 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { } // Update the status of effects which are active for the token actor - const activeEffects = this.actor?.effects || []; + const activeEffects = this.actor?.getActiveEffects() || []; for (const effect of activeEffects) { for (const statusId of effect.statuses) { const status = choices[statusId]; + status.instances = 1 + (status.instances ?? 0); + status.locked = status.locked || effect.condition || status.instances > 1; if (!status) continue; if (status._id) { if (status._id !== effect.id) continue; } status.isActive = true; - if (effect.getFlag('core', 'overlay')) status.isOverlay = true; + if (effect.getFlag?.('core', 'overlay')) status.isOverlay = true; } } diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index c4962b18..7da49eb7 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -717,6 +717,7 @@ export default class CharacterSheet extends DHBaseActorSheet { }; const result = await this.document.diceRoll({ ...config, + actionType: 'action', headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`, title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { ability: abilityLabel diff --git a/module/applications/sheets/api/base-actor.mjs b/module/applications/sheets/api/base-actor.mjs index e11d841d..348ffa99 100644 --- a/module/applications/sheets/api/base-actor.mjs +++ b/module/applications/sheets/api/base-actor.mjs @@ -271,7 +271,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { cancel = true; } } else { - await originActor.deleteEmbeddedDocuments('Item', [data.originId], { render: false }); + await originActor.deleteEmbeddedDocuments('Item', [data.originId]); const createData = dropDocument.toObject(); await this.document.createEmbeddedDocuments('Item', [createData]); } diff --git a/module/applications/sidebar/_module.mjs b/module/applications/sidebar/_module.mjs index f19f697c..1f3207bc 100644 --- a/module/applications/sidebar/_module.mjs +++ b/module/applications/sidebar/_module.mjs @@ -1,2 +1,3 @@ export { default as DaggerheartMenu } from './tabs/daggerheartMenu.mjs'; +export { default as DhActorDirectory } from './tabs/actorDirectory.mjs'; export { default as DhSidebar } from './sidebar.mjs'; diff --git a/module/applications/sidebar/tabs/actorDirectory.mjs b/module/applications/sidebar/tabs/actorDirectory.mjs new file mode 100644 index 00000000..4a528e74 --- /dev/null +++ b/module/applications/sidebar/tabs/actorDirectory.mjs @@ -0,0 +1,20 @@ +export default class DhActorDirectory extends foundry.applications.sidebar.tabs.ActorDirectory { + static DEFAULT_OPTIONS = { + renderUpdateKeys: ['system.levelData.level.current', 'system.partner', 'system.tier'] + }; + + static _entryPartial = 'systems/daggerheart/templates/ui/sidebar/actor-document-partial.hbs'; + + async _prepareDirectoryContext(context, options) { + await super._prepareDirectoryContext(context, options); + const adversaryTypes = CONFIG.DH.ACTOR.allAdversaryTypes(); + const environmentTypes = CONFIG.DH.ACTOR.environmentTypes; + context.getTypeLabel = document => { + return document.type === 'adversary' + ? game.i18n.localize(adversaryTypes[document.system.type]?.label ?? 'TYPES.Actor.adversary') + : document.type === 'environment' + ? game.i18n.localize(environmentTypes[document.system.type]?.label ?? 'TYPES.Actor.environment') + : null; + }; + } +} diff --git a/module/applications/sidebar/tabs/daggerheartMenu.mjs b/module/applications/sidebar/tabs/daggerheartMenu.mjs index 0f98f5a0..4166614d 100644 --- a/module/applications/sidebar/tabs/daggerheartMenu.mjs +++ b/module/applications/sidebar/tabs/daggerheartMenu.mjs @@ -58,7 +58,7 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract if (['character', 'adversary'].includes(actor.type) && actor.prototypeToken.actorLink) { const updates = {}; for (let item of actor.items) { - if (item.system.metadata.hasResource && types.includes(item.system.resource?.recovery)) { + if (item.system.metadata?.hasResource && types.includes(item.system.resource?.recovery)) { if (!refreshedActors[actor.id]) refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() }; refreshedActors[actor.id].refreshed.add( @@ -76,7 +76,7 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract : Roll.replaceFormulaData(item.system.resource.max, actor.getRollData()) }; } - if (item.system.metadata.hasActions) { + if (item.system.metadata?.hasActions) { const refreshTypes = new Set(); const actions = item.system.actions.filter(action => { if (types.includes(action.uses.recovery)) { diff --git a/module/applications/ui/_module.mjs b/module/applications/ui/_module.mjs index 35a58566..d5f31906 100644 --- a/module/applications/ui/_module.mjs +++ b/module/applications/ui/_module.mjs @@ -2,6 +2,7 @@ export { default as CountdownEdit } from './countdownEdit.mjs'; export { default as DhCountdowns } from './countdowns.mjs'; export { default as DhChatLog } from './chatLog.mjs'; export { default as DhCombatTracker } from './combatTracker.mjs'; +export { default as DhEffectsDisplay } from './effectsDisplay.mjs'; export { default as DhFearTracker } from './fearTracker.mjs'; export { default as DhHotbar } from './hotbar.mjs'; export { ItemBrowser } from './itemBrowser.mjs'; diff --git a/module/applications/ui/countdowns.mjs b/module/applications/ui/countdowns.mjs index d35f2eb8..96315b17 100644 --- a/module/applications/ui/countdowns.mjs +++ b/module/applications/ui/countdowns.mjs @@ -14,7 +14,6 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application constructor(options = {}) { super(options); - this.sidebarCollapsed = true; this.setupHooks(); } @@ -98,11 +97,10 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application async _prepareContext(options) { const context = await super._prepareContext(options); context.isGM = game.user.isGM; - context.sidebarCollapsed = this.sidebarCollapsed; + context.iconOnly = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode) === CONFIG.DH.GENERAL.countdownAppMode.iconOnly; - const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); context.countdowns = this.#getCountdowns().reduce((acc, { key, countdown, ownership }) => { const playersWithAccess = game.users.reduce((acc, user) => { diff --git a/module/applications/ui/effectsDisplay.mjs b/module/applications/ui/effectsDisplay.mjs new file mode 100644 index 00000000..7f90e30b --- /dev/null +++ b/module/applications/ui/effectsDisplay.mjs @@ -0,0 +1,117 @@ +import { RefreshType } from '../../systemRegistration/socket.mjs'; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +/** + * A UI element which displays the Active Effects on a selected token. + * + * @extends ApplicationV2 + * @mixes HandlebarsApplication + */ + +export default class DhEffectsDisplay extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(options = {}) { + super(options); + + this.setupHooks(); + } + + /** @inheritDoc */ + static DEFAULT_OPTIONS = { + id: 'effects-display', + tag: 'div', + classes: ['daggerheart', 'dh-style', 'effects-display'], + window: { + frame: false, + positioned: false, + resizable: false, + minimizable: false + }, + actions: {} + }; + + /** @override */ + static PARTS = { + resources: { + root: true, + template: 'systems/daggerheart/templates/ui/effects-display.hbs' + } + }; + + get element() { + return document.body.querySelector('.daggerheart.dh-style.effects-display'); + } + + get hidden() { + return this.element.classList.contains('hidden'); + } + + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + if (this.element) { + this.element.querySelectorAll('.effect-container a').forEach(element => { + element.addEventListener('contextmenu', this.removeEffect.bind(this)); + }); + } + } + + /** @override */ + async _prepareContext(options) { + const context = await super._prepareContext(options); + context.effects = DhEffectsDisplay.getTokenEffects(); + + return context; + } + + static getTokenEffects = token => { + const actor = token + ? token.actor + : canvas.tokens.controlled.length === 0 + ? !game.user.isGM + ? game.user.character + : null + : canvas.tokens.controlled[0].actor; + return actor?.getActiveEffects() ?? []; + }; + + toggleHidden(token, focused) { + const effects = DhEffectsDisplay.getTokenEffects(focused ? token : null); + this.element.hidden = effects.length === 0; + + Hooks.callAll(CONFIG.DH.HOOKS.effectDisplayToggle, this.element.hidden, token); + + this.render(); + } + + async removeEffect(event) { + const element = event.target.closest('.effect-container'); + const effects = DhEffectsDisplay.getTokenEffects(); + const effect = effects.find(x => x.id === element.id); + await effect.delete(); + this.render(); + } + + setupHooks() { + Hooks.on('controlToken', this.toggleHidden.bind(this)); + Hooks.on(RefreshType.EffectsDisplay, this.toggleHidden.bind(this)); + } + + async close(options) { + /* Opt out of Foundry's standard behavior of closing all application windows marked as UI when Escape is pressed */ + if (options.closeKey) return; + + Hooks.off('controlToken', this.toggleHidden); + Hooks.off(RefreshType.EffectsDisplay, this.toggleHidden); + return super.close(options); + } + + async _onRender(context, options) { + await super._onRender(context, options); + + this.element.hidden = context.effects.length === 0; + if (options?.force) { + document.getElementById('ui-right-column-1')?.appendChild(this.element); + } + } +} diff --git a/module/canvas/placeables/token.mjs b/module/canvas/placeables/token.mjs index 367e7682..64ec3fa9 100644 --- a/module/canvas/placeables/token.mjs +++ b/module/canvas/placeables/token.mjs @@ -10,29 +10,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { this.effects.overlay = null; // Categorize effects - const statusMap = new Map(foundry.CONFIG.statusEffects.map(status => [status.id, status])); - const activeEffects = (this.actor ? this.actor.effects.filter(x => !x.disabled) : []).reduce((acc, effect) => { - acc.push(effect); - - const currentStatusActiveEffects = acc.filter( - x => x.statuses.size === 1 && x.name === game.i18n.localize(statusMap.get(x.statuses.first())?.name) - ); - for (var status of effect.statuses) { - if (!currentStatusActiveEffects.find(x => x.statuses.has(status))) { - const statusData = statusMap.get(status); - if (statusData) { - acc.push({ - name: game.i18n.localize(statusData.name), - statuses: [status], - img: statusData.icon ?? statusData.img, - tint: effect.tint - }); - } - } - } - - return acc; - }, []); + const activeEffects = this.actor?.getActiveEffects() ?? []; const overlayEffect = activeEffects.findLast(e => e.img && e.getFlag?.('core', 'overlay')); // Draw effects diff --git a/module/config/_module.mjs b/module/config/_module.mjs index 63797607..8631ab32 100644 --- a/module/config/_module.mjs +++ b/module/config/_module.mjs @@ -4,6 +4,7 @@ export * as domainConfig from './domainConfig.mjs'; export * as effectConfig from './effectConfig.mjs'; export * as flagsConfig from './flagsConfig.mjs'; export * as generalConfig from './generalConfig.mjs'; +export * as hooksConfig from './hooksConfig.mjs'; export * as itemConfig from './itemConfig.mjs'; export * as settingsConfig from './settingsConfig.mjs'; export * as systemConfig from './system.mjs'; diff --git a/module/config/hooksConfig.mjs b/module/config/hooksConfig.mjs new file mode 100644 index 00000000..d316c4d4 --- /dev/null +++ b/module/config/hooksConfig.mjs @@ -0,0 +1,5 @@ +const hooksConfig = { + effectDisplayToggle: 'DHEffectDisplayToggle' +}; + +export default hooksConfig; diff --git a/module/config/system.mjs b/module/config/system.mjs index 374fd58c..c2c84509 100644 --- a/module/config/system.mjs +++ b/module/config/system.mjs @@ -6,7 +6,8 @@ import * as SETTINGS from './settingsConfig.mjs'; import * as EFFECTS from './effectConfig.mjs'; import * as ACTIONS from './actionConfig.mjs'; import * as FLAGS from './flagsConfig.mjs'; -import * as ITEMBROWSER from './itemBrowserConfig.mjs' +import HOOKS from './hooksConfig.mjs'; +import * as ITEMBROWSER from './itemBrowserConfig.mjs'; export const SYSTEM_ID = 'daggerheart'; @@ -20,5 +21,6 @@ export const SYSTEM = { EFFECTS, ACTIONS, FLAGS, + HOOKS, ITEMBROWSER }; diff --git a/module/data/actor/base.mjs b/module/data/actor/base.mjs index 772a5af3..c7f7ee75 100644 --- a/module/data/actor/base.mjs +++ b/module/data/actor/base.mjs @@ -106,6 +106,21 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel { return data; } + async _preDelete() { + /* Clear all partyMembers from tagTeam setting.*/ + /* Revisit this when tagTeam is improved for many parties */ + if (this.parent.parties.size > 0) { + const tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); + await tagTeam.updateSource({ + initiator: this.parent.id === tagTeam.initiator ? null : tagTeam.initiator, + members: Object.keys(tagTeam.members).find(x => x === this.parent.id) + ? { [`-=${this.parent.id}`]: null } + : {} + }); + await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, tagTeam); + } + } + async _preUpdate(changes, options, userId) { const allowed = await super._preUpdate(changes, options, userId); if (allowed === false) return; diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 638da365..c5ab914c 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -678,6 +678,8 @@ export default class DhCharacter extends BaseDataActor { } async _preDelete() { + super._preDelete(); + if (this.companion) { this.companion.updateLevel(1); } diff --git a/module/data/actor/party.mjs b/module/data/actor/party.mjs index 06731246..18fe9959 100644 --- a/module/data/actor/party.mjs +++ b/module/data/actor/party.mjs @@ -36,6 +36,23 @@ export default class DhParty extends BaseDataActor { } } + async _preDelete() { + /* Clear all partyMembers from tagTeam setting.*/ + /* Revisit this when tagTeam is improved for many parties */ + const tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); + await tagTeam.updateSource({ + initiator: this.partyMembers.some(x => x.id === tagTeam.initiator) ? null : tagTeam.initiator, + members: Object.keys(tagTeam.members).reduce((acc, key) => { + if (this.partyMembers.find(x => x.id === key)) { + acc[`-=${key}`] = null; + } + + return acc; + }, {}) + }); + await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, tagTeam); + } + _onDelete(options, userId) { super._onDelete(options, userId); diff --git a/module/data/fields/action/effectsField.mjs b/module/data/fields/action/effectsField.mjs index d9658736..2233a383 100644 --- a/module/data/fields/action/effectsField.mjs +++ b/module/data/fields/action/effectsField.mjs @@ -51,7 +51,7 @@ export default class EffectsField extends fields.ArrayField { let effects = this.effects; const messageTargets = []; targets.forEach(async baseToken => { - if (this.hasSave && token.saved.success === true) effects = this.effects.filter(e => e.onSave === true); + if (this.hasSave && baseToken.saved.success === true) effects = this.effects.filter(e => e.onSave === true); if (!effects.length) return; const token = diff --git a/module/data/fields/action/saveField.mjs b/module/data/fields/action/saveField.mjs index 19486b85..473286b1 100644 --- a/module/data/fields/action/saveField.mjs +++ b/module/data/fields/action/saveField.mjs @@ -46,7 +46,7 @@ export default class SaveField extends fields.SchemaField { if (SaveField.getAutomation() !== CONFIG.DH.SETTINGS.actionAutomationChoices.never.id || force) { targets ??= config.targets.filter(t => !config.hasRoll || t.hit); await SaveField.rollAllSave.call(this, targets, config.event, message); - } else return false; + } else return; } /** diff --git a/module/documents/activeEffect.mjs b/module/documents/activeEffect.mjs index 1724ec13..fcf1d590 100644 --- a/module/documents/activeEffect.mjs +++ b/module/documents/activeEffect.mjs @@ -1,4 +1,5 @@ import { itemAbleRollParse } from '../helpers/utils.mjs'; +import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs'; export default class DhActiveEffect extends foundry.documents.ActiveEffect { /* -------------------------------------------- */ @@ -85,6 +86,20 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { await super._preCreate(data, options, user); } + /** @inheritdoc */ + _onCreate(data, options, userId) { + super._onCreate(data, options, userId); + + Hooks.callAll(RefreshType.EffectsDisplay); + } + + /** @inheritdoc */ + _onDelete(data, options, userId) { + super._onDelete(data, options, userId); + + Hooks.callAll(RefreshType.EffectsDisplay); + } + /* -------------------------------------------- */ /* Methods */ /* -------------------------------------------- */ diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 8faf1350..82d413d1 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -204,7 +204,7 @@ export default class DhpActor extends Actor { for (let domainCard of domainCards) { const itemCard = this.items.find(x => x.uuid === domainCard); - itemCard.delete(); + itemCard?.delete(); } } @@ -337,6 +337,8 @@ export default class DhpActor extends Actor { const embeddedItem = await this.createEmbeddedDocuments('Item', [ { ...multiclassData, + uuid: multiclassItem.uuid, + _stats: multiclassItem._stats, system: { ...multiclassData.system, features: multiclassData.system.features.filter(x => x.type !== 'hope'), @@ -349,6 +351,8 @@ export default class DhpActor extends Actor { await this.createEmbeddedDocuments('Item', [ { ...subclassData, + uuid: subclassItem.uuid, + _stats: subclassItem._stats, system: { ...subclassData.system, isMulticlass: true @@ -363,12 +367,15 @@ export default class DhpActor extends Actor { for (var domainCard of domainCards) { if (levelupAuto) { - const itemData = (await foundry.utils.fromUuid(domainCard.data[0])).toObject(); + const cardItem = await foundry.utils.fromUuid(domainCard.data[0]); + const cardData = cardItem.toObject(); const embeddedItem = await this.createEmbeddedDocuments('Item', [ { - ...itemData, + ...cardData, + uuid: cardItem.uuid, + _stats: cardItem._stats, system: { - ...itemData.system, + ...cardData.system, inVault: true } } @@ -382,12 +389,15 @@ export default class DhpActor extends Actor { const achievementDomainCards = []; if (levelupAuto) { for (var card of Object.values(level.achievements.domainCards)) { - const itemData = (await foundry.utils.fromUuid(card.uuid)).toObject(); + const cardItem = await foundry.utils.fromUuid(card.uuid); + const cardData = cardItem.toObject(); const embeddedItem = await this.createEmbeddedDocuments('Item', [ { - ...itemData, + ...cardData, + uuid: cardItem.uuid, + _stats: cardItem._stats, system: { - ...itemData.system, + ...cardData.system, inVault: true } } @@ -834,4 +844,37 @@ export default class DhpActor extends Actor { if (this.system._getTags) tags.push(...this.system._getTags()); return tags; } + + /** Get active effects */ + getActiveEffects() { + const statusMap = new Map(foundry.CONFIG.statusEffects.map(status => [status.id, status])); + return this.effects + .filter(x => !x.disabled) + .reduce((acc, effect) => { + acc.push(effect); + + const currentStatusActiveEffects = acc.filter( + x => x.statuses.size === 1 && x.name === game.i18n.localize(statusMap.get(x.statuses.first()).name) + ); + + for (var status of effect.statuses) { + if (!currentStatusActiveEffects.find(x => x.statuses.has(status))) { + const statusData = statusMap.get(status); + if (statusData) { + acc.push({ + condition: status, + appliedBy: game.i18n.localize(effect.name), + name: game.i18n.localize(statusData.name), + statuses: new Set([status]), + img: statusData.icon ?? statusData.img, + description: game.i18n.localize(statusData.description), + tint: effect.tint + }); + } + } + } + + return acc; + }, []); + } } diff --git a/module/documents/tooltipManager.mjs b/module/documents/tooltipManager.mjs index 95621441..25cf19b5 100644 --- a/module/documents/tooltipManager.mjs +++ b/module/documents/tooltipManager.mjs @@ -1,8 +1,45 @@ export default class DhTooltipManager extends foundry.helpers.interaction.TooltipManager { + #bordered = false; + async activate(element, options = {}) { const { TextEditor } = foundry.applications.ux; let html = options.html; + if (element.dataset.tooltip === '#effect-display#') { + this.#bordered = true; + let effect = {}; + if (element.dataset.uuid) { + const effectData = (await foundry.utils.fromUuid(element.dataset.uuid)).toObject(); + effect = { + ...effectData, + name: game.i18n.localize(effectData.name), + description: game.i18n.localize(effectData.description ?? effectData.parent.system.description) + }; + } else { + const conditions = CONFIG.DH.GENERAL.conditions(); + const condition = conditions[element.dataset.condition]; + effect = { + ...condition, + name: game.i18n.localize(condition.name), + description: game.i18n.localize(condition.description), + appliedBy: element.dataset.appliedBy, + isLockedCondition: true + }; + } + + html = await foundry.applications.handlebars.renderTemplate( + `systems/daggerheart/templates/ui/tooltip/effect-display.hbs`, + { + effect + } + ); + + this.tooltip.innerHTML = html; + options.direction = this._determineItemTooltipDirection(element); + } else { + this.#bordered = false; + } + if (element.dataset.tooltip?.startsWith('#item#')) { const itemUuid = element.dataset.tooltip.slice(6); const item = await foundry.utils.fromUuid(itemUuid); @@ -112,6 +149,14 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti super.activate(element, { ...options, html: html }); } + _setStyle(position = {}) { + super._setStyle(position); + + if (this.#bordered) { + this.tooltip.classList.add('bordered-tooltip'); + } + } + _determineItemTooltipDirection(element, prefered = this.constructor.TOOLTIP_DIRECTIONS.LEFT) { const pos = element.getBoundingClientRect(); const dirs = this.constructor.TOOLTIP_DIRECTIONS; diff --git a/module/systemRegistration/migrations.mjs b/module/systemRegistration/migrations.mjs index 00b07dc1..b3116459 100644 --- a/module/systemRegistration/migrations.mjs +++ b/module/systemRegistration/migrations.mjs @@ -191,6 +191,25 @@ export async function runMigrations() { lastMigrationVersion = '1.2.0'; } + + if (foundry.utils.isNewerVersion('1.2.7', lastMigrationVersion)) { + const tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); + const initatorMissing = tagTeam.initiator && !game.actors.some(actor => actor.id === tagTeam.initiator); + const missingMembers = Object.keys(tagTeam.members).reduce((acc, id) => { + if (!game.actors.some(actor => actor.id === id)) { + acc[`-=${id}`] = null; + } + return acc; + }, {}); + + await tagTeam.updateSource({ + initiator: initatorMissing ? null : tagTeam.initiator, + members: missingMembers + }); + await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, tagTeam); + + lastMigrationVersion = '1.2.7'; + } //#endregion await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion, lastMigrationVersion); diff --git a/module/systemRegistration/socket.mjs b/module/systemRegistration/socket.mjs index ac61238f..27bf48c5 100644 --- a/module/systemRegistration/socket.mjs +++ b/module/systemRegistration/socket.mjs @@ -36,7 +36,8 @@ export const GMUpdateEvent = { export const RefreshType = { Countdown: 'DhCoundownRefresh', - TagTeamRoll: 'DhTagTeamRollRefresh' + TagTeamRoll: 'DhTagTeamRollRefresh', + EffectsDisplay: 'DhEffectsDisplayRefresh' }; export const registerSocketHooks = () => { diff --git a/src/packs/classes/class_Bard_vegl3bFOq3pcFTWT.json b/src/packs/classes/class_Bard_vegl3bFOq3pcFTWT.json index dfa5f29c..64fbb572 100644 --- a/src/packs/classes/class_Bard_vegl3bFOq3pcFTWT.json +++ b/src/packs/classes/class_Bard_vegl3bFOq3pcFTWT.json @@ -4,7 +4,7 @@ "type": "class", "img": "icons/tools/instruments/harp-red.webp", "system": { - "description": "

Note: At level 5 use Rally (Level 5) instead of Rally under class feature. (Automation will be implemented in a later release)


Bards are the most charismatic people in all the realms. Members of this class are masters of captivation and specialize in a variety of performance types, including singing, playing musical instruments, weaving tales, or telling jokes. Whether performing for an audience or speaking to an individual, bards thrive in social situations. Members of this profession bond and train at schools or guilds, but a current of egotism runs through those of the bardic persuasion. While they may be the most likely class to bring people together, a bard of ill temper can just as easily tear a party apart.

", + "description": "

Note: At level 5 use Rally (Level 5) instead of Rally under class feature. (Automation will be implemented in a later release)


Bards are the most charismatic people in all the realms. Members of this class are masters of captivation and specialize in a variety of performance types, including singing, playing musical instruments, weaving tales, or telling jokes. Whether performing for an audience or speaking to an individual, bards thrive in social situations. Members of this profession bond and train at schools or guilds, but a current of egotism runs through those of the bardic persuasion. While they may be the most likely class to bring people together, a bard of ill temper can just as easily tear a party apart.

CLASS ITEMS

A romance novel or a letter never opened

BARD’S HOPE FEATURE

Make a Scene: Spend 3 Hope to temporarily Distract a target within Close range, giving them a -2 penalty to their Difficulty.

CLASS FEATURE

Rally

Once per session, describe how you rally the party and give yourself and each of your allies a Rally Die. At level 1, your Rally Die is a d6. A PC can spend their Rally Die to roll it, adding the result to their action roll, reaction roll, damage roll, or to clear a number of Stress equal to the result. At the end of each session, clear all unspent Rally Dice.

At level 5, your Rally Die increases to a d8.

", "domains": [ "grace", "codex" @@ -85,12 +85,12 @@ "compendiumSource": null, "duplicateSource": null, "exportSource": null, - "coreVersion": "13.347", + "coreVersion": "13.351", "systemId": "daggerheart", - "systemVersion": "1.1.2", + "systemVersion": "1.2.6", "createdTime": 1754174600538, - "modifiedTime": 1756399046200, - "lastModifiedBy": "gbAAZWyczKwejDNh" + "modifiedTime": 1764073562847, + "lastModifiedBy": "9HOfUKAXuCu7hUPY" }, "_id": "vegl3bFOq3pcFTWT", "sort": 300000, diff --git a/src/packs/classes/class_Druid_ZNwUTCyGCEcidZFv.json b/src/packs/classes/class_Druid_ZNwUTCyGCEcidZFv.json index 5e30b889..27f70356 100644 --- a/src/packs/classes/class_Druid_ZNwUTCyGCEcidZFv.json +++ b/src/packs/classes/class_Druid_ZNwUTCyGCEcidZFv.json @@ -4,7 +4,7 @@ "_id": "ZNwUTCyGCEcidZFv", "img": "icons/creatures/mammals/wolf-howl-moon-black.webp", "system": { - "description": "

Becoming a druid is more than an occupation; it’s a calling for those who wish to learn from and protect the magic of the wilderness. While one might underestimate a gentle druid who practices the often-quiet work of cultivating flora, druids who channel the untamed forces of nature are terrifying to behold. Druids cultivate their abilities in small groups, often connected by a specific ethos or locale, but some choose to work alone. Through years of study and dedication, druids can learn to transform into beasts and shape nature itself.

", + "description": "

Becoming a druid is more than an occupation; it’s a calling for those who wish to learn from and protect the magic of the wilderness. While one might underestimate a gentle druid who practices the often-quiet work of cultivating flora, druids who channel the untamed forces of nature are terrifying to behold. Druids cultivate their abilities in small groups, often connected by a specific ethos or locale, but some choose to work alone. Through years of study and dedication, druids can learn to transform into beasts and shape nature itself.

CLASS ITEMS

A small bag of rocks and bones or a strange pendant found in the dirt

DRUID’S HOPE FEATURE

Evolution: Spend 3 Hope to transform into a Beastform without marking a Stress. When you do, choose one trait to raise by +1 until you drop out of that Beastform.

CLASS FEATURES

Beastform: Mark a Stress to magically transform into a creature of your tier or lower from the Beastform list. You can drop out of this form at any time. While transformed, you can’t use weapons or cast spells from domain cards, but you can still use other features or abilities you have access to. Spells you cast before you transform stay active and last for their normal duration, and you can talk and communicate as normal. Additionally, you gain the Beastform’s features, add their Evasion bonus to your Evasion, and use the trait specified in their statistics for your attack. While you’re in a Beastform, your armor becomes part of your body and you mark Armor Slots as usual; when you drop out of a Beastform, those marked Armor Slots remain marked. If you mark your last Hit Point, you automatically drop out of this form.

Wildtouch: You can perform harmless, subtle effects that involve nature—such as causing a flower to rapidly grow, summoning a slight gust of wind, or starting a campfire—at will.

", "domains": [ "sage", "arcana" @@ -87,12 +87,12 @@ "compendiumSource": null, "duplicateSource": null, "exportSource": null, - "coreVersion": "13.347", + "coreVersion": "13.351", "systemId": "daggerheart", - "systemVersion": "1.1.2", + "systemVersion": "1.2.6", "createdTime": 1754222247012, - "modifiedTime": 1756399003725, - "lastModifiedBy": "gbAAZWyczKwejDNh" + "modifiedTime": 1764073660360, + "lastModifiedBy": "9HOfUKAXuCu7hUPY" }, "_key": "!items!ZNwUTCyGCEcidZFv" } diff --git a/src/packs/classes/class_Guardian_nRAyoC0fOzXPDa4z.json b/src/packs/classes/class_Guardian_nRAyoC0fOzXPDa4z.json index c412abba..8b52874d 100644 --- a/src/packs/classes/class_Guardian_nRAyoC0fOzXPDa4z.json +++ b/src/packs/classes/class_Guardian_nRAyoC0fOzXPDa4z.json @@ -4,7 +4,7 @@ "_id": "nRAyoC0fOzXPDa4z", "img": "icons/equipment/shield/heater-wooden-sword-green.webp", "system": { - "description": "

The title of guardian represents an array of martial professions, speaking more to their moral compass and unshakeable fortitude than the means by which they fight. While many guardians join groups of militants for either a country or cause, they’re more likely to follow those few they truly care for, majority be damned. Guardians are known for fighting with remarkable ferocity even against overwhelming odds, defending their cohort above all else. Woe betide those who harm the ally of a guardian, as the guardian will answer this injury in kind.

", + "description": "

The title of guardian represents an array of martial professions, speaking more to their moral compass and unshakeable fortitude than the means by which they fight. While many guardians join groups of militants for either a country or cause, they’re more likely to follow those few they truly care for, majority be damned. Guardians are known for fighting with remarkable ferocity even against overwhelming odds, defending their cohort above all else. Woe betide those who harm the ally of a guardian, as the guardian will answer this injury in kind.

CLASS ITEMS

A totem from your mentor or a secret key

GUARDIAN’S HOPE FEATURE

Frontline Tank: Spend 3 Hope to clear 2 Armor Slots.

CLASS FEATURE

Unstoppable: Once per long rest, you can become Unstoppable. You gain an Unstoppable Die. At level 1, your Unstoppable Die is a d4. Place it on your character sheet in the space provided, starting with the 1 value facing up. After you make a damage roll that deals 1 or more Hit Points to a target, increase the Unstoppable Die value by one. When the die’s value would exceed its maximum value or when the scene ends, remove the die and drop out of Unstoppable. At level 5, your Unstoppable Die increases to a d6.

While Unstoppable, you gain the following benefits:

• You reduce the severity of physical damage by one threshold (Severe to Major, Major to Minor, Minor to None).

• You add the current value of the Unstoppable Die to your damage roll.

• You can’t be Restrained or Vulnerable.

", "domains": [ "valor", "blade" @@ -83,12 +83,12 @@ "compendiumSource": null, "duplicateSource": null, "exportSource": null, - "coreVersion": "13.347", + "coreVersion": "13.351", "systemId": "daggerheart", - "systemVersion": "1.1.2", + "systemVersion": "1.2.6", "createdTime": 1754246931974, - "modifiedTime": 1756398951257, - "lastModifiedBy": "gbAAZWyczKwejDNh" + "modifiedTime": 1764073702031, + "lastModifiedBy": "9HOfUKAXuCu7hUPY" }, "_key": "!items!nRAyoC0fOzXPDa4z" } diff --git a/src/packs/classes/class_Ranger_BTyfve69LKqoOi9S.json b/src/packs/classes/class_Ranger_BTyfve69LKqoOi9S.json index f85f6d59..3bb01173 100644 --- a/src/packs/classes/class_Ranger_BTyfve69LKqoOi9S.json +++ b/src/packs/classes/class_Ranger_BTyfve69LKqoOi9S.json @@ -4,7 +4,7 @@ "_id": "BTyfve69LKqoOi9S", "img": "icons/weapons/bows/shortbow-recurve-yellow-blue.webp", "system": { - "description": "

Rangers are highly skilled hunters who, despite their martial abilities, rarely lend their skills to an army. Through mastery of the body and a deep understanding of the wilderness, rangers become sly tacticians, pursuing their quarry with cunning and patience. Many rangers track and fight alongside an animal companion with whom they’ve forged a powerful spiritual bond. By honing their skills in the wild, rangers become expert trackers, as likely to ensnare their foes in a trap as they are to assail them head-on.

", + "description": "

Rangers are highly skilled hunters who, despite their martial abilities, rarely lend their skills to an army. Through mastery of the body and a deep understanding of the wilderness, rangers become sly tacticians, pursuing their quarry with cunning and patience. Many rangers track and fight alongside an animal companion with whom they’ve forged a powerful spiritual bond. By honing their skills in the wild, rangers become expert trackers, as likely to ensnare their foes in a trap as they are to assail them head-on.

CLASS ITEMS

A trophy from your first kill or a seemingly broken compass

RANGER’S HOPE FEATURE

Hold Them Off: Spend 3 Hope when you succeed on an attack with a weapon to use that same roll against two additional adversaries within range of the attack.

CLASS FEATURE

Ranger’s Focus: Spend a Hope and make an attack against a target. On a success, deal your attack’s normal damage and temporarily make the attack’s target your Focus. Until this feature ends or you make a different creature your Focus, you gain the following benefits against your Focus:

• You know precisely what direction they are in.

• When you deal damage to them, they must mark a Stress.

• When you fail an attack against them, you can end your Ranger’s Focus feature to reroll your Duality Dice.

", "domains": [ "bone", "sage" @@ -83,12 +83,12 @@ "compendiumSource": null, "duplicateSource": null, "exportSource": null, - "coreVersion": "13.347", + "coreVersion": "13.351", "systemId": "daggerheart", - "systemVersion": "1.1.2", + "systemVersion": "1.2.6", "createdTime": 1754268869310, - "modifiedTime": 1756398897309, - "lastModifiedBy": "gbAAZWyczKwejDNh" + "modifiedTime": 1764074066172, + "lastModifiedBy": "9HOfUKAXuCu7hUPY" }, "_key": "!items!BTyfve69LKqoOi9S" } diff --git a/src/packs/classes/class_Rogue_CvHlkHZfpMiCz5uT.json b/src/packs/classes/class_Rogue_CvHlkHZfpMiCz5uT.json index a0a59613..68b1e5b4 100644 --- a/src/packs/classes/class_Rogue_CvHlkHZfpMiCz5uT.json +++ b/src/packs/classes/class_Rogue_CvHlkHZfpMiCz5uT.json @@ -4,7 +4,7 @@ "_id": "CvHlkHZfpMiCz5uT", "img": "icons/magic/defensive/shield-barrier-blades-teal.webp", "system": { - "description": "

Rogues are scoundrels, often in both attitude and practice. Broadly known as liars and thieves, the best among this class move through the world anonymously. Utilizing their sharp wits and blades, rogues trick their foes through social manipulation as easily as breaking locks, climbing through windows, or dealing underhanded blows. These masters of magical craft manipulate shadow and movement, adding an array of useful and deadly tools to their repertoire. Rogues frequently establish guilds to meet future accomplices, hire out jobs, and hone secret skills, proving that there’s honor among thieves for those who know where to look.

", + "description": "

Rogues are scoundrels, often in both attitude and practice. Broadly known as liars and thieves, the best among this class move through the world anonymously. Utilizing their sharp wits and blades, rogues trick their foes through social manipulation as easily as breaking locks, climbing through windows, or dealing underhanded blows. These masters of magical craft manipulate shadow and movement, adding an array of useful and deadly tools to their repertoire. Rogues frequently establish guilds to meet future accomplices, hire out jobs, and hone secret skills, proving that there’s honor among thieves for those who know where to look.

CLASS ITEMS

A set of forgery tools or a grappling hook

ROGUE’S HOPE FEATURE

Rogue’s Dodge: Spend 3 Hope to gain a +2 bonus to your Evasion until the next time an attack succeeds against you. Otherwise, this bonus lasts until your next rest.

CLASS FEATURES

Cloaked: Any time you would be Hidden, you are instead Cloaked. In addition to the benefits of the Hidden condition, while Cloaked you remain unseen if you are stationary when an adversary moves to where they would normally see you.

After you make an attack or end a move within line of sight of an adversary, you are no longer Cloaked.

Sneak Attack: When you succeed on an attack while Cloaked or while an ally is within Melee range of your target, add a number of d6s equal to your tier to your damage roll.

", "domains": [ "midnight", "grace" @@ -87,12 +87,12 @@ "compendiumSource": null, "duplicateSource": null, "exportSource": null, - "coreVersion": "13.347", + "coreVersion": "13.351", "systemId": "daggerheart", - "systemVersion": "1.1.2", + "systemVersion": "1.2.6", "createdTime": 1754325275832, - "modifiedTime": 1756398839983, - "lastModifiedBy": "gbAAZWyczKwejDNh" + "modifiedTime": 1764074347573, + "lastModifiedBy": "9HOfUKAXuCu7hUPY" }, "_key": "!items!CvHlkHZfpMiCz5uT" } diff --git a/src/packs/classes/class_Seraph_5ZnlJ5bEoyOTkUJv.json b/src/packs/classes/class_Seraph_5ZnlJ5bEoyOTkUJv.json index 7c3ce471..7c9b2882 100644 --- a/src/packs/classes/class_Seraph_5ZnlJ5bEoyOTkUJv.json +++ b/src/packs/classes/class_Seraph_5ZnlJ5bEoyOTkUJv.json @@ -4,7 +4,7 @@ "_id": "5ZnlJ5bEoyOTkUJv", "img": "icons/magic/holy/barrier-shield-winged-cross.webp", "system": { - "description": "

Seraphs are divine fighters and healers imbued with sacred purpose. A wide array of deities exist within the realms, and thus numerous kinds of seraphs are appointed by these gods. Their ethos traditionally aligns with the domain or goals of their god, such as defending the weak, exacting vengeance, protecting a land or artifact, or upholding a particular faith. Some seraphs ally themselves with an army or locale, much to the satisfaction of their rulers, but other crusaders fight in opposition to the follies of the Mortal Realm. It is better to be a seraph’s ally than their enemy, as they are terrifying foes to those who defy their purpose.

", + "description": "

Seraphs are divine fighters and healers imbued with sacred purpose. A wide array of deities exist within the realms, and thus numerous kinds of seraphs are appointed by these gods. Their ethos traditionally aligns with the domain or goals of their god, such as defending the weak, exacting vengeance, protecting a land or artifact, or upholding a particular faith. Some seraphs ally themselves with an army or locale, much to the satisfaction of their rulers, but other crusaders fight in opposition to the follies of the Mortal Realm. It is better to be a seraph’s ally than their enemy, as they are terrifying foes to those who defy their purpose.

CLASS ITEMS

A bundle of offerings or a sigil of your god

SERAPH’S HOPE FEATURE

Life Support: Spend 3 Hope to clear a Hit Point on an ally within Close range.

CLASS FEATURE

Prayer Dice: At the beginning of each session, roll a number of d4s equal to your subclass’s Spellcast trait and place them on your character sheet in the space provided. These are your Prayer Dice. You can spend any number of Prayer Dice to aid yourself or an ally within Far range. You can use a spent die’s value to reduce incoming damage, add to a roll’s result after the roll is made, or gain Hope equal to the result. At the end of each session, clear all unspent Prayer Dice.

", "domains": [ "valor", "splendor" @@ -83,12 +83,12 @@ "compendiumSource": null, "duplicateSource": null, "exportSource": null, - "coreVersion": "13.350", + "coreVersion": "13.351", "systemId": "daggerheart", - "systemVersion": "1.1.2", + "systemVersion": "1.2.6", "createdTime": 1754351482530, - "modifiedTime": 1760018751908, - "lastModifiedBy": "fBcTgyTzoARBvohY" + "modifiedTime": 1764074673043, + "lastModifiedBy": "9HOfUKAXuCu7hUPY" }, "_key": "!items!5ZnlJ5bEoyOTkUJv" } diff --git a/src/packs/classes/class_Sorcerer_DchOzHcWIJE9FKcR.json b/src/packs/classes/class_Sorcerer_DchOzHcWIJE9FKcR.json index ce1ced71..e1076faf 100644 --- a/src/packs/classes/class_Sorcerer_DchOzHcWIJE9FKcR.json +++ b/src/packs/classes/class_Sorcerer_DchOzHcWIJE9FKcR.json @@ -4,7 +4,7 @@ "_id": "DchOzHcWIJE9FKcR", "img": "icons/magic/symbols/rune-sigil-horned-white-purple.webp", "system": { - "description": "

Not all innate magic users choose to hone their craft, but those who do can become powerful sorcerers. The gifts of these wielders are passed down through families, even if the family is unaware of or reluctant to practice them. A sorcerer’s abilities can range from the elemental to the illusionary and beyond, and many practitioners band together into collectives based on their talents. The act of becoming a formidable sorcerer is not the practice of acquiring power, but learning to cultivate and control the power one already possesses. The magic of a misguided or undisciplined sorcerer is a dangerous

force indeed.

", + "description": "

Not all innate magic users choose to hone their craft, but those who do can become powerful sorcerers. The gifts of these wielders are passed down through families, even if the family is unaware of or reluctant to practice them. A sorcerer’s abilities can range from the elemental to the illusionary and beyond, and many practitioners band together into collectives based on their talents. The act of becoming a formidable sorcerer is not the practice of acquiring power, but learning to cultivate and control the power one already possesses. The magic of a misguided or undisciplined sorcerer is a dangerous force indeed.

CLASS ITEMS

A whispering orb or a family heirloom

SORCERER’S HOPE FEATURE

Volatile Magic: Spend 3 Hope to reroll any number of your damage dice on an attack that deals magic damage.

CLASS FEATURES

Arcane Sense: You can sense the presence of magical people and objects within Close range.

Minor Illusion: Make a Spellcast Roll (10). On a success, you create a minor visual illusion no larger than yourself

within Close range. This illusion is convincing to anyone at Close range or farther.

Channel Raw Power: Once per long rest, you can place a domain card from your loadout into your vault and choose to either:

• Gain Hope equal to the level of the card.

• Enhance a spell that deals damage, gaining a bonus to your damage roll equal to twice the level of the card.

", "domains": [ "arcana", "midnight" @@ -91,12 +91,12 @@ "compendiumSource": null, "duplicateSource": null, "exportSource": null, - "coreVersion": "13.350", + "coreVersion": "13.351", "systemId": "daggerheart", - "systemVersion": "1.1.2", + "systemVersion": "1.2.6", "createdTime": 1754349743129, - "modifiedTime": 1760018753854, - "lastModifiedBy": "fBcTgyTzoARBvohY" + "modifiedTime": 1764074848299, + "lastModifiedBy": "9HOfUKAXuCu7hUPY" }, "_key": "!items!DchOzHcWIJE9FKcR" } diff --git a/src/packs/classes/class_Warrior_xCUWwJz4WSthvLfy.json b/src/packs/classes/class_Warrior_xCUWwJz4WSthvLfy.json index 3ecb2b72..5b07f909 100644 --- a/src/packs/classes/class_Warrior_xCUWwJz4WSthvLfy.json +++ b/src/packs/classes/class_Warrior_xCUWwJz4WSthvLfy.json @@ -4,7 +4,7 @@ "_id": "xCUWwJz4WSthvLfy", "img": "icons/weapons/swords/sword-broad-crystal-paired.webp", "system": { - "description": "

Becoming a warrior requires years, often a lifetime, of training and dedication to the mastery of weapons and violence. While many who seek to fight hone only their strength, warriors understand the importance of an agile body and mind, making them some of the most sought-after fighters across the realms. Frequently, warriors find employment within an army, a band of mercenaries, or even a royal guard, but their potential is wasted in any position where they cannot continue to improve and expand their skills. Warriors are known to have a favored weapon; to come between them and their blade would be a grievous mistake.

", + "description": "

Becoming a warrior requires years, often a lifetime, of training and dedication to the mastery of weapons and violence. While many who seek to fight hone only their strength, warriors understand the importance of an agile body and mind, making them some of the most sought-after fighters across the realms. Frequently, warriors find employment within an army, a band of mercenaries, or even a royal guard, but their potential is wasted in any position where they cannot continue to improve and expand their skills. Warriors are known to have a favored weapon; to come between them and their blade would be a grievous mistake.

CLASS ITEMS

The drawing of a lover or a sharpening stone

WARRIOR’S HOPE FEATURE

No Mercy: Spend 3 Hope to gain a +1 bonus to your attack rolls until your next rest.

CLASS FEATURES

Attack of Opportunity: If an adversary within Melee range attempts to leave that range, make a reaction roll using a trait of your choice against their Difficulty. Choose one effect on a success, or two if you critically succeed:

• They can’t move from where they are.

• You deal damage to them equal to your primary weapon’s damage.

• You move with them.

Combat Training: You ignore burden when equipping weapons. When you deal physical damage, you gain a bonus to your damage roll equal to your level.

", "domains": [ "blade", "bone" @@ -87,12 +87,12 @@ "compendiumSource": null, "duplicateSource": null, "exportSource": null, - "coreVersion": "13.347", + "coreVersion": "13.351", "systemId": "daggerheart", - "systemVersion": "1.1.2", + "systemVersion": "1.2.6", "createdTime": 1754255776706, - "modifiedTime": 1756398696324, - "lastModifiedBy": "gbAAZWyczKwejDNh" + "modifiedTime": 1764075006528, + "lastModifiedBy": "9HOfUKAXuCu7hUPY" }, "_key": "!items!xCUWwJz4WSthvLfy" } diff --git a/src/packs/classes/class_Wizard_5LwX4m8ziY3F1ZGC.json b/src/packs/classes/class_Wizard_5LwX4m8ziY3F1ZGC.json index d5cc53ca..febf0f50 100644 --- a/src/packs/classes/class_Wizard_5LwX4m8ziY3F1ZGC.json +++ b/src/packs/classes/class_Wizard_5LwX4m8ziY3F1ZGC.json @@ -4,7 +4,7 @@ "_id": "5LwX4m8ziY3F1ZGC", "img": "icons/magic/symbols/circled-gem-pink.webp", "system": { - "description": "

Whether through an institution or individual study, those known as wizards acquire and hone immense magical power over years of learning using a variety of tools, including books, stones, potions, and herbs. Some wizards dedicate their lives to mastering a particular school of magic, while others learn from a wide variety of disciplines. Many wizards become wise and powerful figures in their communities, advising rulers, providing medicines and healing, and even leading war councils. While these mages all work toward the common goal of collecting magical knowledge, wizards often have the most conflict within their own ranks, as the acquisition, keeping, and sharing of powerful secrets is a topic of intense debate that has resulted in innumerable deaths.

", + "description": "

Whether through an institution or individual study, those known as wizards acquire and hone immense magical power over years of learning using a variety of tools, including books, stones, potions, and herbs. Some wizards dedicate their lives to mastering a particular school of magic, while others learn from a wide variety of disciplines. Many wizards become wise and powerful figures in their communities, advising rulers, providing medicines and healing, and even leading war councils. While these mages all work toward the common goal of collecting magical knowledge, wizards often have the most conflict within their own ranks, as the acquisition, keeping, and sharing of powerful secrets is a topic of intense debate that has resulted in innumerable deaths.

CLASS ITEMS

A book you’re trying to translate or a tiny, harmless elemental pet

WIZARD’S HOPE FEATURE

Not This Time: Spend 3 Hope to force an adversary within Far range to reroll an attack or damage roll.

CLASS FEATURES

Prestidigitation: You can perform harmless, subtle magical effects at will. For example, you can change an object’s color, create a smell, light a candle, cause a tiny object to float, illuminate a room, or repair a small object.

Strange Patterns: Choose a number between 1 and 12. When you roll that number on a Duality Die, gain a Hope or clear a Stress.

You can change this number when you take a long rest.

", "domains": [ "codex", "splendor" @@ -87,12 +87,12 @@ "compendiumSource": null, "duplicateSource": null, "exportSource": null, - "coreVersion": "13.347", + "coreVersion": "13.351", "systemId": "daggerheart", - "systemVersion": "1.1.2", + "systemVersion": "1.2.6", "createdTime": 1754253505323, - "modifiedTime": 1756391897762, - "lastModifiedBy": "gbAAZWyczKwejDNh" + "modifiedTime": 1764075272924, + "lastModifiedBy": "9HOfUKAXuCu7hUPY" }, "_key": "!items!5LwX4m8ziY3F1ZGC" } diff --git a/styles/less/hud/token-hud/token-hud.less b/styles/less/hud/token-hud/token-hud.less index ec67b524..ac269172 100644 --- a/styles/less/hud/token-hud/token-hud.less +++ b/styles/less/hud/token-hud/token-hud.less @@ -52,6 +52,15 @@ align-items: center; justify-content: center; } + + .effect-locked { + position: absolute; + bottom: 2px; + right: 2px; + font-size: 12px; + color: @golden; + filter: drop-shadow(0 0 3px black); + } } } } diff --git a/styles/less/sheets/actors/character/biography.less b/styles/less/sheets/actors/character/biography.less index 036da6fb..12a662ff 100644 --- a/styles/less/sheets/actors/character/biography.less +++ b/styles/less/sheets/actors/character/biography.less @@ -16,5 +16,24 @@ scrollbar-width: thin; scrollbar-color: light-dark(@dark-blue, @golden) transparent; } + + .biography-section { + prose-mirror { + --min-height: 50px; + } + + prose-mirror.inactive .editor-content { + position: relative; + } + } + + &:has(.prosemirror.active) { + .biography-section { + flex: 1; + &:not(:has(prose-mirror.active)) { + display: none; + } + } + } } } diff --git a/styles/less/ui/countdown/countdown.less b/styles/less/ui/countdown/countdown.less index 829a59b5..47f06eb7 100644 --- a/styles/less/ui/countdown/countdown.less +++ b/styles/less/ui/countdown/countdown.less @@ -12,18 +12,23 @@ } .daggerheart.dh-style.countdowns { - position: initial; + position: relative; border: 0; border-radius: 4px; box-shadow: none; width: 300px; pointer-events: all; align-self: flex-end; + transition: 0.3s right ease-in-out; .window-title { font-family: @font-body; } + #ui-right:has(#effects-display .effect-container) & { + right: 62px; + } + &.icon-only { width: 180px; min-width: 180px; diff --git a/styles/less/ui/effects-display/sheet.less b/styles/less/ui/effects-display/sheet.less new file mode 100644 index 00000000..1331b094 --- /dev/null +++ b/styles/less/ui/effects-display/sheet.less @@ -0,0 +1,40 @@ +.daggerheart.dh-style.effects-display { + position: absolute; + right: 0; + width: 46px; + max-height: 600px; + overflow: hidden; + border: 0; + + .window-content { + > div { + height: 100%; + } + } + + .effects-display-container { + display: flex; + flex-direction: column; + gap: 4px; + + .effect-container { + position: relative; + pointer-events: all; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 3px; + + img { + border-radius: 3px; + } + + .effect-locked { + position: absolute; + bottom: 4px; + right: 4px; + font-size: 16px; + color: @golden; + filter: drop-shadow(0 0 3px black); + } + } + } +} diff --git a/styles/less/ui/index.less b/styles/less/ui/index.less index 743d16ae..7f9ada25 100644 --- a/styles/less/ui/index.less +++ b/styles/less/ui/index.less @@ -31,3 +31,5 @@ @import './sidebar/daggerheartMenu.less'; @import './scene-config/scene-config.less'; + +@import './effects-display/sheet.less'; diff --git a/styles/less/ui/sidebar/tabs.less b/styles/less/ui/sidebar/tabs.less index ec4bbe9f..cb75bf08 100644 --- a/styles/less/ui/sidebar/tabs.less +++ b/styles/less/ui/sidebar/tabs.less @@ -4,12 +4,31 @@ } } -#interface #ui-right #sidebar { - menu li button { - img { - width: 22px; - max-width: unset; - filter: @beige-filter; +#interface #ui-right { + #ui-right-column-1 { + position: relative; + } + + #sidebar { + menu li button { + img { + width: 22px; + max-width: unset; + filter: @beige-filter; + } } } } + +.actors-sidebar { + .directory-item.actor .entry-name:has(.entry-subtitle) { + display: flex; + flex-direction: column; + line-height: 1rem; + padding-top: 0.125rem; + .entry-subtitle { + color: var(--color-text-subtle); + font-size: var(--font-size-12); + } + } +} \ No newline at end of file diff --git a/styles/less/utils/colors.less b/styles/less/utils/colors.less index 0adb6bf2..8f6418bf 100755 --- a/styles/less/utils/colors.less +++ b/styles/less/utils/colors.less @@ -166,6 +166,7 @@ @primary-color-fear: var(--primary-color-fear, rgba(9, 71, 179, 0.75)); +@rustic-brown-80: #4c340780; @keyframes glow { 0% { box-shadow: 0 0 1px 1px @golden; diff --git a/styles/less/ux/index.less b/styles/less/ux/index.less index 68cfc7e5..a5e507ea 100644 --- a/styles/less/ux/index.less +++ b/styles/less/ux/index.less @@ -1,2 +1,3 @@ @import './tooltip/tooltip.less'; +@import './tooltip/bordered-tooltip.less'; @import './autocomplete/autocomplete.less'; diff --git a/styles/less/ux/tooltip/bordered-tooltip.less b/styles/less/ux/tooltip/bordered-tooltip.less new file mode 100644 index 00000000..a4779d71 --- /dev/null +++ b/styles/less/ux/tooltip/bordered-tooltip.less @@ -0,0 +1,44 @@ +#tooltip.bordered-tooltip { + border: 1px solid @golden; + background-image: url('../assets/parchments/dh-parchment-dark.png'); + + .daggerheart.dh-style.tooltip { + display: flex; + flex-direction: column; + text-align: start; + width: 100%; + gap: 5px; + padding: 0px; + border-radius: 3px; + + .tooltip-header { + display: flex; + flex-direction: column; + text-align: start; + padding: 5px; + gap: 0px; + + h2 { + display: flex; + justify-content: start; + font-size: var(--font-size-20); + color: @golden; + font-weight: 700; + margin: 0; + padding: 0; + } + + .subtitle { + font-size: 12px; + } + } + + .close-hint { + border-radius: 3px; + padding: 3px; + background: @rustic-brown-80; + color: @golden; + font-size: 12px; + } + } +} diff --git a/system.json b/system.json index bfcf2a31..4972a395 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.2.6", + "version": "1.2.7", "compatibility": { "minimum": "13", "verified": "13.351", diff --git a/templates/hud/tokenHUD.hbs b/templates/hud/tokenHUD.hbs index aaf2f5f9..09259d4a 100644 --- a/templates/hud/tokenHUD.hbs +++ b/templates/hud/tokenHUD.hbs @@ -46,17 +46,21 @@
{{#each systemStatusEffects as |status|}} -
- +
+ {{#if status.disabled}} / {{/if}} + {{#if status.locked}}{{/if}}
{{/each}} {{#if genericStatusEffects}} {{#each genericStatusEffects as |status|}} - +
+ + {{#if status.locked}}{{/if}} +
{{/each}} {{/if}}
diff --git a/templates/sheets-settings/companion-settings/attack.hbs b/templates/sheets-settings/companion-settings/attack.hbs index 0ec43d35..f99f7d8c 100644 --- a/templates/sheets-settings/companion-settings/attack.hbs +++ b/templates/sheets-settings/companion-settings/attack.hbs @@ -18,5 +18,5 @@ {{/if}} {{/if}} - {{> 'systems/daggerheart/templates/actionTypes/damage.hbs' fields=systemFields.attack.fields.damage.fields.parts.element.fields source=document.system.attack.damage path="system.attack."}} + {{> 'systems/daggerheart/templates/actionTypes/damage.hbs' fields=systemFields.attack.fields.damage.fields.parts.element.fields source=document.system.attack.damage path="system.attack." directField=systemFields.attack.fields.damage.fields.direct}} \ No newline at end of file diff --git a/templates/sheets/actors/character/biography.hbs b/templates/sheets/actors/character/biography.hbs index 6913f279..6f2fe8c1 100644 --- a/templates/sheets/actors/character/biography.hbs +++ b/templates/sheets/actors/character/biography.hbs @@ -3,7 +3,6 @@ data-tab='{{tabs.biography.id}}' data-group='{{tabs.biography.group}}' > -
{{localize 'DAGGERHEART.ACTORS.Character.story.characteristics'}} @@ -22,13 +21,14 @@ {{localize 'DAGGERHEART.ACTORS.Character.faith'}} {{formInput systemFields.biography.fields.characteristics.fields.faith value=source.system.biography.characteristics.faith enriched=source.system.biography.characteristics.faith localize=true toggled=true}}
- -
+ +
{{localize 'DAGGERHEART.ACTORS.Character.story.backgroundTitle'}} {{formInput background.field value=background.value enriched=background.enriched toggled=true}}
-
+ +
{{localize 'DAGGERHEART.ACTORS.Character.story.connectionsTitle'}} {{formInput connections.field value=connections.value enriched=connections.enriched toggled=true}}
diff --git a/templates/ui/effects-display.hbs b/templates/ui/effects-display.hbs new file mode 100644 index 00000000..da37d8eb --- /dev/null +++ b/templates/ui/effects-display.hbs @@ -0,0 +1,14 @@ +
+
+ {{#each effects as | effect |}} + + + + + {{#if effect.condition}}{{/if}} + + {{/each}} +
+
\ No newline at end of file diff --git a/templates/ui/sidebar/actor-document-partial.hbs b/templates/ui/sidebar/actor-document-partial.hbs new file mode 100644 index 00000000..ec261f85 --- /dev/null +++ b/templates/ui/sidebar/actor-document-partial.hbs @@ -0,0 +1,19 @@ +
  • + {{#if thumbnail}} + {{name}} + {{/if}} + + {{name}} + {{#if (or (eq type "adversary") (eq type "environment"))}} + {{localize "DAGGERHEART.UI.Sidebar.actorDirectory.tier" tier=system.tier type=(@root.getTypeLabel this)}} + {{else if (eq type "character")}} + {{localize "DAGGERHEART.UI.Sidebar.actorDirectory.character" level=system.levelData.level.current}} + {{else if (eq type "companion")}} + {{#if system.partner}} + {{localize "DAGGERHEART.UI.Sidebar.actorDirectory.companion" level=system.levelData.level.current partner=system.partner.name}} + {{else}} + {{localize "DAGGERHEART.UI.Sidebar.actorDirectory.companionNoPartner"}} + {{/if}} + {{/if}} + +
  • diff --git a/templates/ui/tooltip/effect-display.hbs b/templates/ui/tooltip/effect-display.hbs new file mode 100644 index 00000000..5ca4fec2 --- /dev/null +++ b/templates/ui/tooltip/effect-display.hbs @@ -0,0 +1,24 @@ +
    +
    +

    {{effect.name}}

    + {{#if effect.appliedBy}} +

    {{localize "DAGGERHEART.UI.EffectsDisplay.appliedBy" by=effect.appliedBy}}

    + {{/if}} +
    + + {{#if effect.description}} +
    + {{{effect.description}}} +
    + {{else if effect.parent.system.description}} +
    + {{{effect.parent.system.description}}} +
    + {{/if}} + + {{#unless effect.isLockedCondition}} +

    + {{localize "DAGGERHEART.UI.EffectsDisplay.removeThing" thing=(localize "DAGGERHEART.GENERAL.Effect.single")}} +

    + {{/unless}} +
    \ No newline at end of file