diff --git a/daggerheart.mjs b/daggerheart.mjs index 3b1ea3f4..434824ef 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -16,6 +16,7 @@ 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 { DHRoll, DualityRoll, D20Roll, DamageRoll, DualityDie } from './module/applications/roll.mjs'; import { DhMeasuredTemplate } from './module/placeables/_module.mjs'; import { renderDualityButton } from './module/enrichers/DualityRollEnricher.mjs'; import { renderMeasuredTemplate } from './module/enrichers/TemplateEnricher.mjs'; @@ -50,6 +51,15 @@ Hooks.once('init', () => { name: game.i18n.localize(x.name) })); + CONFIG.Dice.daggerheart = { + DualityDie: DualityDie, + DHRoll: DHRoll, + DualityRoll: DualityRoll, + D20Roll: D20Roll, + DamageRoll: DamageRoll + }; + + CONFIG.Dice.rolls = [...CONFIG.Dice.rolls, ...[DHRoll, DualityRoll, D20Roll, DamageRoll]]; CONFIG.MeasuredTemplate.objectClass = DhMeasuredTemplate; CONFIG.Item.documentClass = documents.DhpItem; @@ -308,9 +318,12 @@ const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/views/actionTypes/uuid.hbs', 'systems/daggerheart/templates/views/actionTypes/uses.hbs', 'systems/daggerheart/templates/views/actionTypes/roll.hbs', + 'systems/daggerheart/templates/views/actionTypes/save.hbs', 'systems/daggerheart/templates/views/actionTypes/cost.hbs', 'systems/daggerheart/templates/views/actionTypes/range-target.hbs', 'systems/daggerheart/templates/views/actionTypes/effect.hbs', - 'systems/daggerheart/templates/settings/components/settings-item-line.hbs' + 'systems/daggerheart/templates/settings/components/settings-item-line.hbs', + + 'systems/daggerheart/templates/chat/parts/target-chat.hbs' ]); }; diff --git a/lang/en.json b/lang/en.json index bd962928..4a02ae96 100755 --- a/lang/en.json +++ b/lang/en.json @@ -208,6 +208,12 @@ "Session": "Session", "Shortrest": "Short Rest", "Longrest": "Long Rest" + }, + "Damage": { + "Severe": "Severe", + "Major": "Major", + "Minor": "Minor", + "None": "None" } }, "ActionType": { @@ -618,6 +624,14 @@ "Stress": { "Name": "Stress", "Stress": "STR" + }, + "Hope": { + "Name": "Hope", + "Abbreviation": "HO" + }, + "ArmorStack": { + "Name": "Armor Stack", + "Stress": "AS" } }, "ArmorFeature": { @@ -1005,14 +1019,20 @@ }, "AttackRoll": { "Title": "Attack - {attack}", - "RollDamage": "Roll Damage" + "RollDamage": "Roll Damage", + "RollHealing": "Roll Healing", + "ApplyEffect": "Apply Effects" }, "DamageRoll": { "Title": "Damage - {damage}", "DealDamageToTargets": "Damage Hit Targets", "DealDamage": "Deal Damage" }, + "ApplyEffect": { + "Title": "Apply Effects - {name}" + }, "HealingRoll": { + "Title": "Heal - {healing}", "Heal": "Heal" }, "DeathMove": { @@ -1071,6 +1091,21 @@ "Title": "Ownership Selection - {name}", "Default": "Default Ownership" }, + "DamageReduction": { + "Title": "Damage Reduction", + "ArmorMarks": "Armor Marks", + "UsedMarks": "Used Marks", + "Stress": "Stress", + "ArmorWithStress": "Spend 1 stress to use an extra mark", + "UnncessaryStress": "You don't need to expend stress", + "StressReduction": "Reduce By Stress", + "Notifications": { + "DamageAlreadyNone": "The damage has already been reduced to none", + "NoAvailableArmorMarks": "You have no more available armor marks", + "NotEnoughStress": "You don't have enough stress", + "DamageIgnore": "{character} did not take damage" + } + }, "Sheets": { "TABS": { "features": "Features", @@ -1524,6 +1559,11 @@ "Macro": { "Name": "Macro" } + }, + "Settings": { + "ResultBased": { + "label": "Formula based on Hope/Fear result." + } } }, "RollTypes": { diff --git a/module/applications/chatMessage.mjs b/module/applications/chatMessage.mjs index e7bcd269..1328de57 100644 --- a/module/applications/chatMessage.mjs +++ b/module/applications/chatMessage.mjs @@ -1,23 +1,36 @@ -import { DualityRollColor } from '../data/settings/Appearance.mjs'; -import DHDualityRoll from '../data/chat-message/dualityRoll.mjs'; - export default class DhpChatMessage extends foundry.documents.ChatMessage { async renderHTML() { - if (this.type === 'dualityRoll' || this.type === 'adversaryRoll') { - this.content = await foundry.applications.handlebars.renderTemplate(this.content, this.system); - } + if(this.system.messageTemplate) this.content = await foundry.applications.handlebars.renderTemplate(this.system.messageTemplate, this.system); /* We can change to fully implementing the renderHTML function if needed, instead of augmenting it. */ const html = await super.renderHTML(); + this.applyPermission(html); if (this.type === 'dualityRoll') { html.classList.add('duality'); - const dualityResult = this.system.dualityResult; - if (dualityResult === DHDualityRoll.dualityResult.hope) html.classList.add('hope'); - else if (dualityResult === DHDualityRoll.dualityResult.fear) html.classList.add('fear'); - else html.classList.add('critical'); + switch (this.system.roll.result.duality) { + case 1: + html.classList.add('hope'); + break; + case -1: + html.classList.add('fear'); + break; + default: + html.classList.add('critical'); + break; + } } return html; } + + applyPermission(html) { + const elements = html.querySelectorAll('[data-perm-id]'); + elements.forEach(e => { + const uuid = e.dataset.permId, + document = fromUuidSync(uuid); + e.setAttribute('data-view-perm', document.testUserPermission(game.user, 'OBSERVER')); + e.setAttribute('data-use-perm', document.testUserPermission(game.user, 'OWNER')); + }); + } } diff --git a/module/applications/config/Action.mjs b/module/applications/config/Action.mjs index d8118fa3..6b5f7b41 100644 --- a/module/applications/config/Action.mjs +++ b/module/applications/config/Action.mjs @@ -38,6 +38,8 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { } }; + static CLEAN_ARRAYS = ["damage.parts", "cost", "effects"]; + _getTabs() { const tabs = { base: { active: true, cssClass: '', group: 'primary', id: 'base', icon: null, label: 'Base' }, @@ -60,8 +62,14 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { context.tabs = this._getTabs(); context.config = SYSTEM; if (!!this.action.effects) context.effects = this.action.effects.map(e => this.action.item.effects.get(e._id)); - if (this.action.damage?.hasOwnProperty('includeBase')) context.hasBaseDamage = !!this.action.parent.damage; + if (this.action.damage?.hasOwnProperty('includeBase') && this.action.type === 'attack') + context.hasBaseDamage = !!this.action.parent.damage; context.getRealIndex = this.getRealIndex.bind(this); + context.getEffectDetails = this.getEffectDetails.bind(this); + context.disableOption = this.disableOption.bind(this); + context.isNPC = this.action.actor && this.action.actor.type !== 'character'; + context.hasRoll = this.action.hasRoll; + console.log(context) return context; } @@ -70,27 +78,47 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { this.render(true); } + disableOption(index, options, choices) { + const filtered = foundry.utils.deepClone(options); + Object.keys(filtered).forEach(o => { + if (choices.find((c, idx) => c.type === o && index !== idx)) delete filtered[o]; + }); + return filtered; + } + getRealIndex(index) { const data = this.action.toObject(false); return data.damage.parts.find(d => d.base) ? index - 1 : index; } + getEffectDetails(id) { + return this.action.item.effects.get(id); + } + _prepareSubmitData(event, formData) { const submitData = foundry.utils.expandObject(formData.object); - // this.element.querySelectorAll("fieldset[disabled] :is(input, select)").forEach(input => { - // foundry.utils.setProperty(submitData, input.name, input.value); - // }); + for ( const keyPath of this.constructor.CLEAN_ARRAYS ) { + const data = foundry.utils.getProperty(submitData, keyPath); + if ( data ) foundry.utils.setProperty(submitData, keyPath, Object.values(data)); + } return submitData; } static async updateForm(event, _, formData) { const submitData = this._prepareSubmitData(event, formData), - data = foundry.utils.expandObject(foundry.utils.mergeObject(this.action.toObject(), submitData)), + data = foundry.utils.mergeObject(this.action.toObject(), submitData), + container = foundry.utils.getProperty(this.action.parent, this.action.systemPath); + let newActions; + if (Array.isArray(container)) { newActions = foundry.utils.getProperty(this.action.parent, this.action.systemPath).map(x => x.toObject()); // Find better way - if (!newActions.findSplice(x => x._id === data._id, data)) newActions.push(data); + if (!newActions.findSplice(x => x._id === data._id, data)) newActions.push(data); + } else newActions = data; + const updates = await this.action.parent.parent.update({ [`system.${this.action.systemPath}`]: newActions }); if (!updates) return; - this.action = foundry.utils.getProperty(updates.system, this.action.systemPath)[this.action.index]; + this.action = Array.isArray(container) + ? foundry.utils.getProperty(updates.system, this.action.systemPath)[this.action.index] + : foundry.utils.getProperty(updates.system, this.action.systemPath); this.render(); } @@ -103,6 +131,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { } static removeElement(event) { + event.stopPropagation(); const data = this.action.toObject(), key = event.target.closest('.action-category-data').dataset.key, index = event.target.dataset.index; @@ -132,6 +161,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { data = this.action.toObject(); data.effects.push({ _id: created._id }); this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); + this.action.item.effects.get(created._id).sheet.render(true); } /** @@ -156,5 +186,8 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { this.action.item.deleteEmbeddedDocuments('ActiveEffect', [effectId]); } - static editEffect(event) {} + static editEffect(event) { + const id = event.target.closest('[data-effect-id]')?.dataset?.effectId; + this.action.item.effects.get(id).sheet.render(true); + } } diff --git a/module/applications/contextMenu.mjs b/module/applications/contextMenu.mjs index 68c6fee4..ff171bfe 100644 --- a/module/applications/contextMenu.mjs +++ b/module/applications/contextMenu.mjs @@ -21,4 +21,15 @@ export default class DhContextMenu extends ContextMenu { item?.callback(this.#jQuery ? $(this.target) : this.target, event); this.close(); } + + static triggerContextMenu(event) { + event.preventDefault(); + event.stopPropagation(); + const { clientX, clientY } = event; + const selector = "[data-item-id]"; + const target = event.target.closest(selector) ?? event.currentTarget.closest(selector); + target?.dispatchEvent(new PointerEvent("contextmenu", { + view: window, bubbles: true, cancelable: true, clientX, clientY + })); + } } diff --git a/module/applications/costSelectionDialog.mjs b/module/applications/costSelectionDialog.mjs new file mode 100644 index 00000000..08bfb1d9 --- /dev/null +++ b/module/applications/costSelectionDialog.mjs @@ -0,0 +1,66 @@ +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; + +export default class CostSelectionDialog extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(costs, uses, action, resolve) { + super({}); + this.costs = costs; + this.uses = uses; + this.action = action; + this.resolve = resolve; + } + + static DEFAULT_OPTIONS = { + tag: 'form', + classes: ['daggerheart', 'views', 'damage-selection'], + position: { + width: 400, + height: 'auto' + }, + actions: { + sendCost: this.sendCost + }, + form: { + handler: this.updateForm, + submitOnChange: true, + closeOnSubmit: false + } + }; + + /** @override */ + static PARTS = { + costSelection: { + id: 'costSelection', + template: 'systems/daggerheart/templates/views/costSelection.hbs' + } + }; + + /* -------------------------------------------- */ + + /** @inheritDoc */ + get title() { + return `Cost Options`; + } + + async _prepareContext(_options) { + const updatedCosts = this.action.calcCosts(this.costs), + updatedUses = this.action.calcUses(this.uses); + return { + costs: updatedCosts, + uses: updatedUses, + canUse: this.action.hasCost(updatedCosts) && this.action.hasUses(updatedUses) + }; + } + + static async updateForm(event, _, formData) { + const data = foundry.utils.expandObject(formData.object); + this.costs = foundry.utils.mergeObject(this.costs, data.costs); + this.uses = foundry.utils.mergeObject(this.uses, data.uses); + this.render(true); + } + + static sendCost(event) { + event.preventDefault(); + this.resolve({ costs: this.action.getRealCosts(this.costs), uses: this.uses }); + this.close(); + } +} diff --git a/module/applications/damageReductionDialog.mjs b/module/applications/damageReductionDialog.mjs new file mode 100644 index 00000000..676def9e --- /dev/null +++ b/module/applications/damageReductionDialog.mjs @@ -0,0 +1,220 @@ +import { damageKeyToNumber, getDamageLabel } from '../helpers/utils.mjs'; + +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; + +export default class DamageReductionDialog extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(resolve, reject, actor, damage) { + super({}); + + this.resolve = resolve; + this.reject = reject; + this.actor = actor; + this.damage = damage; + + const maxArmorMarks = Math.min( + actor.system.armorScore - actor.system.armor.system.marks.value, + actor.system.rules.maxArmorMarked.total + ); + + const armor = [...Array(maxArmorMarks).keys()].reduce((acc, _) => { + acc[foundry.utils.randomID()] = { selected: false }; + return acc; + }, {}); + const stress = [...Array(actor.system.rules.maxArmorMarked.stressExtra ?? 0).keys()].reduce((acc, _) => { + acc[foundry.utils.randomID()] = { selected: false }; + return acc; + }, {}); + this.marks = { armor, stress }; + + this.availableStressReductions = Object.keys(actor.system.rules.stressDamageReduction).reduce((acc, key) => { + const dr = actor.system.rules.stressDamageReduction[key]; + if (dr.enabled) { + if (acc === null) acc = {}; + + const damage = damageKeyToNumber(key); + acc[damage] = { + cost: dr.cost, + selected: false, + from: getDamageLabel(damage), + to: getDamageLabel(damage - 1) + }; + } + + return acc; + }, null); + } + + get title() { + return game.i18n.localize('DAGGERHEART.DamageReduction.Title'); + } + + static DEFAULT_OPTIONS = { + tag: 'form', + classes: ['daggerheart', 'views', 'damage-reduction'], + position: { + width: 240, + height: 'auto' + }, + actions: { + setMarks: this.setMarks, + useStressReduction: this.useStressReduction, + takeDamage: this.takeDamage + }, + form: { + handler: this.updateData, + submitOnChange: true, + closeOnSubmit: false + } + }; + + /** @override */ + static PARTS = { + damageSelection: { + id: 'damageReduction', + template: 'systems/daggerheart/templates/views/damageReduction.hbs' + } + }; + + /* -------------------------------------------- */ + + /** @inheritDoc */ + get title() { + return game.i18n.localize('DAGGERHEART.DamageReduction.Title'); + } + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + + const { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage } = + this.getDamageInfo(); + + context.armorScore = this.actor.system.armorScore; + context.armorMarks = currentMarks; + context.basicMarksUsed = selectedArmorMarks.length === this.actor.system.rules.maxArmorMarked.total; + + const stressReductionStress = this.availableStressReductions + ? stressReductions.reduce((acc, red) => acc + red.cost, 0) + : 0; + context.stress = + selectedStressMarks.length > 0 || this.availableStressReductions + ? { + value: + this.actor.system.resources.stress.value + selectedStressMarks.length + stressReductionStress, + maxTotal: this.actor.system.resources.stress.maxTotal + } + : null; + + context.marks = this.marks; + context.availableStressReductions = this.availableStressReductions; + + context.damage = getDamageLabel(this.damage); + context.reducedDamage = currentDamage !== this.damage ? getDamageLabel(currentDamage) : null; + context.currentDamage = context.reducedDamage ?? context.damage; + + return context; + } + + static updateData(event, _, formData) { + const form = foundry.utils.expandObject(formData.object); + this.render(true); + } + + getDamageInfo = () => { + const selectedArmorMarks = Object.values(this.marks.armor).filter(x => x.selected); + const selectedStressMarks = Object.values(this.marks.stress).filter(x => x.selected); + const stressReductions = Object.values(this.availableStressReductions).filter(red => red.selected); + const currentMarks = + this.actor.system.armor.system.marks.value + selectedArmorMarks.length + selectedStressMarks.length; + + const currentDamage = + this.damage - selectedArmorMarks.length - selectedStressMarks.length - stressReductions.length; + + return { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage }; + }; + + static setMarks(_, target) { + const currentMark = this.marks[target.dataset.type][target.dataset.key]; + const { selectedStressMarks, stressReductions, currentMarks, currentDamage } = this.getDamageInfo(); + if (!currentMark.selected && currentDamage === 0) { + ui.notifications.info(game.i18n.localize('DAGGERHEART.DamageReduction.Notifications.DamageAlreadyNone')); + return; + } + + if (!currentMark.selected && currentMarks === this.actor.system.armorScore) { + ui.notifications.info( + game.i18n.localize('DAGGERHEART.DamageReduction.Notifications.NoAvailableArmorMarks') + ); + return; + } + + if (currentMark.selected) { + const currentDamageLabel = getDamageLabel(currentDamage); + for (let reduction of stressReductions) { + if (reduction.selected && reduction.to === currentDamageLabel) { + reduction.selected = false; + } + } + + if (target.dataset.type === 'armor' && selectedStressMarks.length > 0) { + selectedStressMarks.forEach(mark => (mark.selected = false)); + } + } + + currentMark.selected = !currentMark.selected; + this.render(); + } + + static useStressReduction(_, target) { + const damageValue = Number(target.dataset.reduction); + const stressReduction = this.availableStressReductions[damageValue]; + const { currentDamage, selectedStressMarks, stressReductions } = this.getDamageInfo(); + + if (stressReduction.selected) { + stressReduction.selected = false; + + const currentDamageLabel = getDamageLabel(currentDamage); + for (let reduction of stressReductions) { + if (reduction.selected && reduction.to === currentDamageLabel) { + reduction.selected = false; + } + } + + this.render(); + } else { + const stressReductionStress = this.availableStressReductions + ? stressReductions.reduce((acc, red) => acc + red.cost, 0) + : 0; + const currentStress = + this.actor.system.resources.stress.value + selectedStressMarks.length + stressReductionStress; + if (currentStress + stressReduction.cost > this.actor.system.resources.stress.maxTotal) { + ui.notifications.info(game.i18n.localize('DAGGERHEART.DamageReduction.Notifications.NotEnoughStress')); + return; + } + + const reducedDamage = currentDamage !== this.damage ? getDamageLabel(currentDamage) : null; + const currentDamageLabel = reducedDamage ?? getDamageLabel(this.damage); + + if (stressReduction.from !== currentDamageLabel) return; + + stressReduction.selected = true; + this.render(); + } + } + + static async takeDamage() { + const { selectedArmorMarks, selectedStressMarks, stressReductions, currentDamage } = this.getDamageInfo(); + const armorSpent = selectedArmorMarks.length + selectedStressMarks.length; + const stressSpent = selectedStressMarks.length + stressReductions.reduce((acc, red) => acc + red.cost, 0); + + this.resolve({ modifiedDamage: currentDamage, armorSpent, stressSpent }); + await this.close(true); + } + + async close(fromSave) { + if (!fromSave) { + this.reject(); + } + + await super.close({}); + } +} diff --git a/module/applications/roll.mjs b/module/applications/roll.mjs new file mode 100644 index 00000000..b5f53abc --- /dev/null +++ b/module/applications/roll.mjs @@ -0,0 +1,403 @@ +import D20RollDialog from '../dialogs/d20RollDialog.mjs'; +import DamageDialog from '../dialogs/damageDialog.mjs'; + +/* + - Damage & other resources roll + - Close dialog => don't roll +*/ + +export class DHRoll extends Roll { + constructor(formula, data, options) { + super(formula, data, options); + } + + static async build(config = {}, message = {}) { + const roll = await this.buildConfigure(config, message); + if (!roll) return; + await this.buildEvaluate(roll, config, (message = {})); + await this.buildPost(roll, config, (message = {})); + return config; + } + + static async buildConfigure(config = {}, message = {}) { + config.hooks = [...(config.hooks ?? []), '']; + config.dialog ??= {}; + for (const hook of config.hooks) { + if (Hooks.call(`${SYSTEM.id}.preRoll${hook.capitalize()}`, config, message) === false) return null; + } + + this.applyKeybindings(config); + + if (config.dialog.configure !== false) { + // Open Roll Dialog + const DialogClass = config.dialog?.class ?? this.DefaultDialog; + config = await DialogClass.configure(config, message); + if (!config) return; + } + let roll = new this(config.formula, config.data, config); + + for (const hook of config.hooks) { + if (Hooks.call(`${SYSTEM.id}.post${hook.capitalize()}RollConfiguration`, roll, config, message) === false) + return []; + } + + return roll; + } + + static async buildEvaluate(roll, config = {}, message = {}) { + if (config.evaluate !== false) await roll.evaluate(); + this.postEvaluate(roll, config); + } + + static async buildPost(roll, config, message) { + for (const hook of config.hooks) { + if (Hooks.call(`${SYSTEM.id}.postRoll${hook.capitalize()}`, config, message) === false) return null; + } + + // Create Chat Message + if (message.data) { + } else { + const messageData = {}; + config.message = await this.toMessage(roll, config); + } + } + + static async postEvaluate(roll, config = {}) {} + + static async toMessage(roll, config) { + const cls = getDocumentClass('ChatMessage'), + msg = { + type: this.messageType, + user: game.user.id, + sound: config.mute ? null : CONFIG.sounds.dice, + system: config, + rolls: [roll] + }; + return await cls.create(msg); + } + + static applyKeybindings(config) { + config.dialog.configure ??= true; + } +} + +// DHopeDie +// DFearDie +// DualityDie +// D20Die + +export class DualityDie extends foundry.dice.terms.Die { + constructor({ number = 1, faces = 12, ...args } = {}) { + super({ number, faces, ...args }); + } +} + +export class D20Roll extends DHRoll { + constructor(formula, data = {}, options = {}) { + super(formula, data, options); + this.createBaseDice(); + this.configureModifiers(); + + this._formula = this.resetFormula(); + } + + static ADV_MODE = { + NORMAL: 0, + ADVANTAGE: 1, + DISADVANTAGE: -1 + }; + + static messageType = 'adversaryRoll'; + + static CRITICAL_TRESHOLD = 20; + + static DefaultDialog = D20RollDialog; + + get d20() { + if (!(this.terms[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); + return this.terms[0]; + } + + set d20(faces) { + if (!(this.terms[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); + this.terms[0].faces = faces; + } + + get dAdvantage() { + return this.dice[2]; + } + + get isCritical() { + if (!this.d20._evaluated) return; + return this.d20.total >= this.constructor.CRITICAL_TRESHOLD; + } + + get hasAdvantage() { + return this.options.advantage === this.constructor.ADV_MODE.ADVANTAGE; + } + + get hasDisadvantage() { + return this.options.advantage === this.constructor.ADV_MODE.DISADVANTAGE; + } + + static applyKeybindings(config) { + const keys = { + normal: config.event.shiftKey || config.event.altKey || config.event.ctrlKey, + advantage: config.event.altKey, + disadvantage: config.event.ctrlKey + }; + + // Should the roll configuration dialog be displayed? + config.dialog.configure ??= !Object.values(keys).some(k => k); + + // Determine advantage mode + const advantage = config.advantage || keys.advantage; + const disadvantage = config.disadvantage || keys.disadvantage; + if (advantage && !disadvantage) config.advantage = this.ADV_MODE.ADVANTAGE; + else if (!advantage && disadvantage) config.advantage = this.ADV_MODE.DISADVANTAGE; + else config.advantage = this.ADV_MODE.NORMAL; + } + + createBaseDice() { + if (this.terms[0] instanceof foundry.dice.terms.Die) return; + this.terms[0] = new foundry.dice.terms.Die({ faces: 20 }); + } + + applyAdvantage() { + this.d20.modifiers.findSplice(m => ['kh', 'kl'].includes(m)); + if (!this.hasAdvantage && !this.hasAdvantage) this.number = 1; + else { + this.d20.number = 2; + this.d20.modifiers.push(this.hasAdvantage ? 'kh' : 'kl'); + } + } + + // Trait bonus != Adversary + configureModifiers() { + this.applyAdvantage(); + + this.applyBaseBonus(); + + this.options.experiences?.forEach(m => { + if (this.options.data.experiences?.[m]) + this.options.roll.modifiers.push({ + label: this.options.data.experiences[m].description, + value: this.options.data.experiences[m].total + }); + }); + this.options.roll.modifiers?.forEach(m => { + this.terms.push(...this.formatModifier(m.value)); + }); + + if (this.options.extraFormula) + this.terms.push( + new foundry.dice.terms.OperatorTerm({ operator: '+' }), + ...this.constructor.parse(this.options.extraFormula, this.getRollData()) + ); + + // this.resetFormula(); + } + + applyBaseBonus() { + if (this.options.type === 'attack') + this.terms.push(...this.formatModifier(this.options.data.attack.roll.bonus)); + } + + static async postEvaluate(roll, config = {}) { + if (config.targets?.length) { + config.targets.forEach(target => { + const difficulty = config.roll.difficulty ?? target.difficulty ?? target.evasion; + target.hit = this.isCritical || roll.total >= difficulty; + }); + } else if (config.roll.difficulty) config.roll.success = roll.isCritical || roll.total >= config.roll.difficulty; + config.roll.total = roll.total; + config.roll.formula = roll.formula; + config.roll.advantage = { + type: config.advantage, + dice: roll.dAdvantage?.denomination, + value: roll.dAdvantage?.total + }; + config.roll.modifierTotal = config.roll.modifiers.reduce((a, c) => a + c.value, 0); + config.roll.dice = []; + roll.dice.forEach(d => { + config.roll.dice.push({ + dice: d.denomination, + total: d.total, + formula: d.formula, + results: d.results + }); + }); + } + + getRollData() { + return this.options.data(); + } + + formatModifier(modifier) { + const numTerm = modifier < 0 ? '-' : '+'; + return [ + new foundry.dice.terms.OperatorTerm({ operator: numTerm }), + new foundry.dice.terms.NumericTerm({ number: Math.abs(modifier) }) + ]; + } + + resetFormula() { + return (this._formula = this.constructor.getFormula(this.terms)); + } +} + +export class DualityRoll extends D20Roll { + constructor(formula, data = {}, options = {}) { + super(formula, data, options); + } + + static messageType = 'dualityRoll'; + + static DefaultDialog = D20RollDialog; + + get dHope() { + // if ( !(this.terms[0] instanceof foundry.dice.terms.Die) ) return; + if (!(this.dice[0] instanceof CONFIG.Dice.daggerheart.DualityDie)) this.createBaseDice(); + return this.dice[0]; + // return this.#hopeDice; + } + + set dHope(faces) { + if (!(this.dice[0] instanceof CONFIG.Dice.daggerheart.DualityDie)) this.createBaseDice(); + this.terms[0].faces = faces; + // this.#hopeDice = `d${face}`; + } + + get dFear() { + // if ( !(this.terms[1] instanceof foundry.dice.terms.Die) ) return; + if (!(this.dice[1] instanceof CONFIG.Dice.daggerheart.DualityDie)) this.createBaseDice(); + return this.dice[1]; + // return this.#fearDice; + } + + set dFear(faces) { + if (!(this.dice[1] instanceof CONFIG.Dice.daggerheart.DualityDie)) this.createBaseDice(); + this.dice[1].faces = faces; + // this.#fearDice = `d${face}`; + } + + get dAdvantage() { + return this.dice[2]; + } + + get isCritical() { + if (!this.dHope._evaluated || !this.dFear._evaluated) return; + return this.dHope.total === this.dFear.total; + } + + get withHope() { + if (!this._evaluated) return; + return this.dHope.total > this.dFear.total; + } + + get withFear() { + if (!this._evaluated) return; + return this.dHope.total < this.dFear.total; + } + + get hasBarRally() { + return null; + } + + get totalLabel() { + const label = this.withHope + ? 'DAGGERHEART.General.Hope' + : this.withFear + ? 'DAGGERHEART.General.Fear' + : 'DAGGERHEART.General.CriticalSuccess'; + + return game.i18n.localize(label); + } + + createBaseDice() { + if ( + this.dice[0] instanceof CONFIG.Dice.daggerheart.DualityDie && + this.dice[1] instanceof CONFIG.Dice.daggerheart.DualityDie + ) + return; + if (!(this.dice[0] instanceof CONFIG.Dice.daggerheart.DualityDie)) + this.terms[0] = new CONFIG.Dice.daggerheart.DualityDie(); + this.terms[1] = new foundry.dice.terms.OperatorTerm({ operator: '+' }); + if (!(this.dice[2] instanceof CONFIG.Dice.daggerheart.DualityDie)) + this.terms[2] = new CONFIG.Dice.daggerheart.DualityDie(); + } + + applyAdvantage() { + const dieFaces = 6, + bardRallyFaces = this.hasBarRally, + advDie = new foundry.dice.terms.Die({ faces: dieFaces }); + if (this.hasAdvantage || this.hasDisadvantage || bardRallyFaces) + this.terms.push(new foundry.dice.terms.OperatorTerm({ operator: '+' })); + if (bardRallyFaces) { + const rallyDie = new foundry.dice.terms.Die({ faces: bardRallyFaces }); + if (this.hasAdvantage) { + this.terms.push( + new foundry.dice.terms.PoolTerm({ + terms: [advDie.formula, rallyDie.formula], + modifiers: ['kh'] + }) + ); + } else if (this.hasDisadvantage) { + this.terms.push(advDie, new foundry.dice.terms.OperatorTerm({ operator: '+' }), rallyDie); + } + } else if (this.hasAdvantage || this.hasDisadvantage) this.terms.push(advDie); + } + + applyBaseBonus() { + if (!this.options.roll.modifiers) this.options.roll.modifiers = []; + if (this.options.roll?.trait) + this.options.roll.modifiers.push({ + label: `DAGGERHEART.Abilities.${this.options.roll.trait}.name`, + value: this.options.data.traits[this.options.roll.trait].total + }); + } + + static async postEvaluate(roll, config = {}) { + super.postEvaluate(roll, config); + config.roll.hope = { + dice: roll.dHope.denomination, + value: roll.dHope.total + }; + config.roll.fear = { + dice: roll.dFear.denomination, + value: roll.dFear.total + }; + config.roll.result = { + duality: roll.withHope ? 1 : roll.withFear ? -1 : 0, + total: roll.dHope.total + roll.dFear.total, + label: roll.totalLabel + }; + } +} + +export class DamageRoll extends DHRoll { + constructor(formula, data = {}, options = {}) { + super(formula, data, options); + } + + static messageType = 'damageRoll'; + + static DefaultDialog = DamageDialog; + + static async postEvaluate(roll, config = {}) { + config.roll = { + total: roll.total, + formula: roll.formula, + type: config.type + }; + config.roll.dice = []; + roll.dice.forEach(d => { + config.roll.dice.push({ + dice: d.denomination, + total: d.total, + formula: d.formula, + results: d.results + }); + }); + } +} diff --git a/module/applications/rollSelectionDialog.mjs b/module/applications/rollSelectionDialog.mjs index eca8c361..0a1972aa 100644 --- a/module/applications/rollSelectionDialog.mjs +++ b/module/applications/rollSelectionDialog.mjs @@ -1,10 +1,12 @@ const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; export default class RollSelectionDialog extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(experiences, hopeResource, resolve) { + constructor(experiences, costs, action, resolve) { super({}, {}); - + this.experiences = experiences; + this.costs = costs; + this.action = action; this.resolve = resolve; this.isNpc; this.selectedExperiences = []; @@ -15,8 +17,7 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl ], hope: ['d12'], fear: ['d12'], - advantage: null, - hopeResource: hopeResource + advantage: null }; } @@ -42,6 +43,10 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl /** @override */ static PARTS = { + costSelection: { + id: 'costSelection', + template: 'systems/daggerheart/templates/views/costSelection.hbs' + }, damageSelection: { id: 'damageSelection', template: 'systems/daggerheart/templates/views/rollSelection.hbs' @@ -60,15 +65,19 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl context.fear = this.data.fear; context.advantage = this.data.advantage; context.experiences = Object.keys(this.experiences).map(id => ({ id, ...this.experiences[id] })); - context.hopeResource = this.data.hopeResource + 1; + if(this.costs?.length) { + const updatedCosts = this.action.calcCosts(this.costs); + context.costs = updatedCosts + context.canRoll = this.action.getRealCosts(updatedCosts)?.hasCost; + } else context.canRoll = true; return context; } static updateSelection(event, _, formData) { const { ...rest } = foundry.utils.expandObject(formData.object); - this.data = foundry.utils.mergeObject(this.data, rest); + this.costs = foundry.utils.mergeObject(this.costs, rest.costs); this.render(); } @@ -90,10 +99,10 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl static async finish() { const { diceOptions, ...rest } = this.data; - this.resolve({ ...rest, - experiences: this.selectedExperiences.map(x => ({ id: x, ...this.experiences[x] })) + experiences: this.selectedExperiences.map(x => ({ id: x, ...this.experiences[x] })), + costs: this.action.getRealCosts(this.costs) }); this.close(); } diff --git a/module/applications/sheets/adversary.mjs b/module/applications/sheets/adversary.mjs index 234d49b9..26791298 100644 --- a/module/applications/sheets/adversary.mjs +++ b/module/applications/sheets/adversary.mjs @@ -1,3 +1,4 @@ +import DHActionConfig from '../config/Action.mjs'; import DaggerheartSheet from './daggerheart-sheet.mjs'; const { ActorSheetV2 } = foundry.applications.sheets; @@ -9,6 +10,7 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) { actions: { reactionRoll: this.reactionRoll, attackRoll: this.attackRoll, + attackConfigure: this.attackConfigure, addExperience: this.addExperience, removeExperience: this.removeExperience, toggleHP: this.toggleHP, @@ -51,7 +53,10 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) { const context = await super._prepareContext(_options); context.document = this.document; context.tabs = super._getTabs(this.constructor.TABS); - + context.systemFields.attack.fields = this.document.system.attack.schema.fields; + context.getEffectDetails = this.getEffectDetails.bind(this); + context.isNPC = true; + console.log(context) return context; } @@ -77,26 +82,16 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) { this.actor.diceRoll(config); } + getEffectDetails(id) { + return {}; + } + static async attackRoll(event) { - const { modifier, damage, name: attackName } = this.actor.system.attack, - config = { - event: event, - title: attackName, - roll: { - modifier: modifier, - type: 'action' - }, - chatMessage: { - type: 'adversaryRoll', - template: 'systems/daggerheart/templates/chat/adversary-attack-roll.hbs' - }, - damage: { - value: damage.value, - type: damage.type - }, - checkTarget: true - }; - this.actor.diceRoll(config); + this.actor.system.attack.use(event); + } + + static async attackConfigure(event) { + await new DHActionConfig(this.document.system.attack).render(true); } static async addExperience() { diff --git a/module/applications/sheets/api/base-item.mjs b/module/applications/sheets/api/base-item.mjs index 5528f8fc..f6607c44 100644 --- a/module/applications/sheets/api/base-item.mjs +++ b/module/applications/sheets/api/base-item.mjs @@ -139,9 +139,10 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { */ static async #removeAction(event, button) { event.stopPropagation(); + const actionIndex = button.closest('[data-index]').dataset.index; await this.document.update({ 'system.actions': this.document.system.actions.filter( - (_, index) => index !== Number.parseInt(button.dataset.index) + (_, index) => index !== Number.parseInt(actionIndex) ) }); } diff --git a/module/applications/sheets/character.mjs b/module/applications/sheets/character.mjs index 468d9379..a114e1c1 100644 --- a/module/applications/sheets/character.mjs +++ b/module/applications/sheets/character.mjs @@ -26,8 +26,8 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { toggleHope: this.toggleHope, toggleGold: this.toggleGold, toggleLoadoutView: this.toggleLoadoutView, + attackRoll: this.attackRoll, useDomainCard: this.useDomainCard, - removeCard: this.removeDomainCard, selectClass: this.selectClass, selectSubclass: this.selectSubclass, selectAncestry: this.selectAncestry, @@ -49,7 +49,8 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { toggleEquipItem: this.toggleEquipItem, toggleVault: this.toggleVault, levelManagement: this.levelManagement, - editImage: this._onEditImage + editImage: this._onEditImage, + triggerContextMenu: this.triggerContextMenu }, window: { resizable: true @@ -222,14 +223,18 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { useItem: { name: 'DAGGERHEART.Sheets.PC.ContextMenu.UseItem', icon: '', - callback: (element, event) => this.constructor.useItem.bind(this)(event, element) + condition: el => { + const item = this.getItem(el); + return !['class', 'subclass'].includes(item.type); + }, + callback: (button, event) => this.constructor.useItem.bind(this)(event, button) }, equip: { name: 'DAGGERHEART.Sheets.PC.ContextMenu.Equip', icon: '', condition: el => { - const item = foundry.utils.fromUuidSync(el.dataset.uuid); - return !item.system.equipped; + const item = this.getItem(el); + return ['weapon', 'armor'].includes(item.type) && !item.system.equipped; }, callback: this.constructor.toggleEquipItem.bind(this) }, @@ -237,11 +242,34 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { name: 'DAGGERHEART.Sheets.PC.ContextMenu.Unequip', icon: '', condition: el => { - const item = foundry.utils.fromUuidSync(el.dataset.uuid); - return item.system.equipped; + const item = this.getItem(el); + return ['weapon', 'armor'].includes(item.type) && item.system.equipped; }, callback: this.constructor.toggleEquipItem.bind(this) }, + sendToLoadout: { + name: 'DAGGERHEART.Sheets.PC.ContextMenu.ToLoadout', + icon: '', + condition: el => { + const item = this.getItem(el); + return ['domainCard'].includes(item.type) && item.system.inVault; + }, + callback: this.constructor.toggleVault.bind(this) + }, + sendToVault: { + name: 'DAGGERHEART.Sheets.PC.ContextMenu.ToVault', + icon: '', + condition: el => { + const item = this.getItem(el); + return ['domainCard'].includes(item.type) && !item.system.inVault; + }, + callback: this.constructor.toggleVault.bind(this) + }, + sendToChat: { + name: 'DAGGERHEART.Sheets.PC.ContextMenu.SendToChat', + icon: '', + callback: this.constructor.toChat.bind(this) + }, edit: { name: 'DAGGERHEART.Sheets.PC.ContextMenu.Edit', icon: '', @@ -251,66 +279,12 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { name: 'DAGGERHEART.Sheets.PC.ContextMenu.Delete', icon: '', callback: this.constructor.deleteItem.bind(this) - }, - sendToLoadout: { - name: 'DAGGERHEART.Sheets.PC.ContextMenu.ToLoadout', - icon: '', - condition: el => { - const item = foundry.utils.fromUuidSync(el.dataset.uuid); - return item.system.inVault; - }, - callback: this.constructor.toggleVault.bind(this) - }, - sendToVault: { - name: 'DAGGERHEART.Sheets.PC.ContextMenu.ToVault', - icon: '', - condition: el => { - const item = foundry.utils.fromUuidSync(el.dataset.uuid); - return !item.system.inVault; - }, - callback: this.constructor.toggleVault.bind(this) - }, - sendToChat: { - name: 'DAGGERHEART.Sheets.PC.ContextMenu.SendToChat', - icon: '', - callback: this.constructor.toChat.bind(this) } }; - const getMenuOptions = type => () => { - let menuItems = ['class', 'subclass'].includes(type) ? [] : [allOptions.useItem]; - switch (type) { - case 'weapon': - case 'armor': - menuItems.push(...[allOptions.equip, allOptions.unequip]); - break; - case 'domainCard': - menuItems.push(...[allOptions.sendToLoadout, allOptions.sendToVault]); - break; - } - menuItems.push(...[allOptions.sendToChat, allOptions.edit, allOptions.delete]); - - return menuItems; - }; - - const menuConfigs = [ - 'armor', - 'weapon', - 'miscellaneous', - 'consumable', - 'domainCard', - 'miscellaneous', - 'ancestry', - 'community', - 'class', - 'subclass' - ]; - menuConfigs.forEach(type => { - this._createContextMenu(getMenuOptions(type), `.${type}-context-menu`, { - eventName: 'click', - parentClassHooks: false, - fixed: true - }); + this._createContextMenu(() => Object.values(allOptions), `[data-item-id]`, { + parentClassHooks: false, + fixed: true }); } @@ -318,10 +292,16 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { super._attachPartListeners(partId, htmlElement, options); htmlElement.querySelector('.level-value')?.addEventListener('change', this.onLevelChange.bind(this)); - // To Remove when ContextMenu Handler is made - htmlElement - .querySelectorAll('[data-item-id]') - .forEach(element => element.addEventListener('contextmenu', this.editItem.bind(this))); + } + + getItem(element) { + const itemId = (element.target ?? element).closest('[data-item-id]').dataset.itemId, + item = this.document.items.get(itemId); + return item; + } + + static triggerContextMenu(event, button) { + return CONFIG.ux.ContextMenu.triggerContextMenu(event); } static _onEditImage() { @@ -391,7 +371,8 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { loadout: { top: loadout.slice(0, Math.min(2, nrLoadoutCards)), bottom: nrLoadoutCards > 2 ? loadout.slice(2, Math.min(5, nrLoadoutCards)) : [], - nrTotal: nrLoadoutCards + nrTotal: nrLoadoutCards, + listView: game.user.getFlag(SYSTEM.id, SYSTEM.FLAGS.displayDomainCardsAsList) }, vault: vault.map(x => ({ ...x, @@ -467,51 +448,12 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { const abilityLabel = game.i18n.localize(abilities[button.dataset.attribute].label); const config = { event: event, - title: game.i18n.format('DAGGERHEART.Chat.DualityRoll.AbilityCheckTitle', { - ability: abilityLabel - }), + title: game.i18n.format('DAGGERHEART.Chat.DualityRoll.AbilityCheckTitle', { ability: abilityLabel }), roll: { - label: abilityLabel, - modifier: button.dataset.value - }, - chatMessage: { - template: 'systems/daggerheart/templates/chat/duality-roll.hbs' + trait: button.dataset.attribute } }; this.document.diceRoll(config); - - // Delete when new roll logic test done - /* const { roll, hope, fear, advantage, disadvantage, modifiers } = await this.document.dualityRoll( - { title: game.i18n.localize(abilities[button.dataset.attribute].label), value: button.dataset.value }, - event.shiftKey - ); - - const cls = getDocumentClass('ChatMessage'); - - const systemContent = new DHDualityRoll({ - title: game.i18n.format('DAGGERHEART.Chat.DualityRoll.AbilityCheckTitle', { - ability: game.i18n.localize(abilities[button.dataset.attribute].label) - }), - origin: this.document.id, - roll: roll._formula, - modifiers: modifiers, - hope: hope, - fear: fear, - advantage: advantage, - disadvantage: disadvantage - }); - - await cls.create({ - type: 'dualityRoll', - sound: CONFIG.sounds.dice, - system: systemContent, - user: game.user.id, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/duality-roll.hbs', - systemContent - ), - rolls: [roll] - }); */ } static async toggleMarks(_, button) { @@ -553,6 +495,22 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { this.render(); } + static async toggleLoadoutView(_, button) { + const newAbilityView = !(button.dataset.value === 'true'); + await game.user.setFlag(SYSTEM.id, SYSTEM.FLAGS.displayDomainCardsAsList, newAbilityView); + this.render(); + } + + static async attackRoll(event, button) { + const weapon = await fromUuid(button.dataset.weapon); + if (!weapon) return; + + const wasUsed = await weapon.use(event); + if (wasUsed) { + Hooks.callAll(SYSTEM.HOOKS.characterAttack, {}); + } + } + static levelManagement() { if (this.document.system.needsCharacterSetup) { this.characterSetup(); @@ -574,8 +532,9 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { new DhlevelUp(this.document).render(true); } - static async useDomainCard(_, button) { - const card = this.document.items.find(x => x.uuid === button.dataset.key); + static async useDomainCard(event, button) { + const card = this.getItem(event); + if (!card) return; const cls = getDocumentClass('ChatMessage'); const systemData = { @@ -599,13 +558,6 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { cls.create(msg.toObject()); } - static async removeDomainCard(_, button) { - if (button.dataset.type === 'domainCard') { - const card = this.document.items.find(x => x.uuid === button.dataset.key); - await card.delete(); - } - } - static async selectClass() { (await game.packs.get('daggerheart.classes'))?.render(true); } @@ -641,23 +593,22 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { } static async useItem(event, button) { - const id = button.closest('a').id, - item = this.document.items.get(id); + const item = this.getItem(button); + if (!item) return; const wasUsed = await item.use(event); if (wasUsed && item.type === 'weapon') { Hooks.callAll(SYSTEM.HOOKS.characterAttack, {}); } } - static async viewObject(element, button) { - const object = await fromUuid((button ?? element).dataset.uuid); - - object.sheet.render(true); + static async viewObject(event, button) { + const item = this.getItem(event); + if (!item) return; + item.sheet.render(true); } editItem(event) { - const uuid = event.target.closest('[data-item-id]').dataset.itemId, - item = this.document.items.find(i => i.uuid === uuid); + const item = this.getItem(event); if (!item) return; if (item.sheet.editMode) item.sheet.editMode = false; @@ -701,29 +652,26 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { } } - 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 onLevelChange(event) { await this.document.updateLevel(Number(event.currentTarget.value)); this.render(); } - static async deleteItem(element, button) { - const item = await fromUuid((button ?? element).closest('a').dataset.uuid); + static async deleteItem(event, button) { + const item = this.getItem(event); + if (!item) return; await item.delete(); } static async setItemQuantity(button, value) { - const item = await fromUuid($(button).closest('[data-item-id]')[0].dataset.itemId); + const item = this.getItem(button); + if (!item) return; await item.update({ 'system.quantity': Math.max(item.system.quantity + value, 1) }); } - static async useFeature(_, button) { - const item = await fromUuid(button.dataset.id); + static async useFeature(event, button) { + const item = this.getItem(event); + if (!item) return; const cls = getDocumentClass('ChatMessage'); const systemData = { @@ -747,13 +695,15 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { cls.create(msg.toObject()); } - static async toChat(element, button) { + static async toChat(event, button) { if (button?.dataset?.type === 'experience') { const experience = this.document.system.experiences[button.dataset.uuid]; const cls = getDocumentClass('ChatMessage'); const systemData = { name: game.i18n.localize('DAGGERHEART.General.Experience.Single'), - description: `${experience.description} ${experience.total < 0 ? experience.total : `+${experience.total}`}` + description: `${experience.description} ${ + experience.total < 0 ? experience.total : `+${experience.total}` + }` }; const msg = new cls({ type: 'abilityUse', @@ -767,7 +717,8 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { cls.create(msg.toObject()); } else { - const item = await fromUuid((button ?? element).dataset.uuid); + const item = this.getItem(event); + if (!item) return; item.toChat(this.document.id); } } @@ -778,7 +729,9 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { ? this.document.system.multiclass.subclass : this.document.system.class.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 title = `${item.name} - ${game.i18n.localize( + `DAGGERHEART.Sheets.PC.DomainCard.${capitalize(button.dataset.key)}Title` + )}`; const cls = getDocumentClass('ChatMessage'); const systemData = { @@ -824,9 +777,9 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { cls.create(msg.toObject()); } - static async toggleEquipItem(element, button) { - const id = (button ?? element).closest('a').id; - const item = this.document.items.get(id); + static async toggleEquipItem(event, button) { + const item = this.getItem(event); + if (!item) return; if (item.system.equipped) { await item.update({ 'system.equipped': false }); return; @@ -850,9 +803,9 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { this.render(); } - static async toggleVault(element, button) { - const id = (button ?? element).closest('a').id; - const item = this.document.items.get(id); + static async toggleVault(event, button) { + const item = this.getItem(event); + if (!item) return; await item.update({ 'system.inVault': !item.system.inVault }); } diff --git a/module/applications/sheets/environment.mjs b/module/applications/sheets/environment.mjs index 7c59fd55..fca33f4d 100644 --- a/module/applications/sheets/environment.mjs +++ b/module/applications/sheets/environment.mjs @@ -53,6 +53,7 @@ export default class DhpEnvironment extends DaggerheartSheet(ActorSheetV2) { const context = await super._prepareContext(_options); context.document = this.document; context.tabs = super._getTabs(this.constructor.TABS); + context.getEffectDetails = this.getEffectDetails.bind(this); return context; } @@ -62,6 +63,10 @@ export default class DhpEnvironment extends DaggerheartSheet(ActorSheetV2) { this.render(); } + getEffectDetails(id) { + return {}; + } + static async addAdversary() { await this.document.update({ [`system.potentialAdversaries.${foundry.utils.randomID()}.label`]: game.i18n.localize( diff --git a/module/config/actionConfig.mjs b/module/config/actionConfig.mjs index 4db96a64..a0af78eb 100644 --- a/module/config/actionConfig.mjs +++ b/module/config/actionConfig.mjs @@ -4,21 +4,21 @@ export const actionTypes = { name: 'DAGGERHEART.Actions.Types.Attack.Name', icon: 'fa-swords' }, - spellcast: { - id: 'spellcast', - name: 'DAGGERHEART.Actions.Types.Spellcast.Name', - icon: 'fa-book-sparkles' - }, + // spellcast: { + // id: 'spellcast', + // name: 'DAGGERHEART.Actions.Types.Spellcast.Name', + // icon: 'fa-book-sparkles' + // }, healing: { id: 'healing', name: 'DAGGERHEART.Actions.Types.Healing.Name', icon: 'fa-kit-medical' }, - resource: { - id: 'resource', - name: 'DAGGERHEART.Actions.Types.Resource.Name', - icon: 'fa-honey-pot' - }, + // resource: { + // id: 'resource', + // name: 'DAGGERHEART.Actions.Types.Resource.Name', + // icon: 'fa-honey-pot' + // }, damage: { id: 'damage', name: 'DAGGERHEART.Actions.Types.Damage.Name', @@ -46,8 +46,34 @@ export const targetTypes = { id: 'self', label: 'Self' }, - other: { - id: 'other', - label: 'Other' + friendly: { + id: 'friendly', + label: 'Friendly' + }, + hostile: { + id: 'hostile', + label: 'Hostile' + }, + any: { + id: 'any', + label: 'Any' } }; + +export const damageOnSave = { + none: { + id: 'none', + label: 'None', + mod: 0 + }, + half: { + id: 'half', + label: 'Half Damage', + mod: 0.5 + }, + full: { + id: 'full', + label: 'Full damage', + mod: 1 + } +} diff --git a/module/config/flagsConfig.mjs b/module/config/flagsConfig.mjs new file mode 100644 index 00000000..b06a36e1 --- /dev/null +++ b/module/config/flagsConfig.mjs @@ -0,0 +1 @@ +export const displayDomainCardsAsList = 'displayDomainCardsAsList'; diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index b1d72435..3e1ef9d7 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -68,8 +68,8 @@ export const damageTypes = { }; export const healingTypes = { - health: { - id: 'health', + hitPoints: { + id: 'hitPoints', label: 'DAGGERHEART.HealingType.HitPoints.Name', abbreviation: 'DAGGERHEART.HealingType.HitPoints.Abbreviation' }, @@ -77,6 +77,16 @@ export const healingTypes = { id: 'stress', label: 'DAGGERHEART.HealingType.Stress.Name', abbreviation: 'DAGGERHEART.HealingType.Stress.Abbreviation' + }, + hope: { + id: 'hope', + label: 'DAGGERHEART.HealingType.Hope.Name', + abbreviation: 'DAGGERHEART.HealingType.Hope.Abbreviation' + }, + armorStack: { + id: 'armorStack', + label: 'DAGGERHEART.HealingType.ArmorStack.Name', + abbreviation: 'DAGGERHEART.HealingType.ArmorStack.Abbreviation' } }; @@ -248,13 +258,15 @@ export const diceTypes = { d4: 'd4', d6: 'd6', d8: 'd8', + d10: 'd10', d12: 'd12', d20: 'd20' }; export const multiplierTypes = { proficiency: 'Proficiency', - spellcast: 'Spellcast' + spellcast: 'Spellcast', + flat: 'Flat' }; export const getDiceSoNicePresets = () => { @@ -318,7 +330,35 @@ export const abilityCosts = { }, stress: { id: 'stress', - label: 'Stress' + label: 'DAGGERHEART.HealingType.Stress.Name' + }, + armor: { + id: 'armor', + label: 'Armor Stack' + }, + hp: { + id: 'hp', + label: 'DAGGERHEART.HealingType.HitPoints.Name' + }, + prayer: { + id: 'prayer', + label: 'Prayer Dice' + }, + favor: { + id: 'favor', + label: 'Favor Points' + }, + slayer: { + id: 'slayer', + label: 'Slayer Dice' + }, + tide: { + id: 'tide', + label: 'Tide' + }, + chaos: { + id: 'chaos', + label: 'Chaos' } }; diff --git a/module/config/system.mjs b/module/config/system.mjs index 41d67154..6ad0e689 100644 --- a/module/config/system.mjs +++ b/module/config/system.mjs @@ -6,6 +6,7 @@ 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'; +import * as FLAGS from './flagsConfig.mjs'; export const SYSTEM_ID = 'daggerheart'; @@ -18,5 +19,6 @@ export const SYSTEM = { SETTINGS, HOOKS, EFFECTS, - ACTIONS + ACTIONS, + FLAGS }; diff --git a/module/data/action/_module.mjs b/module/data/action/_module.mjs index c9088886..23d4e3c1 100644 --- a/module/data/action/_module.mjs +++ b/module/data/action/_module.mjs @@ -5,16 +5,16 @@ import { DHEffectAction, DHHealingAction, DHMacroAction, - DHResourceAction, - DHSpellCastAction, + // DHResourceAction, + // DHSpellCastAction, DHSummonAction } from './action.mjs'; export const actionsTypes = { base: DHBaseAction, attack: DHAttackAction, - spellcast: DHSpellCastAction, - resource: DHResourceAction, + // spellcast: DHSpellCastAction, + // resource: DHResourceAction, damage: DHDamageAction, healing: DHHealingAction, summon: DHSummonAction, diff --git a/module/data/action/action.mjs b/module/data/action/action.mjs index 16f74945..3beb92a9 100644 --- a/module/data/action/action.mjs +++ b/module/data/action/action.mjs @@ -1,64 +1,38 @@ -import { abilities } from '../../config/actorConfig.mjs'; +import CostSelectionDialog from '../../applications/costSelectionDialog.mjs'; import { DHActionDiceData, DHDamageData, DHDamageField } from './actionDice.mjs'; - -export default class DHAction extends foundry.abstract.DataModel { - static defineSchema() { - const fields = foundry.data.fields; - return { - id: new fields.DocumentIdField(), - name: new fields.StringField({ initial: 'New Action' }), - damage: new fields.SchemaField({ - type: new fields.StringField({ choices: SYSTEM.GENERAL.damageTypes, nullable: true, initial: null }), - value: new fields.StringField({}) - }), - healing: new fields.SchemaField({ - type: new fields.StringField({ choices: SYSTEM.GENERAL.healingTypes, nullable: true, initial: null }), - value: new fields.StringField() - }), - conditions: new fields.ArrayField( - new fields.SchemaField({ - name: new fields.StringField(), - icon: new fields.StringField(), - description: new fields.StringField() - }) - ), - cost: new fields.SchemaField({ - type: new fields.StringField({ choices: SYSTEM.GENERAL.abilityCosts, nullable: true, initial: null }), - value: new fields.NumberField({ nullable: true, initial: null }) - }), - target: new fields.SchemaField({ - type: new fields.StringField({ - choices: SYSTEM.ACTIONS.targetTypes, - initial: SYSTEM.ACTIONS.targetTypes.other.id - }) - }) - }; - } -} +import DhpActor from '../../documents/actor.mjs'; +import D20RollDialog from '../../dialogs/d20RollDialog.mjs'; const fields = foundry.data.fields; +/* + !!! I'm currently refactoring the whole Action thing, it's a WIP !!! +*/ + /* ToDo - - Apply ActiveEffect => Add to Chat message like Damage Button ? - - Add Drag & Drop for documentUUID field (Macro & Summon) - - Add optionnal Role for Healing ? - - Handle Roll result as part of formula if needed - - Target Check - - Cost Check + - Add setting and/or checkbox for cost and damage like + - Target Check / Target Picker - Range Check - Area of effect and measurement placement - - Auto use costs and action + - Summon Action create method + + Other + - Auto use action <= Into Roll */ export class DHBaseAction extends foundry.abstract.DataModel { + static extraSchemas = []; + static defineSchema() { return { _id: new fields.DocumentIdField(), systemPath: new fields.StringField({ required: true, initial: 'actions' }), type: new fields.StringField({ initial: undefined, readonly: true, required: true }), name: new fields.StringField({ initial: undefined }), + description: new fields.HTMLField(), img: new fields.FilePathField({ initial: undefined, categories: ['IMAGE'], base64: false }), + chatDisplay: new fields.BooleanField({ initial: true, label: 'Display in chat' }), actionType: new fields.StringField({ choices: SYSTEM.ITEM.actionTypes, initial: 'action', nullable: true }), cost: new fields.ArrayField( new fields.SchemaField({ @@ -84,35 +58,85 @@ export class DHBaseAction extends foundry.abstract.DataModel { }), range: new fields.StringField({ choices: SYSTEM.GENERAL.range, - required: true, - blank: false, - initial: 'self' - }) + required: false, + blank: true + // initial: null + }), + ...this.defineExtraSchema() }; } + static defineExtraSchema() { + const extraFields = { + damage: new DHDamageField(), + roll: new fields.SchemaField({ + type: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.GENERAL.rollTypes }), + trait: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.ACTOR.abilities }), + difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }), + bonus: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }) + }), + save: new fields.SchemaField({ + trait: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.ACTOR.abilities }), + difficulty: new fields.NumberField({ nullable: true, initial: 10, integer: true, min: 0 }), + damageMod: new fields.StringField({ initial: SYSTEM.ACTIONS.damageOnSave.none.id, choices: SYSTEM.ACTIONS.damageOnSave }) + }), + target: new fields.SchemaField({ + type: new fields.StringField({ + choices: SYSTEM.ACTIONS.targetTypes, + initial: SYSTEM.ACTIONS.targetTypes.any.id, + nullable: true, + initial: null + }), + amount: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }) + }), + effects: new fields.ArrayField( // ActiveEffect + new fields.SchemaField({ + _id: new fields.DocumentIdField(), + onSave: new fields.BooleanField({ initial: false }) + }) + ), + healing: new fields.SchemaField({ + type: new fields.StringField({ + choices: SYSTEM.GENERAL.healingTypes, + required: true, + blank: false, + initial: SYSTEM.GENERAL.healingTypes.hitPoints.id, + label: 'Healing' + }), + resultBased: new fields.BooleanField({ initial: false, label: "DAGGERHEART.Actions.Settings.ResultBased.label" }), + value: new fields.EmbeddedDataField(DHActionDiceData), + valueAlt: new fields.EmbeddedDataField(DHActionDiceData), + }) + }, + extraSchemas = {}; + + this.extraSchemas.forEach(s => (extraSchemas[s] = extraFields[s])); + return extraSchemas; + } + prepareData() {} get index() { return foundry.utils.getProperty(this.parent, this.systemPath).indexOf(this); } + get id() { + return this._id; + } + get item() { return this.parent.parent; } get actor() { - return this.item?.actor; + return this.item instanceof DhpActor ? this.item : this.item?.actor; } get chatTemplate() { return 'systems/daggerheart/templates/chat/duality-roll.hbs'; } - get chatTitle() { - return this.item.name; - } - static getRollType() { + static getRollType(parent) { return 'ability'; } @@ -121,96 +145,425 @@ export class DHBaseAction extends foundry.abstract.DataModel { updateSource.img ??= parent?.img ?? parent?.system?.img; if (parent?.system?.trait) { updateSource['roll'] = { - type: this.getRollType(), + type: this.getRollType(parent), trait: parent.system.trait }; } + if (parent?.type === 'weapon' && !!this.schema.fields.damage) { + updateSource['damage'] = { includeBase: true }; + } if (parent?.system?.range) { updateSource['range'] = parent?.system?.range; } return updateSource; } - async use(event) { - if (this.roll.type && this.roll.trait) { - const modifierValue = - this.actor.system.traits[this.roll.trait].value + (this.actor.system.bonuses.attack ?? 0); - const config = { - event: event, - title: this.chatTitle, - roll: { - modifier: modifierValue, - label: game.i18n.localize(abilities[this.roll.trait].label), - type: this.actionType, - difficulty: this.roll?.difficulty - }, - chatMessage: { - template: this.chatTemplate + getRollData() { + const actorData = this.actor.getRollData(false); + + // Remove when included directly in Actor getRollData + actorData.prof = actorData.proficiency?.value ?? 1, + actorData.cast = actorData.spellcast?.value ?? 1, + actorData.scale = this.cost.length + ? this.cost.reduce((a, c) => { + a[c.type] = c.value; + return a; + }, {}) + : 1, + actorData.roll = {} + + return actorData; + } + + async use(event, ...args) { + const isFastForward = event.shiftKey || (!this.hasRoll && !this.hasSave); + // Prepare base Config + const initConfig = this.initActionConfig(event); + // let config = this.initActionConfig(event); + + // Prepare Targets + const targetConfig = this.prepareTarget(); + if (isFastForward && !targetConfig) return ui.notifications.warn('Too many targets selected for that actions.'); + // config = this.prepareTarget(config); + + // Prepare Range + const rangeConfig = this.prepareRange(); + // config = this.prepareRange(config); + + // Prepare Costs + const costsConfig = this.prepareCost(); + if(isFastForward && !this.hasCost(costsConfig)) return ui.notifications.warn("You don't have the resources to use that action."); + // config = this.prepareUseCost(config) + + // Prepare Uses + const usesConfig = this.prepareUse(); + if(isFastForward && !this.hasUses(usesConfig)) return ui.notifications.warn("That action doesn't have remaining uses."); + // config = this.prepareUseCost(config) + + // Prepare Roll Data + const actorData = this.getRollData(); + + let config = { + ...initConfig, + targets: targetConfig, + range: rangeConfig, + costs: costsConfig, + uses: usesConfig, + data: actorData + } + + if ( Hooks.call(`${SYSTEM.id}.preUseAction`, this, config) === false ) return; + + // Display configuration window if necessary + if ( config.dialog.configure && this.requireConfigurationDialog(config) ) { + config = await D20RollDialog.configure(config); + if (!config) return; + } + + if ( this.hasRoll ) { + const rollConfig = this.prepareRoll(config); + config.roll = rollConfig; + config = await this.actor.diceRoll(config); + if (!config) return; + } + + if( this.hasSave ) { + /* config.targets.forEach((t) => { + if(t.hit) { + const target = game.canvas.tokens.get(t.id), + actor = target?.actor; + console.log(actor) + if(!actor) return; + actor.saveRoll({ + event, + title: 'Roll Save', + roll: { + trait: this.save.trait, + difficulty: this.save.difficulty + }, + dialog: { + configure: false + }, + data: actor.getRollData() + }).then(async (result) => { + t.saved = result; + setTimeout(async () => { + const message = ui.chat.collection.get(config.message.id), + msgTargets = message.system.targets, + msgTarget = msgTargets.find(mt => mt.id === t.id); + msgTarget.saved = result; + await message.update({'system.targets': msgTargets}); + },100) + }) } - }; - if (this.target?.type) config.checkTarget = true; - if (this.damage.parts.length) { - config.damage = { - value: this.damage.parts.map(p => p.getFormula(this.actor)).join(' + '), - type: this.damage.parts[0].type - }; - } - if (this.effects.length) { - // Apply Active Effects. In Chat Message ? - } - return this.actor.diceRoll(config); + }) */ + } + + if ( this.doFollowUp() ) { + if(this.rollDamage) await this.rollDamage(event, config); + if(this.rollHealing) await this.rollHealing(event, config); + if(this.trigger) await this.trigger(event, config); + } + + // Consume resources + await this.consume(config); + + if ( Hooks.call(`${SYSTEM.id}.postUseAction`, this, config) === false ) return; + + return config; + } + + /* */ + initActionConfig(event) { + return { + event, + title: this.item.name, + source: { + item: this.item._id, + action: this._id + // action: this + }, + dialog: { + configure: true + }, + type: this.type, + hasDamage: !!this.damage?.parts?.length, + hasHealing: !!this.healing, + hasEffect: !!this.effects?.length, + hasSave: this.hasSave } } -} -const extraDefineSchema = (field, option) => { - return { - [field]: { - // damage: new fields.SchemaField({ - // parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)) - // }), - damage: new DHDamageField(option), - roll: new fields.SchemaField({ - type: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.GENERAL.rollTypes }), - trait: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.ACTOR.abilities }), - difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }) - }), - target: new fields.SchemaField({ - type: new fields.StringField({ - choices: SYSTEM.ACTIONS.targetTypes, - initial: SYSTEM.ACTIONS.targetTypes.other.id - }) - }), - effects: new fields.ArrayField( // ActiveEffect - new fields.SchemaField({ - _id: new fields.DocumentIdField() - }) - ) - }[field] - }; -}; + requireConfigurationDialog(config) { + return !config.event.shiftkey && !this.hasRoll && (config.costs?.length || config.uses); + } -export class DHAttackAction extends DHBaseAction { - static defineSchema() { + prepareCost() { + const costs = this.cost?.length ? foundry.utils.deepClone(this.cost) : []; + return costs; + } + + prepareUse() { + const uses = this.uses?.max ? foundry.utils.deepClone(this.uses) : null; + if (uses && !uses.value) uses.value = 0; + return uses; + } + + prepareTarget() { + let targets; + if (this.target?.type === SYSTEM.ACTIONS.targetTypes.self.id) + targets = this.formatTarget(this.actor.token ?? this.actor.prototypeToken); + targets = Array.from(game.user.targets); + // foundry.CONST.TOKEN_DISPOSITIONS.FRIENDLY + if (this.target?.type && this.target.type !== SYSTEM.ACTIONS.targetTypes.any.id) { + targets = targets.filter(t => this.isTargetFriendly(t)); + if (this.target.amount && targets.length > this.target.amount) targets = []; + } + targets = targets.map(t => this.formatTarget(t)); + return targets; + + } + + prepareRange() { + const range = this.range ?? null; + return range; + } + + prepareRoll() { + const roll = { + modifiers: [], + trait: this.roll?.trait, + label: 'Attack', + type: this.actionType, + difficulty: this.roll?.difficulty + }; + return roll; + } + + doFollowUp(config) { + return !this.hasRoll; + } + + async consume(config) { + const resources = config.costs.filter(c => c.enabled !== false).map(c => { + return { type: c.type, value: c.total * -1 }; + }); + await this.actor.modifyResource(resources); + if(config.uses?.enabled) { + const newActions = foundry.utils.getProperty(this.item.system, this.systemPath).map(x => x.toObject()); + newActions[this.index].uses.value++; + await this.item.update({ [`system.${this.systemPath}`]: newActions }); + } + } + /* */ + + /* ROLL */ + get hasRoll() { + return !!this.roll?.type; + } + /* ROLL */ + + /* SAVE */ + get hasSave() { + return !!this.save?.trait; + } + /* SAVE */ + + /* COST */ + + getRealCosts(costs) { + const realCosts = costs?.length ? costs.filter(c => c.enabled) : []; + return realCosts; + } + + calcCosts(costs) { + return costs.map(c => { + c.scale = c.scale ?? 1; + c.step = c.step ?? 1; + c.total = c.value * c.scale * c.step; + c.enabled = c.hasOwnProperty('enabled') ? c.enabled : true; + return c; + }); + } + + hasCost(costs) { + const realCosts = this.getRealCosts(costs); + return realCosts.reduce((a, c) => a && this.actor.system.resources[c.type]?.value >= (c.total ?? c.value), true); + } + /* COST */ + + /* USES */ + calcUses(uses) { + if(!uses) return null; return { - ...super.defineSchema(), - ...extraDefineSchema('damage', true), - ...extraDefineSchema('roll'), - ...extraDefineSchema('target'), - ...extraDefineSchema('effects') + ...uses, + enabled: uses.hasOwnProperty('enabled') ? uses.enabled : true }; } - static getRollType() { - return 'weapon'; + hasUses(uses) { + if(!uses) return true; + return (uses.hasOwnProperty('enabled') && !uses.enabled) || uses.value + 1 <= uses.max; + } + /* USES */ + + /* TARGET */ + isTargetFriendly(target) { + const actorDisposition = this.actor.token + ? this.actor.token.disposition + : this.actor.prototypeToken.disposition, + targetDisposition = target.document.disposition; + return ( + (this.target.type === SYSTEM.ACTIONS.targetTypes.friendly.id && actorDisposition === targetDisposition) || + (this.target.type === SYSTEM.ACTIONS.targetTypes.hostile.id && actorDisposition + targetDisposition === 0) + ); } - get chatTitle() { - return game.i18n.format('DAGGERHEART.Chat.AttackRoll.Title', { - attack: this.item.name + formatTarget(actor) { + return { + id: actor.id, + actorId: actor.actor.uuid, + name: actor.actor.name, + img: actor.actor.img, + difficulty: actor.actor.system.difficulty, + evasion: actor.actor.system.evasion?.total + }; + } + /* TARGET */ + + /* RANGE */ + + /* RANGE */ + + /* EFFECTS */ + async applyEffects(event, data, force = false) { + if (!this.effects?.length || !data.system.targets.length) return; + let effects = this.effects; + data.system.targets.forEach(async token => { + if (!token.hit && !force) return; + if(this.hasSave && token.saved.success === true) { + effects = this.effects.filter(e => e.onSave === true) + } + if(!effects.length) return; + effects.forEach(async e => { + const actor = canvas.tokens.get(token.id)?.actor, + effect = this.item.effects.get(e._id); + if (!actor || !effect) return; + await this.applyEffect(effect, actor); + }); }); } + async applyEffect(effect, actor) { + // Enable an existing effect on the target if it originated from this effect + const existingEffect = actor.effects.find(e => e.origin === origin.uuid); + if (existingEffect) { + return existingEffect.update( + foundry.utils.mergeObject({ + ...effect.constructor.getInitialDuration(), + disabled: false + }) + ); + } + + // Otherwise, create a new effect on the target + const effectData = foundry.utils.mergeObject({ + ...effect.toObject(), + disabled: false, + transfer: false, + origin: origin.uuid + }); + await ActiveEffect.implementation.create(effectData, { parent: actor }); + } + /* EFFECTS */ + + /* SAVE */ + async rollSave(target, event, message) { + if(!target?.actor) return; + target.actor.diceRoll({ + event, + title: 'Roll Save', + roll: { + trait: this.save.trait, + difficulty: this.save.difficulty, + type: "reaction" + }, + data: target.actor.getRollData() + }).then(async (result) => { + this.updateChatMessage(message, target.id, {result: result.roll.total, success: result.roll.success}); + }) + } + + async updateChatMessage(message, targetId, changes, chain=true) { + setTimeout(async () => { + const chatMessage = ui.chat.collection.get(message._id), + msgTargets = chatMessage.system.targets, + msgTarget = msgTargets.find(mt => mt.id === targetId); + msgTarget.saved = changes; + await chatMessage.update({'system.targets': msgTargets}); + },100); + if(chain) { + if(message.system.source.message) this.updateChatMessage(ui.chat.collection.get(message.system.source.message), targetId, changes, false); + const relatedChatMessages = ui.chat.collection.filter(c => c.system.source.message === message._id); + relatedChatMessages.forEach(c => { + this.updateChatMessage(c, targetId, changes, false); + }) + } + } + /* SAVE */ +} + +export class DHDamageAction extends DHBaseAction { + static extraSchemas = ['damage', 'target', 'effects']; + + /* async use(event, ...args) { + const config = await super.use(event, args); + if (!config || ['error', 'warning'].includes(config.type)) return; + if (!this.directDamage) return; + return await this.rollDamage(event, config); + } */ + + getFormulaValue(part, data) { + let formulaValue = part.value; + if(this.hasRoll && part.resultBased && data.system.roll.result.duality === -1) return part.valueAlt; + return formulaValue; + } + + async rollDamage(event, data) { + let formula = this.damage.parts.map(p => this.getFormulaValue(p, data).getFormula(this.actor)).join(' + '); + + if (!formula || formula == '') return; + let roll = { formula: formula, total: formula }, + bonusDamage = []; + + const config = { + title: game.i18n.format('DAGGERHEART.Chat.DamageRoll.Title', { damage: this.name }), + formula, + targets: (data.system?.targets.filter(t => t.hit) ?? data.targets), + hasSave: this.hasSave, + source: data.system?.source + }; + if(this.hasSave) config.onSave = this.save.damageMod; + if(data.system) { + config.source.message = data._id; + config.directDamage = false; + } + + roll = CONFIG.Dice.daggerheart.DamageRoll.build(config); + } +} + +export class DHAttackAction extends DHDamageAction { + static extraSchemas = [...super.extraSchemas, ...['roll', 'save']]; + + static getRollType(parent) { + return parent.type === 'weapon' ? 'weapon' : 'spellcast'; + } + + get chatTemplate() { + return 'systems/daggerheart/templates/chat/duality-roll.hbs'; + } + prepareData() { super.prepareData(); if (this.damage.includeBase && !!this.item?.system?.damage) { @@ -221,115 +574,49 @@ export class DHAttackAction extends DHBaseAction { getParentDamage() { return { - multiplier: 'proficiency', - dice: this.item?.system?.damage.value, - bonus: this.item?.system?.damage.bonus ?? 0, + value: { + multiplier: 'proficiency', + dice: this.item?.system?.damage.value, + bonus: this.item?.system?.damage.bonus ?? 0 + }, type: this.item?.system?.damage.type, base: true }; } - - // Temporary until full formula parser - // getDamageFormula() { - // return this.damage.parts.map(p => p.formula).join(' + '); - // } -} - -export class DHSpellCastAction extends DHBaseAction { - static defineSchema() { - return { - ...super.defineSchema(), - ...extraDefineSchema('damage'), - ...extraDefineSchema('roll'), - ...extraDefineSchema('target'), - ...extraDefineSchema('effects') - }; - } - - static getRollType() { - return 'spellcast'; - } -} - -export class DHDamageAction extends DHBaseAction { - static defineSchema() { - return { - ...super.defineSchema(), - ...extraDefineSchema('damage', false), - ...extraDefineSchema('target'), - ...extraDefineSchema('effects') - }; - } - - async use(event) { - const formula = this.damage.parts.map(p => p.getFormula(this.actor)).join(' + '); - if (!formula || formula == '') return; - - let roll = { formula: formula, total: formula }; - if (isNaN(formula)) { - roll = await new Roll(formula).evaluate(); - } - - const cls = getDocumentClass('ChatMessage'); - const msg = new cls({ - user: game.user.id, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/damage-roll.hbs', - { - roll: roll.formula, - total: roll.total, - type: this.damage.parts.map(p => p.type) - } - ) - }); - - cls.create(msg.toObject()); - } } export class DHHealingAction extends DHBaseAction { - static defineSchema() { - return { - ...super.defineSchema(), - healing: new fields.SchemaField({ - type: new fields.StringField({ - choices: SYSTEM.GENERAL.healingTypes, - required: true, - blank: false, - initial: SYSTEM.GENERAL.healingTypes.health.id, - label: 'Healing' - }), - value: new fields.EmbeddedDataField(DHActionDiceData) - }), - ...extraDefineSchema('target'), - ...extraDefineSchema('effects') - }; + static extraSchemas = ['target', 'effects', 'healing', 'roll']; + + static getRollType(parent) { + return 'spellcast'; } - async use(event) { - const formula = this.healing.value.getFormula(this.actor); + getFormulaValue(data) { + let formulaValue = this.healing.value; + if(this.hasRoll && this.healing.resultBased && data.system.roll.result.duality === -1) return this.healing.valueAlt; + return formulaValue; + } + + async rollHealing(event, data) { + let formulaValue = this.getFormulaValue(data), + formula = formulaValue.getFormula(this.actor); + if (!formula || formula == '') return; + let roll = { formula: formula, total: formula }, + bonusDamage = []; + + const config = { + title: game.i18n.format('DAGGERHEART.Chat.HealingRoll.Title', { + healing: game.i18n.localize(SYSTEM.GENERAL.healingTypes[this.healing.type].label) + }), + formula, + targets: (data.system?.targets ?? data.targets).filter(t => t.hit), + messageType: 'healing', + type: this.healing.type + }; - // const roll = await super.use(event); - let roll = { formula: formula, total: formula }; - if (isNaN(formula)) { - roll = await new Roll(formula).evaluate(); - } - - const cls = getDocumentClass('ChatMessage'); - const msg = new cls({ - user: game.user.id, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/healing-roll.hbs', - { - roll: roll.formula, - total: roll.total, - type: this.healing.type - } - ) - }); - - cls.create(msg.toObject()); + roll = CONFIG.Dice.daggerheart.DamageRoll.build(config); } get chatTemplate() { @@ -337,42 +624,60 @@ export class DHHealingAction extends DHBaseAction { } } -export class DHResourceAction extends DHBaseAction { - static defineSchema() { - return { - ...super.defineSchema(), - // ...extraDefineSchema('roll'), - ...extraDefineSchema('target'), - ...extraDefineSchema('effects'), - resource: new fields.SchemaField({ - type: new fields.StringField({ - choices: [], - blank: true, - required: false, - initial: '', - label: 'Resource' - }), - value: new fields.NumberField({ initial: 0, label: 'Value' }) - }) - }; - } -} - export class DHSummonAction extends DHBaseAction { static defineSchema() { return { ...super.defineSchema(), - documentUUID: new fields.StringField({ blank: true, initial: '', placeholder: 'Enter a Creature UUID' }) + documentUUID: new fields.DocumentUUIDField({ type: 'Actor' }) }; } + + async trigger(event, ...args) { + if (!this.canSummon || !canvas.scene) return; + // const config = await super.use(event, args); + } + + get canSummon() { + return game.user.can('TOKEN_CREATE'); + } } export class DHEffectAction extends DHBaseAction { - static defineSchema() { - return { - ...super.defineSchema(), - ...extraDefineSchema('effects') - }; + static extraSchemas = ['effects', 'target']; + + async use(event, ...args) { + const config = await super.use(event, args); + if (['error', 'warning'].includes(config.type)) return; + return await this.chatApplyEffects(event, config); + } + + async chatApplyEffects(event, data) { + const cls = getDocumentClass('ChatMessage'), + systemData = { + title: game.i18n.format('DAGGERHEART.Chat.ApplyEffect.Title', { name: this.name }), + origin: this.actor._id, + description: '', + targets: data.targets.map(x => ({ id: x.id, name: x.name, img: x.img, hit: true })), + action: { + itemId: this.item._id, + actionId: this._id + } + }, + msg = new cls({ + type: 'applyEffect', + user: game.user.id, + system: systemData, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/chat/apply-effects.hbs', + systemData + ) + }); + + cls.create(msg.toObject()); + } + + get chatTemplate() { + return 'systems/daggerheart/templates/chat/apply-effects.hbs'; } } @@ -380,11 +685,13 @@ export class DHMacroAction extends DHBaseAction { static defineSchema() { return { ...super.defineSchema(), - documentUUID: new fields.StringField({ blank: true, initial: '', placeholder: 'Enter a macro UUID' }) + documentUUID: new fields.DocumentUUIDField({ type: 'Macro' }) }; } - async use(event) { + async trigger(event, ...args) { + // const config = await super.use(event, args); + // if (['error', 'warning'].includes(config.type)) return; const fixUUID = !this.documentUUID.includes('Macro.') ? `Macro.${this.documentUUID}` : this.documentUUID, macro = await fromUuid(fixUUID); try { diff --git a/module/data/action/actionDice.mjs b/module/data/action/actionDice.mjs index 1d8f1fe7..3bb51b50 100644 --- a/module/data/action/actionDice.mjs +++ b/module/data/action/actionDice.mjs @@ -11,6 +11,7 @@ export class DHActionDiceData extends foundry.abstract.DataModel { initial: 'proficiency', label: 'Multiplier' }), + flatMultiplier: new fields.NumberField({ nullable: true, initial: 1, label: 'Flat Multiplier' }), dice: new fields.StringField({ choices: SYSTEM.GENERAL.diceTypes, initial: 'd6', label: 'Formula' }), bonus: new fields.NumberField({ nullable: true, initial: null, label: 'Bonus' }), custom: new fields.SchemaField({ @@ -21,27 +22,29 @@ export class DHActionDiceData extends foundry.abstract.DataModel { } getFormula(actor) { + const multiplier = this.multiplier === 'flat' ? this.flatMultiplier : actor.system[this.multiplier]?.total; return this.custom.enabled ? this.custom.formula - : `${actor.system[this.multiplier]?.total ?? 1}${this.dice}${this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : ''}`; + : `${multiplier ?? 1}${this.dice}${this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : ''}`; } } export class DHDamageField extends fields.SchemaField { - constructor(hasBase, options, context = {}) { + constructor(options, context = {}) { const damageFields = { - parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)) + parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)), + includeBase: new fields.BooleanField({ initial: false }) }; - if (hasBase) damageFields.includeBase = new fields.BooleanField({ initial: true }); + // if (hasBase) damageFields.includeBase = new fields.BooleanField({ initial: true }); super(damageFields, options, context); } } -export class DHDamageData extends DHActionDiceData { +export class DHDamageData extends foundry.abstract.DataModel { /** @override */ static defineSchema() { return { - ...super.defineSchema(), + // ...super.defineSchema(), base: new fields.BooleanField({ initial: false, readonly: true, label: 'Base' }), type: new fields.StringField({ choices: SYSTEM.GENERAL.damageTypes, @@ -49,7 +52,10 @@ export class DHDamageData extends DHActionDiceData { label: 'Type', nullable: false, required: true - }) + }), + resultBased: new fields.BooleanField({ initial: false, label: "DAGGERHEART.Actions.Settings.ResultBased.label" }), + value: new fields.EmbeddedDataField(DHActionDiceData), + valueAlt: new fields.EmbeddedDataField(DHActionDiceData), }; } } diff --git a/module/data/actor/adversary.mjs b/module/data/actor/adversary.mjs index 834c5f17..5ac80969 100644 --- a/module/data/actor/adversary.mjs +++ b/module/data/actor/adversary.mjs @@ -1,3 +1,4 @@ +import ActionField from '../fields/actionField.mjs'; import BaseDataActor from './base.mjs'; const resourceField = () => @@ -39,30 +40,41 @@ export default class DhpAdversary extends BaseDataActor { hitPoints: resourceField(), stress: resourceField() }), - attack: new fields.SchemaField({ - name: new fields.StringField({}), - modifier: new fields.NumberField({ required: true, integer: true, initial: 0 }), - range: new fields.StringField({ - required: true, - choices: SYSTEM.GENERAL.range, - initial: SYSTEM.GENERAL.range.melee.id - }), - damage: new fields.SchemaField({ - value: new fields.StringField(), - type: new fields.StringField({ - required: true, - choices: SYSTEM.GENERAL.damageTypes, - initial: SYSTEM.GENERAL.damageTypes.physical.id - }) - }) + attack: new ActionField({ + initial: { + name: 'Attack', + _id: foundry.utils.randomID(), + systemPath: 'attack', + type: 'attack', + range: 'melee', + target: { + type: 'any', + amount: 1 + }, + roll: { + type: 'weapon' + }, + damage: { + parts: [ + { + multiplier: 'flat' + } + ] + } + } }), experiences: new fields.TypedObjectField( new fields.SchemaField({ name: new fields.StringField(), value: new fields.NumberField({ required: true, integer: true, initial: 1 }) }) - ) - /* Features waiting on pseudo-document data model addition */ + ), + bonuses: new fields.SchemaField({ + difficulty: new fields.SchemaField({ + all: new fields.NumberField({ integer: true, initial: 0 }), + reaction: new fields.NumberField({ integer: true, initial: 0 }) + }) + }) }; } } diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 027914a0..ce77e4ae 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -17,6 +17,12 @@ const resourceField = max => max: new foundry.data.fields.NumberField({ initial: max, integer: true }) }); +const stressDamageReductionRule = () => + new foundry.data.fields.SchemaField({ + enabled: new foundry.data.fields.BooleanField({ required: true, initial: false }), + cost: new foundry.data.fields.NumberField({ integer: true }) + }); + export default class DhCharacter extends BaseDataActor { static get metadata() { return foundry.utils.mergeObject(super.metadata, { @@ -35,7 +41,9 @@ export default class DhCharacter extends BaseDataActor { bonus: new foundry.data.fields.NumberField({ initial: 0, integer: true }) }), stress: resourceField(6), - hope: resourceField(6) + hope: resourceField(6), + tokens: new fields.ObjectField(), + dice: new fields.ObjectField() }), traits: new fields.SchemaField({ agility: attributeField(), @@ -90,9 +98,42 @@ export default class DhCharacter extends BaseDataActor { }), levelData: new fields.EmbeddedDataField(DhLevelData), bonuses: new fields.SchemaField({ - attack: new fields.NumberField({ integer: true, initial: 0 }), - spellcast: new fields.NumberField({ integer: true, initial: 0 }), - armorScore: new fields.NumberField({ integer: true, initial: 0 }) + armorScore: new fields.NumberField({ integer: true, initial: 0 }), + damageThresholds: new fields.SchemaField({ + severe: new fields.NumberField({ integer: true, initial: 0 }), + major: new fields.NumberField({ integer: true, initial: 0 }) + }), + roll: new fields.SchemaField({ + attack: new fields.NumberField({ integer: true, initial: 0 }), + spellcast: new fields.NumberField({ integer: true, initial: 0 }), + action: new fields.NumberField({ integer: true, initial: 0 }), + hopeOrFear: new fields.NumberField({ integer: true, initial: 0 }) + }), + damage: new fields.SchemaField({ + all: new fields.NumberField({ integer: true, initial: 0 }), + physical: new fields.NumberField({ integer: true, initial: 0 }), + magic: new fields.NumberField({ integer: true, initial: 0 }) + }) + }), + rules: new fields.SchemaField({ + maxArmorMarked: new fields.SchemaField({ + value: new fields.NumberField({ required: true, integer: true, initial: 1 }), + bonus: new fields.NumberField({ required: true, integer: true, initial: 0 }), + stressExtra: new fields.NumberField({ required: true, integer: true, initial: 0 }) + }), + stressDamageReduction: new fields.SchemaField({ + severe: stressDamageReductionRule(), + major: stressDamageReductionRule(), + minor: stressDamageReductionRule() + }), + strangePatterns: new fields.NumberField({ + integer: true, + min: 1, + max: 12, + nullable: true, + initial: null + }), + runeWard: new fields.BooleanField({ initial: false }) }) }; } @@ -253,6 +294,9 @@ export default class DhCharacter extends BaseDataActor { experience.total = experience.value + experience.bonus; } + this.rules.maxArmorMarked.total = this.rules.maxArmorMarked.value + this.rules.maxArmorMarked.bonus; + + this.armorScore = this.armor ? this.armor.system.baseScore + (this.bonuses.armorScore ?? 0) : 0; this.resources.hitPoints.maxTotal = (this.class.value?.system?.hitPoints ?? 0) + this.resources.hitPoints.bonus; this.resources.stress.maxTotal = this.resources.stress.max + this.resources.stress.bonus; this.evasion.total = (this.class?.evasion ?? 0) + this.evasion.bonus; @@ -263,7 +307,11 @@ export default class DhCharacter extends BaseDataActor { const data = super.getRollData(); return { ...data, - tier: this.tier + ...this.resources.tokens, + ...this.resources.dice, + ...this.bonuses, + tier: this.tier, + level: this.levelData.level.current }; } } diff --git a/module/data/chat-message/_modules.mjs b/module/data/chat-message/_modules.mjs index 7ee3d0b6..84f75c1b 100644 --- a/module/data/chat-message/_modules.mjs +++ b/module/data/chat-message/_modules.mjs @@ -1,13 +1,21 @@ -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"; +import DHApplyEffect from './applyEffects.mjs' -export { DHAbilityUse, DHAdversaryRoll, DHDamageRoll, DHDualityRoll }; +export { + DHAbilityUse, + DHAdversaryRoll, + DHDamageRoll, + DHDualityRoll, + DHApplyEffect +} export const config = { - abilityUse: DHAbilityUse, - adversaryRoll: DHAdversaryRoll, - damageRoll: DHDamageRoll, - dualityRoll: DHDualityRoll + abilityUse: DHAbilityUse, + adversaryRoll: DHAdversaryRoll, + damageRoll: DHDamageRoll, + dualityRoll: DHDualityRoll, + applyEffect: DHApplyEffect }; diff --git a/module/data/chat-message/adversaryRoll.mjs b/module/data/chat-message/adversaryRoll.mjs index 0a02677f..d97783c8 100644 --- a/module/data/chat-message/adversaryRoll.mjs +++ b/module/data/chat-message/adversaryRoll.mjs @@ -4,43 +4,35 @@ export default class DHAdversaryRoll extends foundry.abstract.TypeDataModel { return { title: new fields.StringField(), - origin: new fields.StringField({ required: true }), - dice: new fields.DataField(), roll: new fields.DataField(), - modifiers: new fields.ArrayField( - new fields.SchemaField({ - value: new fields.NumberField({ integer: true }), - label: new fields.StringField({}) - }) - ), - advantageState: new fields.BooleanField({ nullable: true, initial: null }), - advantage: new fields.SchemaField({ - dice: new fields.StringField({}), - value: new fields.NumberField({ integer: true }) - }), targets: new fields.ArrayField( new fields.SchemaField({ id: new fields.StringField({}), + actorId: 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 }) + hit: new fields.BooleanField({ initial: false }), + saved: new fields.SchemaField({ + result: new fields.NumberField(), + success: new fields.BooleanField({ nullable: true, initial: null }) + }) }) ), - damage: new fields.SchemaField( - { - value: new fields.StringField({}), - type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.damageTypes), integer: false }) - }, - { nullable: true, initial: null } - ) + hasDamage: new fields.BooleanField({ initial: false }), + hasHealing: new fields.BooleanField({ initial: false }), + hasEffect: new fields.BooleanField({ initial: false }), + hasSave: new fields.BooleanField({ initial: false }), + source: new fields.SchemaField({ + actor: new fields.StringField(), + item: new fields.StringField(), + action: new fields.StringField() + }) }; } - prepareDerivedData() { - this.targets.forEach(target => { - target.hit = target.difficulty ? this.total >= target.difficulty : this.total >= target.evasion; - }); + get messageTemplate() { + return 'systems/daggerheart/templates/chat/adversary-roll.hbs'; } } diff --git a/module/data/chat-message/applyEffects.mjs b/module/data/chat-message/applyEffects.mjs new file mode 100644 index 00000000..838dabfb --- /dev/null +++ b/module/data/chat-message/applyEffects.mjs @@ -0,0 +1,23 @@ +export default class DHApplyEffect extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + title: new fields.StringField(), + origin: new fields.StringField({}), + description: new fields.StringField({}), + targets: new fields.ArrayField( + new fields.SchemaField({ + id: new fields.StringField({ required: true }), + name: new fields.StringField(), + img: new fields.StringField(), + hit: new fields.BooleanField({ initial: false }) + }) + ), + action: new fields.SchemaField({ + itemId: new fields.StringField(), + actionId: new fields.StringField() + }) + }; + } +} diff --git a/module/data/chat-message/damageRoll.mjs b/module/data/chat-message/damageRoll.mjs index 07118d6d..29ad6fbd 100644 --- a/module/data/chat-message/damageRoll.mjs +++ b/module/data/chat-message/damageRoll.mjs @@ -3,32 +3,35 @@ export default class DHDamageRoll extends foundry.abstract.TypeDataModel { const fields = foundry.data.fields; return { + messageType: new fields.StringField({initial: 'damage'}), title: new fields.StringField(), - roll: new fields.StringField({ required: true }), - damage: new fields.SchemaField({ - total: new fields.NumberField({ required: true, integer: true }), - type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.damageTypes), integer: false }) - }), - dice: new fields.ArrayField( - new fields.SchemaField({ - type: new fields.StringField({ required: true }), - rolls: new fields.ArrayField(new fields.NumberField({ required: true, integer: true })), - total: new fields.NumberField({ integer: true }) - }) - ), - modifiers: new fields.ArrayField( - new fields.SchemaField({ - value: new fields.NumberField({ required: true, integer: true }), - operator: new fields.StringField({ required: true, choices: ['+', '-', '*', '/'] }) - }) - ), + roll: new fields.DataField({}), targets: new fields.ArrayField( new fields.SchemaField({ id: new fields.StringField({ required: true }), + actorId: new fields.StringField({}), name: new fields.StringField(), - img: new fields.StringField() + img: new fields.StringField(), + hit: new fields.BooleanField({ initial: false }), + saved: new fields.SchemaField({ + result: new fields.NumberField(), + success: new fields.BooleanField({ nullable: true, initial: null }) + }) }) - ) + ), + hasSave: new fields.BooleanField({ initial: false }), + onSave: new fields.StringField(), + source: new fields.SchemaField({ + actor: new fields.StringField(), + item: new fields.StringField(), + action: new fields.StringField(), + message: new fields.StringField() + }), + directDamage: new fields.BooleanField({initial: true}) }; } + + get messageTemplate() { + return `systems/daggerheart/templates/chat/${this.messageType}-roll.hbs`; + } } diff --git a/module/data/chat-message/dualityRoll.mjs b/module/data/chat-message/dualityRoll.mjs index 60283b7d..0f3f12de 100644 --- a/module/data/chat-message/dualityRoll.mjs +++ b/module/data/chat-message/dualityRoll.mjs @@ -1,11 +1,4 @@ -import { DualityRollColor } from '../settings/Appearance.mjs'; - const fields = foundry.data.fields; -const diceField = () => - new fields.SchemaField({ - dice: new fields.StringField({}), - value: new fields.NumberField({ integer: true }) - }); export default class DHDualityRoll extends foundry.abstract.TypeDataModel { static dualityResult = { @@ -17,92 +10,35 @@ export default class DHDualityRoll extends foundry.abstract.TypeDataModel { static defineSchema() { return { title: new fields.StringField(), - origin: new fields.StringField({ required: true }), roll: new fields.DataField({}), - modifiers: new fields.ArrayField( - new fields.SchemaField({ - value: new fields.NumberField({ integer: true }), - label: new fields.StringField({}) - }) - ), - hope: diceField(), - fear: diceField(), - advantageState: new fields.BooleanField({ nullable: true, initial: null }), - advantage: diceField(), targets: new fields.ArrayField( new fields.SchemaField({ id: new fields.StringField({}), + actorId: 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 }) + hit: new fields.BooleanField({ initial: false }), + saved: new fields.SchemaField({ + result: new fields.NumberField(), + success: new fields.BooleanField({ nullable: true, initial: null }) + }) }) ), - 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 } - ) + hasDamage: new fields.BooleanField({ initial: false }), + hasHealing: new fields.BooleanField({ initial: false }), + hasEffect: new fields.BooleanField({ initial: false }), + hasSave: new fields.BooleanField({ initial: false }), + source: new fields.SchemaField({ + actor: new fields.StringField(), + item: new fields.StringField(), + action: new fields.StringField() }) }; } - 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() { - const label = - this.hope.value > this.fear.value - ? 'DAGGERHEART.General.Hope' - : this.fear.value > this.hope.value - ? 'DAGGERHEART.General.Fear' - : 'DAGGERHEART.General.CriticalSuccess'; - - return game.i18n.localize(label); - } - - get colorful() { - return ( - game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).dualityColorScheme === - DualityRollColor.colorful.value - ); - } - - prepareDerivedData() { - this.hope.discarded = this.hope.value < this.fear.value; - this.fear.discarded = this.fear.value < this.hope.value; + get messageTemplate() { + return 'systems/daggerheart/templates/chat/duality-roll.hbs'; } } diff --git a/module/data/item/armor.mjs b/module/data/item/armor.mjs index b0fdf0ae..c7f5af0b 100644 --- a/module/data/item/armor.mjs +++ b/module/data/item/armor.mjs @@ -29,7 +29,6 @@ export default class DHArmor extends BaseDataItem { }) ), marks: new fields.SchemaField({ - max: new fields.NumberField({ initial: 6, integer: true }), value: new fields.NumberField({ initial: 0, integer: true }) }), baseThresholds: new fields.SchemaField({ diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index 492fcfe1..219b43aa 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -1,3 +1,5 @@ +import { actionsTypes } from '../action/_module.mjs'; + /** * Describes metadata about the item data model type * @typedef {Object} ItemDataModelMetadata @@ -50,4 +52,24 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { const data = { ...actorRollData, item: { ...this } }; return data; } + + async _preCreate(data, options, user) { + if(!this.constructor.metadata.hasInitialAction || !foundry.utils.isEmpty(this.actions)) return; + const actionType = { + weapon: 'attack' + }[this.constructor.metadata.type], + cls = actionsTypes.attack, + action = new cls( + { + _id: foundry.utils.randomID(), + type: actionType, + name: game.i18n.localize(SYSTEM.ACTIONS.actionTypes[actionType].name), + ...cls.getSourceConfig(this.parent) + }, + { + parent: this.parent + } + ); + this.updateSource({actions: [action]}); + } } diff --git a/module/data/item/feature.mjs b/module/data/item/feature.mjs index 026580c3..0e502eb6 100644 --- a/module/data/item/feature.mjs +++ b/module/data/item/feature.mjs @@ -1,5 +1,4 @@ import { getTier } from '../../helpers/utils.mjs'; -import DHAction from '../action/action.mjs'; import BaseDataItem from './base.mjs'; import ActionField from '../fields/actionField.mjs'; diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index 3da7705e..e7551a21 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -2,7 +2,7 @@ import BaseDataItem from './base.mjs'; import FormulaField from '../fields/formulaField.mjs'; import ActionField from '../fields/actionField.mjs'; import { weaponFeatures } from '../../config/itemConfig.mjs'; -import { actionsTypes } from '../../data/_module.mjs'; +import { actionsTypes } from '../action/_module.mjs'; export default class DHWeapon extends BaseDataItem { /** @inheritDoc */ @@ -14,7 +14,8 @@ export default class DHWeapon extends BaseDataItem { isQuantifiable: true, embedded: { feature: 'featureTest' - } + }, + hasInitialAction: true }); } diff --git a/module/dialogs/d20RollDialog.mjs b/module/dialogs/d20RollDialog.mjs new file mode 100644 index 00000000..0b64615b --- /dev/null +++ b/module/dialogs/d20RollDialog.mjs @@ -0,0 +1,113 @@ +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; + +export default class D20RollDialog extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(config = {}, options = {}) { + super(options); + + this.config = config; + this.config.experiences = []; + + if (config.source?.action) { + this.item = config.data.parent.items.get(config.source.item); + this.action = + config.data.attack?._id == config.source.action + ? config.data.attack + : this.item.system.actions.find(a => a._id === config.source.action); + } + } + + static DEFAULT_OPTIONS = { + tag: 'form', + id: 'roll-selection', + classes: ['daggerheart', 'views', 'roll-selection'], + position: { + width: 400, + height: 'auto' + }, + actions: { + updateIsAdvantage: this.updateIsAdvantage, + selectExperience: this.selectExperience, + submitRoll: this.submitRoll + }, + form: { + handler: this.updateRollConfiguration, + submitOnChange: true, + submitOnClose: false + } + }; + + /** @override */ + static PARTS = { + costSelection: { + id: 'costSelection', + template: 'systems/daggerheart/templates/views/costSelection.hbs' + }, + rollSelection: { + id: 'rollSelection', + template: 'systems/daggerheart/templates/views/rollSelection.hbs' + } + }; + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.hasRoll = !!this.config.roll; + context.experiences = Object.keys(this.config.data.experiences).map(id => ({ + id, + ...this.config.data.experiences[id] + })); + context.selectedExperiences = this.config.experiences; + context.advantage = this.config.advantage; + /* context.diceOptions = this.diceOptions; */ + context.canRoll = true; + if (this.config.costs?.length) { + const updatedCosts = this.action.calcCosts(this.config.costs); + context.costs = updatedCosts; + context.canRoll = this.action.hasCost(updatedCosts); + } + if (this.config.uses?.max) { + context.uses = this.action.calcUses(this.config.uses); + context.canRoll = context.canRoll && this.action.hasUses(context.uses); + } + console.log(context, _options) + return context; + } + + static updateRollConfiguration(event, _, formData) { + const { ...rest } = foundry.utils.expandObject(formData.object); + if (this.config.costs) this.config.costs = foundry.utils.mergeObject(this.config.costs, rest.costs); + if (this.config.uses) this.config.uses = foundry.utils.mergeObject(this.config.uses, rest.uses); + this.render(); + } + + static updateIsAdvantage(_, button) { + const advantage = Number(button.dataset.advantage); + this.config.advantage = this.config.advantage === advantage ? 0 : advantage; + this.render(); + } + + static selectExperience(_, button) { + if (this.config.experiences.find(x => x === button.dataset.key)) { + this.config.experiences = this.config.experiences.filter(x => x !== button.dataset.key); + } else { + this.config.experiences = [...this.config.experiences, button.dataset.key]; + } + this.render(); + } + + static async submitRoll() { + await this.close({ submitted: true }); + } + + /** @override */ + _onClose(options = {}) { + if (!options.submitted) this.config = false; + } + + static async configure(config = {}, options={}) { + return new Promise(resolve => { + const app = new this(config, options); + app.addEventListener('close', () => resolve(app.config), { once: true }); + app.render({ force: true }); + }); + } +} diff --git a/module/dialogs/damageDialog.mjs b/module/dialogs/damageDialog.mjs new file mode 100644 index 00000000..5915f6e2 --- /dev/null +++ b/module/dialogs/damageDialog.mjs @@ -0,0 +1,59 @@ +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; + +export default class DamageDialog extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(config={}, options={}) { + super(options); + + this.config = config; + } + + static DEFAULT_OPTIONS = { + tag: 'form', + id: 'roll-selection', + classes: ['daggerheart', 'views', 'damage-selection'], + position: { + width: 400, + height: 'auto' + }, + actions: { + submitRoll: this.submitRoll + }, + form: { + handler: this.updateRollConfiguration, + submitOnChange: true, + submitOnClose: false + } + }; + + /** @override */ + static PARTS = { + damageSelection: { + id: 'damageSelection', + template: 'systems/daggerheart/templates/views/damageSelection.hbs' + } + }; + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.title = this.config.title; + context.formula = this.config.formula; + return context; + } + + static async submitRoll() { + await this.close({ submitted: true }); + } + + /** @override */ + _onClose(options={}) { + if ( !options.submitted ) this.config = false; + } + + static async configure(config={}) { + return new Promise(resolve => { + const app = new this(config); + app.addEventListener("close", () => resolve(app.config), { once: true }); + app.render({ force: true }); + }); + } +} \ No newline at end of file diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index a0bfefd3..f13f81cd 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -1,9 +1,6 @@ import DamageSelectionDialog from '../applications/damageSelectionDialog.mjs'; -import NpcRollSelectionDialog from '../applications/npcRollSelectionDialog.mjs'; -import RollSelectionDialog from '../applications/rollSelectionDialog.mjs'; import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs'; -import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs'; -import DHDualityRoll from '../data/chat-message/dualityRoll.mjs'; +import DamageReductionDialog from '../applications/damageReductionDialog.mjs'; export default class DhpActor extends Actor { async _preCreate(data, options, user) { @@ -268,150 +265,26 @@ export default class DhpActor extends Actor { * @param {boolean} [config.roll.simple=false] * @param {string} [config.roll.type] * @param {number} [config.roll.difficulty] - * @param {any} [config.damage] + * @param {boolean} [config.hasDamage] + * @param {boolean} [config.hasEffect] * @param {object} [config.chatMessage] * @param {string} config.chatMessage.template * @param {boolean} [config.chatMessage.mute] - * @param {boolean} [config.checkTarget] + * @param {object} [config.targets] + * @param {object} [config.costs] */ async diceRoll(config) { - let hopeDice = 'd12', - fearDice = 'd12', - advantageDice = 'd6', - disadvantageDice = 'd6', - advantage = config.event.altKey ? true : config.event.ctrlKey ? false : null, - targets, - damage = config.damage, - modifiers = this.formatRollModifier(config.roll), - rollConfig, - formula, - hope, - fear; + config.source = {...(config.source ?? {}), actor: this.uuid}; + config.data = this.getRollData(); + return await this.rollClass.build(config); + } - if (!config.event.shiftKey && !config.event.altKey && !config.event.ctrlKey) { - const dialogClosed = new Promise((resolve, _) => { - this.type === 'character' - ? new RollSelectionDialog( - this.system.experiences, - this.system.resources.hope.value, - resolve - ).render(true) - : new NpcRollSelectionDialog(this.system.experiences, resolve).render(true); - }); - rollConfig = await dialogClosed; + get rollClass() { + return CONFIG.Dice.daggerheart[this.type === 'character' ? 'DualityRoll' : 'D20Roll']; + } - advantage = rollConfig.advantage; - hopeDice = rollConfig.hope; - fearDice = rollConfig.fear; - - rollConfig.experiences.forEach(x => - modifiers.push({ - value: x.value, - label: x.value >= 0 ? `+${x.value}` : `-${x.value}`, - title: x.description - }) - ); - - if (this.type === 'character') { - const automateHope = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation).hope; - - if (automateHope && result.hopeUsed) { - await this.update({ - 'system.resources.hope.value': this.system.resources.hope.value - result.hopeUsed - }); - } - } - } - - if (this.type === 'character') { - formula = `1${hopeDice} + 1${fearDice}${advantage === true ? ` + 1d6` : advantage === false ? ` - 1d6` : ''}`; - } else { - formula = `${advantage === true || advantage === false ? 2 : 1}d20${advantage === true ? 'kh' : advantage === false ? 'kl' : ''}`; - } - formula += ` ${modifiers.map(x => `+ ${x.value}`).join(' ')}`; - const roll = await Roll.create(formula).evaluate(); - const dice = roll.dice.flatMap(dice => ({ - denomination: dice.denomination, - number: dice.number, - total: dice.total, - results: dice.results.map(result => ({ result: result.result, discarded: !result.active })) - })); - - if (this.type === 'character') { - setDiceSoNiceForDualityRoll(roll, advantage); - 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 && - config.roll.type === 'action' - ) { - if (hope > fear) { - await this.update({ - 'system.resources.hope.value': Math.min( - this.system.resources.hope.value + 1, - this.system.resources.hope.max - ) - }); - } else if (hope === fear) { - await this.update({ - 'system.resources': { - 'hope.value': Math.min( - this.system.resources.hope.value + 1, - this.system.resources.hope.max - ), - 'stress.value': Math.max(this.system.resources.stress.value - 1, 0) - } - }); - } - } - } - - if (config.checkTarget) { - targets = Array.from(game.user.targets).map(x => { - const target = { - id: x.id, - name: x.actor.name, - img: x.actor.img, - difficulty: x.actor.system.difficulty, - evasion: x.actor.system.evasion?.value - }; - - target.hit = target.difficulty ? roll.total >= target.difficulty : roll.total >= target.evasion; - - return target; - }); - } - - if (config.chatMessage) { - const configRoll = { - title: config.title, - origin: this.id, - dice, - roll, - modifiers: modifiers.filter(x => x.label), - advantageState: advantage - }; - if (this.type === 'character') { - configRoll.hope = { dice: hopeDice, value: hope }; - configRoll.fear = { dice: fearDice, value: fear }; - configRoll.advantage = { dice: advantageDice, value: roll.dice[2]?.results[0].result ?? null }; - } - if (damage) configRoll.damage = damage; - if (targets) configRoll.targets = targets; - const systemData = - this.type === 'character' && !config.roll.simple ? new DHDualityRoll(configRoll) : configRoll, - cls = getDocumentClass('ChatMessage'), - msg = new cls({ - type: config.chatMessage.type ?? 'dualityRoll', - sound: config.chatMessage.mute ? null : CONFIG.sounds.dice, - system: systemData, - content: config.chatMessage.template, - rolls: [roll] - }); - - await cls.create(msg.toObject()); - } - return roll; + getRollData() { + return this.system; } formatRollModifier(roll) { @@ -507,124 +380,76 @@ export default class DhpActor extends Actor { ? 1 : 0; - const update = { - 'system.resources.hitPoints.value': Math.min( - this.system.resources.hitPoints.value + hpDamage, - this.system.resources.hitPoints.max - ) - }; - - if (game.user.isGM) { - await this.update(update); + if ( + this.type === 'character' && + this.system.armor && + this.system.armor.system.marks.value < this.system.armorScore + ) { + new Promise((resolve, reject) => { + new DamageReductionDialog(resolve, reject, this, hpDamage).render(true); + }) + .then(async ({ modifiedDamage, armorSpent, stressSpent }) => { + const resources = [ + { value: modifiedDamage, type: 'hitPoints' }, + ...(armorSpent ? [{ value: armorSpent, type: 'armorStack' }] : []), + ...(stressSpent ? [{ value: stressSpent, type: 'stress' }] : []) + ]; + await this.modifyResource(resources); + }) + .catch(() => { + const cls = getDocumentClass('ChatMessage'); + const msg = new cls({ + user: game.user.id, + content: game.i18n.format('DAGGERHEART.DamageReduction.Notifications.DamageIgnore', { + character: this.name + }) + }); + cls.create(msg.toObject()); + }); } else { - await game.socket.emit(`system.${SYSTEM.id}`, { - action: socketEvent.GMUpdate, - data: { - action: GMUpdateEvent.UpdateDocument, - uuid: this.uuid, - update: update - } - }); + await this.modifyResource([{ value: hpDamage, type: 'hitPoints' }]); } } - async takeHealing(healing, type) { - let update = {}; - switch (type) { - case SYSTEM.GENERAL.healingTypes.health.id: - update = { - 'system.resources.hitPoints.value': Math.min( - this.system.resources.hitPoints.value + healing, - this.system.resources.hitPoints.max - ) - }; - break; - case SYSTEM.GENERAL.healingTypes.stress.id: - update = { - 'system.resources.stress.value': Math.min( - this.system.resources.stress.value + healing, - this.system.resources.stress.max - ) - }; - break; - } - - if (game.user.isGM) { - await this.update(update); - } else { - await game.socket.emit(`system.${SYSTEM.id}`, { - action: socketEvent.GMUpdate, - data: { - action: GMUpdateEvent.UpdateDocument, - uuid: this.uuid, - update: update - } - }); - } + async takeHealing(resources) { + resources.forEach(r => (r.value *= -1)); + await this.modifyResource(resources); } - //Move to action-scope? - async useAction(action) { - const userTargets = Array.from(game.user.targets); - const otherTarget = action.target.type === SYSTEM.ACTIONS.targetTypes.other.id; - if (otherTarget && userTargets.length === 0) { - ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.ActionRequiresTarget')); - return; - } - - if (action.cost.type != null && action.cost.value != null) { - if ( - this.system.resources[action.cost.type].value - action.cost.value <= - this.system.resources[action.cost.type].min - ) { - ui.notifications.error(game.i18n.localize(`Insufficient ${action.cost.type} to use this ability`)); - return; + async modifyResource(resources) { + if (!resources.length) return; + let updates = { actor: { target: this, resources: {} }, armor: { target: this.system.armor, resources: {} } }; + resources.forEach(r => { + switch (r.type) { + case 'armorStack': + updates.armor.resources['system.marks.value'] = Math.max( + Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore), + 0 + ); + break; + default: + updates.actor.resources[`system.resources.${r.type}.value`] = Math.max( + Math.min(this.system.resources[r.type].value + r.value, this.system.resources[r.type].max), + 0 + ); + break; } - } - - // const targets = otherTarget ? userTargets : [game.user.character]; - if (action.damage.type) { - let roll = { formula: action.damage.value, result: action.damage.value }; - if (Number.isNaN(Number.parseInt(action.damage.value))) { - roll = await new Roll(`1${action.damage.value}`).evaluate(); + }); + Object.values(updates).forEach(async u => { + if (Object.keys(u.resources).length > 0) { + if (game.user.isGM) { + await u.target.update(u.resources); + } else { + await game.socket.emit(`system.${SYSTEM.id}`, { + action: socketEvent.GMUpdate, + data: { + action: GMUpdateEvent.UpdateDocument, + uuid: u.target.uuid, + update: u.resources + } + }); + } } - - const cls = getDocumentClass('ChatMessage'); - const msg = new cls({ - user: game.user.id, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/damage-roll.hbs', - { - roll: roll.formula, - total: roll.result, - type: action.damage.type - } - ) - }); - - cls.create(msg.toObject()); - } - - if (action.healing.type) { - let roll = { formula: action.healing.value, result: action.healing.value }; - if (Number.isNaN(Number.parseInt(action.healing.value))) { - roll = await new Roll(`1${action.healing.value}`).evaluate(); - } - - const cls = getDocumentClass('ChatMessage'); - const msg = new cls({ - user: game.user.id, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/healing-roll.hbs', - { - roll: roll.formula, - total: roll.result, - type: action.healing.type - } - ) - }); - - cls.create(msg.toObject()); - } + }); } } diff --git a/module/documents/item.mjs b/module/documents/item.mjs index ca86ce98..195b9c27 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -131,14 +131,7 @@ export default class DhpItem extends Item { action = await this.selectActionDialog(); } if (action) response = action.use(event); - // Check Target - // If action.roll => Roll Dialog - // Else If action.cost => Cost Dialog - // Then - // Apply Cost - // Apply Effect } - // Display Item Card in chat return response; } diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 7f711ddb..d62ff148 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -233,3 +233,29 @@ Roll.replaceFormulaData = function (formula, data, { missing, warn = false } = { formula = terms.reduce((a, c) => a.replaceAll(`@${c.term}`, data[c.term] ?? c.default), formula); return nativeReplaceFormulaData(formula, data, { missing, warn }); }; + +export const getDamageLabel = damage => { + switch (damage) { + case 3: + return game.i18n.localize('DAGGERHEART.General.Damage.Severe'); + case 2: + return game.i18n.localize('DAGGERHEART.General.Damage.Major'); + case 1: + return game.i18n.localize('DAGGERHEART.General.Damage.Minor'); + case 0: + return game.i18n.localize('DAGGERHEART.General.Damage.None'); + } +}; + +export const damageKeyToNumber = key => { + switch (key) { + case 'severe': + return 3; + case 'major': + return 2; + case 'minor': + return 1; + case 'none': + return 0; + } +}; diff --git a/module/ui/chatLog.mjs b/module/ui/chatLog.mjs index 75ad4bf7..3f537adf 100644 --- a/module/ui/chatLog.mjs +++ b/module/ui/chatLog.mjs @@ -16,9 +16,18 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo } addChatListeners = async (app, html, data) => { - html.querySelectorAll('.duality-action').forEach(element => + html.querySelectorAll('.duality-action-damage').forEach(element => element.addEventListener('click', event => this.onRollDamage(event, data.message)) ); + html.querySelectorAll('.duality-action-healing').forEach(element => + element.addEventListener('click', event => this.onRollHealing(event, data.message)) + ); + html.querySelectorAll('.target-save-container').forEach(element => + element.addEventListener('click', event => this.onRollSave(event, data.message)) + ); + html.querySelectorAll('.duality-action-effect').forEach(element => + element.addEventListener('click', event => this.onApplyEffect(event, data.message)) + ); html.querySelectorAll('.target-container').forEach(element => { element.addEventListener('mouseenter', this.hoverTarget); element.addEventListener('mouseleave', this.unhoverTarget); @@ -27,7 +36,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo html.querySelectorAll('.damage-button').forEach(element => element.addEventListener('click', event => this.onDamage(event, data.message)) ); - html.querySelectorAll('.healing-button').forEach(element => element.addEventListener('click', this.onHealing)); + html.querySelectorAll('.healing-button').forEach(element => + element.addEventListener('click', event => this.onHealing(event, data.message)) + ); html.querySelectorAll('.target-indicator').forEach(element => element.addEventListener('click', this.onToggleTargets) ); @@ -54,17 +65,65 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo super.close(options); } + async getActor(id) { + // return game.actors.get(id); + return await fromUuid(id); + } + + getAction(actor, itemId, actionId) { + const item = actor.items.get(itemId), + action = + actor.system.attack?._id === actionId + ? actor.system.attack + : item?.system?.actions?.find(a => a._id === actionId); + return action; + } + onRollDamage = async (event, message) => { event.stopPropagation(); - const actor = game.actors.get(message.system.origin); + const actor = await this.getActor(message.system.source.actor); if (!actor || !game.user.isGM) return true; + if (message.system.source.item && message.system.source.action) { + const action = this.getAction(actor, message.system.source.item, message.system.source.action); + if (!action || !action?.rollDamage) return; + await action.rollDamage(event, message); + } + }; - await actor.damageRoll( - message.system.title, - message.system.damage, - message.system.targets.filter(x => x.hit).map(x => ({ id: x.id, name: x.name, img: x.img })), - event.shiftKey - ); + onRollHealing = async (event, message) => { + event.stopPropagation(); + const actor = await this.getActor(message.system.source.actor); + if (!actor || !game.user.isGM) return true; + if (message.system.source.item && message.system.source.action) { + const action = this.getAction(actor, message.system.source.item, message.system.source.action); + if (!action || !action?.rollHealing) return; + await action.rollHealing(event, message); + } + }; + + onRollSave = async (event, message) => { + event.stopPropagation(); + const actor = await this.getActor(message.system.source.actor), + tokenId = event.target.closest('[data-token]')?.dataset.token, + token = game.canvas.tokens.get(tokenId); + if (!token?.actor || !token.isOwner) return true; + console.log(token.actor.canUserModify(game.user, 'update')); + if (message.system.source.item && message.system.source.action) { + const action = this.getAction(actor, message.system.source.item, message.system.source.action); + if (!action || !action?.hasSave) return; + action.rollSave(token, event, message); + } + }; + + onApplyEffect = async (event, message) => { + event.stopPropagation(); + const actor = await this.getActor(message.system.source.actor); + if (!actor || !game.user.isGM) return true; + if (message.system.source.item && message.system.source.action) { + const action = this.getAction(actor, message.system.source.item, message.system.source.action); + if (!action || !action?.applyEffects) return; + await action.applyEffects(event, message); + } }; hoverTarget = event => { @@ -95,24 +154,36 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo ? message.system.targets.map(target => game.canvas.tokens.get(target.id)) : Array.from(game.user.targets); + if(message.system.onSave && event.currentTarget.dataset.targetHit) { + console.log(message.system.targets) + const pendingingSaves = message.system.targets.filter(target => target.hit && target.saved.success === null); + if(pendingingSaves.length) { + const confirm = await foundry.applications.api.DialogV2.confirm({ + window: {title: "Pending Reaction Rolls found"}, + content: `
Some Tokens still need to roll their Reaction Roll.
Are you sure you want to continue ?
Undone reaction rolls will be considered as failed
` + }); + if ( !confirm ) return; + } + } + if (targets.length === 0) ui.notifications.info(game.i18n.localize('DAGGERHEART.Notification.Info.NoTargetsSelected')); - - for (var target of targets) { - await target.actor.takeDamage(message.system.damage.total, message.system.damage.type); + for (let target of targets) { + let damage = message.system.roll.total; + if(message.system.onSave && message.system.targets.find(t => t.id === target.id)?.saved?.success === true) damage = Math.ceil(damage * (SYSTEM.ACTIONS.damageOnSave[message.system.onSave]?.mod ?? 1)); + await target.actor.takeDamage(damage, message.system.roll.type); } }; - onHealing = async event => { + onHealing = async (event, message) => { event.stopPropagation(); - const healing = Number.parseInt(event.currentTarget.dataset.value); const targets = Array.from(game.user.targets); if (targets.length === 0) ui.notifications.info(game.i18n.localize('DAGGERHEART.Notification.Info.NoTargetsSelected')); - + for (var target of targets) { - await target.actor.takeHealing(healing, event.currentTarget.dataset.type); + await target.actor.takeHealing([{ value: message.system.roll.total, type: message.system.roll.type }]); } }; @@ -139,7 +210,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo event.stopPropagation(); const action = message.system.actions[Number.parseInt(event.currentTarget.dataset.index)]; - const actor = game.actors.get(message.system.origin); + const actor = game.actors.get(message.system.source.actor); await actor.useAction(action); }; diff --git a/styles/application.less b/styles/application.less index 8cdbf1ac..66735594 100644 --- a/styles/application.less +++ b/styles/application.less @@ -227,6 +227,10 @@ div.daggerheart.views.multiclass { filter: invert(0%) sepia(100%) saturate(0%) hue-rotate(21deg) brightness(17%) contrast(103%); } } + + #roll-selection-costSelection footer { + display: none; + } .roll-dialog-container { .disadvantage, @@ -368,6 +372,7 @@ div.daggerheart.views.multiclass { } } + .roll-dialog-experience-container { display: flex; align-items: flex-start; diff --git a/styles/chat.less b/styles/chat.less index e075274f..15ff39e7 100644 --- a/styles/chat.less +++ b/styles/chat.less @@ -6,6 +6,13 @@ } } +fieldset.daggerheart.chat { + padding: 0; + border-left-width: 0; + border-right-width: 0; + border-bottom-width: 0; +} + .daggerheart.chat { &.downtime { display: flex; @@ -227,19 +234,35 @@ background: @miss; } - img { - flex: 0; + img, .target-save-container { width: 22px; height: 22px; - margin-left: 8px; align-self: center; border-color: transparent; } + img { + flex: 0; + margin-left: 8px; + } + + .target-save-container { + margin-right: 8px; + justify-content: center; + display: flex; + align-items: center; + min-height: unset; + border: 1px solid black; + } + .target-inner-container { flex: 1; display: flex; justify-content: center; + font-size: var(--font-size-16); + } + + &:not(:has(.target-save-container)) .target-inner-container { margin-right: @hugeMargin; } } @@ -314,12 +337,29 @@ width: 80px; } } + + [data-use-perm='false'] { + pointer-events: none; + border-color: transparent; + } + [data-view-perm='false'] { + > * { + display: none; + } + &::after { + content: "??"; + } + } } .theme-colorful { .chat-message.duality { border-color: black; padding: 8px 0 0 0; + fieldset.daggerheart.chat { + border-top-width: 0; + display: contents; + } .message-header { color: var(--color-light-3); padding: 0 8px; @@ -450,6 +490,10 @@ .duality-action { border-radius: 0 6px 0 0; margin-left: -8px; + &.duality-action-effect { + border-top-left-radius: 6px; + margin-left: initial; + } } .duality-result { border-radius: 6px 0 0 0; diff --git a/styles/daggerheart.css b/styles/daggerheart.css index fcf4dd04..aaae3beb 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -1400,6 +1400,12 @@ .chat-message .dice-title { display: none; } +fieldset.daggerheart.chat { + padding: 0; + border-left-width: 0; + border-right-width: 0; + border-bottom-width: 0; +} .daggerheart.chat.downtime { display: flex; flex-direction: column; @@ -1553,18 +1559,32 @@ .daggerheart.chat.roll .target-section .target-container.miss { background: #ff0000; } -.daggerheart.chat.roll .target-section .target-container img { - flex: 0; +.daggerheart.chat.roll .target-section .target-container img, +.daggerheart.chat.roll .target-section .target-container .target-save-container { width: 22px; height: 22px; - margin-left: 8px; align-self: center; border-color: transparent; } +.daggerheart.chat.roll .target-section .target-container img { + flex: 0; + margin-left: 8px; +} +.daggerheart.chat.roll .target-section .target-container .target-save-container { + margin-right: 8px; + justify-content: center; + display: flex; + align-items: center; + min-height: unset; + border: 1px solid black; +} .daggerheart.chat.roll .target-section .target-container .target-inner-container { flex: 1; display: flex; justify-content: center; + font-size: var(--font-size-16); +} +.daggerheart.chat.roll .target-section .target-container:not(:has(.target-save-container)) .target-inner-container { margin-right: 32px; } .daggerheart.chat.roll .dice-actions { @@ -1622,10 +1642,24 @@ .daggerheart.chat.domain-card img { width: 80px; } +.daggerheart.chat [data-use-perm='false'] { + pointer-events: none; + border-color: transparent; +} +.daggerheart.chat [data-view-perm='false'] > * { + display: none; +} +.daggerheart.chat [data-view-perm='false']::after { + content: "??"; +} .theme-colorful .chat-message.duality { border-color: black; padding: 8px 0 0 0; } +.theme-colorful .chat-message.duality fieldset.daggerheart.chat { + border-top-width: 0; + display: contents; +} .theme-colorful .chat-message.duality .message-header { color: var(--color-light-3); padding: 0 8px; @@ -1752,6 +1786,10 @@ border-radius: 0 6px 0 0; margin-left: -8px; } +.theme-colorful .chat-message.duality .message-content .dice-result .dice-actions .duality-action.duality-action-effect { + border-top-left-radius: 6px; + margin-left: initial; +} .theme-colorful .chat-message.duality .message-content .dice-result .dice-actions .duality-result { border-radius: 6px 0 0 0; margin-right: -8px; @@ -1978,6 +2016,9 @@ div.daggerheart.views.multiclass { .daggerheart.views.roll-selection .roll-selection-container i { filter: invert(0%) sepia(100%) saturate(0%) hue-rotate(21deg) brightness(17%) contrast(103%); } +.daggerheart.views.roll-selection #roll-selection-costSelection footer { + display: none; +} .daggerheart.views.roll-selection .roll-dialog-container .disadvantage, .daggerheart.views.roll-selection .roll-dialog-container .advantage { border: 2px solid #708090; @@ -3110,6 +3151,125 @@ div.daggerheart.views.multiclass { .daggerheart.views.ownership-selection .ownership-outer-container .ownership-container select { margin: 4px 0; } +.daggerheart.views.damage-reduction .window-content { + padding: 8px 0; +} +.daggerheart.views.damage-reduction .damage-reduction-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; +} +.daggerheart.views.damage-reduction .damage-reduction-container .section-container { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; +} +.daggerheart.views.damage-reduction .damage-reduction-container .padded { + padding: 0 8px; +} +.daggerheart.views.damage-reduction .damage-reduction-container .armor-title { + margin: 0; + white-space: nowrap; +} +.daggerheart.views.damage-reduction .damage-reduction-container .resources-container { + display: flex; + gap: 8px; + width: 100%; +} +.daggerheart.views.damage-reduction .damage-reduction-container .resources-container .resource-container { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; +} +.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection { + display: flex; + align-items: center; + width: 100%; + margin: 0; +} +.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection .mark-selection-inner { + display: flex; + gap: 2px; +} +.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection .mark-selection-inner:not(:last-child) { + margin-right: 8px; +} +.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection .mark-selection-inner .mark-container { + cursor: pointer; + border: 1px solid light-dark(#18162e, #f3c267); + border-radius: 6px; + height: 26px; + padding: 0 1px; + font-size: 18px; + display: flex; + align-items: center; + justify-content: center; + opacity: 0.4; +} +.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection .mark-selection-inner .mark-container.selected { + opacity: 1; +} +.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection .mark-selection-inner .mark-container.inactive { + cursor: initial; + opacity: 0.2; +} +.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection .mark-selection-inner .mark-container .fa-shield { + position: relative; + right: 0.5px; +} +.daggerheart.views.damage-reduction .damage-reduction-container .stress-reduction-container { + margin: 0; + width: 100%; +} +.daggerheart.views.damage-reduction .damage-reduction-container .stress-reduction-container .stress-reduction { + border: 1px solid light-dark(#18162e, #f3c267); + border-radius: 6px; + height: 26px; + padding: 0 4px; + font-size: 18px; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + opacity: 0.4; +} +.daggerheart.views.damage-reduction .damage-reduction-container .stress-reduction-container .stress-reduction.active { + opacity: 1; + cursor: pointer; +} +.daggerheart.views.damage-reduction .damage-reduction-container .stress-reduction-container .stress-reduction.selected { + opacity: 1; + background: var(--color-warm-2); + color: white; +} +.daggerheart.views.damage-reduction .damage-reduction-container .stress-reduction-container .stress-reduction .stress-reduction-cost { + display: flex; + align-items: center; +} +.daggerheart.views.damage-reduction .damage-reduction-container .markers-subtitle { + margin: -4px 0 0 0; +} +.daggerheart.views.damage-reduction .damage-reduction-container .markers-subtitle.bold { + font-variant: all-small-caps; + font-weight: bold; +} +.daggerheart.views.damage-reduction .damage-reduction-container footer { + display: flex; + width: 100%; +} +.daggerheart.views.damage-reduction .damage-reduction-container footer button { + flex: 1; +} +.daggerheart.views.damage-reduction .damage-reduction-container footer button .damage-value { + font-weight: bold; +} +.daggerheart.views.damage-reduction .damage-reduction-container footer button .damage-value.reduced-value { + opacity: 0.4; + text-decoration: line-through; +} :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; @@ -3979,7 +4139,7 @@ div.daggerheart.views.multiclass { z-index: 1; color: light-dark(#18162e50, #efe6d850); } -.application.sheet.daggerheart.actor.dh-style.character .tab.loadout .search-section .btn-toogle-view { +.application.sheet.daggerheart.actor.dh-style.character .tab.loadout .search-section .btn-toggle-view { background: light-dark(#18162e10, #18162e); border: 1px solid light-dark(#18162e, #f3c267); border-radius: 15px; @@ -3987,24 +4147,24 @@ div.daggerheart.views.multiclass { gap: 0; width: 62px; } -.application.sheet.daggerheart.actor.dh-style.character .tab.loadout .search-section .btn-toogle-view span { +.application.sheet.daggerheart.actor.dh-style.character .tab.loadout .search-section .btn-toggle-view span { margin: 1px; width: 26px; color: light-dark(#18162e, #f3c267); } -.application.sheet.daggerheart.actor.dh-style.character .tab.loadout .search-section .btn-toogle-view span.list-icon i { +.application.sheet.daggerheart.actor.dh-style.character .tab.loadout .search-section .btn-toggle-view span.list-icon i { margin-left: 3px; } -.application.sheet.daggerheart.actor.dh-style.character .tab.loadout .search-section .btn-toogle-view span.grid-icon i { +.application.sheet.daggerheart.actor.dh-style.character .tab.loadout .search-section .btn-toggle-view span.grid-icon i { margin-right: 3px; } -.application.sheet.daggerheart.actor.dh-style.character .tab.loadout .search-section .btn-toogle-view span.list-active { +.application.sheet.daggerheart.actor.dh-style.character .tab.loadout .search-section .btn-toggle-view span.list-active { border-radius: 32px 3px 3px 32px; background-color: light-dark(#18162e, #f3c267); color: light-dark(#efe6d8, #18162e); padding: 2px; } -.application.sheet.daggerheart.actor.dh-style.character .tab.loadout .search-section .btn-toogle-view span.grid-active { +.application.sheet.daggerheart.actor.dh-style.character .tab.loadout .search-section .btn-toggle-view span.grid-active { border-radius: 3px 32px 32px 3px; background-color: light-dark(#18162e, #f3c267); color: light-dark(#efe6d8, #18162e); diff --git a/styles/daggerheart.less b/styles/daggerheart.less index 67cbdc9b..c1d19c7a 100755 --- a/styles/daggerheart.less +++ b/styles/daggerheart.less @@ -11,6 +11,7 @@ @import './characterCreation.less'; @import './levelup.less'; @import './ownershipSelection.less'; +@import './damageReduction.less'; @import './resources.less'; @import './countdown.less'; @import './settings.less'; diff --git a/styles/damageReduction.less b/styles/damageReduction.less new file mode 100644 index 00000000..e3ffc2e9 --- /dev/null +++ b/styles/damageReduction.less @@ -0,0 +1,145 @@ +.daggerheart.views.damage-reduction { + .window-content { + padding: 8px 0; + } + + .damage-reduction-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + + .section-container { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + } + + .padded { + padding: 0 8px; + } + + .armor-title { + margin: 0; + white-space: nowrap; + } + + .resources-container { + display: flex; + gap: 8px; + width: 100%; + + .resource-container { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + } + } + + .mark-selection { + display: flex; + align-items: center; + width: 100%; + margin: 0; + + .mark-selection-inner { + display: flex; + gap: 2px; + + &:not(:last-child) { + margin-right: 8px; + } + + .mark-container { + cursor: pointer; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + height: 26px; + padding: 0 1px; + font-size: 18px; + display: flex; + align-items: center; + justify-content: center; + opacity: 0.4; + + &.selected { + opacity: 1; + } + + &.inactive { + cursor: initial; + opacity: 0.2; + } + + .fa-shield { + position: relative; + right: 0.5px; + } + } + } + } + + .stress-reduction-container { + margin: 0; + width: 100%; + + .stress-reduction { + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + height: 26px; + padding: 0 4px; + font-size: 18px; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + opacity: 0.4; + + &.active { + opacity: 1; + cursor: pointer; + } + + &.selected { + opacity: 1; + background: var(--color-warm-2); + color: white; + } + + .stress-reduction-cost { + display: flex; + align-items: center; + } + } + } + + .markers-subtitle { + margin: -4px 0 0 0; + + &.bold { + font-variant: all-small-caps; + font-weight: bold; + } + } + + footer { + display: flex; + width: 100%; + + button { + flex: 1; + + .damage-value { + font-weight: bold; + + &.reduced-value { + opacity: 0.4; + text-decoration: line-through; + } + } + } + } + } +} diff --git a/styles/less/actors/character.less b/styles/less/actors/character.less index e8d7d5fd..e3833e3b 100644 --- a/styles/less/actors/character.less +++ b/styles/less/actors/character.less @@ -1,11 +1,11 @@ -@import '../utils/colors.less'; -@import '../utils/fonts.less'; - -.application.sheet.daggerheart.actor.dh-style.character { - .window-content { - display: flex; - flex-direction: row; - height: 100%; - width: 100%; - } -} +@import '../utils/colors.less'; +@import '../utils/fonts.less'; + +.application.sheet.daggerheart.actor.dh-style.character { + .window-content { + display: flex; + flex-direction: row; + height: 100%; + width: 100%; + } +} diff --git a/styles/less/actors/character/biography.less b/styles/less/actors/character/biography.less index c4ce81ac..635cf8d5 100644 --- a/styles/less/actors/character/biography.less +++ b/styles/less/actors/character/biography.less @@ -1,20 +1,20 @@ -@import '../../utils/colors.less'; -@import '../../utils/fonts.less'; - -.application.sheet.daggerheart.actor.dh-style.character { - .tab.biography { - .items-section { - display: flex; - flex-direction: column; - gap: 10px; - height: 100%; - overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 10%, black 98%, transparent 100%); - padding-bottom: 40px; - height: 100%; - - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; - } - } -} +@import '../../utils/colors.less'; +@import '../../utils/fonts.less'; + +.application.sheet.daggerheart.actor.dh-style.character { + .tab.biography { + .items-section { + display: flex; + flex-direction: column; + gap: 10px; + height: 100%; + overflow-y: auto; + mask-image: linear-gradient(0deg, transparent 0%, black 10%, black 98%, transparent 100%); + padding-bottom: 40px; + height: 100%; + + scrollbar-width: thin; + scrollbar-color: light-dark(@dark-blue, @golden) transparent; + } + } +} diff --git a/styles/less/actors/character/features.less b/styles/less/actors/character/features.less index 406544e6..6fc86ac3 100644 --- a/styles/less/actors/character/features.less +++ b/styles/less/actors/character/features.less @@ -1,20 +1,20 @@ -@import '../../utils/colors.less'; -@import '../../utils/fonts.less'; - -.application.sheet.daggerheart.actor.dh-style.character { - .tab.features { - .features-sections { - display: flex; - flex-direction: column; - gap: 10px; - overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%); - padding: 20px 0; - padding-top: 10px; - height: 95%; - - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; - } - } -} +@import '../../utils/colors.less'; +@import '../../utils/fonts.less'; + +.application.sheet.daggerheart.actor.dh-style.character { + .tab.features { + .features-sections { + display: flex; + flex-direction: column; + gap: 10px; + overflow-y: auto; + mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%); + padding: 20px 0; + padding-top: 10px; + height: 95%; + + scrollbar-width: thin; + scrollbar-color: light-dark(@dark-blue, @golden) transparent; + } + } +} diff --git a/styles/less/actors/character/inventory.less b/styles/less/actors/character/inventory.less index 1b2beaf8..a6caf22b 100644 --- a/styles/less/actors/character/inventory.less +++ b/styles/less/actors/character/inventory.less @@ -1,65 +1,65 @@ -@import '../../utils/colors.less'; -@import '../../utils/fonts.less'; - -.application.sheet.daggerheart.actor.dh-style.character { - .tab.inventory { - .search-section { - display: flex; - gap: 10px; - align-items: center; - - .search-bar { - position: relative; - color: light-dark(@dark-blue-50, @beige-50); - width: 100%; - padding-top: 5px; - - input { - border-radius: 50px; - font-family: @font-body; - background: light-dark(@dark-blue-10, @golden-10); - border: none; - outline: 2px solid transparent; - transition: all 0.3s ease; - padding: 0 20px; - - &:hover { - outline: 2px solid light-dark(@dark, @golden); - } - - &:placeholder { - color: light-dark(@dark-blue-50, @beige-50); - } - } - - .icon { - align-content: center; - height: 32px; - position: absolute; - right: 20px; - font-size: 16px; - z-index: 1; - color: light-dark(@dark-blue-50, @beige-50); - } - } - } - - .items-section { - display: flex; - flex-direction: column; - gap: 10px; - overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%); - padding: 20px 0; - height: 80%; - - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; - } - - .currency-section { - display: flex; - gap: 10px; - } - } -} +@import '../../utils/colors.less'; +@import '../../utils/fonts.less'; + +.application.sheet.daggerheart.actor.dh-style.character { + .tab.inventory { + .search-section { + display: flex; + gap: 10px; + align-items: center; + + .search-bar { + position: relative; + color: light-dark(@dark-blue-50, @beige-50); + width: 100%; + padding-top: 5px; + + input { + border-radius: 50px; + font-family: @font-body; + background: light-dark(@dark-blue-10, @golden-10); + border: none; + outline: 2px solid transparent; + transition: all 0.3s ease; + padding: 0 20px; + + &:hover { + outline: 2px solid light-dark(@dark, @golden); + } + + &:placeholder { + color: light-dark(@dark-blue-50, @beige-50); + } + } + + .icon { + align-content: center; + height: 32px; + position: absolute; + right: 20px; + font-size: 16px; + z-index: 1; + color: light-dark(@dark-blue-50, @beige-50); + } + } + } + + .items-section { + display: flex; + flex-direction: column; + gap: 10px; + overflow-y: auto; + mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%); + padding: 20px 0; + height: 80%; + + scrollbar-width: thin; + scrollbar-color: light-dark(@dark-blue, @golden) transparent; + } + + .currency-section { + display: flex; + gap: 10px; + } + } +} diff --git a/styles/less/actors/character/loadout.less b/styles/less/actors/character/loadout.less index 0eb514e2..bf1474a2 100644 --- a/styles/less/actors/character/loadout.less +++ b/styles/less/actors/character/loadout.less @@ -1,101 +1,101 @@ -@import '../../utils/colors.less'; -@import '../../utils/fonts.less'; - -.application.sheet.daggerheart.actor.dh-style.character { - .tab.loadout { - .search-section { - display: flex; - align-items: center; - justify-content: space-between; - - .search-bar { - position: relative; - color: light-dark(@dark-blue-50, @beige-50); - width: 80%; - padding-top: 5px; - - input { - border-radius: 50px; - font-family: @font-body; - background: light-dark(@dark-blue-10, @golden-10); - border: none; - outline: 2px solid transparent; - transition: all 0.3s ease; - padding: 0 20px; - - &:hover { - outline: 2px solid light-dark(@dark, @golden); - } - - &:placeholder { - color: light-dark(@dark-blue-50, @beige-50); - } - } - - .icon { - align-content: center; - height: 32px; - position: absolute; - right: 20px; - font-size: 16px; - z-index: 1; - color: light-dark(@dark-blue-50, @beige-50); - } - } - - .btn-toogle-view { - background: light-dark(@dark-blue-10, @dark-blue); - border: 1px solid light-dark(@dark-blue, @golden); - border-radius: 15px; - padding: 0; - gap: 0; - width: 62px; - - span { - margin: 1px; - width: 26px; - color: light-dark(@dark-blue, @golden); - - &.list-icon { - i { - margin-left: 3px; - } - } - &.grid-icon { - i { - margin-right: 3px; - } - } - - &.list-active { - border-radius: 32px 3px 3px 32px; - background-color: light-dark(@dark-blue, @golden); - color: light-dark(@beige, @dark-blue); - padding: 2px; - } - - &.grid-active { - border-radius: 3px 32px 32px 3px; - background-color: light-dark(@dark-blue, @golden); - color: light-dark(@beige, @dark-blue); - padding: 2px; - } - } - } - } - - .items-section { - display: flex; - flex-direction: column; - gap: 10px; - height: 100%; - overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 10%, black 98%, transparent 100%); - padding: 20px 0; - height: 90%; - - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; - } - } -} +@import '../../utils/colors.less'; +@import '../../utils/fonts.less'; + +.application.sheet.daggerheart.actor.dh-style.character { + .tab.loadout { + .search-section { + display: flex; + align-items: center; + justify-content: space-between; + + .search-bar { + position: relative; + color: light-dark(@dark-blue-50, @beige-50); + width: 80%; + padding-top: 5px; + + input { + border-radius: 50px; + font-family: @font-body; + background: light-dark(@dark-blue-10, @golden-10); + border: none; + outline: 2px solid transparent; + transition: all 0.3s ease; + padding: 0 20px; + + &:hover { + outline: 2px solid light-dark(@dark, @golden); + } + + &:placeholder { + color: light-dark(@dark-blue-50, @beige-50); + } + } + + .icon { + align-content: center; + height: 32px; + position: absolute; + right: 20px; + font-size: 16px; + z-index: 1; + color: light-dark(@dark-blue-50, @beige-50); + } + } + + .btn-toggle-view { + background: light-dark(@dark-blue-10, @dark-blue); + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 15px; + padding: 0; + gap: 0; + width: 62px; + + span { + margin: 1px; + width: 26px; + color: light-dark(@dark-blue, @golden); + + &.list-icon { + i { + margin-left: 3px; + } + } + &.grid-icon { + i { + margin-right: 3px; + } + } + + &.list-active { + border-radius: 32px 3px 3px 32px; + background-color: light-dark(@dark-blue, @golden); + color: light-dark(@beige, @dark-blue); + padding: 2px; + } + + &.grid-active { + border-radius: 3px 32px 32px 3px; + background-color: light-dark(@dark-blue, @golden); + color: light-dark(@beige, @dark-blue); + padding: 2px; + } + } + } + } + + .items-section { + display: flex; + flex-direction: column; + gap: 10px; + height: 100%; + overflow-y: auto; + mask-image: linear-gradient(0deg, transparent 0%, black 10%, black 98%, transparent 100%); + padding: 20px 0; + height: 90%; + + scrollbar-width: thin; + scrollbar-color: light-dark(@dark-blue, @golden) transparent; + } + } +} diff --git a/styles/less/actors/character/sheet.less b/styles/less/actors/character/sheet.less index 5317b3b5..6ca1ca4f 100644 --- a/styles/less/actors/character/sheet.less +++ b/styles/less/actors/character/sheet.less @@ -1,32 +1,32 @@ -@import '../../utils/colors.less'; -@import '../../utils/fonts.less'; - -.application.sheet.daggerheart.actor.dh-style.character { - .window-content { - display: grid; - grid-template-columns: 275px 1fr; - grid-template-rows: 283px 1fr; - gap: 15px 0; - height: 100%; - width: 100%; - - .character-sidebar-sheet { - grid-row: 1 / span 2; - grid-column: 1; - } - - .character-header-sheet { - grid-row: 1; - grid-column: 2; - } - - .tab { - grid-row: 2; - grid-column: 2; - } - - .old-sheet { - width: 500px; - } - } -} +@import '../../utils/colors.less'; +@import '../../utils/fonts.less'; + +.application.sheet.daggerheart.actor.dh-style.character { + .window-content { + display: grid; + grid-template-columns: 275px 1fr; + grid-template-rows: 283px 1fr; + gap: 15px 0; + height: 100%; + width: 100%; + + .character-sidebar-sheet { + grid-row: 1 / span 2; + grid-column: 1; + } + + .character-header-sheet { + grid-row: 1; + grid-column: 2; + } + + .tab { + grid-row: 2; + grid-column: 2; + } + + .old-sheet { + width: 500px; + } + } +} diff --git a/styles/less/actors/character/sidebar.less b/styles/less/actors/character/sidebar.less index 0a37aff4..bde3996c 100644 --- a/styles/less/actors/character/sidebar.less +++ b/styles/less/actors/character/sidebar.less @@ -1,310 +1,310 @@ -@import '../../utils/colors.less'; -@import '../../utils/fonts.less'; - -.application.sheet.daggerheart.actor.dh-style.character { - .character-sidebar-sheet { - width: 275px; - min-width: 275px; - border-right: 1px solid light-dark(@dark-blue, @golden); - background-image: url('../assets/parchments/dh-parchment-dark.png'); - - .theme-light & { - background: transparent; - } - - .portrait { - position: relative; - height: 235px; - width: 275px; - border-bottom: 1px solid light-dark(@dark-blue, @golden); - cursor: pointer; - - img { - height: 235px; - width: 275px; - object-fit: cover; - } - - .death-roll-btn { - display: none; - } - - &.death-roll { - filter: grayscale(1); - - .death-roll-btn { - display: flex; - position: absolute; - top: 30%; - right: 30%; - font-size: 6rem; - color: @beige; - - &:hover { - text-shadow: 0 0 8px @beige; - } - } - } - } - - .info-section { - position: relative; - display: flex; - flex-direction: column; - top: -20px; - gap: 30px; - margin-bottom: -10px; - - .resources-section { - display: flex; - justify-content: space-evenly; - - .status-bar { - position: relative; - width: 100px; - height: 40px; - justify-items: center; - - .status-label { - position: relative; - top: 40px; - height: 22px; - width: 79px; - clip-path: path('M0 0H79L74 16.5L39 22L4 16.5L0 0Z'); - background: light-dark(@dark-blue, @golden); - - h4 { - font-weight: bold; - text-align: center; - line-height: 18px; - color: light-dark(@beige, @dark-blue); - } - } - .status-value { - position: absolute; - display: flex; - padding: 0 6px; - font-size: 1.5rem; - align-items: center; - width: 100px; - height: 40px; - justify-content: center; - text-align: center; - z-index: 2; - color: @beige; - - input[type='number'] { - background: transparent; - font-size: 1.5rem; - width: 40px; - height: 30px; - text-align: center; - border: none; - outline: 2px solid transparent; - color: @beige; - - &.bar-input { - padding: 0; - color: @beige; - backdrop-filter: none; - background: transparent; - transition: all 0.3s ease; - - &:hover, - &:focus { - background: @semi-transparent-dark-blue; - backdrop-filter: blur(9.5px); - } - } - } - - .bar-label { - width: 40px; - } - } - .progress-bar { - position: absolute; - appearance: none; - width: 100px; - height: 40px; - border: 1px solid light-dark(@dark-blue, @golden); - border-radius: 6px; - z-index: 1; - - &::-webkit-progress-bar { - border: none; - background: @dark-blue; - border-radius: 6px; - } - &::-webkit-progress-value { - background: @gradient-hp; - border-radius: 6px; - } - &.stress-color::-webkit-progress-value { - background: @gradient-stress; - border-radius: 6px; - } - &::-moz-progress-value, - &::-moz-progress-bar { - border-radius: 6px; - } - - &::-moz-progress-bar { - background: @gradient-hp; - } - &.stress-color::-moz-progress-bar { - background: @gradient-stress; - border-radius: 6px; - } - } - } - } - - .status-section { - display: flex; - flex-wrap: wrap; - gap: 5px; - justify-content: center; - - .status-number { - justify-items: center; - - .status-value { - position: relative; - display: flex; - width: 50px; - height: 30px; - border: 1px solid light-dark(@dark-blue, @golden); - border-bottom: none; - border-radius: 6px 6px 0 0; - padding: 0 6px; - font-size: 1.2rem; - align-items: center; - justify-content: center; - background: light-dark(transparent, @dark-blue); - z-index: 2; - - &.armor-slots { - width: 80px; - height: 30px; - } - } - - .status-label { - padding: 2px 10px; - width: 100%; - border-radius: 3px; - background: light-dark(@dark-blue, @golden); - - h4 { - font-weight: bold; - text-align: center; - line-height: 18px; - font-size: 12px; - color: light-dark(@beige, @dark-blue); - } - } - } - } - } - - .items-sidebar-list { - display: flex; - flex-direction: column; - gap: 5px; - - .inventory-item { - padding: 0 10px; - } - } - - .equipment-section { - .title { - display: flex; - gap: 15px; - align-items: center; - - h3 { - font-size: 20px; - } - } - .items-list { - display: flex; - flex-direction: column; - gap: 10px; - align-items: center; - } - } - - .loadout-section { - .title { - display: flex; - gap: 15px; - align-items: center; - - h3 { - font-size: 20px; - } - } - } - - .experience-section { - .title { - display: flex; - gap: 15px; - align-items: center; - - h3 { - font-size: 20px; - } - } - - .experience-list { - display: flex; - flex-direction: column; - gap: 5px; - width: 100%; - margin-top: 10px; - align-items: center; - - .experience-row { - display: flex; - gap: 5px; - width: 250px; - align-items: center; - justify-content: space-between; - - input[type='text'] { - height: 32px; - width: 180px; - border: 1px solid transparent; - outline: 2px solid transparent; - font-size: 14px; - font-family: @font-body; - transition: all 0.3s ease; - color: light-dark(@dark, @beige); - - &:hover { - outline: 2px solid light-dark(@dark, @beige); - } - } - } - - .experience-value { - height: 25px; - width: 35px; - font-size: 14px; - font-family: @font-body; - color: light-dark(@dark, @beige); - align-content: center; - text-align: center; - background: url(../assets/svg/experience-shield.svg) no-repeat; - - .theme-light & { - background: url('../assets/svg/experience-shield-light.svg') no-repeat; - } - } - } - } - } -} +@import '../../utils/colors.less'; +@import '../../utils/fonts.less'; + +.application.sheet.daggerheart.actor.dh-style.character { + .character-sidebar-sheet { + width: 275px; + min-width: 275px; + border-right: 1px solid light-dark(@dark-blue, @golden); + background-image: url('../assets/parchments/dh-parchment-dark.png'); + + .theme-light & { + background: transparent; + } + + .portrait { + position: relative; + height: 235px; + width: 275px; + border-bottom: 1px solid light-dark(@dark-blue, @golden); + cursor: pointer; + + img { + height: 235px; + width: 275px; + object-fit: cover; + } + + .death-roll-btn { + display: none; + } + + &.death-roll { + filter: grayscale(1); + + .death-roll-btn { + display: flex; + position: absolute; + top: 30%; + right: 30%; + font-size: 6rem; + color: @beige; + + &:hover { + text-shadow: 0 0 8px @beige; + } + } + } + } + + .info-section { + position: relative; + display: flex; + flex-direction: column; + top: -20px; + gap: 30px; + margin-bottom: -10px; + + .resources-section { + display: flex; + justify-content: space-evenly; + + .status-bar { + position: relative; + width: 100px; + height: 40px; + justify-items: center; + + .status-label { + position: relative; + top: 40px; + height: 22px; + width: 79px; + clip-path: path('M0 0H79L74 16.5L39 22L4 16.5L0 0Z'); + background: light-dark(@dark-blue, @golden); + + h4 { + font-weight: bold; + text-align: center; + line-height: 18px; + color: light-dark(@beige, @dark-blue); + } + } + .status-value { + position: absolute; + display: flex; + padding: 0 6px; + font-size: 1.5rem; + align-items: center; + width: 100px; + height: 40px; + justify-content: center; + text-align: center; + z-index: 2; + color: @beige; + + input[type='number'] { + background: transparent; + font-size: 1.5rem; + width: 40px; + height: 30px; + text-align: center; + border: none; + outline: 2px solid transparent; + color: @beige; + + &.bar-input { + padding: 0; + color: @beige; + backdrop-filter: none; + background: transparent; + transition: all 0.3s ease; + + &:hover, + &:focus { + background: @semi-transparent-dark-blue; + backdrop-filter: blur(9.5px); + } + } + } + + .bar-label { + width: 40px; + } + } + .progress-bar { + position: absolute; + appearance: none; + width: 100px; + height: 40px; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + z-index: 1; + + &::-webkit-progress-bar { + border: none; + background: @dark-blue; + border-radius: 6px; + } + &::-webkit-progress-value { + background: @gradient-hp; + border-radius: 6px; + } + &.stress-color::-webkit-progress-value { + background: @gradient-stress; + border-radius: 6px; + } + &::-moz-progress-value, + &::-moz-progress-bar { + border-radius: 6px; + } + + &::-moz-progress-bar { + background: @gradient-hp; + } + &.stress-color::-moz-progress-bar { + background: @gradient-stress; + border-radius: 6px; + } + } + } + } + + .status-section { + display: flex; + flex-wrap: wrap; + gap: 5px; + justify-content: center; + + .status-number { + justify-items: center; + + .status-value { + position: relative; + display: flex; + width: 50px; + height: 30px; + border: 1px solid light-dark(@dark-blue, @golden); + border-bottom: none; + border-radius: 6px 6px 0 0; + padding: 0 6px; + font-size: 1.2rem; + align-items: center; + justify-content: center; + background: light-dark(transparent, @dark-blue); + z-index: 2; + + &.armor-slots { + width: 80px; + height: 30px; + } + } + + .status-label { + padding: 2px 10px; + width: 100%; + border-radius: 3px; + background: light-dark(@dark-blue, @golden); + + h4 { + font-weight: bold; + text-align: center; + line-height: 18px; + font-size: 12px; + color: light-dark(@beige, @dark-blue); + } + } + } + } + } + + .items-sidebar-list { + display: flex; + flex-direction: column; + gap: 5px; + + .inventory-item { + padding: 0 10px; + } + } + + .equipment-section { + .title { + display: flex; + gap: 15px; + align-items: center; + + h3 { + font-size: 20px; + } + } + .items-list { + display: flex; + flex-direction: column; + gap: 10px; + align-items: center; + } + } + + .loadout-section { + .title { + display: flex; + gap: 15px; + align-items: center; + + h3 { + font-size: 20px; + } + } + } + + .experience-section { + .title { + display: flex; + gap: 15px; + align-items: center; + + h3 { + font-size: 20px; + } + } + + .experience-list { + display: flex; + flex-direction: column; + gap: 5px; + width: 100%; + margin-top: 10px; + align-items: center; + + .experience-row { + display: flex; + gap: 5px; + width: 250px; + align-items: center; + justify-content: space-between; + + input[type='text'] { + height: 32px; + width: 180px; + border: 1px solid transparent; + outline: 2px solid transparent; + font-size: 14px; + font-family: @font-body; + transition: all 0.3s ease; + color: light-dark(@dark, @beige); + + &:hover { + outline: 2px solid light-dark(@dark, @beige); + } + } + } + + .experience-value { + height: 25px; + width: 35px; + font-size: 14px; + font-family: @font-body; + color: light-dark(@dark, @beige); + align-content: center; + text-align: center; + background: url(../assets/svg/experience-shield.svg) no-repeat; + + .theme-light & { + background: url('../assets/svg/experience-shield-light.svg') no-repeat; + } + } + } + } + } +} diff --git a/styles/less/global/inventory-fieldset-items.less b/styles/less/global/inventory-fieldset-items.less index b09ad1b3..0c427bf7 100644 --- a/styles/less/global/inventory-fieldset-items.less +++ b/styles/less/global/inventory-fieldset-items.less @@ -1,17 +1,17 @@ -@import '../utils/colors.less'; -@import '../utils/fonts.less'; - -.application.sheet.daggerheart.actor.dh-style.character { - .items-list { - display: flex; - flex-direction: column; - gap: 10px; - align-items: center; - } - .card-list { - display: flex; - flex-direction: row; - gap: 10px; - align-items: center; - } -} +@import '../utils/colors.less'; +@import '../utils/fonts.less'; + +.application.sheet.daggerheart.actor.dh-style.character { + .items-list { + display: flex; + flex-direction: column; + gap: 10px; + align-items: center; + } + .card-list { + display: flex; + flex-direction: row; + gap: 10px; + align-items: center; + } +} diff --git a/styles/less/global/inventory-item.less b/styles/less/global/inventory-item.less index a5b829ed..a14bd83e 100644 --- a/styles/less/global/inventory-item.less +++ b/styles/less/global/inventory-item.less @@ -1,133 +1,133 @@ -@import '../utils/colors.less'; -@import '../utils/fonts.less'; - -.application.sheet.daggerheart.actor.dh-style.character { - .inventory-item { - display: grid; - grid-template-columns: 40px 1fr 60px; - gap: 10px; - width: 100%; - - .item-img { - height: 40px; - width: 40px; - border-radius: 3px; - border: none; - cursor: pointer; - object-fit: cover; - } - - .item-label { - font-family: @font-body; - align-self: center; - - .item-name { - font-size: 14px; - } - - .item-tags, - .item-labels { - display: flex; - gap: 10px; - - .tag { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - padding: 3px 5px; - font-size: 12px; - - background: light-dark(@dark-15, @beige-15); - border: 1px solid light-dark(@dark, @beige); - border-radius: 3px; - } - - .label { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - font-size: 12px; - } - } - } - - .controls { - display: flex; - align-items: center; - justify-content: end; - gap: 8px; - - a { - text-align: center; - - &.unequipped { - opacity: 0.4; - } - } - } - } - .card-item { - position: relative; - height: 120px; - width: 100px; - border: 1px solid light-dark(@dark-blue, @golden); - border-radius: 6px; - cursor: pointer; - - &:hover { - .card-label { - padding-top: 15px; - .controls { - opacity: 1; - visibility: visible; - transition: all 0.3s ease; - max-height: 16px; - } - } - } - - .card-img { - height: 100%; - width: 100%; - object-fit: cover; - } - - .card-label { - display: flex; - flex-direction: column; - height: fit-content; - align-items: center; - gap: 5px; - padding-top: 5px; - padding-bottom: 5px; - width: 100%; - position: absolute; - background-color: @dark-blue; - bottom: 0; - mask-image: linear-gradient(180deg, transparent 0%, black 20%); - - .card-name { - font-family: @font-body; - font-style: normal; - font-weight: 400; - font-size: 12px; - line-height: 15px; - - color: @beige; - } - - .controls { - display: flex; - gap: 15px; - align-items: center; - max-height: 0px; - opacity: 0; - visibility: collapse; - transition: all 0.3s ease; - color: @beige; - } - } - } -} +@import '../utils/colors.less'; +@import '../utils/fonts.less'; + +.application.sheet.daggerheart.actor.dh-style.character { + .inventory-item { + display: grid; + grid-template-columns: 40px 1fr 60px; + gap: 10px; + width: 100%; + + .item-img { + height: 40px; + width: 40px; + border-radius: 3px; + border: none; + cursor: pointer; + object-fit: cover; + } + + .item-label { + font-family: @font-body; + align-self: center; + + .item-name { + font-size: 14px; + } + + .item-tags, + .item-labels { + display: flex; + gap: 10px; + + .tag { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 3px 5px; + font-size: 12px; + + background: light-dark(@dark-15, @beige-15); + border: 1px solid light-dark(@dark, @beige); + border-radius: 3px; + } + + .label { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + font-size: 12px; + } + } + } + + .controls { + display: flex; + align-items: center; + justify-content: end; + gap: 8px; + + a { + text-align: center; + + &.unequipped { + opacity: 0.4; + } + } + } + } + .card-item { + position: relative; + height: 120px; + width: 100px; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + cursor: pointer; + + &:hover { + .card-label { + padding-top: 15px; + .controls { + opacity: 1; + visibility: visible; + transition: all 0.3s ease; + max-height: 16px; + } + } + } + + .card-img { + height: 100%; + width: 100%; + object-fit: cover; + } + + .card-label { + display: flex; + flex-direction: column; + height: fit-content; + align-items: center; + gap: 5px; + padding-top: 5px; + padding-bottom: 5px; + width: 100%; + position: absolute; + background-color: @dark-blue; + bottom: 0; + mask-image: linear-gradient(180deg, transparent 0%, black 20%); + + .card-name { + font-family: @font-body; + font-style: normal; + font-weight: 400; + font-size: 12px; + line-height: 15px; + + color: @beige; + } + + .controls { + display: flex; + gap: 15px; + align-items: center; + max-height: 0px; + opacity: 0; + visibility: collapse; + transition: all 0.3s ease; + color: @beige; + } + } + } +} diff --git a/styles/less/global/tab-navigation.less b/styles/less/global/tab-navigation.less index a3b48d87..a13c7ea2 100755 --- a/styles/less/global/tab-navigation.less +++ b/styles/less/global/tab-navigation.less @@ -1,18 +1,18 @@ -@import '../utils/colors.less'; -@import '../utils/fonts.less'; - -.sheet.daggerheart.dh-style { - .tab-navigation { - margin: 5px 0; - height: 40px; - - .feature-tab { - border: none; - - a { - color: light-dark(@dark-blue, @golden); - font-family: @font-body; - } - } - } -} +@import '../utils/colors.less'; +@import '../utils/fonts.less'; + +.sheet.daggerheart.dh-style { + .tab-navigation { + margin: 5px 0; + height: 40px; + + .feature-tab { + border: none; + + a { + color: light-dark(@dark-blue, @golden); + font-family: @font-body; + } + } + } +} diff --git a/system.json b/system.json index dc25f130..c1a5c501 100644 --- a/system.json +++ b/system.json @@ -35,6 +35,9 @@ }, { "name": "JimCanE" + }, + { + "name": "Po0lp" } ], "scripts": ["build/daggerheart.js"], @@ -253,7 +256,8 @@ "dualityRoll": {}, "adversaryRoll": {}, "damageRoll": {}, - "abilityUse": {} + "abilityUse": {}, + "applyEffect": {} } }, "primaryTokenAttribute": "resources.health", diff --git a/templates/chat/adversary-attack-roll.hbs b/templates/chat/adversary-attack-roll.hbs index 75d661ae..f6051bfe 100644 --- a/templates/chat/adversary-attack-roll.hbs +++ b/templates/chat/adversary-attack-roll.hbs @@ -41,7 +41,7 @@ {{/if}}