From c15d55a50537861fc37be6a6e2c2e2149ab98af0 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sat, 21 Jun 2025 21:37:22 +0200 Subject: [PATCH] 120 - Countdowns (#158) * Added the shell of the Countdown application * Added countdown automation * Fixed overflow layout and added confirmation on countdown removal * Added ownership to countdowns --- daggerheart.mjs | 56 ++-- lang/en.json | 43 +++ module/applications/_module.mjs | 2 +- module/applications/chatMessage.mjs | 4 +- module/applications/countdowns.mjs | 343 ++++++++++++++++++++ module/applications/ownershipSelection.mjs | 72 ++++ module/applications/settings.mjs | 7 + module/applications/sheets/character.mjs | 6 +- module/config/generalConfig.mjs | 14 + module/config/hooksConfig.mjs | 4 + module/config/settingsConfig.mjs | 3 +- module/config/system.mjs | 4 +- module/data/chat-message/_modules.mjs | 25 +- module/data/countdowns.mjs | 139 ++++++++ module/data/item/base.mjs | 71 ++-- module/data/item/class.mjs | 10 +- module/data/item/consumable.mjs | 10 +- module/data/item/miscellaneous.mjs | 6 +- module/data/item/subclass.mjs | 2 +- module/data/settings/Automation.mjs | 3 +- module/documents/actor.mjs | 4 +- module/helpers/socket.mjs | 58 +++- module/ui/combatTracker.mjs | 11 +- styles/chat.less | 21 +- styles/countdown.less | 137 ++++++++ styles/daggerheart.css | 151 ++++++++- styles/daggerheart.less | 2 + styles/ownershipSelection.less | 22 ++ styles/ui.less | 46 ++- templates/settings/automation-settings.hbs | 6 + templates/ui/combat/combatTrackerHeader.hbs | 13 +- templates/views/countdowns.hbs | 42 +++ templates/views/ownershipSelection.hbs | 22 ++ 33 files changed, 1222 insertions(+), 137 deletions(-) create mode 100644 module/applications/countdowns.mjs create mode 100644 module/applications/ownershipSelection.mjs create mode 100644 module/config/hooksConfig.mjs create mode 100644 module/data/countdowns.mjs create mode 100644 styles/countdown.less create mode 100644 styles/ownershipSelection.less create mode 100644 templates/views/countdowns.hbs create mode 100644 templates/views/ownershipSelection.hbs diff --git a/daggerheart.mjs b/daggerheart.mjs index 8c329868..2b25ce77 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -4,7 +4,7 @@ import * as models from './module/data/_module.mjs'; import * as documents from './module/documents/_module.mjs'; import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs'; import DhCombatTracker from './module/ui/combatTracker.mjs'; -import { GMUpdateEvent, handleSocketEvent, socketEvent } from './module/helpers/socket.mjs'; +import { handleSocketEvent, registerSocketHooks } from './module/helpers/socket.mjs'; import { registerDHSettings } from './module/applications/settings.mjs'; import DhpChatLog from './module/ui/chatLog.mjs'; import DhpRuler from './module/ui/ruler.mjs'; @@ -13,11 +13,13 @@ import { DhDualityRollEnricher, DhTemplateEnricher } from './module/enrichers/_m import { getCommandTarget, rollCommandToJSON, setDiceSoNiceForDualityRoll } from './module/helpers/utils.mjs'; import { abilities } from './module/config/actorConfig.mjs'; import Resources from './module/applications/resources.mjs'; +import { NarrativeCountdowns, registerCountdownApplicationHooks } from './module/applications/countdowns.mjs'; import DHDualityRoll from './module/data/chat-message/dualityRoll.mjs'; import { DualityRollColor } from './module/data/settings/Appearance.mjs'; import { DhMeasuredTemplate } from './module/placeables/_module.mjs'; import { renderDualityButton } from './module/enrichers/DualityRollEnricher.mjs'; import { renderMeasuredTemplate } from './module/enrichers/TemplateEnricher.mjs'; +import { registerCountdownHooks } from './module/data/countdowns.mjs'; globalThis.SYSTEM = SYSTEM; @@ -126,39 +128,20 @@ Hooks.on('ready', () => { ui.resources = new CONFIG.ui.resources(); if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).displayFear !== 'hide') ui.resources.render({ force: true }); + document.body.classList.toggle( 'theme-colorful', game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).dualityColorScheme === DualityRollColor.colorful.value ); + + registerCountdownHooks(); + registerSocketHooks(); + registerCountdownApplicationHooks(); }); Hooks.once('dicesoniceready', () => {}); -Hooks.on(socketEvent.GMUpdate, async (action, uuid, update) => { - if (game.user.isGM) { - const document = uuid ? await fromUuid(uuid) : null; - switch (action) { - case GMUpdateEvent.UpdateDocument: - if (document && update) { - await document.update(update); - } - break; - case GMUpdateEvent.UpdateFear: - if (game.user.isGM) { - await game.settings.set( - SYSTEM.id, - SYSTEM.SETTINGS.gameSettings.Resources.Fear, - Math.max(Math.min(update, 6), 0) - ); - Hooks.callAll(socketEvent.DhpFearUpdate); - await game.socket.emit(`system.${SYSTEM.id}`, { action: socketEvent.DhpFearUpdate }); - } - break; - } - } -}); - Hooks.on('renderChatMessageHTML', (_, element) => { element .querySelectorAll('.duality-roll-button') @@ -264,6 +247,29 @@ Hooks.on('chatMessage', (_, message) => { } }); +Hooks.on('renderJournalDirectory', async (tab, html, _, options) => { + if (tab.id === 'journal') { + if (options.parts && !options.parts.includes('footer')) return; + + const buttons = tab.element.querySelector('.directory-footer.action-buttons'); + const title = game.i18n.format('DAGGERHEART.Countdown.Title', { + type: game.i18n.localize('DAGGERHEART.Countdown.Types.narrative') + }); + buttons.insertAdjacentHTML( + 'afterbegin', + ` + ` + ); + + buttons.querySelector('#narrative-countdown-button').onclick = async () => { + new NarrativeCountdowns().open(); + }; + } +}); + const preloadHandlebarsTemplates = async function () { return foundry.applications.handlebars.loadTemplates([ 'systems/daggerheart/templates/sheets/parts/attributes.hbs', diff --git a/lang/en.json b/lang/en.json index 868ee74b..e4ad9cd5 100755 --- a/lang/en.json +++ b/lang/en.json @@ -83,6 +83,10 @@ "actionPoints": { "label": "Action Points", "hint": "Automatically give and take Action Points as combatants take their turns." + }, + "countdowns": { + "label": "Countdowns", + "hint": "Automatically progress non-custom countdowns" } } }, @@ -1027,6 +1031,45 @@ "Title": "Downtime" } }, + "Countdown": { + "FIELDS": { + "countdowns": { + "element": { + "name": { "label": "Name" }, + "progress": { + "current": { "label": "Current" }, + "max": { "label": "Max" }, + "type": { + "value": { "label": "Value" }, + "label": { "label": "Label", "hint": "Used for custom" } + } + } + } + } + }, + "Type": { + "Spotlight": "Spotlight", + "Custom": "Custom", + "CharacterAttack": "Character Attack" + }, + "NewCountdown": "New Countdown", + "AddCountdown": "Add Countdown", + "RemoveCountdownTitle": "Remove Countdown", + "RemoveCountdownText": "Are you sure you want to remove the countdown: {name}?", + "OpenOwnership": "Edit Player Ownership", + "Title": "{type} Countdowns", + "Types": { + "narrative": "Narrative", + "encounter": "Encounter" + }, + "Notifications": { + "LimitedOwnershipMaximise": "You don't have permission to enter edit view" + } + }, + "OwnershipSelection": { + "Title": "Ownership Selection - {name}", + "Default": "Default Ownership" + }, "Sheets": { "PC": { "Name": "Name", diff --git a/module/applications/_module.mjs b/module/applications/_module.mjs index 65eafe09..b1a1d59e 100644 --- a/module/applications/_module.mjs +++ b/module/applications/_module.mjs @@ -12,4 +12,4 @@ export { default as DhpWeapon } from './sheets/items/weapon.mjs'; export { default as DhpArmor } from './sheets/items/armor.mjs'; export { default as DhpChatMessage } from './chatMessage.mjs'; export { default as DhpEnvironment } from './sheets/environment.mjs'; -export { default as DhActiveEffectConfig } from './sheets/activeEffectConfig.mjs'; \ No newline at end of file +export { default as DhActiveEffectConfig } from './sheets/activeEffectConfig.mjs'; diff --git a/module/applications/chatMessage.mjs b/module/applications/chatMessage.mjs index 4850a0d2..e7bcd269 100644 --- a/module/applications/chatMessage.mjs +++ b/module/applications/chatMessage.mjs @@ -10,9 +10,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { /* We can change to fully implementing the renderHTML function if needed, instead of augmenting it. */ const html = await super.renderHTML(); - if ( - this.type === 'dualityRoll' - ) { + if (this.type === 'dualityRoll') { html.classList.add('duality'); const dualityResult = this.system.dualityResult; if (dualityResult === DHDualityRoll.dualityResult.hope) html.classList.add('hope'); diff --git a/module/applications/countdowns.mjs b/module/applications/countdowns.mjs new file mode 100644 index 00000000..354541fe --- /dev/null +++ b/module/applications/countdowns.mjs @@ -0,0 +1,343 @@ +import { countdownTypes } from '../config/generalConfig.mjs'; +import { GMUpdateEvent, RefreshType, socketEvent } from '../helpers/socket.mjs'; +import OwnershipSelection from './ownershipSelection.mjs'; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(basePath) { + super({}); + + this.basePath = basePath; + } + + get title() { + return game.i18n.format('DAGGERHEART.Countdown.Title', { + type: game.i18n.localize(`DAGGERHEART.Countdown.Types.${this.basePath}`) + }); + } + + static DEFAULT_OPTIONS = { + classes: ['daggerheart', 'dh-style', 'countdown'], + tag: 'form', + position: { width: 740, height: 700 }, + window: { + frame: true, + title: 'Countdowns', + resizable: true, + minimizable: true + }, + actions: { + addCountdown: this.addCountdown, + removeCountdown: this.removeCountdown, + editImage: this.onEditImage, + openOwnership: this.openOwnership, + openCountdownOwnership: this.openCountdownOwnership + }, + form: { handler: this.updateData, submitOnChange: true } + }; + + static PARTS = { + countdowns: { + template: 'systems/daggerheart/templates/views/countdowns.hbs', + scrollable: ['.expanded-view'] + } + }; + + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + htmlElement.querySelectorAll('.mini-countdown-container').forEach(element => { + element.addEventListener('click', event => this.updateCountdownValue.bind(this)(event, true)); + element.addEventListener('contextmenu', event => this.updateCountdownValue.bind(this)(event, false)); + }); + } + + async _onFirstRender(context, options) { + super._onFirstRender(context, options); + + this.element.querySelector('.expanded-view').classList.toggle('hidden'); + this.element.querySelector('.minimized-view').classList.toggle('hidden'); + } + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + const countdownData = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath]; + + context.isGM = game.user.isGM; + context.base = this.basePath; + + context.canCreate = countdownData.playerOwnership[game.user.id].value === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER; + context.source = { + ...countdownData, + countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => { + const countdown = countdownData.countdowns[key]; + + const ownershipValue = countdown.playerOwnership[game.user.id].value; + if (ownershipValue > CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) { + acc[key] = { ...countdown, canEdit: ownershipValue === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER }; + } + + return acc; + }, {}) + }; + context.systemFields = countdownData.schema.fields; + context.countdownFields = context.systemFields.countdowns.element.fields; + context.minimized = this.minimized || _options.isFirstRender; + + return context; + } + + static async updateData(event, _, formData) { + const data = foundry.utils.expandObject(formData.object); + const newSetting = foundry.utils.mergeObject( + game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns).toObject(), + data + ); + + if (game.user.isGM) { + await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, newSetting); + this.render(); + } else { + await game.socket.emit(`system.${SYSTEM.id}`, { + action: socketEvent.GMUpdate, + data: { + action: GMUpdateEvent.UpdateSetting, + uuid: SYSTEM.SETTINGS.gameSettings.Countdowns, + update: newSetting + } + }); + } + } + + async minimize() { + await super.minimize(); + + this.element.querySelector('.expanded-view').classList.toggle('hidden'); + this.element.querySelector('.minimized-view').classList.toggle('hidden'); + } + + async maximize() { + if (this.minimized) { + const settings = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath]; + if (settings.playerOwnership[game.user.id].value <= CONST.DOCUMENT_OWNERSHIP_LEVELS.LIMITED) { + ui.notifications.info(game.i18n.localize('DAGGERHEART.Countdown.Notifications.LimitedOwnership')); + return; + } + + this.element.querySelector('.expanded-view').classList.toggle('hidden'); + this.element.querySelector('.minimized-view').classList.toggle('hidden'); + } + + await super.maximize(); + } + + async updateSetting(update) { + if (game.user.isGM) { + await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, update); + await game.socket.emit(`system.${SYSTEM.id}`, { + action: socketEvent.Refresh, + data: { + refreshType: RefreshType.Countdown, + application: `${this.basePath}-countdowns` + } + }); + + this.render(); + } else { + await game.socket.emit(`system.${SYSTEM.id}`, { + action: socketEvent.GMUpdate, + data: { + action: GMUpdateEvent.UpdateSetting, + uuid: SYSTEM.SETTINGS.gameSettings.Countdowns, + update: update, + refresh: { refreshType: RefreshType.Countdown, application: `${this.basePath}-countdowns` } + } + }); + } + } + + static onEditImage(_, target) { + const setting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath]; + const current = setting.countdowns[target.dataset.countdown].img; + const fp = new FilePicker({ + current, + type: 'image', + callback: async path => this.updateImage.bind(this)(path, target.dataset.countdown), + top: this.position.top + 40, + left: this.position.left + 10 + }); + return fp.browse(); + } + + async updateImage(path, countdown) { + const setting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns); + await setting.updateSource({ + [`${this.basePath}.countdowns.${countdown}.img`]: path + }); + + await this.updateSetting(setting); + } + + static openOwnership(_, target) { + new Promise((resolve, reject) => { + const setting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath]; + const ownership = { default: setting.ownership.default, players: setting.playerOwnership }; + new OwnershipSelection(resolve, reject, this.title, ownership).render(true); + }).then(async ownership => { + const setting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns); + await setting.updateSource({ + [`${this.basePath}.ownership`]: ownership + }); + + await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, setting.toObject()); + this.render(); + }); + } + + static openCountdownOwnership(_, target) { + const countdownId = target.dataset.countdown; + new Promise((resolve, reject) => { + const countdown = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath] + .countdowns[countdownId]; + const ownership = { default: countdown.ownership.default, players: countdown.playerOwnership }; + new OwnershipSelection(resolve, reject, countdown.name, ownership).render(true); + }).then(async ownership => { + const setting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns); + await setting.updateSource({ + [`${this.basePath}.countdowns.${countdownId}.ownership`]: ownership + }); + + await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, setting); + this.render(); + }); + } + + async updateCountdownValue(event, increase) { + const countdownSetting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns); + const countdown = countdownSetting[this.basePath].countdowns[event.currentTarget.dataset.countdown]; + + if (countdown.playerOwnership[game.user.id] < CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER) { + return; + } + + const currentValue = countdown.progress.current; + + if (increase && currentValue === countdown.progress.max) return; + if (!increase && currentValue === 0) return; + + await countdownSetting.updateSource({ + [`${this.basePath}.countdowns.${event.currentTarget.dataset.countdown}.progress.current`]: increase + ? currentValue + 1 + : currentValue - 1 + }); + + await this.updateSetting(countdownSetting.toObject()); + } + + static async addCountdown() { + const countdownSetting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns); + await countdownSetting.updateSource({ + [`${this.basePath}.countdowns.${foundry.utils.randomID()}`]: { + name: game.i18n.localize('DAGGERHEART.Countdown.NewCountdown'), + ownership: game.user.isGM + ? {} + : { + players: { + [game.user.id]: { type: CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER } + } + } + } + }); + + await this.updateSetting(countdownSetting.toObject()); + } + + static async removeCountdown(_, target) { + const countdownSetting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns); + const countdownName = countdownSetting[this.basePath].countdowns[target.dataset.countdown].name; + + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.localize('DAGGERHEART.Countdown.RemoveCountdownTitle') + }, + content: game.i18n.format('DAGGERHEART.Countdown.RemoveCountdownText', { name: countdownName }) + }); + if (!confirmed) return; + + await countdownSetting.updateSource({ [`${this.basePath}.countdowns.-=${target.dataset.countdown}`]: null }); + + await this.updateSetting(countdownSetting.toObject()); + } + + async open() { + await this.render(true); + if ( + Object.keys(game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath].countdowns) + .length > 0 + ) { + this.minimize(); + } + } +} + +export class NarrativeCountdowns extends Countdowns { + constructor() { + super('narrative'); + } + + static DEFAULT_OPTIONS = { + id: 'narrative-countdowns' + }; +} + +export class EncounterCountdowns extends Countdowns { + constructor() { + super('encounter'); + } + + static DEFAULT_OPTIONS = { + id: 'encounter-countdowns' + }; +} + +export const registerCountdownApplicationHooks = () => { + const updateCountdowns = async shouldIncrease => { + if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation).countdowns) { + const countdownSetting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns); + for (let countdownCategoryKey in countdownSetting) { + const countdownCategory = countdownSetting[countdownCategoryKey]; + for (let countdownKey in countdownCategory.countdowns) { + const countdown = countdownCategory.countdowns[countdownKey]; + + if (shouldIncrease(countdown)) { + await countdownSetting.updateSource({ + [`${countdownCategoryKey}.countdowns.${countdownKey}.progress.current`]: + countdown.progress.current + 1 + }); + await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, countdownSetting); + foundry.applications.instances.get(`${countdownCategoryKey}-countdowns`)?.render(); + } + } + } + } + }; + + Hooks.on(SYSTEM.HOOKS.characterAttack, async () => { + updateCountdowns(countdown => { + return ( + countdown.progress.type.value === countdownTypes.characterAttack.id && + countdown.progress.current < countdown.progress.max + ); + }); + }); + + Hooks.on(SYSTEM.HOOKS.spotlight, async () => { + updateCountdowns(countdown => { + return ( + countdown.progress.type.value === countdownTypes.spotlight.id && + countdown.progress.current < countdown.progress.max + ); + }); + }); +}; diff --git a/module/applications/ownershipSelection.mjs b/module/applications/ownershipSelection.mjs new file mode 100644 index 00000000..b8de22f8 --- /dev/null +++ b/module/applications/ownershipSelection.mjs @@ -0,0 +1,72 @@ +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +export default class OwnershipSelection extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(resolve, reject, name, ownership) { + super({}); + + this.resolve = resolve; + this.reject = reject; + this.name = name; + this.ownership = ownership; + } + + static DEFAULT_OPTIONS = { + tag: 'form', + classes: ['daggerheart', 'views', 'ownership-selection'], + position: { + width: 600, + height: 'auto' + }, + form: { handler: this.updateData } + }; + + static PARTS = { + selection: { + template: 'systems/daggerheart/templates/views/ownershipSelection.hbs' + } + }; + + get title() { + return game.i18n.format('DAGGERHEART.OwnershipSelection.Title', { name: this.name }); + } + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.ownershipOptions = Object.keys(CONST.DOCUMENT_OWNERSHIP_LEVELS).map(level => ({ + value: CONST.DOCUMENT_OWNERSHIP_LEVELS[level], + label: game.i18n.localize(`OWNERSHIP.${level}`) + })); + context.ownership = { + default: this.ownership.default, + players: Object.keys(this.ownership.players).reduce((acc, x) => { + const user = game.users.get(x); + if (!user.isGM) { + acc[x] = { + img: user.character?.img, + name: user.name, + ownership: this.ownership.players[x].value + }; + } + + return acc; + }, {}) + }; + + return context; + } + + static async updateData(event, _, formData) { + const { ownership } = foundry.utils.expandObject(formData.object); + + this.resolve(ownership); + this.close(true); + } + + async close(fromSave) { + if (!fromSave) { + this.reject(); + } + + await super.close(); + } +} diff --git a/module/applications/settings.mjs b/module/applications/settings.mjs index cb1aa1bd..8cfc5161 100644 --- a/module/applications/settings.mjs +++ b/module/applications/settings.mjs @@ -1,4 +1,5 @@ import { defaultLevelTiers, DhLevelTiers } from '../data/levelTier.mjs'; +import DhCountdowns from '../data/countdowns.mjs'; import { DhAppearance, DhAutomation, @@ -130,4 +131,10 @@ const registerNonConfigSettings = () => { ui.combat.render({ force: true }); } }); + + game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, { + scope: 'world', + config: false, + type: DhCountdowns + }); }; diff --git a/module/applications/sheets/character.mjs b/module/applications/sheets/character.mjs index fe0beec9..0c98d2bc 100644 --- a/module/applications/sheets/character.mjs +++ b/module/applications/sheets/character.mjs @@ -371,7 +371,11 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { static async attackRoll(event, button) { const weapon = await fromUuid(button.dataset.weapon); if (!weapon) return; - weapon.use(event); + + const wasUsed = await weapon.use(event); + if (wasUsed) { + Hooks.callAll(SYSTEM.HOOKS.characterAttack, {}); + } } static openLevelUp() { diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 96545cf3..279300fc 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -364,6 +364,20 @@ export const abilityCosts = { } }; +export const countdownTypes = { + spotlight: { + id: 'spotlight', + label: 'DAGGERHEART.Countdown.Type.Spotlight' + }, + characterAttack: { + id: 'characterAttack', + label: 'DAGGERHEART.Countdown.Type.CharacterAttack' + }, + custom: { + id: 'custom', + label: 'DAGGERHEART.Countdown.Type.Custom' + } +}; export const rollTypes = { weapon: { id: 'weapon', diff --git a/module/config/hooksConfig.mjs b/module/config/hooksConfig.mjs new file mode 100644 index 00000000..8410c0de --- /dev/null +++ b/module/config/hooksConfig.mjs @@ -0,0 +1,4 @@ +export const hooks = { + characterAttack: 'characterAttackHook', + spotlight: 'spotlightHook' +}; diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs index be793199..95a3de0e 100644 --- a/module/config/settingsConfig.mjs +++ b/module/config/settingsConfig.mjs @@ -26,7 +26,8 @@ export const gameSettings = { Resources: { Fear: 'ResourcesFear' }, - LevelTiers: 'LevelTiers' + LevelTiers: 'LevelTiers', + Countdowns: 'Countdowns' }; export const DualityRollColor = { diff --git a/module/config/system.mjs b/module/config/system.mjs index b9755317..41d67154 100644 --- a/module/config/system.mjs +++ b/module/config/system.mjs @@ -3,6 +3,7 @@ import * as DOMAIN from './domainConfig.mjs'; import * as ACTOR from './actorConfig.mjs'; import * as ITEM from './itemConfig.mjs'; import * as SETTINGS from './settingsConfig.mjs'; +import { hooks as HOOKS } from './hooksConfig.mjs'; import * as EFFECTS from './effectConfig.mjs'; import * as ACTIONS from './actionConfig.mjs'; @@ -15,6 +16,7 @@ export const SYSTEM = { ACTOR, ITEM, SETTINGS, + HOOKS, EFFECTS, - ACTIONS, + ACTIONS }; diff --git a/module/data/chat-message/_modules.mjs b/module/data/chat-message/_modules.mjs index 0432a789..7ee3d0b6 100644 --- a/module/data/chat-message/_modules.mjs +++ b/module/data/chat-message/_modules.mjs @@ -1,18 +1,13 @@ -import DHAbilityUse from "./abilityUse.mjs"; -import DHAdversaryRoll from "./adversaryRoll.mjs"; -import DHDamageRoll from "./damageRoll.mjs"; -import DHDualityRoll from "./dualityRoll.mjs"; +import DHAbilityUse from './abilityUse.mjs'; +import DHAdversaryRoll from './adversaryRoll.mjs'; +import DHDamageRoll from './damageRoll.mjs'; +import DHDualityRoll from './dualityRoll.mjs'; -export { - DHAbilityUse, - DHAdversaryRoll, - DHDamageRoll, - DHDualityRoll, -} +export { DHAbilityUse, DHAdversaryRoll, DHDamageRoll, DHDualityRoll }; export const config = { - abilityUse: DHAbilityUse, - adversaryRoll: DHAdversaryRoll, - damageRoll: DHDamageRoll, - dualityRoll: DHDualityRoll, -}; \ No newline at end of file + abilityUse: DHAbilityUse, + adversaryRoll: DHAdversaryRoll, + damageRoll: DHDamageRoll, + dualityRoll: DHDualityRoll +}; diff --git a/module/data/countdowns.mjs b/module/data/countdowns.mjs new file mode 100644 index 00000000..fec2b790 --- /dev/null +++ b/module/data/countdowns.mjs @@ -0,0 +1,139 @@ +import { countdownTypes } from '../config/generalConfig.mjs'; +import { RefreshType, socketEvent } from '../helpers/socket.mjs'; + +export default class DhCountdowns extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + narrative: new fields.EmbeddedDataField(DhCountdownData), + encounter: new fields.EmbeddedDataField(DhCountdownData) + }; + } + + static CountdownCategories = { narrative: 'narrative', combat: 'combat' }; +} + +class DhCountdownData extends foundry.abstract.DataModel { + static LOCALIZATION_PREFIXES = ['DAGGERHEART.Countdown']; // Nots ure why this won't work. Setting labels manually for now + + static defineSchema() { + const fields = foundry.data.fields; + return { + countdowns: new fields.TypedObjectField(new fields.EmbeddedDataField(DhCountdown)), + ownership: new fields.SchemaField({ + default: new fields.NumberField({ + required: true, + choices: Object.values(CONST.DOCUMENT_OWNERSHIP_LEVELS), + initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE + }), + players: new fields.TypedObjectField( + new fields.SchemaField({ + type: new fields.NumberField({ + required: true, + choices: Object.values(CONST.DOCUMENT_OWNERSHIP_LEVELS), + initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT + }) + }) + ) + }) + }; + } + + get playerOwnership() { + return Array.from(game.users).reduce((acc, user) => { + acc[user.id] = { + value: user.isGM + ? CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER + : this.ownership.players[user.id] && this.ownership.players[user.id].type !== -1 + ? this.ownership.players[user.id].type + : this.ownership.default, + isGM: user.isGM + }; + + return acc; + }, {}); + } +} + +class DhCountdown extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + return { + name: new fields.StringField({ + required: true, + label: 'DAGGERHEART.Countdown.FIELDS.countdowns.element.name.label' + }), + img: new fields.FilePathField({ + categories: ['IMAGE'], + base64: false, + initial: 'icons/magic/time/hourglass-yellow-green.webp' + }), + ownership: new fields.SchemaField({ + default: new fields.NumberField({ + required: true, + choices: Object.values(CONST.DOCUMENT_OWNERSHIP_LEVELS), + initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE + }), + players: new fields.TypedObjectField( + new fields.SchemaField({ + type: new fields.NumberField({ + required: true, + choices: Object.values(CONST.DOCUMENT_OWNERSHIP_LEVELS), + initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT + }) + }) + ) + }), + progress: new fields.SchemaField({ + current: new fields.NumberField({ + required: true, + integer: true, + initial: 0, + label: 'DAGGERHEART.Countdown.FIELDS.countdowns.element.progress.current.label' + }), + max: new fields.NumberField({ + required: true, + integer: true, + initial: 1, + label: 'DAGGERHEART.Countdown.FIELDS.countdowns.element.progress.max.label' + }), + type: new fields.SchemaField({ + value: new fields.StringField({ + required: true, + choices: countdownTypes, + initial: countdownTypes.spotlight.id, + label: 'DAGGERHEART.Countdown.FIELDS.countdowns.element.progress.type.value.label' + }), + label: new fields.StringField({ + label: 'DAGGERHEART.Countdown.FIELDS.countdowns.element.progress.type.label.label' + }) + }) + }) + }; + } + + get playerOwnership() { + return Array.from(game.users).reduce((acc, user) => { + acc[user.id] = { + value: user.isGM + ? CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER + : this.ownership.players[user.id] && this.ownership.players[user.id].type !== -1 + ? this.ownership.players[user.id].type + : this.ownership.default, + isGM: user.isGM + }; + + return acc; + }, {}); + } +} + +export const registerCountdownHooks = () => { + Hooks.on(socketEvent.Refresh, ({ refreshType, application }) => { + if (refreshType === RefreshType.Countdown) { + foundry.applications.instances.get(application)?.render(); + return false; + } + }); +}; diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index 6f557866..492fcfe1 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -10,45 +10,44 @@ const fields = foundry.data.fields; export default class BaseDataItem extends foundry.abstract.TypeDataModel { - /** @returns {ItemDataModelMetadata}*/ - static get metadata() { - return { - label: "Base Item", - type: "base", - hasDescription: false, - isQuantifiable: false, - }; - } + /** @returns {ItemDataModelMetadata}*/ + static get metadata() { + return { + label: 'Base Item', + type: 'base', + hasDescription: false, + isQuantifiable: false + }; + } - /** @inheritDoc */ - static defineSchema() { - const schema = {}; + /** @inheritDoc */ + static defineSchema() { + const schema = {}; - if (this.metadata.hasDescription) - schema.description = new fields.HTMLField({ required: true, nullable: true }); + if (this.metadata.hasDescription) schema.description = new fields.HTMLField({ required: true, nullable: true }); - if (this.metadata.isQuantifiable) - schema.quantity = new fields.NumberField({ integer: true, initial: 1, min: 0, required: true }); + if (this.metadata.isQuantifiable) + schema.quantity = new fields.NumberField({ integer: true, initial: 1, min: 0, required: true }); - return schema; - } + return schema; + } - /** - * Convenient access to the item's actor, if it exists. - * @returns {foundry.documents.Actor | null} - */ - get actor() { - return this.parent.actor; - } + /** + * Convenient access to the item's actor, if it exists. + * @returns {foundry.documents.Actor | null} + */ + get actor() { + return this.parent.actor; + } - /** - * Obtain a data object used to evaluate any dice rolls associated with this Item Type - * @param {object} [options] - Options which modify the getRollData method. - * @returns {object} - */ - getRollData(options = {}) { - const actorRollData = this.actor?.getRollData() ?? {}; - const data = { ...actorRollData, item: { ...this } }; - return data; - } -} \ No newline at end of file + /** + * Obtain a data object used to evaluate any dice rolls associated with this Item Type + * @param {object} [options] - Options which modify the getRollData method. + * @returns {object} + */ + getRollData(options = {}) { + const actorRollData = this.actor?.getRollData() ?? {}; + const data = { ...actorRollData, item: { ...this } }; + return data; + } +} diff --git a/module/data/item/class.mjs b/module/data/item/class.mjs index e796dd75..cd69648d 100644 --- a/module/data/item/class.mjs +++ b/module/data/item/class.mjs @@ -19,16 +19,16 @@ export default class DHClass extends BaseDataItem { return { ...super.defineSchema(), domains: new fields.ArrayField(new fields.StringField(), { max: 2 }), - classItems: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}), + classItems: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), evasion: new fields.NumberField({ initial: 0, integer: true }), hopeFeatures: new foundry.data.fields.ArrayField(new ActionField()), classFeatures: new foundry.data.fields.ArrayField(new ActionField()), - subclasses: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}), + subclasses: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), inventory: new fields.SchemaField({ - take: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}), - choiceA: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}), - choiceB: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}), + take: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), + choiceA: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), + choiceB: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }) }), characterGuide: new fields.SchemaField({ suggestedTraits: new fields.SchemaField({ diff --git a/module/data/item/consumable.mjs b/module/data/item/consumable.mjs index aff7eea0..6c8df798 100644 --- a/module/data/item/consumable.mjs +++ b/module/data/item/consumable.mjs @@ -1,14 +1,14 @@ -import BaseDataItem from "./base.mjs"; +import BaseDataItem from './base.mjs'; import ActionField from '../fields/actionField.mjs'; export default class DHConsumable extends BaseDataItem { - /** @inheritDoc */ + /** @inheritDoc */ static get metadata() { return foundry.utils.mergeObject(super.metadata, { - label: "TYPES.Item.consumable", - type: "consumable", + label: 'TYPES.Item.consumable', + type: 'consumable', hasDescription: true, - isQuantifiable: true, + isQuantifiable: true }); } diff --git a/module/data/item/miscellaneous.mjs b/module/data/item/miscellaneous.mjs index 71daad57..d7687dc7 100644 --- a/module/data/item/miscellaneous.mjs +++ b/module/data/item/miscellaneous.mjs @@ -5,10 +5,10 @@ export default class DHMiscellaneous extends BaseDataItem { /** @inheritDoc */ static get metadata() { return foundry.utils.mergeObject(super.metadata, { - label: "TYPES.Item.miscellaneous", - type: "miscellaneous", + label: 'TYPES.Item.miscellaneous', + type: 'miscellaneous', hasDescription: true, - isQuantifiable: true, + isQuantifiable: true }); } diff --git a/module/data/item/subclass.mjs b/module/data/item/subclass.mjs index 75345625..61eec6c4 100644 --- a/module/data/item/subclass.mjs +++ b/module/data/item/subclass.mjs @@ -5,7 +5,7 @@ import BaseDataItem from './base.mjs'; const featureSchema = () => { return new foundry.data.fields.SchemaField({ name: new foundry.data.fields.StringField({ required: true }), - effects: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}), + effects: new ForeignDocumentUUIDArrayField({ type: 'ActiveEffect', required: false }), actions: new foundry.data.fields.ArrayField(new ActionField()) }); }; diff --git a/module/data/settings/Automation.mjs b/module/data/settings/Automation.mjs index 85ed8bd4..bf2aed4b 100644 --- a/module/data/settings/Automation.mjs +++ b/module/data/settings/Automation.mjs @@ -5,7 +5,8 @@ export default class DhAutomation extends foundry.abstract.DataModel { const fields = foundry.data.fields; return { hope: new fields.BooleanField({ required: true, initial: false }), - actionPoints: new fields.BooleanField({ required: true, initial: false }) + actionPoints: new fields.BooleanField({ required: true, initial: false }), + countdowns: new fields.BooleanField({ requireD: true, initial: false }) }; } } diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index fea4a426..6b9571a9 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -301,7 +301,7 @@ export default class DhpActor extends Actor { ); if (this.type === 'character') { - const automateHope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope); + const automateHope = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation).hope; if (automateHope && result.hopeUsed) { await this.update({ @@ -330,7 +330,7 @@ export default class DhpActor extends Actor { hope = roll.dice[0].results[0].result; fear = roll.dice[1].results[0].result; if ( - game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope) && + game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation).hope && config.roll.type === 'action' ) { if (hope > fear) { diff --git a/module/helpers/socket.mjs b/module/helpers/socket.mjs index fd768d34..6cd041aa 100644 --- a/module/helpers/socket.mjs +++ b/module/helpers/socket.mjs @@ -1,20 +1,68 @@ export function handleSocketEvent({ action = null, data = {} } = {}) { switch (action) { case socketEvent.GMUpdate: - Hooks.callAll(socketEvent.GMUpdate, data.action, data.uuid, data.update); + Hooks.callAll(socketEvent.GMUpdate, data); break; case socketEvent.DhpFearUpdate: Hooks.callAll(socketEvent.DhpFearUpdate); break; + case socketEvent.Refresh: + Hooks.call(socketEvent.Refresh, data); + break; } } export const socketEvent = { - GMUpdate: 'DhpGMUpdate', - DhpFearUpdate: 'DhpFearUpdate' + GMUpdate: 'DhGMUpdate', + Refresh: 'DhRefresh', + DhpFearUpdate: 'DhFearUpdate' }; export const GMUpdateEvent = { - UpdateDocument: 'DhpGMUpdateDocument', - UpdateFear: 'DhpUpdateFear' + UpdateDocument: 'DhGMUpdateDocument', + UpdateSetting: 'DhGMUpdateSetting', + UpdateFear: 'DhGMUpdateFear' +}; + +export const RefreshType = { + Countdown: 'DhCoundownRefresh' +}; + +export const registerSocketHooks = () => { + Hooks.on(socketEvent.GMUpdate, async data => { + if (game.user.isGM) { + const document = data.uuid ? await fromUuid(data.uuid) : null; + switch (data.action) { + case GMUpdateEvent.UpdateDocument: + if (document && data.update) { + await document.update(data.update); + } + break; + case GMUpdateEvent.UpdateSetting: + if (game.user.isGM) { + await game.settings.set(SYSTEM.id, data.uuid, data.update); + } + break; + case GMUpdateEvent.UpdateFear: + if (game.user.isGM) { + await game.settings.set( + SYSTEM.id, + SYSTEM.SETTINGS.gameSettings.Resources.Fear, + Math.max(Math.min(data.update, 6), 0) + ); + Hooks.callAll(socketEvent.DhpFearUpdate); + await game.socket.emit(`system.${SYSTEM.id}`, { action: socketEvent.DhpFearUpdate }); + } + break; + } + + if (data.refresh) { + await game.socket.emit(`system.${SYSTEM.id}`, { + action: socketEvent.Refresh, + data: data.refresh + }); + Hooks.call(socketEvent.Refresh, data.refresh); + } + } + }); }; diff --git a/module/ui/combatTracker.mjs b/module/ui/combatTracker.mjs index 8b71f627..d6d64f98 100644 --- a/module/ui/combatTracker.mjs +++ b/module/ui/combatTracker.mjs @@ -1,9 +1,12 @@ +import { EncounterCountdowns } from '../applications/countdowns.mjs'; + export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker { static DEFAULT_OPTIONS = { actions: { requestSpotlight: this.requestSpotlight, toggleSpotlight: this.toggleSpotlight, - setActionTokens: this.setActionTokens + setActionTokens: this.setActionTokens, + openCountdowns: this.openCountdowns } }; @@ -83,6 +86,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C .map(x => x.id) .indexOf(combatantId); + if (this.viewed.turn !== toggleTurn) Hooks.callAll(SYSTEM.HOOKS.spotlight, {}); + await this.viewed.update({ turn: this.viewed.turn === toggleTurn ? null : toggleTurn }); await combatant.update({ 'system.spotlight.requesting': false }); } @@ -97,4 +102,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C await combatant.update({ 'system.actionTokens': newIndex }); this.render(); } + + static openCountdowns() { + new EncounterCountdowns().open(); + } } diff --git a/styles/chat.less b/styles/chat.less index 883af72b..c1de3106 100644 --- a/styles/chat.less +++ b/styles/chat.less @@ -1,5 +1,7 @@ .chat-message { - .duality-modifiers, .duality-result, .dice-title { + .duality-modifiers, + .duality-result, + .dice-title { display: none; } } @@ -67,7 +69,8 @@ align-items: center; justify-content: center; position: relative; - &.hope, &.fear { + &.hope, + &.fear { .dice-wrapper { clip-path: polygon( 50% 0%, @@ -335,8 +338,10 @@ > * { padding: 0 8px; } - .message-content { - .duality-modifiers, .duality-result, .dice-title { + .message-content { + .duality-modifiers, + .duality-result, + .dice-title { display: flex; } .duality-modifiers { @@ -364,7 +369,7 @@ } .dice-result { .duality-modifiers { - display: flex; // Default => display: none; + display: flex; // Default => display: none; gap: 2px; margin-bottom: 4px; .duality-modifier { @@ -375,7 +380,9 @@ font-size: 12px; } } - .dice-formula, > .dice-total, .part-header { + .dice-formula, + > .dice-total, + .part-header { display: none; } .dice-tooltip { @@ -384,7 +391,7 @@ .tooltip-part { display: flex; align-items: end; - gap: .25rem; + gap: 0.25rem; .dice { .dice-rolls { margin-bottom: 0; diff --git a/styles/countdown.less b/styles/countdown.less new file mode 100644 index 00000000..ef5279aa --- /dev/null +++ b/styles/countdown.less @@ -0,0 +1,137 @@ +.daggerheart.dh-style.countdown { + overflow: hidden; + + fieldset { + align-items: center; + margin-top: 5px; + border-radius: 6px; + border-color: light-dark(@dark-blue, @golden); + + legend { + font-family: @font-body; + font-weight: bold; + color: light-dark(@dark-blue, @golden); + + a { + text-shadow: none; + } + } + } + + &.minimized { + height: auto !important; + max-height: unset !important; + max-width: 740px !important; + width: auto !important; + + .window-content { + display: flex; + padding: 4px 8px; + justify-content: center; + } + + .minimized-view { + display: flex; + gap: 8px; + flex-wrap: wrap; + + .mini-countdown-container { + width: fit-content; + display: flex; + align-items: center; + gap: 8px; + border: 2px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + padding: 0 4px 0 0; + background-image: url('../assets/parchments/dh-parchment-light.png'); + color: light-dark(@beige, @dark); + cursor: pointer; + + &.disabled { + cursor: initial; + } + + img { + width: 30px; + height: 30px; + border-radius: 6px 0 0 6px; + } + + .mini-countdown-name { + white-space: nowrap; + } + + .mini-countdown-value { + } + } + } + } + + .hidden { + display: none; + } + + .window-content { + > div { + height: 100%; + + .expanded-view { + height: 100%; + display: flex; + flex-direction: column; + + .countdowns-container { + display: flex; + gap: 8px; + flex-wrap: wrap; + overflow: auto; + max-height: 100%; + + .countdown-fieldset { + width: 340px; + height: min-content; + position: relative; + + .ownership-button { + position: absolute; + top: 8px; + right: 8px; + font-size: 18px; + } + + .countdown-container { + display: flex; + align-items: center; + gap: 16px; + + img { + width: 150px; + height: 150px; + cursor: pointer; + + &.disabled { + cursor: initial; + } + } + + .countdown-inner-container { + display: flex; + flex-direction: column; + gap: 4px; + + .countdown-value-container { + display: flex; + gap: 4px; + + input { + max-width: 80px; + } + } + } + } + } + } + } + } + } +} diff --git a/styles/daggerheart.css b/styles/daggerheart.css index e87b899a..1adab278 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -1297,25 +1297,38 @@ .combat-sidebar .encounter-controls.combat { justify-content: space-between; } -.combat-sidebar .encounter-controls.combat .encounter-control-fear-container { +.combat-sidebar .encounter-controls.combat .encounter-fear-controls { + display: flex; + align-items: center; + gap: 8px; +} +.combat-sidebar .encounter-controls.combat .encounter-fear-controls .encounter-fear-dice-container { + display: flex; + gap: 2px; +} +.combat-sidebar .encounter-controls.combat .encounter-fear-controls .encounter-fear-dice-container .encounter-control-fear-container { display: flex; position: relative; align-items: center; justify-content: center; color: black; } -.combat-sidebar .encounter-controls.combat .encounter-control-fear-container .dice { - height: 24px; +.combat-sidebar .encounter-controls.combat .encounter-fear-controls .encounter-fear-dice-container .encounter-control-fear-container .dice { + height: 22px; + width: 22px; } -.combat-sidebar .encounter-controls.combat .encounter-control-fear-container .encounter-control-fear { +.combat-sidebar .encounter-controls.combat .encounter-fear-controls .encounter-fear-dice-container .encounter-control-fear-container .encounter-control-fear { position: absolute; font-size: 16px; } -.combat-sidebar .encounter-controls.combat .encounter-control-fear-container .encounter-control-counter { +.combat-sidebar .encounter-controls.combat .encounter-fear-controls .encounter-fear-dice-container .encounter-control-fear-container .encounter-control-counter { position: absolute; right: -10px; color: var(--color-text-secondary); } +.combat-sidebar .encounter-controls.combat .encounter-fear-controls .encounter-countdowns { + color: var(--content-link-icon-color); +} .combat-sidebar .encounter-controls.combat .control-buttons { width: min-content; } @@ -3064,6 +3077,24 @@ div.daggerheart.views.multiclass { .daggerheart.levelup .levelup-footer { display: flex; } +.daggerheart.views.ownership-selection .ownership-outer-container { + display: flex; + flex-direction: column; + gap: 8px; +} +.daggerheart.views.ownership-selection .ownership-outer-container .ownership-container { + display: flex; + border: 2px solid light-dark(#18162e, #f3c267); + border-radius: 6px; + padding: 0 4px 0 0; + align-items: center; + gap: 8px; +} +.daggerheart.views.ownership-selection .ownership-outer-container .ownership-container img { + height: 40px; + width: 40px; + border-radius: 6px 0 0 6px; +} :root { --shadow-text-stroke: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; --fear-animation: background 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease, opacity 0.3s ease; @@ -3178,6 +3209,116 @@ div.daggerheart.views.multiclass { #resources:has(.fear-bar) { min-width: 200px; } +.daggerheart.dh-style.countdown { + overflow: hidden; +} +.daggerheart.dh-style.countdown fieldset { + align-items: center; + margin-top: 5px; + border-radius: 6px; + border-color: light-dark(#18162e, #f3c267); +} +.daggerheart.dh-style.countdown fieldset legend { + font-family: 'Montserrat', sans-serif; + font-weight: bold; + color: light-dark(#18162e, #f3c267); +} +.daggerheart.dh-style.countdown fieldset legend a { + text-shadow: none; +} +.daggerheart.dh-style.countdown.minimized { + height: auto !important; + max-height: unset !important; + max-width: 740px !important; + width: auto !important; +} +.daggerheart.dh-style.countdown.minimized .window-content { + display: flex; + padding: 4px 8px; + justify-content: center; +} +.daggerheart.dh-style.countdown.minimized .minimized-view { + display: flex; + gap: 8px; + flex-wrap: wrap; +} +.daggerheart.dh-style.countdown.minimized .minimized-view .mini-countdown-container { + width: fit-content; + display: flex; + align-items: center; + gap: 8px; + border: 2px solid light-dark(#18162e, #f3c267); + border-radius: 6px; + padding: 0 4px 0 0; + background-image: url('../assets/parchments/dh-parchment-light.png'); + color: light-dark(#efe6d8, #222); + cursor: pointer; +} +.daggerheart.dh-style.countdown.minimized .minimized-view .mini-countdown-container.disabled { + cursor: initial; +} +.daggerheart.dh-style.countdown.minimized .minimized-view .mini-countdown-container img { + width: 30px; + height: 30px; + border-radius: 6px 0 0 6px; +} +.daggerheart.dh-style.countdown.minimized .minimized-view .mini-countdown-container .mini-countdown-name { + white-space: nowrap; +} +.daggerheart.dh-style.countdown .hidden { + display: none; +} +.daggerheart.dh-style.countdown .window-content > div { + height: 100%; +} +.daggerheart.dh-style.countdown .window-content > div .expanded-view { + height: 100%; + display: flex; + flex-direction: column; +} +.daggerheart.dh-style.countdown .window-content > div .expanded-view .countdowns-container { + display: flex; + gap: 8px; + flex-wrap: wrap; + overflow: auto; + max-height: 100%; +} +.daggerheart.dh-style.countdown .window-content > div .expanded-view .countdowns-container .countdown-fieldset { + width: 340px; + height: min-content; + position: relative; +} +.daggerheart.dh-style.countdown .window-content > div .expanded-view .countdowns-container .countdown-fieldset .ownership-button { + position: absolute; + top: 8px; + right: 8px; + font-size: 18px; +} +.daggerheart.dh-style.countdown .window-content > div .expanded-view .countdowns-container .countdown-fieldset .countdown-container { + display: flex; + align-items: center; + gap: 16px; +} +.daggerheart.dh-style.countdown .window-content > div .expanded-view .countdowns-container .countdown-fieldset .countdown-container img { + width: 150px; + height: 150px; + cursor: pointer; +} +.daggerheart.dh-style.countdown .window-content > div .expanded-view .countdowns-container .countdown-fieldset .countdown-container img.disabled { + cursor: initial; +} +.daggerheart.dh-style.countdown .window-content > div .expanded-view .countdowns-container .countdown-fieldset .countdown-container .countdown-inner-container { + display: flex; + flex-direction: column; + gap: 4px; +} +.daggerheart.dh-style.countdown .window-content > div .expanded-view .countdowns-container .countdown-fieldset .countdown-container .countdown-inner-container .countdown-value-container { + display: flex; + gap: 4px; +} +.daggerheart.dh-style.countdown .window-content > div .expanded-view .countdowns-container .countdown-fieldset .countdown-container .countdown-inner-container .countdown-value-container input { + max-width: 80px; +} .daggerheart.dh-style.setting fieldset { display: flex; flex-direction: column; diff --git a/styles/daggerheart.less b/styles/daggerheart.less index 3ad972fc..d5e7f76a 100755 --- a/styles/daggerheart.less +++ b/styles/daggerheart.less @@ -10,8 +10,10 @@ @import './dialog.less'; @import './characterCreation.less'; @import './levelup.less'; +@import './ownershipSelection.less'; @import '../node_modules/@yaireo/tagify/dist/tagify.css'; @import './resources.less'; +@import './countdown.less'; @import './settings.less'; // new styles imports diff --git a/styles/ownershipSelection.less b/styles/ownershipSelection.less new file mode 100644 index 00000000..f5093bee --- /dev/null +++ b/styles/ownershipSelection.less @@ -0,0 +1,22 @@ +.daggerheart.views.ownership-selection { + .ownership-outer-container { + display: flex; + flex-direction: column; + gap: 8px; + + .ownership-container { + display: flex; + border: 2px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + padding: 0 4px 0 0; + align-items: center; + gap: 8px; + + img { + height: 40px; + width: 40px; + border-radius: 6px 0 0 6px; + } + } + } +} diff --git a/styles/ui.less b/styles/ui.less index c54b0b3b..09a3511f 100644 --- a/styles/ui.less +++ b/styles/ui.less @@ -2,26 +2,42 @@ .encounter-controls.combat { justify-content: space-between; - .encounter-control-fear-container { + .encounter-fear-controls { display: flex; - position: relative; align-items: center; - justify-content: center; - color: black; + gap: 8px; - .dice { - height: 24px; + .encounter-fear-dice-container { + display: flex; + gap: 2px; + + .encounter-control-fear-container { + display: flex; + position: relative; + align-items: center; + justify-content: center; + color: black; + + .dice { + height: 22px; + width: 22px; + } + + .encounter-control-fear { + position: absolute; + font-size: 16px; + } + + .encounter-control-counter { + position: absolute; + right: -10px; + color: var(--color-text-secondary); + } + } } - .encounter-control-fear { - position: absolute; - font-size: 16px; - } - - .encounter-control-counter { - position: absolute; - right: -10px; - color: var(--color-text-secondary); + .encounter-countdowns { + color: var(--content-link-icon-color); } } diff --git a/templates/settings/automation-settings.hbs b/templates/settings/automation-settings.hbs index bd50c548..8db980c7 100644 --- a/templates/settings/automation-settings.hbs +++ b/templates/settings/automation-settings.hbs @@ -11,6 +11,12 @@ {{formInput settingFields.schema.fields.actionPoints value=settingFields._source.actionPoints }} +