From d1a0a9ab24d3755e2419f1f49f4bb0188d9ead7b Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Mon, 26 May 2025 16:34:32 +0200 Subject: [PATCH 1/3] Embedding Duality Rolls (#52) * Added DualityRoll direct rolls in chat * Added button render to renderJournalEntryPageProseMirrorSheet and renderHandlebarsApplication * Hope and Fear dice totals are now properly added together * Added Colorful/Normal DualityRoll color settings --- daggerheart.mjs | 137 ++- lang/en.json | 18 +- module/applications/chatMessage.mjs | 19 +- module/applications/damageSelectionDialog.mjs | 4 +- module/applications/settings.mjs | 12 + module/applications/sheets/pc.mjs | 1029 +---------------- module/config/settingsConfig.mjs | 12 + module/data/dualityRoll.mjs | 124 +- module/documents/actor.mjs | 5 +- module/enrichers/DualityRollEnricher.mjs | 36 + module/helpers/utils.mjs | 58 +- module/ui/chatLog.mjs | 2 +- styles/chat.less | 318 +++++ styles/daggerheart.css | 262 ++++- styles/daggerheart.less | 1 + styles/variables/colors.less | 18 +- templates/chat/attack-roll.hbs | 220 +++- templates/chat/duality-roll.hbs | 180 ++- templates/sheets/pc/pc.hbs | 1 - 19 files changed, 1192 insertions(+), 1264 deletions(-) create mode 100644 module/enrichers/DualityRollEnricher.mjs diff --git a/daggerheart.mjs b/daggerheart.mjs index 855ae5e2..d6b180d3 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -10,6 +10,9 @@ import DhpChatLog from './module/ui/chatLog.mjs'; import DhpPlayers from './module/ui/players.mjs'; import DhpRuler from './module/ui/ruler.mjs'; import DhpTokenRuler from './module/ui/tokenRuler.mjs'; +import { dualityRollEnricher, getDualityMessage } from './module/enrichers/DualityRollEnricher.mjs'; +import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs'; +import { abilities } from './module/config/actorConfig.mjs'; globalThis.SYSTEM = SYSTEM; @@ -21,6 +24,11 @@ Hooks.once('init', () => { documents }; + CONFIG.TextEditor.enrichers.push({ + pattern: /\[\[\/dr\s?(.*?)\]\]/g, + enricher: dualityRollEnricher + }); + CONFIG.statusEffects = Object.values(SYSTEM.GENERAL.conditions).map(x => ({ ...x, name: game.i18n.localize(x.name) @@ -90,7 +98,6 @@ Hooks.once('init', () => { game.socket.on(`system.${SYSTEM.id}`, handleSocketEvent); registerDHPSettings(); - RegisterHandlebarsHelpers.registerHelpers(); return preloadHandlebarsTemplates(); @@ -122,6 +129,134 @@ Hooks.on(socketEvent.GMUpdate, async (action, uuid, update) => { } }); +const renderDualityButton = async event => { + const button = event.currentTarget; + const attributeValue = button.dataset.attribute?.toLowerCase(); + + const target = getCommandTarget(); + if (!target) return; + + const rollModifier = attributeValue ? target.system.attributes[attributeValue].data.value : null; + const { roll, hope, fear, advantage, disadvantage, modifiers } = await target.diceRoll({ + title: button.dataset.label, + value: rollModifier + }); + const cls = getDocumentClass('ChatMessage'); + const msgData = { + type: 'dualityRoll', + sound: CONFIG.sounds.dice, + system: { + title: button.dataset.label, + origin: target.id, + roll: roll._formula, + modifiers: modifiers, + hope: hope, + fear: fear, + advantage: advantage, + disadvantage: disadvantage + }, + user: game.user.id, + content: 'systems/daggerheart/templates/chat/duality-roll.hbs', + rolls: [roll] + }; + + await cls.create(msgData); +}; + +Hooks.on('renderChatMessageHTML', (_, element) => { + element + .querySelectorAll('.duality-roll-button') + .forEach(element => element.addEventListener('click', renderDualityButton)); +}); + +Hooks.on('renderJournalEntryPageProseMirrorSheet', (_, element) => { + element + .querySelectorAll('.duality-roll-button') + .forEach(element => element.addEventListener('click', renderDualityButton)); +}); + +Hooks.on('renderHandlebarsApplication', (_, element) => { + element + .querySelectorAll('.duality-roll-button') + .forEach(element => element.addEventListener('click', renderDualityButton)); +}); + +Hooks.on('chatMessage', (_, message) => { + if (message.startsWith('/dr')) { + const rollCommand = rollCommandToJSON(message.replace(/\/dr\s?/, '')); + if (!rollCommand) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.DualityParsing')); + return false; + } + + const attributeValue = rollCommand.attribute?.toLowerCase(); + + // Target not required if an attribute is not used. + const target = attributeValue ? getCommandTarget() : undefined; + if (target || !attributeValue) { + new Promise(async (resolve, reject) => { + const attribute = target ? target.system.attributes[attributeValue] : undefined; + if (attributeValue && !attribute) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.AttributeFaulty')); + reject(); + return; + } + + const title = attributeValue + ? game.i18n.format('DAGGERHEART.Chat.DualityRoll.AbilityCheckTitle', { + ability: game.i18n.localize(abilities[attributeValue].label) + }) + : game.i18n.localize('DAGGERHEART.General.Duality'); + + const hopeAndFearRoll = `1${rollCommand.hope ?? 'd12'}+1${rollCommand.fear ?? 'd12'}`; + const advantageRoll = `${rollCommand.advantage && !rollCommand.disadvantage ? '+d6' : rollCommand.disadvantage && !rollCommand.advantage ? '-d6' : ''}`; + const attributeRoll = `${attribute?.data?.value ? `${attribute.data.value > 0 ? `+${attribute.data.value}` : `${attribute.data.value}`}` : ''}`; + const roll = new Roll(`${hopeAndFearRoll}${advantageRoll}${attributeRoll}`); + await roll.evaluate(); + resolve({ + roll, + attribute: attribute + ? { + value: attribute.data.value, + label: `${game.i18n.localize(abilities[attributeValue].label)} ${attribute.data.value >= 0 ? `+` : `-`}${attribute.data.value}` + } + : undefined, + title + }); + }).then(({ roll, attribute, title }) => { + const cls = getDocumentClass('ChatMessage'); + const msgData = { + type: 'dualityRoll', + sound: CONFIG.sounds.dice, + system: { + title: title, + origin: target?.id, + roll: roll._formula, + modifiers: attribute ? [attribute] : [], + hope: { dice: rollCommand.hope ?? 'd12', value: roll.dice[0].total }, + fear: { dice: rollCommand.fear ?? 'd12', value: roll.dice[1].total }, + advantage: + rollCommand.advantage && !rollCommand.disadvantage + ? { dice: 'd6', value: roll.dice[2].total } + : undefined, + disadvantage: + rollCommand.disadvantage && !rollCommand.advantage + ? { dice: 'd6', value: roll.dice[2].total } + : undefined + }, + user: game.user.id, + content: 'systems/daggerheart/templates/chat/duality-roll.hbs', + rolls: [roll] + }; + + cls.create(msgData); + }); + } + + return false; + } +}); + 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 a8b365a8..e4c758ae 100644 --- a/lang/en.json +++ b/lang/en.json @@ -77,6 +77,14 @@ "Name": "Enable Range Measurement", "Hint": "Enable measuring of ranges with the ruler according to set distances." } + }, + "DualityRollColor": { + "Name": "Duality Roll Colour Scheme", + "Hint": "The display type for Duality Rolls", + "Options": { + "Colorful": "Colorful", + "Normal": "Normal" + } } }, "Notification": { @@ -92,13 +100,19 @@ "LacksDomain": "Your character doesn't have the domain of the card!", "MaxLoadoutReached": "You can't have any more domain cards at this level!", "DuplicateDomainCard": "You already have a domain card with that name!", - "ActionRequiresTarget": "The action requires at least one target" + "ActionRequiresTarget": "The action requires at least one target", + "NoAssignedPlayerCharacter": "You have no assigned character.", + "NoSelectedToken": "You have no selected token", + "OnlyUseableByPC": "This can only be used with a PC token", + "DualityParsing": "Duality roll not properly formated", + "AttributeFaulty": "The supplied Attribute doesn't exist" } }, "General": { - "OpenBetaDisclaimer": "Daggerheart Open Beta {version}", "Hope": "Hope", "Fear": "Fear", + "Duality": "Duality", + "Check": "{check} Check", "CriticalSuccess": "Critical Success", "Advantage": { "Full": "Advantage", diff --git a/module/applications/chatMessage.mjs b/module/applications/chatMessage.mjs index 004e7dd5..c5964023 100644 --- a/module/applications/chatMessage.mjs +++ b/module/applications/chatMessage.mjs @@ -1,3 +1,6 @@ +import { DualityRollColor } from '../config/settingsConfig.mjs'; +import DhpDualityRoll from '../data/dualityRoll.mjs'; + export default class DhpChatMesssage extends ChatMessage { async renderHTML() { if ( @@ -9,6 +12,20 @@ export default class DhpChatMesssage extends ChatMessage { this.content = await foundry.applications.handlebars.renderTemplate(this.content, this.system); } - return super.renderHTML(); + /* We can change to fully implementing the renderHTML function if needed, instead of augmenting it. */ + const html = await super.renderHTML(); + if ( + this.type === 'dualityRoll' && + game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.DualityRollColor) === + DualityRollColor.colorful.value + ) { + html.classList.add('duality'); + const dualityResult = this.system.dualityResult; + if (dualityResult === DhpDualityRoll.dualityResult.hope) html.classList.add('hope'); + else if (dualityResult === DhpDualityRoll.dualityResult.fear) html.classList.add('fear'); + else html.classList.add('critical'); + } + + return html; } } diff --git a/module/applications/damageSelectionDialog.mjs b/module/applications/damageSelectionDialog.mjs index 06d700f8..8b7071f7 100644 --- a/module/applications/damageSelectionDialog.mjs +++ b/module/applications/damageSelectionDialog.mjs @@ -113,7 +113,9 @@ export default class DamageSelectionDialog extends HandlebarsApplicationMixin(Ap } } - static rollDamage() { + static rollDamage(event) { + event.preventDefault(); + this.resolve({ rollString: this.getRollString(), bonusDamage: this.data.bonusDamage, diff --git a/module/applications/settings.mjs b/module/applications/settings.mjs index d66a2460..80036bb5 100644 --- a/module/applications/settings.mjs +++ b/module/applications/settings.mjs @@ -1,3 +1,5 @@ +import { DualityRollColor } from '../config/settingsConfig.mjs'; + class DhpAutomationSettings extends FormApplication { constructor(object = {}, options = {}) { super(object, options); @@ -213,6 +215,16 @@ export const registerDHPSettings = () => { } }); + game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.DualityRollColor, { + name: game.i18n.localize('DAGGERHEART.Settings.DualityRollColor.Name'), + hint: game.i18n.localize('DAGGERHEART.Settings.DualityRollColor.Hint'), + scope: 'world', + config: true, + type: Number, + choices: Object.values(DualityRollColor), + default: DualityRollColor.colorful.value + }); + game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.Automation.Name, { name: game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Name'), label: game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Label'), diff --git a/module/applications/sheets/pc.mjs b/module/applications/sheets/pc.mjs index 16bd362f..53ac00d4 100644 --- a/module/applications/sheets/pc.mjs +++ b/module/applications/sheets/pc.mjs @@ -496,6 +496,7 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { const cls = getDocumentClass('ChatMessage'); const msgData = { type: 'dualityRoll', + sound: CONFIG.sounds.dice, system: { title: game.i18n.format('DAGGERHEART.Chat.DualityRoll.AbilityCheckTitle', { ability: game.i18n.localize(abilities[button.dataset.attribute].label) @@ -572,7 +573,7 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { const { roll, hope, fear, advantage, disadvantage, modifiers, bonusDamageString } = await this.document.dualityRoll( - { title: 'Attribute Modifier', value: modifier }, + { title: game.i18n.localize(abilities[weapon.system.trait].label), value: modifier }, event.shiftKey, damage.bonusDamage ); @@ -1216,1029 +1217,3 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { return this._onDropItem(event, data); } } - -// export default class PCSheet extends DhpApplicationMixin(ActorSheet) { -// static applicationType = "sheets/pc"; -// static documentType = "pc"; - -// constructor(actor, options){ -// super(actor, options); - -// this.editAttributes = false; -// this.onVaultTab = false; -// this.currentInventoryPage = 0; -// this.selectedScar = null; -// this.storyEditor = null; -// this.dropItemBlock = false; -// this.multiclassFeatureSetSelected = false; -// } - -// /** @override */ -// static get defaultOptions() { -// return foundry.utils.mergeObject(super.defaultOptions, { -// classes: ["daggerheart", "sheet", "pc"], -// width: 810, -// height: 1080, -// resizable: false, -// tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "features" }, { navSelector: ".loadout-tabs", contentSelector: ".loadout-body", initial: "loadout"}], -// dragDrop: [ -// {dragSelector: null, dropSelector: ".weapon-section" }, -// {dragSelector: null, dropSelector: ".armor-section"}, -// {dragSelector: null, dropSelector: ".inventory-weapon-section-first"}, -// {dragSelector: null, dropSelector: ".inventory-weapon-section-second"}, -// {dragSelector: ".item-list .item", dropSelector: null}, -// ] -// }, { overwrite: true, inplace: true }); -// } - -// async mapFeatureType(data, configType){ -// return await Promise.all(data.map(async x => { -// const abilities = x.system.abilities ? await Promise.all(x.system.abilities.map(async x => await fromUuid(x.uuid))) : []; - -// return { -// ...x, -// uuid: x.uuid, -// system: { -// ...x.system, -// abilities: abilities, -// type: game.i18n.localize(configType[x.system.type??x.type].label) -// } -// } -// })); -// } - -// mapAdvancementFeatures(actor, config) { -// if(!actor.system.subclass) return { foundation: null, advancements: [] }; - -// const { subclass, multiclassSubclass } = actor.system.subclassFeatures; - -// const foundation = { -// type: 'foundation', -// multiclass: false, -// img: actor.system.subclass.img, -// subtitle: game.i18n.localize("DAGGERHEART.Sheets.PC.DomainCard.FoundationTitle"), -// domains: actor.system.class.system.domains.map(x => config.DOMAIN.domains[x].src), -// className: actor.system.class.name, -// subclassUuid: actor.system.subclass.uuid, -// subclassName: actor.system.subclass.name, -// spellcast: config.ACTOR.abilities[actor.system.subclass.system.spellcastingTrait]?.name ?? null, -// description: actor.system.subclass.system.foundationFeature.description, -// abilities: subclass.foundation, -// abilityKey: 'foundationFeature', -// }; - -// const firstKey = actor.system.subclass.system.specializationFeature.unlocked && actor.system.subclass.system.specializationFeature.tier === 2 ? 'sub' : -// actor.system.multiclass?.system?.multiclassTier === 2 ? 'multi' : null; -// const firstType = firstKey === 'sub' ? 'specialization' : 'foundation'; -// const firstBase = firstKey === 'sub' ? actor.system.subclass : firstKey === 'multi' ? actor.system.multiclassSubclass : null; -// const first = !firstBase ? null : { -// type: firstType, -// multiclass: firstKey === 'multi', -// img: firstBase.img, -// subtitle: firstKey === 'sub' ? game.i18n.localize("DAGGERHEART.Sheets.PC.DomainCard.SpecializationTitle") : game.i18n.localize("DAGGERHEART.Sheets.PC.DomainCard.FoundationTitle"), -// domains: firstKey === 'sub' ? actor.system.class.system.domains.map(x => config.DOMAIN.domains[x].src) : actor.system.multiclass.system.domains.map(x => config.DOMAIN.domains[x].src), -// className: firstKey === 'sub' ? actor.system.class.name : actor.system.multiclass.name, -// subclassUuid: firstBase.uuid, -// subclassName: firstBase.name, -// spellcast: firstKey === 'sub' ? null : config.ACTOR.abilities[firstBase.system.spellcastingTrait]?.name ?? null, -// description: firstKey === 'sub' ? firstBase.system.specializationFeature.description : firstBase.system.foundationFeature.description, -// abilities: firstKey === 'sub' ? subclass.specialization : multiclassSubclass.foundation, -// abilityKey: firstKey === 'sub' ? 'specializationFeature' : 'foundationFeature', -// }; - -// const secondKey = (actor.system.subclass.system.specializationFeature.unlocked && actor.system.subclass.system.specializationFeature.tier === 3) || (actor.system.subclass.system.masteryFeature.unlocked && actor.system.subclass.system.masteryFeature.tier === 3) ? 'sub' : -// (actor.system.multiclass?.system?.multiclassTier === 3) || (actor.system.multiclassSubclass?.system?.specializationFeature?.unlocked) ? 'multi' : null; -// const secondBase = secondKey === 'sub' ? actor.system.subclass : secondKey === 'multi' ? actor.system.multiclassSubclass : null; -// const secondAbilities = secondKey === 'sub' ? subclass : multiclassSubclass; -// const secondType = secondBase ? secondBase.system.masteryFeature.unlocked ? 'mastery' : secondBase.system.specializationFeature.unlocked ? 'specialization' : 'foundation' : null; -// const second = !secondBase ? null : { -// type: secondType, -// multiclass: secondKey === 'multi', -// img: secondBase.img, -// subtitle: secondBase.system.masteryFeature.unlocked ? game.i18n.localize("DAGGERHEART.Sheets.PC.DomainCard.MasteryTitle") : -// secondBase.system.specializationFeature.unlocked ? game.i18n.localize("DAGGERHEART.Sheets.PC.DomainCard.SpecializationTitle") : game.i18n.localize("DAGGERHEART.Sheets.PC.DomainCard.FoundationTitle"), -// domains: secondKey === 'sub' ? actor.system.class.system.domains.map(x => config.DOMAIN.domains[x].src) : actor.system.multiclass.system.domains.map(x => config.DOMAIN.domains[x].src), -// className: secondKey === 'sub' ? actor.system.class.name : actor.system.multiclass.name, -// subclassUuid: secondBase.uuid, -// subclassName: secondBase.name, -// spellcast: secondKey === 'sub' || secondBase.system.specializationFeature.unlocked ? null : config.ACTOR.abilities[firstBase.system.spellcastingTrait]?.name ?? null, -// description: -// secondBase.system.masteryFeature.unlocked ? secondBase.system.masteryFeature.description : -// secondBase.system.specializationFeature.unlocked ? secondBase.system.specializationFeature.description : firstBase.system.foundationFeature.description, -// abilities: -// secondBase.system.masteryFeature.unlocked ? secondAbilities.mastery : -// secondBase.system.specializationFeature.unlocked ? secondAbilities.specialization : secondAbilities.foundation, -// abilityKey: secondBase.system.masteryFeature.unlocked ? 'masteryFeature' : secondBase.system.specializationFeature.unlocked ? 'specializationFeature' : 'foundationFeature', -// }; - -// return { -// foundation: foundation, -// first: first, -// second: second, -// } -// } - -// /** @override */ -// async getData() { -// const context = super.getData(); -// context.config = SYSTEM; -// context.editAttributes = this.editAttributes; -// context.onVaultTab = this.onVaultTab; -// context.selectedScar = this.selectedScar; -// context.storyEditor = this.storyEditor; -// context.multiclassFeatureSetSelected = this.multiclassFeatureSetSelected; - -// const selectedAttributes = Object.values(this.actor.system.attributes).map(x => x.data.base); -// context.abilityScoreArray = JSON.parse(await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.AbilityArray)).reduce((acc, x) => { -// const selectedIndex = selectedAttributes.indexOf(x); -// if(selectedIndex !== -1){ -// selectedAttributes.splice(selectedIndex, 1); -// } else { -// acc.push({ name: x, value: x }); -// } - -// return acc; -// }, []); -// if(!context.abilityScoreArray.includes(0)) context.abilityScoreArray.push({ name: 0, value: 0 }); -// context.abilityScoresFinished = context.abilityScoreArray.every(x => x.value === 0); - -// context.domains = this.actor.system.class ? { -// first: this.actor.system.class.system.domains[0] ? SYSTEM.DOMAIN.domains[this.actor.system.class.system.domains[0]].src : null, -// second: this.actor.system.class.system.domains[1] ? SYSTEM.DOMAIN.domains[this.actor.system.class.system.domains[1]].src : null, -// } : { }; - -// context.attributes = Object.keys(this.actor.system.attributes).reduce((acc, key) => { -// acc[key] = { -// ...this.actor.system.attributes[key], -// name: game.i18n.localize(SYSTEM.ACTOR.abilities[key].name), -// verbs: SYSTEM.ACTOR.abilities[key].verbs.map(x => game.i18n.localize(x)), -// }; - -// return acc; -// }, {}); - -// const ancestry = await this.mapFeatureType(this.actor.system.ancestry ? [this.actor.system.ancestry] : [], SYSTEM.GENERAL.objectTypes); -// const community = await this.mapFeatureType(this.actor.system.community ? [this.actor.system.community] : [], SYSTEM.GENERAL.objectTypes); -// const foundation = { -// ancestry: ancestry[0], -// community: community[0], -// advancement: { -// ...this.mapAdvancementFeatures(this.actor, SYSTEM) -// } -// }; - -// const nrLoadoutCards = this.actor.system.domainCards.loadout.length; -// const loadout = await this.mapFeatureType(this.actor.system.domainCards.loadout, SYSTEM.DOMAIN.cardTypes); -// const vault = await this.mapFeatureType(this.actor.system.domainCards.vault, SYSTEM.DOMAIN.cardTypes); -// context.abilities = { -// foundation: foundation, -// loadout: { -// top: loadout.slice(0, Math.min(2, nrLoadoutCards)), -// bottom: nrLoadoutCards > 2 ? loadout.slice(2, Math.min(5, nrLoadoutCards)) : [], -// nrTotal: nrLoadoutCards, -// }, -// vault: vault.map(x => ({ ...x, uuid: x.uuid, sendToLoadoutDisabled: this.actor.system.domainCards.loadout.length >= this.actor.system.domainData.maxLoadout })) -// }; - -// context.inventory = { -// consumable: { -// titles: { -// name: game.i18n.localize("DAGGERHEART.Sheets.PC.InventoryTab.ConsumableTitle"), -// quantity: game.i18n.localize("DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle"), -// }, -// items: this.actor.items.filter(x => x.type === 'consumable'), -// }, -// miscellaneous: { -// titles: { -// name: game.i18n.localize("DAGGERHEART.Sheets.PC.InventoryTab.MiscellaneousTitle"), -// quantity: game.i18n.localize("DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle"), -// }, -// items: this.actor.items.filter(x => x.type === 'miscellaneous'), -// }, -// } - -// if(context.inventory.length === 0){ -// context.inventory = Array(1).fill(Array(5).fill([])); -// } - -// context.classFeatures = (this.multiclassFeatureSetSelected ? this.actor.system.multiclassFeatures : this.actor.system.classFeatures).map(x => { -// if(x.system.featureType.type !== 'dice'){ -// return x; -// } - -// return { ...x, uuid: x.uuid, system: { ...x.system, featureType: { ...x.system.featureType, data: { ...x.system.featureType.data, property: this.actor.system.subclass ? SYSTEM.ACTOR.featureProperties[x.system.featureType.data.property].path(this.actor) : 0 }}}}; -// }); - -// return context; -// } - -// activateListeners(html) { -// super.activateListeners(html); - -// html.find(".attribute-value").on("change", this.attributeChange.bind(this)); -// html.on('click', '.tab-selector', this.tabSwitch.bind(this)); -// html.on('click', '.level-title.levelup', this.openLevelUp.bind(this)); -// html.find(".feature-input").on("change", this.onFeatureInputBlur.bind(this)); -// html.find(".experience-description").on("change", this.experienceDescriptionChange.bind(this)); -// html.find(".experience-value").on("change", this.experienceValueChange.bind(this)); -// html.on("change", "[data-item]", this.itemUpdate.bind(this)); -// } - -// async _handleAction(action, event, button) { -// switch(action){ -// case 'toggleEditAttributes': -// this.toggleEditAttributes(); -// break; -// case 'attributeRoll': -// await this.rollAttribute(event); -// break; -// case 'toggleMarks': -// await this.toggleMarks(button); -// break; -// case 'toggleAttributeMark': -// await this.toggleAttributeMark(button); -// break; -// case 'toggleHP': -// await this.toggleHP(button); -// break; -// case 'toggleStress': -// await this.toggleStress(button); -// break; -// case 'toggleHope': -// await this.toggleHope(button); -// break; -// case 'toggleGold': -// await this.toggleGold(button); -// break; -// case 'attackRoll': -// await this.attackRoll(event); -// break; -// case 'tabToLoadout': -// this.domainCardsTab(false); -// break; -// case 'tabToVault': -// this.domainCardsTab(true); -// break; -// case 'sendToVault': -// await this.moveDomainCard(button, true); -// break; -// case 'sendToLoadout': -// await this.moveDomainCard(button, false); -// break; -// case 'useDomainCard': -// await this.useDomainCard(button); -// break; -// case 'selectClass': -// await this.selectClass(); -// break; -// case 'selectSubclass': -// await this.selectSubclass(); -// break; -// case 'selectAncestry': -// await this.selectAncestry(); -// break; -// case 'selectCommunity': -// await this.selectCommunity(); -// break; -// case 'viewObject': -// await this.viewObject(button); -// break; -// case 'useFeature': -// await this.useFeature(button); -// break; -// case 'takeShortRest': -// await this.takeShortRest(); -// break; -// case 'takeLongRest': -// await this.takeLongRest(); -// break; -// case 'removeActiveItem': -// await this.removeActiveItem(event); -// break; -// case 'removeInventoryWeapon': -// await this.removeInventoryWeapon(event); -// break; -// case 'addMiscItem': -// await this.addMiscItem(); -// break; -// case 'deleteItem': -// await this.deleteItem(button); -// break; -// case 'addScar': -// await this.addScar(); -// break; -// case 'selectScar': -// await this.selectScar(button); -// break; -// case 'deleteScar': -// await this.deleteScar(event); -// break; -// case 'makeDeathMove': -// await this.makeDeathMove(); -// break; -// case 'toggleFeatureDice': -// await this.toggleFeatureDice(button); -// break; -// case 'setStoryEditor': -// this.setStoryEditor(button); -// break; -// case 'itemQuantityDecrease': -// await this.setItemQuantity(button, -1); -// break; -// case 'itemQuantityIncrease': -// await this.setItemQuantity(button, 1); -// break; -// case 'useAbility': -// await this.useAbility(button); -// break; -// case 'useAdvancementCard': -// await this.useAdvancementCard(button); -// break; -// case 'useAdvancementAbility': -// await this.useAdvancementAbility(button); -// break; -// case 'selectFeatureSet': -// await this.selectFeatureSet(button); -// break; -// } -// } - -// async attributeChange(event){ -// const path = `system.attributes.${event.currentTarget.dataset.attribute}.data.base`; -// await this.actor.update({ [path]: event.currentTarget.value }); -// } - -// toggleEditAttributes(){ -// this.editAttributes = !this.editAttributes; -// this.render(); -// } - -// async rollAttribute(event){ -// const { roll, hope, fear, advantage, disadvantage, modifiers } = await this.actor.dualityRoll({ title: 'Attribute Bonus', value: event.currentTarget.dataset.value }, event.shiftKey); - -// const cls = getDocumentClass("ChatMessage"); -// const msg = new cls({ -// type: 'dualityRoll', -// system: { -// roll: roll._formula, -// modifiers: modifiers, -// hope: hope, -// fear: fear, -// advantage: advantage, -// disadvantage: disadvantage, -// }, -// user: game.user.id, -// content: "systems/daggerheart/templates/chat/duality-roll.hbs", -// rolls: [roll] -// }); - -// await cls.create(msg.toObject()); -// } - -// async toggleMarks(button){ -// const markValue = Number.parseInt(button.dataset.value); -// const newValue = this.actor.system.armor.system.marks.value >= markValue ? markValue-1 : markValue; -// await this.actor.system.armor.update({ 'system.marks.value': newValue }); -// } - -// async toggleAttributeMark(button){ -// const attribute = this.actor.system.attributes[button.dataset.attribute]; -// const newMark = this.actor.system.availableAttributeMarks.filter(x => x > Math.max.apply(null, this.actor.system.attributes[button.dataset.attribute].levelMarks)).sort((a, b) => a > b ? 1 : -1)[0]; - -// if(attribute.levelMark || !newMark) return; - -// const path = `system.attributes.${button.dataset.attribute}.levelMarks`; -// await this.actor.update({ [path]: [...attribute.levelMarks, newMark] }); -// } - -// async toggleHP(button){ -// const healthValue = Number.parseInt(button.dataset.value); -// const newValue = this.actor.system.resources.health.value >= healthValue ? healthValue-1 : healthValue; -// await this.actor.update({ 'system.resources.health.value': newValue }); -// } - -// async toggleStress(button){ -// const healthValue = Number.parseInt(button.dataset.value); -// const newValue = this.actor.system.resources.stress.value >= healthValue ? healthValue-1 : healthValue; -// await this.actor.update({ 'system.resources.stress.value': newValue }); -// } - -// async toggleHope(button){ -// const hopeValue = Number.parseInt(button.dataset.value); -// const newValue = this.actor.system.resources.hope.value >= hopeValue ? hopeValue-1 : hopeValue; -// await this.actor.update({ 'system.resources.hope.value': newValue }); -// } - -// async toggleGold(button){ -// const goldValue = Number.parseInt(button.dataset.value); -// const goldType = button.dataset.type; -// const newValue = this.actor.system.gold[goldType] >= goldValue ? goldValue-1 : goldValue; - -// const update = `system.gold.${goldType}`; -// await this.actor.update({ [update]: newValue }); -// } - -// async attackRoll(event){ -// const weapon = await fromUuid(event.currentTarget.dataset.weapon); -// const damage = { -// value: `${this.actor.system.proficiency.value}${weapon.system.damage.value}`, -// type: weapon.system.damage.type, -// bonusDamage: this.actor.system.bonuses.damage -// }; -// const modifier = this.actor.system.attributes[weapon.system.trait].data.value; - -// const { roll, hope, fear, advantage, disadvantage, modifiers, bonusDamageString } = await this.actor.dualityRoll({ title: 'Attribute Modifier', value: modifier }, event.shiftKey, damage.bonusDamage); - -// damage.value = damage.value.concat(bonusDamageString); - -// const targets = Array.from(game.user.targets).map(x => ({ -// id: x.id, -// name: x.actor.name, -// img: x.actor.img, -// difficulty: x.actor.system.difficulty, -// evasion: x.actor.system.evasion, -// })); - -// const cls = getDocumentClass("ChatMessage"); -// const msg = new cls({ -// type: 'dualityRoll', -// system: { -// roll: roll._formula, -// modifiers: modifiers, -// hope: hope, -// fear: fear, -// advantage: advantage, -// disadvantage: disadvantage, -// damage: damage, -// targets: targets, -// }, -// content: "systems/daggerheart/templates/chat/attack-roll.hbs", -// rolls: [roll] -// }); - -// await cls.create(msg.toObject()); -// } - -// tabSwitch(event){ -// const tab = event.currentTarget.dataset.tab; -// if(tab !== 'loadout'){ -// this.onVaultTab = false; -// } - -// this.render(); -// } - -// openLevelUp(event){ -// new DhpLevelup(this.actor).render(true); -// } - -// domainCardsTab(toVault){ -// this.onVaultTab = toVault; -// this.render(); -// } - -// async moveDomainCard(button, toVault){ -// if(!toVault && this.actor.system.domainCards.loadout.length >= this.actor.system.domainData.maxLoadout){ -// return; -// } - -// const card = this.actor.items.find(x => x.uuid === button.dataset.domain); -// await card.update({ "system.inVault": toVault }); -// } - -// async useDomainCard(button){ -// const card = this.actor.items.find(x => x.uuid === button.dataset.key); - -// const cls = getDocumentClass("ChatMessage"); -// const msg = new cls({ -// type: 'abilityUse', -// user: game.user.id, -// content: "systems/daggerheart/templates/chat/ability-use.hbs", -// system: { -// title: `${game.i18n.localize("DAGGERHEART.Chat.DomainCard.Title")} - ${capitalize(button.dataset.domain)}`, -// img: card.img, -// name: card.name, -// description: card.system.effect, -// actions: card.system.actions, -// }, -// }); - -// cls.create(msg.toObject()); -// } - -// async selectClass(){ -// (await game.packs.get('daggerheart.playtest-classes'))?.render(true); -// } - -// async selectSubclass(){ -// (await game.packs.get('daggerheart.playtest-subclasses'))?.render(true); -// } - -// async selectAncestry(){ -// const dialogClosed = new Promise((resolve, _) => { -// new AncestrySelectionDialog(resolve).render(true); -// }); -// const result = await dialogClosed; - -// // await this.emulateItemDrop({ type: 'item', data: result }); -// for(var ancestry of this.actor.items.filter(x => x => x.type === 'ancestry')){ -// await ancestry.delete(); -// } - -// const createdItems = []; -// for(var feature of this.actor.items.filter(x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.ancestry.id)){ -// await feature.delete(); -// } - -// // createdItems.push(...result.data.system.abilities); -// createdItems.push(result.data); - -// await this.actor.createEmbeddedDocuments('Item', createdItems); - -// // await this.actor.createEmbeddedDocuments("Item", [result.toObject()]); -// // (await game.packs.get('daggerheart.playtest-ancestries'))?.render(true); -// } - -// async selectCommunity(){ -// (await game.packs.get('daggerheart.playtest-communities'))?.render(true);; -// } - -// async viewObject(button){ -// const object = await fromUuid(button.dataset.value); -// if(!object) return; - -// const tab = button.dataset.tab; -// if(tab) object.sheet._tabs[0].active = tab; - -// if(object.sheet.editMode) object.sheet.editMode = false; - -// object.sheet.render(true); -// } - -// async takeShortRest(){ -// await new DhpDowntime(this.actor, true).render(true); -// await this.minimize(); -// } - -// async takeLongRest(){ -// await new DhpDowntime(this.actor, false).render(true); -// await this.minimize(); -// } - -// async removeActiveItem(event){ -// event.stopPropagation(); -// const item = await fromUuid(event.currentTarget.dataset.item); -// await item.delete(); -// } - -// async removeInventoryWeapon(event){ -// event.stopPropagation(); -// const item = await fromUuid(event.currentTarget.dataset.item); -// await item.delete(); -// } - -// async addMiscItem(){ -// const result = await this.actor.createEmbeddedDocuments("Item", [{ -// name: game.i18n.localize('DAGGERHEART.Sheets.PC.NewItem'), -// type: 'miscellaneous' -// }]); - -// await result[0].sheet.render(true); -// } - -// async addScar(){ -// if(this.actor.system.story.scars.length === 5) return; - -// await this.actor.update({ "system.story.scars": [...this.actor.system.story.scars, { name: game.i18n.localize("DAGGERHEART.Sheets.PC.NewScar"), description: '' }] }); -// } - -// async selectScar(button){ -// this.selectedScar = Number.parseInt(button.dataset.value); -// this.render(); -// } - -// async deleteScar(event) { -// event.stopPropagation(); -// await this.actor.update({ "system.story.scars": this.actor.system.story.scars.filter((_, index) => index !== Number.parseInt(event.currentTarget.dataset.scar) ) }) -// } - -// async makeDeathMove() { -// if(this.actor.system.resources.health.value === this.actor.system.resources.health.max){ -// await new DhpDeathMove(this.actor).render(true); -// await this.minimize(); -// } -// } - -// async toggleFeatureDice(button){ -// const index = Number.parseInt(button.dataset.index); -// const feature = this.actor.system.classFeatures.find(x => x.uuid === button.dataset.feature); -// const path = `system.featureType.data.numbers.${index}`; -// if(feature.system.featureType.data.numbers[index]?.used) return; - -// if(Object.keys(feature.system.featureType.data.numbers).length <= index) { -// const roll = new Roll(feature.system.featureType.data.value); -// const rollData = await roll.evaluate(); -// const cls = getDocumentClass("ChatMessage"); -// const msg = new cls({ -// user: game.user.id, -// rolls: [roll] -// }); - -// await cls.create(msg.toObject()); - -// await feature.update({ [path]: { value: Number.parseInt(rollData.total), used: false } }); -// } else { -// await Dialog.confirm({ -// title: game.i18n.localize("Confirm feature use"), -// content: `Are you sure you want to use ${feature.name}?`, -// yes: async () => { -// await feature.update({ [path]: { used: true } }); - -// const cls = getDocumentClass("ChatMessage"); -// const msg = new cls({ -// user: game.user.id, -// content: await renderTemplate("systems/daggerheart/templates/chat/ability-use.hbs", { -// title: game.i18n.localize("DAGGERHEART.Chat.FeatureTitle"), -// card: { name: `${feature.name} - Roll Of ${feature.system.featureType.data.numbers[index].value}`, img: feature.img }, -// }), -// }); - -// cls.create(msg.toObject()); -// }, -// no: () => { return; }, -// defaultYes: false -// }); - -// } -// } - -// async onFeatureInputBlur(event){ -// const feature = this.actor.system.classFeatures.find(x => x.uuid === event.currentTarget.dataset.feature); -// const value = Number.parseInt(event.currentTarget.value); -// if(!Number.isNaN(value)) await feature?.update({ "system.featureType.data.value": value }); -// } - -// async experienceDescriptionChange(event){ -// const newExperiences = [...this.actor.system.experiences]; -// newExperiences[event.currentTarget.dataset.index].description = event.currentTarget.value; -// await this.actor.update({ "system.experiences": newExperiences }); -// } - -// async experienceValueChange(event){ -// const newExperiences = [...this.actor.system.experiences]; -// newExperiences[event.currentTarget.dataset.index].value = event.currentTarget.value; -// await this.actor.update({ "system.experiences": newExperiences }); -// } - -// setStoryEditor(button) { -// this.storyEditor = this.storyEditor === button.dataset.value ? null : button.dataset.value; -// this.render(); -// } - -// async itemUpdate(event){ -// const name = event.currentTarget.dataset.item; -// const item = await fromUuid($(event.currentTarget).closest('[data-item-id]')[0].dataset.itemId); -// await item.update({ [name]: event.currentTarget.value }); -// } - -// async deleteItem(button){ -// const item = await fromUuid($(button).closest('[data-item-id]')[0].dataset.itemId); -// await item.delete(); -// } - -// async setItemQuantity(button, value){ -// const item = await fromUuid($(button).closest('[data-item-id]')[0].dataset.itemId); -// await item.update({ "system.quantity": Math.max(item.system.quantity + value, 1) }); -// } - -// async useFeature(button) { -// const item = await fromUuid(button.dataset.id); - -// const cls = getDocumentClass("ChatMessage"); -// const msg = new cls({ -// type: 'abilityUse', -// user: game.user.id, -// content: "systems/daggerheart/templates/chat/ability-use.hbs", -// system: { -// title: game.i18n.localize("DAGGERHEART.Chat.FeatureTitle"), -// img: item.img, -// name: item.name, -// description: item.system.description, -// actions: item.system.actions, -// }, -// }); - -// cls.create(msg.toObject()); -// } - -// async useAbility(button) { -// const item = await fromUuid(button.dataset.feature); -// const type = button.dataset.type - -// const cls = getDocumentClass("ChatMessage"); -// const msg = new cls({ -// type: 'abilityUse', -// user: game.user.id, -// system: { -// title: type === 'ancestry' ? game.i18n.localize("DAGGERHEART.Chat.FoundationCard.AncestryTitle") : -// type === 'community' ? game.i18n.localize("DAGGERHEART.Chat.FoundationCard.CommunityTitle") : -// game.i18n.localize("DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle"), -// img: item.img, -// name: item.name, -// description: item.system.description, -// actions: [], -// }, -// content: "systems/daggerheart/templates/chat/ability-use.hbs", -// }); - -// cls.create(msg.toObject()); -// } - -// async useAdvancementCard(button){ -// const item = button.dataset.multiclass === 'true' ? this.actor.system.multiclassSubclass : this.actor.system.subclass; -// const ability = item.system[`${button.dataset.key}Feature`]; -// const title = `${item.name} - ${game.i18n.localize(`DAGGERHEART.Sheets.PC.DomainCard.${capitalize(button.dataset.key)}Title`)}`; - -// const cls = getDocumentClass("ChatMessage"); -// const msg = new cls({ -// user: game.user.id, -// content: await renderTemplate("systems/daggerheart/templates/chat/ability-use.hbs", { -// title: game.i18n.localize("DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle"), -// card: { name: title, img: item.img, description: ability.description }, -// }), -// }); - -// cls.create(msg.toObject()); -// } - -// async useAdvancementAbility(button){ -// // const item = await fromUuid(button.dataset.id); -// const item = this.actor.items.find(x => x.uuid === button.dataset.id); - -// const cls = getDocumentClass("ChatMessage"); -// const msg = new cls({ -// user: game.user.id, -// content: await renderTemplate("systems/daggerheart/templates/chat/ability-use.hbs", { -// title: game.i18n.localize("DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle"), -// card: { name: item.name, img: item.img, description: item.system.description }, -// }), -// }); - -// cls.create(msg.toObject()); -// } - -// async selectFeatureSet(button){ -// const multiclass = button.dataset.multiclass === 'true'; -// this.multiclassFeatureSetSelected = multiclass; -// this.render(); -// } - -// async close(options){ -// this.onVaultTab = false; -// super.close(options); -// } - -// async _onDragStart(event){ -// if(event.currentTarget.classList.contains('inventory-item')){ -// if(!['weapon', 'armor'].includes(event.currentTarget.dataset.type)){ -// return; -// } - -// const targets = { -// 'weapon': ['weapon-section', 'inventory-weapon-section'], -// 'armor': ['armor-section', 'inventory-armor-section'], -// }; - -// event.dataTransfer.setData("text/plain", JSON.stringify({ uuid: event.currentTarget.dataset.item, internal: true, targets: targets[event.currentTarget.dataset.type] })); -// } - -// super._onDragStart(event); -// } - -// async _onDrop(event){ -// const itemData = event.dataTransfer?.getData('text/plain'); -// const item = itemData ? JSON.parse(itemData) : null; -// if (item?.internal){ -// let target = null; -// event.currentTarget.classList.forEach(x => { -// if(item.targets.some(target => target === x)){ -// target = x; -// } -// }); -// if(target){ -// const itemObject = await fromUuid(item.uuid); -// switch(target){ -// case 'weapon-section': -// if(itemObject.system.secondary && this.actor.system.activeWeapons.burden === 'twoHanded'){ -// ui.notifications.info(game.i18n.localize("DAGGERHEART.Notification.Info.SecondaryEquipWhileTwohanded")); -// return; -// } -// else if(itemObject.system.burden === 'twoHanded' && this.actor.system.activeWeapons.secondary){ -// ui.notifications.info(game.i18n.localize("DAGGERHEART.Notification.Info.TwohandedEquipWhileSecondary")); -// return; -// } - -// const existingWeapon = this.actor.items.find(x => x.system.active && x.system.secondary === itemObject.system.secondary); -// await existingWeapon?.update({ "system.active": false }); -// await itemObject.update({ "system.active": true }); -// break; -// case 'armor-section': -// const existingArmor = this.actor.items.find(x => x.type === 'armor' && x.system.active); -// await existingArmor?.update({ "system.active": false }); -// await itemObject.update({ "system.active": true }); -// break; -// case 'inventory-weapon-section': -// const existingInventoryWeapon = this.actor.items.find(x => x.system.inventoryWeapon); -// await existingInventoryWeapon?.update({ "system.inventoryWeapon": false }); -// await itemObject.update({ "system.inventoryWeapon": true }); -// break; -// case 'inventory-armor-section': -// const existingInventoryArmor = this.actor.items.find(x => x.system.inventoryArmor); -// await existingInventoryArmor?.update({ "system.inventoryArmor": false }); -// await itemObject.update({ "system.inventoryArmor": true }); -// break; -// } -// } -// } -// else { -// super._onDrop(event); -// } -// } - -// async _onDropItem(event, data){ -// if(this.dropItemBlock){ -// return; -// } -// else { -// this.dropItemBlock = true; -// setTimeout(() => this.dropItemBlock = false, 500); -// } - -// const element = event.currentTarget; -// const item = await Item.implementation.fromDropData(data); -// const itemData = item.toObject(); - -// const createdItems = []; - -// if(item.type === 'domainCard'){ -// if(!this.actor.system.class) -// { -// ui.notifications.error(game.i18n.localize("DAGGERHEART.Notification.Error.NoClassSelected")); -// return; -// } - -// if(!this.actor.system.domains.find(x => x === item.system.domain)){ -// ui.notifications.error(game.i18n.localize("DAGGERHEART.Notification.Error.LacksDomain")); -// return; -// } - -// if(this.actor.system.domainCards.total.length === this.actor.system.domainData.maxCards){ -// ui.notifications.error(game.i18n.localize("DAGGERHEART.Notification.Error.MaxLoadoutReached")); -// return; -// } - -// if(this.actor.system.domainCards.total.find(x => x.name === item.name)){ -// ui.notifications.error(game.i18n.localize("DAGGERHEART.Notification.Error.DuplicateDomainCard")); -// return; -// } - -// if(this.actor.system.domainCards.loadout.length >= this.actor.system.domainData.maxLoadout){ -// itemData.system.inVault = true; -// } - -// if ( this.actor.uuid === item.parent?.uuid ) return this._onSortItem(event, itemData); -// const createdItem = await this._onDropItemCreate(itemData); - -// return createdItem; -// } -// else { -// if(!item.system.multiclass && ['class', 'subclass', 'ancestry', 'community'].includes(item.type)){ -// const existing = this.actor.items.find(x => x.type === item.type); -// await existing?.delete(); -// } - -// if(item.type === 'subclass'){ -// if(!item.system.multiclass){ -// if(!this.actor.system.class){ -// ui.notifications.info(game.i18n.localize("DAGGERHEART.Notification.Info.SelectClassBeforeSubclass")); -// return; -// } -// else if(!this.actor.system.class.system.subclasses.some(x => x.uuid === item.uuid)){ -// ui.notifications.info(game.i18n.localize("DAGGERHEART.Notification.Info.SubclassNotOfClass")); -// return; -// } - -// for(var feature of this.actor.items.filter(x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.subclass.id)){ -// await feature.delete(); -// } -// } - -// const features = [itemData.system.foundationFeature, itemData.system.specializationFeature, itemData.system.masteryFeature]; -// for(var i = 0; i < features.length; i++){ -// const feature = features[i]; -// for(var ability of feature.abilities){ -// const data = (await fromUuid(ability.uuid)).toObject(); -// if(i > 0 ) data.system.disabled = true; -// data.uuid = itemData.uuid; - -// const abilityData = await this._onDropItemCreate(data); -// ability.uuid = abilityData[0].uuid; - -// createdItems.push(abilityData); -// } -// } -// } -// else if(item.type === 'class'){ -// if(!item.system.multiclass){ -// for(var feature of this.actor.items.filter(x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.class.id)){ -// await feature.delete(); -// } -// } - -// for(var feature of item.system.features){ -// const data = (await fromUuid(feature.uuid)).toObject(); -// const itemData = await this._onDropItemCreate(data); -// createdItems.push(itemData); -// } -// } -// else if(item.type === 'ancestry'){ -// for(var feature of this.actor.items.filter(x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.ancestry.id)){ -// await feature.delete(); -// } - -// for(var feature of item.system.abilities){ -// const data = (await fromUuid(feature.uuid)).toObject(); -// const itemData = await this._onDropItemCreate(data); -// createdItems.push(itemData); -// } -// } -// else if(item.type === 'community'){ -// for(var feature of this.actor.items.filter(x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.community.id)){ -// await feature.delete(); -// } - -// for(var feature of item.system.abilities){ -// const data = (await fromUuid(feature.uuid)).toObject(); -// const itemData = await this._onDropItemCreate(data); -// createdItems.push(itemData); -// } -// } - -// if ( this.actor.uuid === item.parent?.uuid ) return this._onSortItem(event, item); - -// if(item.type === 'weapon'){ -// if(!element) return; - -// if(element.classList.contains('weapon-section')){ -// if(item.system.secondary && this.actor.system.activeWeapons.burden === 'twoHanded'){ -// ui.notifications.info(game.i18n.localize("DAGGERHEART.Notification.Info.SecondaryEquipWhileTwohanded")); -// return; -// } -// else if(item.system.burden === 'twoHanded' && this.actor.system.activeWeapons.secondary){ -// ui.notifications.info(game.i18n.localize("DAGGERHEART.Notification.Info.TwohandedEquipWhileSecondary")); -// return; -// } - -// const existing = this.actor.system.activeWeapons.primary && !item.system.secondary ? await fromUuid(this.actor.system.activeWeapons.primary.uuid) : -// this.actor.system.activeWeapons.secondary && item.system.secondary ? await fromUuid(this.actor.system.activeWeapons.secondary.uuid) : null; -// await existing?.delete(); -// itemData.system.active = true; -// } -// else if(element.classList.contains('inventory-weapon-section-first')){ -// const existing = this.actor.system.inventoryWeapons.first ? await fromUuid(this.actor.system.inventoryWeapons.first.uuid) : null; -// await existing?.delete(); - -// itemData.system.inventoryWeapon = 1; -// } -// else if(element.classList.contains('inventory-weapon-section-second')){ -// const existing = this.actor.system.inventoryWeapons.second ? await fromUuid(this.actor.system.inventoryWeapons.second.uuid) : null; -// await existing?.delete(); - -// itemData.system.inventoryWeapon = 2; -// } -// else return []; -// } - -// if(item.type === 'armor'){ -// if(!element) return; - -// if(element.classList.contains('armor-section')){ -// const existing = this.actor.system.armor ? await fromUuid(this.actor.system.armor.uuid) : null; -// await existing?.delete(); -// } - -// else return; -// } - -// const createdItem = await this._onDropItemCreate(itemData); -// createdItems.push(createdItem); - -// return createdItems; -// } -// } - -// async emulateItemDrop(data) { -// const event = new DragEvent("drop", { altKey: game.keyboard.isModifierActive("Alt") }); -// return this._onDropItem(event, data); -// } -// } diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs index 5a4674da..d844c1ff 100644 --- a/module/config/settingsConfig.mjs +++ b/module/config/settingsConfig.mjs @@ -24,5 +24,17 @@ export const gameSettings = { General: { AbilityArray: 'AbilityArray', RangeMeasurement: 'RangeMeasurement' + }, + DualityRollColor: 'DualityRollColor' +}; + +export const DualityRollColor = { + colorful: { + value: 0, + label: 'DAGGERHEART.Settings.DualityRollColor.Options.Colorful' + }, + normal: { + value: 1, + label: 'DAGGERHEART.Settings.DualityRollColor.Options.Normal' } }; diff --git a/module/data/dualityRoll.mjs b/module/data/dualityRoll.mjs index d071f84f..50909a46 100644 --- a/module/data/dualityRoll.mjs +++ b/module/data/dualityRoll.mjs @@ -1,3 +1,5 @@ +import { DualityRollColor } from '../config/settingsConfig.mjs'; + const fields = foundry.data.fields; const diceField = () => new fields.SchemaField({ @@ -6,6 +8,12 @@ const diceField = () => }); export default class DhpDualityRoll extends foundry.abstract.TypeDataModel { + static dualityResult = { + hope: 1, + fear: 2, + critical: 3 + }; + static defineSchema() { return { title: new fields.StringField(), @@ -57,17 +65,32 @@ export default class DhpDualityRoll extends foundry.abstract.TypeDataModel { } get total() { - const modifiers = this.modifiers.reduce((acc, x) => acc + x.value, 0); const advantage = this.advantage.value ? this.advantage.value : this.disadvantage.value ? -this.disadvantage.value : 0; - return this.highestRoll + advantage + modifiers; + return this.diceTotal + advantage + this.modifierTotal.value; } - get highestRoll() { - return Math.max(this.hope.value, this.fear.value); + get diceTotal() { + return this.hope.value + this.fear.value; + } + + get modifierTotal() { + const total = this.modifiers.reduce((acc, x) => acc + x.value, 0); + return { + value: total, + label: total > 0 ? `+${total}` : total < 0 ? `-${total}` : '' + }; + } + + get dualityResult() { + return this.hope.value > this.fear.value + ? this.constructor.dualityResult.hope + : this.fear.value > this.hope.value + ? this.constructor.dualityResult.fear + : this.constructor.dualityResult.critical; } get totalLabel() { @@ -81,6 +104,13 @@ export default class DhpDualityRoll extends foundry.abstract.TypeDataModel { return game.i18n.localize(label); } + get colorful() { + return ( + game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.DualityRollColor) === + DualityRollColor.colorful.value + ); + } + prepareDerivedData() { const total = this.total; @@ -92,89 +122,3 @@ export default class DhpDualityRoll extends foundry.abstract.TypeDataModel { }); } } - -//V1.3 -// const fields = foundry.data.fields; -// const diceField = () => new fields.SchemaField({ -// dice: new fields.StringField({}), -// value: new fields.NumberField({ integer: true}), -// }); - -// export default class DhpDualityRoll extends foundry.abstract.TypeDataModel { -// static defineSchema() { - -// return { -// roll: new fields.StringField({}), -// modifiers: new fields.ArrayField(new fields.SchemaField({ -// value: new fields.NumberField({ integer: true }), -// label: new fields.StringField({}), -// title: new fields.StringField({}), -// })), -// hope: diceField(), -// fear: diceField(), -// advantage: diceField(), -// disadvantage: diceField(), -// advantageSelected: new fields.NumberField({ initial: 0 }), -// targets: new fields.ArrayField(new fields.SchemaField({ -// id: new fields.StringField({}), -// name: new fields.StringField({}), -// img: new fields.StringField({}), -// difficulty: new fields.NumberField({ integer: true, nullable: true }), -// evasion: new fields.NumberField({ integer: true }), -// hit: new fields.BooleanField({ initial: false }), -// })), -// damage: new fields.SchemaField({ -// value: new fields.StringField({}), -// type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.damageTypes), integer: false }), -// bonusDamage: new fields.ArrayField(new fields.SchemaField({ -// value: new fields.StringField({}), -// type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.damageTypes), integer: false }), -// initiallySelected: new fields.BooleanField(), -// appliesOn: new fields.StringField({ choices: Object.keys(SYSTEM.EFFECTS.applyLocations) }, { nullable: true, initial: null }), -// description: new fields.StringField({}), -// hopeIncrease: new fields.StringField({ nullable: true }) -// }), { nullable: true, initial: null }) -// }) -// } -// } - -// get total() { -// const modifiers = this.modifiers.reduce((acc, x) => acc+x.value, 0); -// const regular = { -// normal: this.disadvantage.value ? Math.min(this.disadvantage.value, this.hope.value) + this.fear.value + modifiers : this.hope.value + this.fear.value + modifiers, -// alternate: this.advantage.value ? this.advantage.value + this.fear.value + modifiers : null, -// }; - -// const advantageSolve = this.advantageSelected === 0 ? null : { -// normal: this.advantageSelected === 1 ? this.hope.value + this.fear.value + modifiers : this.advantage.value + this.fear.value + modifiers, -// alternate: null, -// }; - -// return advantageSolve ?? regular; -// } - -// get totalLabel() { -// if(this.advantage.value && this.advantageSelected === 0) return game.i18n.localize("DAGGERHEART.Chat.DualityRoll.AdvantageChooseTitle"); - -// const hope = !this.advantage.value || this.advantageSelected === 1 ? this.hope.value : this.advantage.value; -// const label = hope > this.fear.value ? "DAGGERHEART.General.Hope" : this.fear.value > hope ? "DAGGERHEART.General.Fear" : "DAGGERHEART.General.CriticalSuccess"; - -// return game.i18n.localize(label); -// } - -// get dualityDiceStates() { -// return { -// hope: this.hope.value > this.fear.value ? 'hope' : this.fear.value > this.hope.value ? 'fear' : 'critical', -// alternate: this.advantage.value > this.fear.value ? 'hope' : this.fear.value > this.advantage.value ? 'fear' : 'critical', -// } -// } - -// prepareDerivedData(){ -// const total = this.total; -// if(total.alternate) return false; - -// this.targets.forEach(target => { -// target.hit = target.difficulty ? total.normal >= target.difficulty : total.normal >= target.evasion; -// }); -// } -// } diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 2da03417..6db5f18e 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -119,7 +119,10 @@ export default class DhpActor extends Actor { const modifiers = [ { value: modifier.value ? Number.parseInt(modifier.value) : 0, - label: modifier.value >= 0 ? `+${modifier.value}` : `-${modifier.value}`, + label: + modifier.value >= 0 + ? `${modifier.title} +${modifier.value}` + : `${modifier.title} -${modifier.value}`, title: modifier.title } ]; diff --git a/module/enrichers/DualityRollEnricher.mjs b/module/enrichers/DualityRollEnricher.mjs new file mode 100644 index 00000000..93c3a999 --- /dev/null +++ b/module/enrichers/DualityRollEnricher.mjs @@ -0,0 +1,36 @@ +import { abilities } from '../config/actorConfig.mjs'; +import { rollCommandToJSON } from '../helpers/utils.mjs'; + +export function dualityRollEnricher(match, _options) { + const roll = rollCommandToJSON(match[1]); + if (!roll) return match[0]; + + return getDualityMessage(roll); +} + +export function getDualityMessage(roll) { + const attributeLabel = + roll.attribute && abilities[roll.attribute] + ? game.i18n.format('DAGGERHEART.General.Check', { + check: game.i18n.localize(abilities[roll.attribute].label) + }) + : null; + const label = attributeLabel ?? game.i18n.localize('DAGGERHEART.General.Duality'); + + const dualityElement = document.createElement('span'); + dualityElement.innerHTML = ` + + `; + + return dualityElement; +} diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 2b1bc536..1c09970f 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -22,17 +22,6 @@ const getCompendiumOptions = async compendium => { }; export const getWidthOfText = (txt, fontsize, allCaps, bold) => { - // if(getWidthOfText.e === undefined){ - // getWidthOfText.e = document.createElement('span'); - // getWidthOfText.e.style.display = "none"; - // document.body.appendChild(getWidthOfText.e); - // } - // if(getWidthOfText.e.style.fontSize !== fontsize) - // getWidthOfText.e.style.fontSize = fontsize; - // if(getWidthOfText.e.style.fontFamily !== 'Signika, sans-serif') - // getWidthOfText.e.style.fontFamily = 'Signika, sans-serif'; - // getWidthOfText.e.innerText = txt; - // return getWidthOfText.e.offsetWidth; const text = allCaps ? txt.toUpperCase() : txt; if (getWidthOfText.c === undefined) { getWidthOfText.c = document.createElement('canvas'); @@ -82,3 +71,50 @@ export const generateId = (title, length) => { .join(''); return Number.isNumeric(length) ? id.slice(0, length).padEnd(length, '0') : id; }; + +export function rollCommandToJSON(text) { + if (!text) return {}; + + // Match key="quoted string" OR key=unquotedValue + const PAIR_RE = /(\w+)=("(?:[^"\\]|\\.)*"|\S+)/g; + const result = {}; + for (const [, key, raw] of text.matchAll(PAIR_RE)) { + let value; + if (raw.startsWith('"') && raw.endsWith('"')) { + // Strip the surrounding quotes, un-escape any \" sequences + value = raw.slice(1, -1).replace(/\\"/g, '"'); + } else if (/^(true|false)$/i.test(raw)) { + // Boolean + value = raw.toLowerCase() === 'true'; + } else if (!Number.isNaN(Number(raw))) { + // Numeric + value = Number(raw); + } else { + // Fallback to string + value = raw; + } + result[key] = value; + } + return Object.keys(result).length > 0 ? result : null; +} + +export const getCommandTarget = () => { + let target = game.canvas.tokens.controlled.length > 0 ? game.canvas.tokens.controlled[0].actor : null; + if (!game.user.isGM) { + target = game.user.character; + if (!target) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.NoAssignedPlayerCharacter')); + return null; + } + } + if (!target) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.NoSelectedToken')); + return null; + } + if (target.type !== 'pc') { + ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.OnlyUseableByPC')); + return null; + } + + return target; +}; diff --git a/module/ui/chatLog.mjs b/module/ui/chatLog.mjs index ec5e257f..183060b0 100644 --- a/module/ui/chatLog.mjs +++ b/module/ui/chatLog.mjs @@ -14,7 +14,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo } addChatListeners = async (app, html, data) => { - html.querySelectorAll('.roll-damage-button').forEach(element => + html.querySelectorAll('.duality-action').forEach(element => element.addEventListener('click', event => this.onRollDamage(event, data.message)) ); html.querySelectorAll('.target-container').forEach(element => { diff --git a/styles/chat.less b/styles/chat.less index 1494966c..f54de5d2 100644 --- a/styles/chat.less +++ b/styles/chat.less @@ -1,3 +1,303 @@ +.chat-message { + &.duality { + border-color: black; + padding: 8px 0 0 0; + + .message-header { + color: var(--color-light-3); + padding: 0 8px; + } + + .duality-data { + display: flex; + flex-direction: column; + + .duality-title { + color: var(--color-light-1); + text-shadow: 0 0 1px black; + border-bottom: 1px solid; + margin-bottom: 2px; + display: flex; + align-items: end; + justify-content: space-between; + padding: 0 8px; + + .duality-result-value { + border: 1px solid var(--color-dark-5); + padding: 2px; + font-weight: bold; + background: var(--color-dark-1); + margin-bottom: 4px; + font-size: 16px; + } + } + + .duality-modifiers { + display: flex; + gap: 2px; + margin-bottom: 4px; + padding: 0 8px; + + .duality-modifier { + padding: 2px; + border-radius: 6px; + border: 1px solid; + background: var(--color-dark-6); + font-size: 12px; + } + } + + .duality-line { + display: flex; + align-items: end; + justify-content: space-between; + padding: 0 8px; + + &.simple { + padding-right: 0; + } + + .dice-outer-container { + display: flex; + align-items: center; + gap: 4px; + margin-bottom: 4px; + + .dice-container { + display: flex; + flex-direction: column; + gap: 2px; + + .dice-title { + color: var(--color-light-1); + text-shadow: 0 0 1px black; + } + + .dice-inner-container { + display: flex; + align-items: center; + justify-content: center; + position: relative; + + .dice-wrapper { + height: 24px; + width: 24px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + clip-path: polygon( + 50% 0%, + 80% 10%, + 100% 35%, + 100% 70%, + 80% 90%, + 50% 100%, + 20% 90%, + 0% 70%, + 0% 35%, + 20% 10% + ); + + .dice { + height: 26px; + width: 26px; + max-width: unset; + position: absolute; + } + } + + .dice-value { + position: absolute; + font-weight: bold; + font-size: 16px; + } + + &.hope { + .dice-wrapper { + background: black; + + .dice { + filter: brightness(0) saturate(100%) invert(79%) sepia(79%) saturate(333%) + hue-rotate(352deg) brightness(102%) contrast(103%); + } + } + + .dice-value { + color: var(--color-dark-1); + text-shadow: 0 0 4px white; + } + } + + &.fear { + .dice-wrapper { + background: white; + + .dice { + filter: brightness(0) saturate(100%) invert(12%) sepia(88%) saturate(4321%) + hue-rotate(221deg) brightness(92%) contrast(110%); + } + } + + .dice-value { + color: var(--color-light-1); + text-shadow: 0 0 4px black; + } + } + } + } + + .advantage-container { + padding-top: 21px; + + .dice-wrapper { + height: 24px; + width: 24px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + + .dice { + height: 26px; + width: 26px; + max-width: unset; + position: absolute; + } + + .dice-value { + position: absolute; + font-weight: bold; + font-size: 16px; + } + } + + &.advantage { + .dice-wrapper { + .dice { + filter: brightness(0) saturate(100%) invert(18%) sepia(92%) saturate(4133%) + hue-rotate(96deg) brightness(104%) contrast(107%); + } + } + } + + &.disadvantage { + .dice-wrapper { + .dice { + filter: brightness(0) saturate(100%) invert(9%) sepia(78%) saturate(6903%) + hue-rotate(11deg) brightness(93%) contrast(117%); + } + } + } + } + + .duality-modifier { + padding-top: 21px; + color: var(--color-light-1); + text-shadow: 0 0 1px black; + font-size: 16px; + } + } + } + } + + .duality-result { + display: flex; + flex-direction: column; + align-items: end; + justify-content: center; + gap: 2px; + color: var(--color-light-1); + text-shadow: 0 0 1px black; + font-weight: bold; + background: var(--color-dark-1); + padding: 4px; + border-radius: 6px 0 0 0; + } + + .duality-actions { + display: flex; + justify-content: space-between; + + .duality-action { + color: var(--color-light-1); + text-shadow: 0 0 1px black; + font-weight: bold; + background: var(--color-dark-1); + padding: 4px; + border-radius: 0 6px 0 0; + border-color: black; + min-height: unset; + height: 26px; + + &:hover { + background: var(--button-hover-background-color); + } + } + } + + .target-section { + margin: 4px 4px; + border: 2px solid; + + .target-container { + display: flex; + align-items: center; + transition: all 0.2s ease-in-out; + + &:hover { + filter: drop-shadow(0 0 3px @secondaryShadow); + border-color: gold; + } + + &.hit { + background: @hit; + } + + &.miss { + background: @miss; + } + + img { + flex: 0; + width: 22px; + height: 22px; + margin-left: 8px; + align-self: center; + border-color: transparent; + } + + .target-inner-container { + flex: 1; + display: flex; + justify-content: center; + margin-right: @hugeMargin; + font-weight: bold; + font-size: 17px; + } + } + } + + &.hope { + background: linear-gradient(0, @hopeBackgroundEnd 40px, @hopeBackgroundStart); + } + &.fear { + background: linear-gradient(0, @fearBackgroundEnd, @fearBackgroundStart); + } + &.critical { + background: linear-gradient(0, @criticalBackgroundEnd, @criticalBackgroundStart); + } + + .dice-roll { + color: var(--color-dark-1); + + .dice-flavor { + color: var(--color-light-1); + } + } + } +} + .daggerheart.chat { &.downtime { display: flex; @@ -110,6 +410,24 @@ } .dice-total { + &.duality { + &.hope { + border-color: @hope; + border-width: 3px; + background: rgba(@hope, 0.5); + } + &.fear { + border-color: @fear; + border-width: 3px; + background: rgba(@fear, 0.5); + } + &.critical { + border-color: @critical; + border-width: 3px; + background: rgba(@critical, 0.5); + } + } + .dice-total-value { .hope { color: @hope; diff --git a/styles/daggerheart.css b/styles/daggerheart.css index 4f856d8c..0cd5f177 100644 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -1,11 +1,12 @@ -/* General */ -/* Drop Shadows */ -/* Background */ /* Base Value */ /* Margins */ /* Borders */ /* Padding */ /* Inputs */ +/* General */ +/* Drop Shadows */ +/* Background */ +/* Duality */ @import '../node_modules/@yaireo/tagify/dist/tagify.css'; .daggerheart.sheet.class .editor { height: 500px; @@ -1346,6 +1347,234 @@ cursor: pointer; filter: drop-shadow(0 0 3px red); } +.chat-message.duality { + border-color: black; + padding: 8px 0 0 0; +} +.chat-message.duality .message-header { + color: var(--color-light-3); + padding: 0 8px; +} +.chat-message.duality .duality-data { + display: flex; + flex-direction: column; +} +.chat-message.duality .duality-data .duality-title { + color: var(--color-light-1); + text-shadow: 0 0 1px black; + border-bottom: 1px solid; + margin-bottom: 2px; + display: flex; + align-items: end; + justify-content: space-between; + padding: 0 8px; +} +.chat-message.duality .duality-data .duality-title .duality-result-value { + border: 1px solid var(--color-dark-5); + padding: 2px; + font-weight: bold; + background: var(--color-dark-1); + margin-bottom: 4px; + font-size: 16px; +} +.chat-message.duality .duality-data .duality-modifiers { + display: flex; + gap: 2px; + margin-bottom: 4px; + padding: 0 8px; +} +.chat-message.duality .duality-data .duality-modifiers .duality-modifier { + padding: 2px; + border-radius: 6px; + border: 1px solid; + background: var(--color-dark-6); + font-size: 12px; +} +.chat-message.duality .duality-data .duality-line { + display: flex; + align-items: end; + justify-content: space-between; + padding: 0 8px; +} +.chat-message.duality .duality-data .duality-line.simple { + padding-right: 0; +} +.chat-message.duality .duality-data .duality-line .dice-outer-container { + display: flex; + align-items: center; + gap: 4px; + margin-bottom: 4px; +} +.chat-message.duality .duality-data .duality-line .dice-outer-container .dice-container { + display: flex; + flex-direction: column; + gap: 2px; +} +.chat-message.duality .duality-data .duality-line .dice-outer-container .dice-container .dice-title { + color: var(--color-light-1); + text-shadow: 0 0 1px black; +} +.chat-message.duality .duality-data .duality-line .dice-outer-container .dice-container .dice-inner-container { + display: flex; + align-items: center; + justify-content: center; + position: relative; +} +.chat-message.duality .duality-data .duality-line .dice-outer-container .dice-container .dice-inner-container .dice-wrapper { + height: 24px; + width: 24px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + clip-path: polygon(50% 0%, 80% 10%, 100% 35%, 100% 70%, 80% 90%, 50% 100%, 20% 90%, 0% 70%, 0% 35%, 20% 10%); +} +.chat-message.duality .duality-data .duality-line .dice-outer-container .dice-container .dice-inner-container .dice-wrapper .dice { + height: 26px; + width: 26px; + max-width: unset; + position: absolute; +} +.chat-message.duality .duality-data .duality-line .dice-outer-container .dice-container .dice-inner-container .dice-value { + position: absolute; + font-weight: bold; + font-size: 16px; +} +.chat-message.duality .duality-data .duality-line .dice-outer-container .dice-container .dice-inner-container.hope .dice-wrapper { + background: black; +} +.chat-message.duality .duality-data .duality-line .dice-outer-container .dice-container .dice-inner-container.hope .dice-wrapper .dice { + filter: brightness(0) saturate(100%) invert(79%) sepia(79%) saturate(333%) hue-rotate(352deg) brightness(102%) contrast(103%); +} +.chat-message.duality .duality-data .duality-line .dice-outer-container .dice-container .dice-inner-container.hope .dice-value { + color: var(--color-dark-1); + text-shadow: 0 0 4px white; +} +.chat-message.duality .duality-data .duality-line .dice-outer-container .dice-container .dice-inner-container.fear .dice-wrapper { + background: white; +} +.chat-message.duality .duality-data .duality-line .dice-outer-container .dice-container .dice-inner-container.fear .dice-wrapper .dice { + filter: brightness(0) saturate(100%) invert(12%) sepia(88%) saturate(4321%) hue-rotate(221deg) brightness(92%) contrast(110%); +} +.chat-message.duality .duality-data .duality-line .dice-outer-container .dice-container .dice-inner-container.fear .dice-value { + color: var(--color-light-1); + text-shadow: 0 0 4px black; +} +.chat-message.duality .duality-data .duality-line .dice-outer-container .advantage-container { + padding-top: 21px; +} +.chat-message.duality .duality-data .duality-line .dice-outer-container .advantage-container .dice-wrapper { + height: 24px; + width: 24px; + position: relative; + display: flex; + align-items: center; + justify-content: center; +} +.chat-message.duality .duality-data .duality-line .dice-outer-container .advantage-container .dice-wrapper .dice { + height: 26px; + width: 26px; + max-width: unset; + position: absolute; +} +.chat-message.duality .duality-data .duality-line .dice-outer-container .advantage-container .dice-wrapper .dice-value { + position: absolute; + font-weight: bold; + font-size: 16px; +} +.chat-message.duality .duality-data .duality-line .dice-outer-container .advantage-container.advantage .dice-wrapper .dice { + filter: brightness(0) saturate(100%) invert(18%) sepia(92%) saturate(4133%) hue-rotate(96deg) brightness(104%) contrast(107%); +} +.chat-message.duality .duality-data .duality-line .dice-outer-container .advantage-container.disadvantage .dice-wrapper .dice { + filter: brightness(0) saturate(100%) invert(9%) sepia(78%) saturate(6903%) hue-rotate(11deg) brightness(93%) contrast(117%); +} +.chat-message.duality .duality-data .duality-line .dice-outer-container .duality-modifier { + padding-top: 21px; + color: var(--color-light-1); + text-shadow: 0 0 1px black; + font-size: 16px; +} +.chat-message.duality .duality-result { + display: flex; + flex-direction: column; + align-items: end; + justify-content: center; + gap: 2px; + color: var(--color-light-1); + text-shadow: 0 0 1px black; + font-weight: bold; + background: var(--color-dark-1); + padding: 4px; + border-radius: 6px 0 0 0; +} +.chat-message.duality .duality-actions { + display: flex; + justify-content: space-between; +} +.chat-message.duality .duality-actions .duality-action { + color: var(--color-light-1); + text-shadow: 0 0 1px black; + font-weight: bold; + background: var(--color-dark-1); + padding: 4px; + border-radius: 0 6px 0 0; + border-color: black; + min-height: unset; + height: 26px; +} +.chat-message.duality .duality-actions .duality-action:hover { + background: var(--button-hover-background-color); +} +.chat-message.duality .target-section { + margin: 4px 4px; + border: 2px solid; +} +.chat-message.duality .target-section .target-container { + display: flex; + align-items: center; + transition: all 0.2s ease-in-out; +} +.chat-message.duality .target-section .target-container:hover { + filter: drop-shadow(0 0 3px gold); + border-color: gold; +} +.chat-message.duality .target-section .target-container.hit { + background: #008000; +} +.chat-message.duality .target-section .target-container.miss { + background: #ff0000; +} +.chat-message.duality .target-section .target-container img { + flex: 0; + width: 22px; + height: 22px; + margin-left: 8px; + align-self: center; + border-color: transparent; +} +.chat-message.duality .target-section .target-container .target-inner-container { + flex: 1; + display: flex; + justify-content: center; + margin-right: 32px; + font-weight: bold; + font-size: 17px; +} +.chat-message.duality.hope { + background: linear-gradient(0, rgba(165, 42, 42, 0.6) 40px, rgba(0, 0, 0, 0.6)); +} +.chat-message.duality.fear { + background: linear-gradient(0, rgba(0, 0, 255, 0.6), rgba(15, 15, 97, 0.6)); +} +.chat-message.duality.critical { + background: linear-gradient(0, rgba(128, 0, 128, 0.6), rgba(37, 8, 37, 0.6)); +} +.chat-message.duality .dice-roll { + color: var(--color-dark-1); +} +.chat-message.duality .dice-roll .dice-flavor { + color: var(--color-light-1); +} .daggerheart.chat.downtime { display: flex; flex-direction: column; @@ -1391,7 +1620,7 @@ } .daggerheart.chat.roll .dice-tooltip .dice-rolls.duality .roll.die.hope { color: white; - -webkit-text-stroke-color: #008080; + -webkit-text-stroke-color: #ffe760; -webkit-text-stroke-width: 1.5px; font-weight: 400; } @@ -1400,7 +1629,7 @@ } .daggerheart.chat.roll .dice-tooltip .dice-rolls.duality .roll.die.fear { color: white; - -webkit-text-stroke-color: #430070; + -webkit-text-stroke-color: #0032b1; -webkit-text-stroke-width: 1.5px; font-weight: 400; } @@ -1415,7 +1644,7 @@ } .daggerheart.chat.roll .dice-tooltip .dice-rolls.duality .roll.die.advantage { color: white; - -webkit-text-stroke-color: green; + -webkit-text-stroke-color: #008000; -webkit-text-stroke-width: 1.5px; font-weight: 400; } @@ -1431,14 +1660,29 @@ text-align: end; font-weight: bold; } +.daggerheart.chat.roll .dice-total.duality.hope { + border-color: #ffe760; + border-width: 3px; + background: rgba(255, 231, 96, 0.5); +} +.daggerheart.chat.roll .dice-total.duality.fear { + border-color: #0032b1; + border-width: 3px; + background: rgba(0, 50, 177, 0.5); +} +.daggerheart.chat.roll .dice-total.duality.critical { + border-color: #430070; + border-width: 3px; + background: rgba(67, 0, 112, 0.5); +} .daggerheart.chat.roll .dice-total .dice-total-value .hope { - color: #008080; + color: #ffe760; } .daggerheart.chat.roll .dice-total .dice-total-value .fear { - color: #430070; + color: #0032b1; } .daggerheart.chat.roll .dice-total .dice-total-value .critical { - color: #ffd700; + color: #430070; } .daggerheart.chat.roll .dice-total-label { font-size: 12px; diff --git a/styles/daggerheart.less b/styles/daggerheart.less index 53384143..cadae587 100644 --- a/styles/daggerheart.less +++ b/styles/daggerheart.less @@ -1,4 +1,5 @@ @import './variables/variables.less'; +@import './variables/colors.less'; @import './class.less'; @import './pc.less'; @import './ui.less'; diff --git a/styles/variables/colors.less b/styles/variables/colors.less index 19c32885..d189af51 100644 --- a/styles/variables/colors.less +++ b/styles/variables/colors.less @@ -1,8 +1,5 @@ /* General */ -@hope: #008080; -@fear: #430070; -@critical: #ffd700; -@advantage: green; +@advantage: #008000; @disadvantage: #b30000; @miss: rgb(255, 0, 0); @hit: rgb(0, 128, 0); @@ -22,3 +19,16 @@ @secondaryAccent: #708090; @formBackground: #782e22; @hoverBackground: #2f4f4f40; + +/* Duality */ +@hope: #ffe760; +@hopeBackgroundStart: rgba(0, 0, 0, 0.6); +@hopeBackgroundEnd: rgba(165, 42, 42, 0.6); +@fear: #0032b1; +@fearAccent: #2555cd; +@fearBackgroundStart: rgba(15, 15, 97, 0.6); +@fearBackgroundEnd: rgba(0, 0, 255, 0.6); +@critical: #430070; +@criticalAccent: #66159c; +@criticalBackgroundStart: rgba(37, 8, 37, 0.6); +@criticalBackgroundEnd: rgba(128, 0, 128, 0.6); diff --git a/templates/chat/attack-roll.hbs b/templates/chat/attack-roll.hbs index 8a80b103..3daf7485 100644 --- a/templates/chat/attack-roll.hbs +++ b/templates/chat/attack-roll.hbs @@ -1,70 +1,76 @@ -
aa^{ft_5sE9E~Dwlx-9W^{3DOjEMWVSVlfM;_@RlmzbtD^NgHE
zlQw}=EH NF>T69@rg##b?VG-P07eGUzkr~Swm8H_
zzIz3lviEUtQGQ^03~eQ>E<+0k_uTq+9KclO!0n*evl~)ZC%*q15jL7XJd7B37?jmf
zHc>Xn70j02ENYR-oPuj55!f0{Bkzm=*HpBRhDN_AGbCr-BE=&~+LF%YT3%qz`pkut
z!3addQ6^rmiBZ8W+C*ksrN2=|l5Dca5PA_>6wld6a&T(CSVa@A +rQb
zL}uF1_s6qk$Kn%%xQyV2m4JJu2fbo};x?gmkVO=)^L{EpOIgguPyK4$D3@D%=<3G;
zBo-L7i)o-RLXX0g(Yp|hv33xbs^|e88!w#IA(& (*BQ>sPrw&%&A4ZxJ42o!
z$nNmu_IW$cxe897D@spP5{QUqq+HWt)0ZovW45sYE$u%WCl59U_i17v_w_A=nq}0_
z4G_(EpYXS23B~W}23K-VX*6&(`y9s6<^MCo8o%%0F}o*;4v28jZF_B|KX5!k6Kh&?
z#HE(2u7wt(pB_K?S|83PI7Q18g*6)3J&Plf{z0o9?z{Mb^QSy}il!;o-p?;v+NDS9
zxi5@Qe*I(_ABLvgJ@TEE;qGAD6ct>Gmd8s4cpCH63+@X0!Qr-qu~rlG%q!k
z5pE25u0KrHHpen;@CFtvQc6uO@J~EsYoirUy8&$ha(_QLVbKNL^P+caTY>3;?>CJ-%hAy9)+Y^R`NgpHnIBqjh4)J&@*a=PHWd{KBpN#pPVycPK!^Y~z(;RAAq8oRp
zihd8gYuBYl5&E8yBFr?MX1}8S(3b!F^26)uC}Q}s__#27J08pF#NrK_t30v{ALYI}
z-2|)2+4fedOs%Xc%m!qm;6?ca$6vHA7r1KIf2pGWddP;q$ikx
fL&%{rHRegoUvNA;DTqS&J1~Y`1uf8p^M@Mh9
zWr5sSO1C-)<^pZb;v(bKj+aY1*fh4E6alTxFghZa_sDG8QOO-pj(Nf_N0j^HqjpMx
za$&xQGe*q3iJuY@7~|ATEuCr=5dXq3ATtw8h1JM_)2lkmK?EDT!?F+>MES;S&0H;?
z=D>1^uI#@L