diff --git a/daggerheart.mjs b/daggerheart.mjs index 96b407d3..e09b26b8 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -15,6 +15,7 @@ import { abilities } from './module/config/actorConfig.mjs'; import Resources from './module/applications/resources.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' globalThis.SYSTEM = SYSTEM; @@ -37,6 +38,16 @@ 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.Item.documentClass = documents.DhpItem; //Registering the Item DataModel diff --git a/lang/en.json b/lang/en.json index 2f04cc32..c8301736 100755 --- a/lang/en.json +++ b/lang/en.json @@ -927,13 +927,17 @@ }, "AttackRoll": { "Title": "Attack - {attack}", - "RollDamage": "Roll Damage" + "RollDamage": "Roll Damage", + "ApplyEffect": "Apply Effects" }, "DamageRoll": { "Title": "Damage - {damage}", "DealDamageToTargets": "Damage Hit Targets", "DealDamage": "Deal Damage" }, + "ApplyEffect": { + "Title": "Apply Effects - {name}" + }, "HealingRoll": { "Heal": "Heal" }, diff --git a/module/applications/config/Action.mjs b/module/applications/config/Action.mjs index 84152578..a3ae12b2 100644 --- a/module/applications/config/Action.mjs +++ b/module/applications/config/Action.mjs @@ -62,6 +62,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { if (!!this.action.effects) context.effects = this.action.effects.map(e => this.action.item.effects.get(e._id)); if (this.action.damage?.hasOwnProperty('includeBase') && this.action.type === 'attack') context.hasBaseDamage = !!this.action.parent.damage; context.getRealIndex = this.getRealIndex.bind(this); + context.disableOption = this.disableOption.bind(this); return context; } @@ -70,6 +71,14 @@ 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; diff --git a/module/applications/costSelectionDialog.mjs b/module/applications/costSelectionDialog.mjs index 74855f2c..da8af9a2 100644 --- a/module/applications/costSelectionDialog.mjs +++ b/module/applications/costSelectionDialog.mjs @@ -1,9 +1,10 @@ const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; export default class CostSelectionDialog extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(cost, resolve) { + constructor(costs, action, resolve) { super({}); - this.cost = cost; + this.costs = costs; + this.action = action; this.resolve = resolve; } @@ -15,7 +16,7 @@ export default class CostSelectionDialog extends HandlebarsApplicationMixin(Appl height: 'auto' }, actions: { - sendHope: this.sendHope + sendCost: this.sendCost }, form: { handler: this.updateForm, @@ -26,7 +27,7 @@ export default class CostSelectionDialog extends HandlebarsApplicationMixin(Appl /** @override */ static PARTS = { - damageSelection: { + costSelection: { id: 'costSelection', template: 'systems/daggerheart/templates/views/costSelection.hbs' } @@ -40,25 +41,21 @@ export default class CostSelectionDialog extends HandlebarsApplicationMixin(Appl } async _prepareContext(_options) { + const updatedCosts = this.action.calcCosts(this.costs); return { - cost: this.cost.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 - }) + costs: updatedCosts, + canUse: this.action.getRealCosts(updatedCosts)?.hasCost }; } static async updateForm(event, _, formData) { - this.cost = foundry.utils.mergeObject(this.cost, foundry.utils.expandObject(formData.object)); + this.costs = foundry.utils.mergeObject(this.costs, foundry.utils.expandObject(formData.object).costs); this.render(true) } - static sendHope(event) { + static sendCost(event) { event.preventDefault(); - this.resolve(this.cost.filter(c => c.enabled)); + this.resolve(this.action.getRealCosts(this.costs)); this.close(); } } \ No newline at end of file diff --git a/module/applications/roll.mjs b/module/applications/roll.mjs index 0d4d2a05..cd0881ae 100644 --- a/module/applications/roll.mjs +++ b/module/applications/roll.mjs @@ -1,34 +1,344 @@ +import D20RollDialog from '../dialogs/d20RollDialog.mjs'; + +export class DHRoll extends Roll { + constructor(formula, data, options) { + super(formula, data, options); + } -export default class DHRoll extends Roll { static async build(config={}, message={}) { - const roll = await this.buildConfigure(); - await this.buildEvaluate(config, message={}); - await this.buildPost(config, message={}); + const roll = await this.buildConfigure(config, message); + await this.buildEvaluate(roll, config, message={}); + await this.buildPost(roll, config, message={}); return roll; } static async buildConfigure(config={}, message={}) { config.hooks = [...(config.hooks ?? []), ""]; + config.dialog ??= {}; for ( const hook of config.hooks ) { - if ( Hooks.call(`dnd5e.preRoll${hook.capitalize()}`, config, message) === false ) return null; + if ( Hooks.call(`${SYSTEM.id}.preRoll${hook.capitalize()}`, config, message) === false ) return null; } + + this.applyKeybindings(config); + + // let roll; + // if(config.dialog?.configure === false) { + // roll = new this('', config.actor, config); + // } else { + if(config.dialog.configure !== false) { + // Open Roll Dialog + const DialogClass = config.dialog?.class ?? this.DefaultDialog; + config = await DialogClass.configure(config, message); + } + console.log(config) + let roll = new this('', config.actor, config); + + for ( const hook of config.hooks ) { + if ( Hooks.call(`${SYSTEM.id}.post${hook.capitalize()}RollConfiguration`, roll, config, message) === false ) return []; + } + console.log(roll); + + return roll; } static async buildEvaluate(roll, config={}, message={}) { - if(config.evaluate !== false) await roll.evalutate(); + if(config.evaluate !== false) await roll.evaluate(); + this.postEvaluate(roll, config); } - static async buildPost(config, message) { + static async buildPost(roll, config, message) { for ( const hook of config.hooks ) { - if ( Hooks.call(`dnd5e.postRoll${hook.capitalize()}`, config, message) === false ) return null; + if ( Hooks.call(`${SYSTEM.id}.postRoll${hook.capitalize()}`, config, message) === false ) return null; } + // Create Chat Message - await this.toMessage(roll, message.data); + if(message.data) { + + } else { + const messageData = {}; + await this.toMessage(roll, config); + } } - static async toMessage(roll, data) { + static async postEvaluate(roll, config={}) {} + + static async toMessage(roll, config) { + console.log(config) + const cls = getDocumentClass("ChatMessage"), + msg = { + type: this.messageType, + sound: config.mute ? null : CONFIG.sounds.dice, + system: config, + content: config.chatMessage.template, + rolls: [roll] + }; + 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); + // console.log(data, options) + // this.options = this._prepareData(data); + // this.options = 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 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 + }; - const cls = getDocumentClass("ChatMessage"); - const msg = new cls(data); + // 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; + } + + static async postEvaluate(roll, config={}) { + if (config.targets?.length) { + targets = config.targets.map(target => { + const difficulty = config.roll.difficulty ?? target.difficulty ?? target.evasion + target.hit = roll.total >= difficulty; + return target; + }); + } else if(config.roll.difficulty) roll.success = roll.total >= config.roll.difficulty; + // config.roll.advantage = { + // dice: roll.dHope.faces, + // value: roll.dHope.total + // } + config.roll.total = roll.total; + } + + 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.actor.experiences?.[m]) this.terms.push(...this.formatModifier(this.options.actor.experiences[m].total)); + }) + + 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.action) { + if(this.options.type === "attack") this.terms.push(...this.formatModifier(this.options.actor.system.attack.modifier)); + this.options.roll.modifiers?.forEach(m => { + this.terms.push(...this.formatModifier(m)); + }) + // } + } + + getRollData() { + return {...this.options.actor.getRollData(), ...(this.options.action?.getRollData() ?? {})} + } + + 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 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; + } + + 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 = null, + advDie = new foundry.dice.terms.Die({faces: dieFaces}); + // console.log(this.hasAdvantage, this.hasDisadvantage) + 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.action) { + // console.log(this.options, this.options.actor.system.traits[this.options.roll.trait].bonus) + // console.log(this.options.actor.system); + if(this.options.roll?.trait) this.terms.push(...this.formatModifier(this.options.actor.traits[this.options.roll.trait].total)); + this.options.roll.modifiers?.forEach(m => { + this.terms.push(...this.formatModifier(m.value)); + }) + // } else if(this.options.trait) this.terms.push(...this.formatModifier(this.options.actor.system.traits[this.options.roll.trait].total)); + } + + static async postEvaluate(roll, config={}) { + super.postEvaluate(roll, config); + config.roll.hope = { + dice: roll.dHope.faces, + value: roll.dHope.total + } + config.roll.fear = { + dice: roll.dFear.faces, + value: roll.dFear.total + } + config.roll.dualityResult = roll.withHope ? 1 : roll.withFear ? 2 : 0; + } +} + +export class DamageRoll extends DHRoll { + constructor(formula, data={}, options={}) { + super(formula, data, options) + } + + static messageType = 'damageRoll'; + + static DefaultDialog = D20RollDialog; + + get messageType() { + return 'damageRoll'; + } + + get messageTemplate() { + return ''; } } \ No newline at end of file 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/data/action/action.mjs b/module/data/action/action.mjs index e8a9c9b2..1446a3d5 100644 --- a/module/data/action/action.mjs +++ b/module/data/action/action.mjs @@ -7,17 +7,24 @@ const fields = foundry.data.fields; /* 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 + - Target Check / Target Picker - Range Check - Area of effect and measurement placement - - Auto use costs and action + - Handle Roll result as part of formula if needed + - Summon Action create method + - Create classes form Target, Cost, etc ? + + Other + - Add optionnal Role for Healing ? + - Auto use action <= Into Roll + + Done + - Cost Check + - Auto use costs - Auto disable selected Cost from other cost list + - Apply ActiveEffect => Add to Chat message like Damage Button ? + - Add Drag & Drop for documentUUID field (Macro & Summon) Activity Types List - Attack => Weapon Attack, Spell Attack, etc... @@ -28,6 +35,16 @@ const fields = foundry.data.fields; - Summon - Sequencer => Trigger a list of Activities set on the item one by one - Macro + + Actor Modifier + - Weapon Attack + - Spell Attack + - Weapon Damage + - Magical Damage + - Physical Damage ? + - Magical Damage ? + - Healing + - Bard Rally (Math.ceil(LeveL / 5)) */ export class DHBaseAction extends foundry.abstract.DataModel { @@ -163,7 +180,8 @@ export class DHBaseAction extends foundry.abstract.DataModel { ...actorData.toObject(), prof: actorData.proficiency?.value ?? 1, cast: actorData.spellcast?.value ?? 1, - scale: this.cost.length ? this.cost.reduce((a,c) => {a[c.type] = c.value; return a},{}) : 1 + scale: this.cost.length ? this.cost.reduce((a,c) => {a[c.type] = c.value; return a},{}) : 1, + roll: {} } } @@ -175,7 +193,9 @@ export class DHBaseAction extends foundry.abstract.DataModel { itemId: this.item._id, actionId: this._id }, + type: this.type, hasDamage: !!this.damage?.parts?.length, + hasEffect: !!this.effects?.length, chatMessage: { template: this.chatTemplate } @@ -183,10 +203,6 @@ export class DHBaseAction extends foundry.abstract.DataModel { this.proceedChatDisplay(config); - // Display Costs Dialog & Check if Actor get enough resources - config.costs = await this.getCost(config); - if(!config.costs.hasCost) return ui.notifications.warn("You don't have the resources to use that action."); - // Filter selected targets based on Target parameters config.targets = await this.getTarget(config); if(!config.targets) return ui.notifications.warn("Too many targets selected for that actions."); @@ -195,33 +211,41 @@ export class DHBaseAction extends foundry.abstract.DataModel { config.range = await this.checkRange(config); if(!config.range.hasRange) return ui.notifications.warn("No Target within range."); - // Proceed with Roll - await this.proceedRoll(config); + // Display Costs Dialog & Check if Actor get enough resources + config.costs = await this.getCost(config); + if(!this.hasRoll() && !config.costs.hasCost) return ui.notifications.warn("You don't have the resources to use that action."); - if (this.effects.length) { - // Apply Active Effects. In Chat Message ? - } + // Proceed with Roll + config = await this.proceedRoll(config); // Update Actor resources based on Action Cost configuration this.spendCost(config.costs.values); + // console.log(config) + return config; } /* ROLL */ + hasRoll() { + return this.roll?.type && this.roll?.trait; + } + async proceedRoll(config) { - if (!this.roll?.type || !this.roll?.trait) return; + if (!this.hasRoll()) return config; const modifierValue = this.actor.system.traits[this.roll.trait].value; - config = { - ...config, - roll: { - modifier: modifierValue, - label: game.i18n.localize(abilities[this.roll.trait].label), - type: this.actionType, - difficulty: this.roll?.difficulty + config = { + ...config, + roll: { + // modifier: modifierValue, + modifier: [], + trait: this.roll?.trait, + label: game.i18n.localize(abilities[this.roll.trait].label), + type: this.actionType, + difficulty: this.roll?.difficulty + } } - } - config.roll.evaluated = await this.actor.diceRoll(config); + return await this.actor.diceRoll(config, this); } /* ROLL */ @@ -229,13 +253,32 @@ export class DHBaseAction extends foundry.abstract.DataModel { async getCost(config) { if(!this.cost?.length || !this.actor) return {values: [], hasCost: true}; let cost = foundry.utils.deepClone(this.cost); - if (!config.event.shiftKey) { + if (!config.event.shiftKey && !this.hasRoll()) { const dialogClosed = new Promise((resolve, _) => { - new CostSelectionDialog(cost, resolve).render(true); + new CostSelectionDialog(cost, this, resolve).render(true); }); cost = await dialogClosed; } - return {values: cost, hasCost: cost.reduce((a, c) => a && this.actor.system.resources[c.type]?.value >= (c.total ?? c.value), true)}; + return cost; + } + + getRealCosts(costs) { + const realCosts = costs?.length ? costs.filter(c => c.enabled) : []; + return {values: realCosts, hasCost: this.hasCost(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) { + return costs.reduce((a, c) => a && this.actor.system.resources[c.type]?.value >= (c.total ?? c.value), true) } async spendCost(config) { @@ -246,7 +289,7 @@ export class DHBaseAction extends foundry.abstract.DataModel { /* TARGET */ async getTarget(config) { - if(this.target.type === SYSTEM.ACTIONS.targetTypes.self.id) return this.formatTarget(this.actor.token ?? this.actor.prototypeToken); + if(this.target?.type === SYSTEM.ACTIONS.targetTypes.self.id) return this.formatTarget(this.actor.token ?? this.actor.prototypeToken); let targets = Array.from(game.user.targets); // foundry.CONST.TOKEN_DISPOSITIONS.FRIENDLY if(this.target?.type && this.target.type !== SYSTEM.ACTIONS.targetTypes.any.id) { @@ -281,8 +324,39 @@ export class DHBaseAction extends foundry.abstract.DataModel { /* RANGE */ /* EFFECTS */ - async applyEffects(config) { - if(!this.effects?.length) return; + async applyEffects(event, data, force=false) { + if(!this.effects?.length || !data.system.targets.length) return; + data.system.targets.forEach(async (token) => { + // console.log(token, force) + if(!token.hit && !force) return; + this.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 */ @@ -293,60 +367,19 @@ export class DHBaseAction extends foundry.abstract.DataModel { /* CHAT */ } -/* const extraDefineSchema = (field, option) => { - return { - [field]: { - // damage: new fields.SchemaField({ - // parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)) - // }), - 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 }) - }), - save: new fields.SchemaField({ - 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.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() - }) - ) - }[field] - }; -}; */ - export class DHDamageAction extends DHBaseAction { directDamage = true; static extraSchemas = ['damage', 'target', 'effects']; - /* static defineSchema() { - return { - ...super.defineSchema(), - ...extraDefineSchema('damage'), - ...extraDefineSchema('target'), - ...extraDefineSchema('effects') - }; - } */ - async use(event, ...args) { - const messageData = await super.use(event, args); + const config = await super.use(event, args); + if(['error', 'warning'].includes(config.type)) return; if(!this.directDamage) return; - return await this.rollDamage(event, messageData); + return await this.rollDamage(event, config); } - async rollDamage(event, messageData) { + async rollDamage(event, data) { let formula = this.damage.parts.map(p => p.getFormula(this.actor)).join(' + '); if (!formula || formula == '') return; @@ -360,13 +393,6 @@ export class DHDamageAction extends DHBaseAction { const result = await dialogClosed; bonusDamage = result.bonusDamage; formula = result.rollString; - - /* const automateHope = await 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 (isNaN(formula)) { @@ -390,115 +416,43 @@ export class DHDamageAction extends DHBaseAction { } } - // if(messageData?.system?.damage) { - // } else { - const cls = getDocumentClass('ChatMessage'), - systemData = { - title: game.i18n.format('DAGGERHEART.Chat.DamageRoll.Title', { damage: this.name }), - roll: formula, - damage: { - total: roll.total, - type: this.damage.parts[0].type // Handle multiple type damage - }, - dice: dice, - modifiers: modifiers, - targets: [] + const cls = getDocumentClass('ChatMessage'), + systemData = { + title: game.i18n.format('DAGGERHEART.Chat.DamageRoll.Title', { damage: this.name }), + roll: formula, + damage: { + total: roll.total, + type: this.damage.parts[0].type // Handle multiple type damage }, - msg = new cls({ - type: 'damageRoll', - user: game.user.id, - sound: CONFIG.sounds.dice, - system: systemData, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/damage-roll.hbs', - systemData - ), - rolls: [roll] - }); - - cls.create(msg.toObject()); - // } - - - /* const cls = getDocumentClass('ChatMessage'), + dice: dice, + modifiers: modifiers, + targets: (data.system?.targets ?? data.targets).map(x => ({ id: x.id, name: x.name, img: x.img, hit: true })) + }, msg = new cls({ + type: 'damageRoll', user: game.user.id, + sound: CONFIG.sounds.dice, + system: systemData, content: await foundry.applications.handlebars.renderTemplate( - this.chatTemplate, - { - ...{ - roll: roll.formula, - total: roll.total, - dice: roll.dice, - type: this.damage.parts.map(p => p.type) - }, - ...messageData - } - ) + 'systems/daggerheart/templates/chat/damage-roll.hbs', + systemData + ), + rolls: [roll] }); - cls.create(msg.toObject()); */ + cls.create(msg.toObject()); } get chatTemplate() { return 'systems/daggerheart/templates/chat/damage-roll.hbs'; } - - /* async use(event, ...args) { - 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()); - } */ - - /* async applyDamage(targets, value) { - const promises = []; - for(let t of targets) { - if(!t) continue; - promises.push(new Promise(async (resolve, reject) => { - await t.takeDamage(value, 'physical'); // Apply one instance of damage per parts ? - resolve(); - }) - ) - } - return Promise.all(promises).then((values) => { - return values; - }); - } */ } export class DHAttackAction extends DHDamageAction { directDamage = false; - // static extraSchemas = []; static extraSchemas = [...super.extraSchemas, ...['roll', 'save']]; - /* static defineSchema() { - return { - ...super.defineSchema(), - ...extraDefineSchema('roll'), - ...extraDefineSchema('save') - }; - } */ - static getRollType(parent) { return parent.type === 'weapon' ? 'weapon' : 'spellcast'; } @@ -524,39 +478,15 @@ export class DHAttackAction extends DHDamageAction { base: true }; } - - /* async use(event, ...args) { - - } */ - - // 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(parent) { - return 'spellcast'; - } -} */ - export class DHHealingAction extends DHBaseAction { static extraSchemas = ['target', 'effects', 'healing']; async use(event, ...args) { - const messageData = await super.use(event, args), - roll = await this.rollHealing(), + const config = await super.use(event, args); + if(['error', 'warning'].includes(config.type)) return; + const roll = await this.rollHealing(), cls = getDocumentClass('ChatMessage'), msg = new cls({ user: game.user.id, @@ -564,7 +494,7 @@ export class DHHealingAction extends DHBaseAction { this.chatTemplate, { ...roll, - ...messageData + ...config } ) }); @@ -593,49 +523,76 @@ 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 use(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 extraSchemas = ['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) { + // console.log(data, this.effects, this.effectsDetails) + 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'; + } } 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, ...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/chat-message/_modules.mjs b/module/data/chat-message/_modules.mjs index 0432a789..dce35576 100644 --- a/module/data/chat-message/_modules.mjs +++ b/module/data/chat-message/_modules.mjs @@ -2,12 +2,14 @@ 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, + DHApplyEffect } export const config = { @@ -15,4 +17,5 @@ export const config = { adversaryRoll: DHAdversaryRoll, damageRoll: DHDamageRoll, dualityRoll: DHDualityRoll, + applyEffect: DHApplyEffect }; \ No newline at end of file diff --git a/module/data/chat-message/adversaryRoll.mjs b/module/data/chat-message/adversaryRoll.mjs index eefd5cd3..6e970388 100644 --- a/module/data/chat-message/adversaryRoll.mjs +++ b/module/data/chat-message/adversaryRoll.mjs @@ -29,6 +29,7 @@ export default class DHAdversaryRoll extends foundry.abstract.TypeDataModel { }) ), hasDamage: new fields.BooleanField({ initial: false }), + hasEffect: new fields.BooleanField({ initial: false }), /* damage: new fields.SchemaField( { value: new fields.StringField({}), 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..390acf70 100644 --- a/module/data/chat-message/damageRoll.mjs +++ b/module/data/chat-message/damageRoll.mjs @@ -26,7 +26,8 @@ export default class DHDamageRoll extends foundry.abstract.TypeDataModel { new fields.SchemaField({ id: new fields.StringField({ required: true }), name: new fields.StringField(), - img: new fields.StringField() + img: new fields.StringField(), + hit: new fields.BooleanField({ initial: false }) }) ) }; diff --git a/module/data/chat-message/dualityRoll.mjs b/module/data/chat-message/dualityRoll.mjs index fe5f6833..9d0367be 100644 --- a/module/data/chat-message/dualityRoll.mjs +++ b/module/data/chat-message/dualityRoll.mjs @@ -40,27 +40,7 @@ export default class DHDualityRoll extends foundry.abstract.TypeDataModel { }) ), hasDamage: new fields.BooleanField({ initial: false }), - // damage: new fields.SchemaField({ - // value: new fields.StringField({}), - // type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.damageTypes), integer: false }), - // bonusDamage: new fields.ArrayField( - // new fields.SchemaField({ - // value: new fields.StringField({}), - // type: new fields.StringField({ - // choices: Object.keys(SYSTEM.GENERAL.damageTypes), - // integer: false - // }), - // initiallySelected: new fields.BooleanField(), - // appliesOn: new fields.StringField( - // { choices: Object.keys(SYSTEM.EFFECTS.applyLocations) }, - // { nullable: true, initial: null } - // ), - // description: new fields.StringField({}), - // hopeIncrease: new fields.StringField({ nullable: true }) - // }), - // { nullable: true, initial: null } - // ) - // }), + hasEffect: new fields.BooleanField({ initial: false }), action: new fields.SchemaField({ itemId: new fields.StringField(), actionId: new fields.StringField() diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index 3da7705e..00ab19b3 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 */ diff --git a/module/dialogs/d20RollDialog.mjs b/module/dialogs/d20RollDialog.mjs new file mode 100644 index 00000000..ae8bd24f --- /dev/null +++ b/module/dialogs/d20RollDialog.mjs @@ -0,0 +1,85 @@ +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; + +export default class D20RollDialog extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(config={}, options={}) { + super(options); + + this.config = config; + this.config.experiences = []; + } + + 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, + // finish: this.finish + }, + 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.experiences = Object.keys(this.config.actor.experiences).map(id => ({ id, ...this.config.actor.experiences[id] })); + context.selectedExperiences = this.config.experiences; + context.advantage = this.config.advantage; + context.diceOptions = [{id:12, value: 'd12'},{id:20, value: 'd20'}] + if(this.config.costs?.length) { + const updatedCosts = this.config.action.calcCosts(this.config.costs); + context.costs = updatedCosts + context.canRoll = this.config.action.getRealCosts(updatedCosts)?.hasCost; + } else context.canRoll = true; + return context; + } + + static updateRollConfiguration(event, _, formData) { + const { ...rest } = foundry.utils.expandObject(formData.object); + this.config.costs = foundry.utils.mergeObject(this.config.costs, rest.costs); + 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 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 62c160d5..558edeae 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -257,13 +257,30 @@ export default class DhpActor extends Actor { * @param {string} [config.roll.type] * @param {number} [config.roll.difficulty] * @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', + async diceRoll(config, action) { + // console.log(config) + const newConfig = { + // data: { + ...config, + action, + actor: this.getRollData(), + // }, + // options: { + // dialog: false, + // }, + // event: config.event + } + // console.log(this, newConfig) + const roll = CONFIG.Dice.daggerheart[this.type === 'character' ? 'DualityRoll' : 'D20Roll'].build(newConfig) + return config; + /* let hopeDice = 'd12', fearDice = 'd12', advantageDice = 'd6', disadvantageDice = 'd6', @@ -280,16 +297,21 @@ export default class DhpActor extends Actor { this.type === 'character' ? new RollSelectionDialog( this.system.experiences, - this.system.resources.hope.value, + config.costs, + action, resolve ).render(true) - : new NpcRollSelectionDialog(this.system.experiences, resolve).render(true); + : new NpcRollSelectionDialog( + this.system.experiences, + resolve + ).render(true); }); rollConfig = await dialogClosed; advantage = rollConfig.advantage; hopeDice = rollConfig.hope; fearDice = rollConfig.fear; + if(rollConfig.costs) config.costs = rollConfig.costs; rollConfig.experiences.forEach(x => modifiers.push({ @@ -316,13 +338,14 @@ export default class DhpActor extends Actor { 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 })) - })); + const roll = await Roll.create(formula).evaluate(), + 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 })) + })); + config.roll.evaluated = roll; if (this.type === 'character') { setDiceSoNiceForDualityRoll(roll, advantage); @@ -355,10 +378,11 @@ export default class DhpActor extends Actor { if (config.targets?.length) { targets = config.targets.map(target => { - target.hit = target.difficulty ? roll.total >= target.difficulty : roll.total >= target.evasion; + const difficulty = config.roll.difficulty ?? target.difficulty ?? target.evasion + target.hit = roll.total >= difficulty; return target; }); - } + } else if(config.roll.difficulty) roll.success = roll.total >= config.roll.difficulty; if (config.chatMessage) { const configRoll = { @@ -369,7 +393,8 @@ export default class DhpActor extends Actor { modifiers: modifiers.filter(x => x.label), advantageState: advantage, action: config.source, - hasDamage: config.hasDamage + hasDamage: config.hasDamage, + hasEffect: config.hasEffect }; if (this.type === 'character') { configRoll.hope = { dice: hopeDice, value: hope }; @@ -391,7 +416,8 @@ export default class DhpActor extends Actor { await cls.create(msg.toObject()); } - return roll; + + return config; */ } formatRollModifier(roll) { diff --git a/module/ui/chatLog.mjs b/module/ui/chatLog.mjs index a3a59f6d..91d932c7 100644 --- a/module/ui/chatLog.mjs +++ b/module/ui/chatLog.mjs @@ -14,9 +14,12 @@ 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-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); @@ -57,7 +60,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo const item = actor.items.get(message.system.action?.itemId), action = item?.system?.actions?.find(a => a._id === message.system.action.actionId); if(!item || !action || !action?.rollDamage) return; - await action.rollDamage(event, this); + await action.rollDamage(event, message); } else { await actor.damageRoll( message.system.title, @@ -68,6 +71,18 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo } }; + onApplyEffect = async (event, message) => { + event.stopPropagation(); + const actor = game.actors.get(message.system.origin); + if (!actor || !game.user.isGM) return true; + if(message.system.action?.itemId && message.system.action?.actionId) { + const item = actor.items.get(message.system.action?.itemId), + action = item?.system?.actions?.find(a => a._id === message.system.action.actionId); + if(!item || !action) return; + await action.applyEffects(event, message); + } + } + hoverTarget = event => { event.stopPropagation(); const token = canvas.tokens.get(event.currentTarget.dataset.token); diff --git a/styles/application.less b/styles/application.less index 5319a35e..b6c21220 100644 --- a/styles/application.less +++ b/styles/application.less @@ -201,6 +201,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, @@ -342,6 +346,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 883af72b..fc046d5c 100644 --- a/styles/chat.less +++ b/styles/chat.less @@ -442,6 +442,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/system.json b/system.json index 35f69cde..56c8c7e7 100644 --- a/system.json +++ b/system.json @@ -35,6 +35,9 @@ }, { "name": "JimCanE" + }, + { + "name": "Po0lp" } ], "scripts": ["build/daggerheart.js"], @@ -252,7 +255,8 @@ "dualityRoll": {}, "adversaryRoll": {}, "damageRoll": {}, - "abilityUse": {} + "abilityUse": {}, + "applyEffect": {} } }, "primaryTokenAttribute": "resources.health", diff --git a/templates/chat/apply-effects.hbs b/templates/chat/apply-effects.hbs new file mode 100644 index 00000000..2b54fed4 --- /dev/null +++ b/templates/chat/apply-effects.hbs @@ -0,0 +1,9 @@ +