${game.i18n.format("PTU.Action.Hypermode.Create", { name: this.actor.name })}
` + }); + return await this.actor.createEmbeddedDocuments("Item", [hypermode.toObject()]); + } + /** @override */ prepareBaseData() { super.prepareBaseData(); @@ -339,8 +363,11 @@ class PTUCondition extends BaseEffectPTU { const result = await statistic.roll({ skipDialog: true, targets }); const effective = statistic.dc.value <= result.total; - if (effective) { - //TODO: Apply Vulnerable for 1 round + if (!effective) { + const vulnerable = await fromUuid('Compendium.ptu.effects.Item.wqzPWKMbwvOWLVoI'); + const data = vulnerable.toObject(); + data.system.duration = { value: 1, unit: 'rounds', expiry: 'turn-start'}; + await actor.createEmbeddedDocuments("Item", [data]); } return await ChatMessage.create({ @@ -349,6 +376,40 @@ class PTUCondition extends BaseEffectPTU { content: `${game.i18n.localize(`PTU.Action.Paralyzed.${effective ? "Suppressed" : "Applicable"}.One`)} @UUID[${paralyzed.uuid}] ${game.i18n.localize(`PTU.Action.Paralyzed.${effective ? "Suppressed" : "Applicable"}.Two`)}
`, }) } + + /** + * @param {PTUActor} actor + * @param {PTUCondition} hypermode + */ + static async HandleHyperMode(actor, hypermode) { + + const dcModifiers = [new PTUModifier({ slug: "dc", label: "DC", modifier: 11 })] + const saveModifiers = []; + + const statistic = new Statistic(actor, { + slug: "save-check", + label: game.i18n.format("PTU.SaveCheck", { name: actor.name, save: hypermode.name }), + check: { type: "save-check", domains: ["save-check"], modifiers: saveModifiers }, + dc: { modifiers: dcModifiers, domains: ["save-dc"] }, + domains: ["hypermode"] + }); + + const target = actor.getActiveTokens().shift(); + const targets = target ? [{ + actor: actor.toObject(), + token: target.document.toObject(), + dc: { value: statistic.dc.value, flat: true, slug: hypermode.slug }, + }] : []; + + const result = await statistic.roll({ skipDialog: true, targets }); + const effective = statistic.dc.value <= result.total; + + return await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor }), + flavor: ` `, + content: `${game.i18n.localize(`PTU.Action.Hypermode.${effective ? "Suppressed" : "Applicable"}.One`)} @UUID[${hypermode.uuid}] ${game.i18n.localize(`PTU.Action.Hypermode.${effective ? "Suppressed" : "Applicable"}.Two`)}
`, + }) + } } export { PTUCondition } \ No newline at end of file diff --git a/src/module/item/effect-types/effect/document.js b/src/module/item/effect-types/effect/document.js index 9ee345c25..0ad45fc21 100644 --- a/src/module/item/effect-types/effect/document.js +++ b/src/module/item/effect-types/effect/document.js @@ -35,7 +35,7 @@ class PTUEffect extends BaseEffectPTU { const badge = this.system.badge; if(badge?.type === "counter") { const max = badge.labels?.length ?? Infinity; - badge.value = Math.clamped(badge.value, 1, max); + badge.value = Math.clamp(badge.value, 1, max); badge.label = badge.labels?.at(badge.value -1)?.trim() || null; } } @@ -54,14 +54,14 @@ class PTUEffect extends BaseEffectPTU { if(badge?.type === "counter" && !this.isExpired) { if(badge.value >= (badge.labels?.length ?? Infinity)) return; - return await this.update({"system.badge.value": duplicate(badge.value)+1}); + return await this.update({"system.badge.value": foundry.utils.duplicate(badge.value)+1}); } } async decrease() { if(this.system.badge?.type !== "counter" || this.system.badge.value === 1 || this.isExpired) return await this.delete(); - return await this.update({"system.badge.value": duplicate(this.system.badge.value)-1}); + return await this.update({"system.badge.value": foundry.utils.duplicate(this.system.badge.value)-1}); } async apply(targets, source = null) { diff --git a/src/module/item/feat/document.js b/src/module/item/feat/document.js index 24b37ed80..630fe75d9 100644 --- a/src/module/item/feat/document.js +++ b/src/module/item/feat/document.js @@ -2,8 +2,7 @@ import { sluggify } from '../../../util/misc.js'; import { PTUItem } from '../index.js'; class PTUFeat extends PTUItem { get category() { - const { tags } = this.system; - if (tags.includes("[Class]")) return "class"; + if(this.system.keywords?.includes("Class")) return "class"; return "feat"; } @@ -24,6 +23,7 @@ class PTUFeat extends PTUItem { if(this.system.class) { const classItem = this.actor?.items.find((item) => item.isClass && item.slug === sluggify(this.system.class)); if(classItem) { + this._source.flags.ptu ??= {}; this._source.flags.ptu.grantedBy = {id: classItem._id, onDelete: "detach"}; await classItem.update({"flags.ptu.itemGrants": {[this._source._id]: {id: this._source._id, onDelete: "detach"}}}); } @@ -38,7 +38,7 @@ class PTUFeat extends PTUItem { if(changed.system?.class !== undefined) { newClass.actor = this.actor?.items.find((item) => item.isClass && item.name === changed.system.class); if(newClass.actor) { - changed.flags = expandObject({"ptu.grantedBy": {id: newClass.actor._id, onDelete: "detach"}}); + changed.flags = foundry.utils.expandObject({"ptu.grantedBy": {id: newClass.actor._id, onDelete: "detach"}}); newClass.update = {"flags.ptu.itemGrants": {[this._id]: {id: this._id, onDelete: "detach"}}}; } if(this.system.class) { @@ -46,7 +46,7 @@ class PTUFeat extends PTUItem { if(oldClass.actor) { if(!changed.flags?.ptu?.grantedBy?.id && this.flags?.ptu?.grantedBy?.id === oldClass.actor._id) { delete changed.flags?.ptu?.grantedBy; - changed.flags= expandObject({"ptu.-=grantedBy": null}); + changed.flags= foundry.utils.expandObject({"ptu.-=grantedBy": null}); }; oldClass.update = {"flags.ptu.itemGrants": {[`-=${this._id}`]: null}}; } diff --git a/src/module/item/item/document.js b/src/module/item/item/document.js index 7967e28fe..3ff0f43ec 100644 --- a/src/module/item/item/document.js +++ b/src/module/item/item/document.js @@ -9,6 +9,15 @@ class PTUItemItem extends PTUItem { return this.system.container; } + prepareBaseData() { + super.prepareBaseData(); + + if(this.enabled) { + this.flags.ptu.rollOptions.all[`item:equipped`] = true; + this.flags.ptu.rollOptions.item[`item:equipped`] = true; + } + } + /** @override */ prepareSiblingData() { const itemGrants = this.flags.ptu.itemGrants; @@ -49,6 +58,37 @@ class PTUItemItem extends PTUItem { if(oldClass?.actor) await oldClass.actor.update(oldClass.update); if(newClass?.actor) await newClass.actor.update(newClass.update); } + + async purchase() { + const actor = (() => { + const character = game.user.character; + if(character) return character; + + const token = canvas.tokens.controlled[0]; + if(token) return token.actor; + + return null; + })(); + if(!actor) return ui.notifications.error("No actor selected"); + + const amount = this.system.cost; + + if ((actor.system.money ?? 0) < amount) return ui.notifications.error(`${actor.name} does not have enough money to pay for ${this.name} (Cost: ${amount} Poké, Current: ${actor.system.money})`); + await actor.update({ + "system.money": actor.system.money - amount, + }); + + // If duplicate item gets added instead increase the quantity + const existingItem = actor.items.getName(this.name); + if (existingItem && existingItem.system.quantity) { + const quantity = foundry.utils.duplicate(existingItem.system.quantity); + await existingItem.update({ "system.quantity": Number(quantity) + (this.system.quantity > 0 ? Number(this.system.quantity) : 1) }); + } + else { + await Item.create(this.toObject(), {parent: actor}); + } + return ui.notifications.info(`${actor.name} Paid ${amount} Poké for ${this.name} (New Total: ${actor.system.money})`); + } } export { PTUItemItem } \ No newline at end of file diff --git a/src/module/item/item/pokeball.js b/src/module/item/item/pokeball.js index 0a4616184..e68dfb83f 100644 --- a/src/module/item/item/pokeball.js +++ b/src/module/item/item/pokeball.js @@ -44,7 +44,7 @@ class PokeballItem extends PTUItemItem { const rollOptions = [...this.getRollOptions(selectors), ...attackRollOptions]; const modifier = new StatisticModifier(this.slug, modifiers); - const action = mergeObject(modifier, { + const action = foundry.utils.mergeObject(modifier, { label: this.name, img: this.img, domains: selectors, @@ -62,8 +62,8 @@ class PokeballItem extends PTUItemItem { params.options ??= []; const target = params.target ?? game.user.targets.first(); - if(!target?.actor) return ui.notifications.warn("PTU.Action.CaptureNoTarget", { localize: true }) - if(target.actor.type === "character") return ui.notifications.warn("PTU.Action.CaptureWrongTarget", { localize: true }) + if (!target?.actor) return ui.notifications.warn("PTU.Action.CaptureNoTarget", { localize: true }) + if (target.actor.type === "character") return ui.notifications.warn("PTU.Action.CaptureWrongTarget", { localize: true }) const context = await this.actor.getCheckContext({ item: this, @@ -105,7 +105,7 @@ class PokeballItem extends PTUItemItem { type: "capture-throw", actor: context.self.actor, token: context.self.token, - targets: [{...context.target, dc: params.dc ?? context.dc, options: context.options ?? []}], + targets: [{ ...context.target, dc: params.dc ?? context.dc, options: context.options ?? [] }], item: context.self.item, domains: selectors, options: context.options, @@ -146,8 +146,8 @@ class PokeballItem extends PTUItemItem { params.options ??= []; const target = params.target ?? game.user.targets.first(); - if(!target?.actor) return ui.notifications.warn("PTU.Action.CaptureNoTarget", { localize: true }) - if(target.actor.type === "character") return ui.notifications.warn("PTU.Action.CaptureWrongTarget", { localize: true }) + if (!target?.actor) return ui.notifications.warn("PTU.Action.CaptureNoTarget", { localize: true }) + if (target.actor.type === "character") return ui.notifications.warn("PTU.Action.CaptureWrongTarget", { localize: true }) const context = await this.actor.getCheckContext({ item: this, @@ -161,12 +161,11 @@ class PokeballItem extends PTUItemItem { const DCModifiers = []; // Capture DC mods { - const levelMod = (-(target.actor.system?.level?.current ?? 0) * 2); // Level mods DCModifiers.push(new PTUModifier({ slug: "level-modifier", label: "Level Modifier", - modifier: game.settings.get("ptu", "variant.trainerRevamp") ? levelMod * 2 : levelMod + modifier: (-(target.actor.system?.level?.current ?? 0) * 2) })); // HP mods @@ -193,19 +192,19 @@ class PokeballItem extends PTUItemItem { // If 1 remaining add +5 // If 0 remaining add +0 const evolutions = target.actor.species?.system?.evolutions ?? []; - if(evolutions.length > 1) { + if (evolutions.length > 1) { const currentEvolution = evolutions.find(e => e.slug == target.actor.species.slug); const remaining = new Set(evolutions.filter(e => e.level > currentEvolution.level).map(x => x.level)).size; const stage = (() => { - const stage =new Set(evolutions.map(x => x.level)).size - remaining; - switch(stage) { + const stage = new Set(evolutions.map(x => x.level)).size - remaining; + switch (stage) { case 1: return "1st Stage"; case 2: return "2nd Stage"; case 3: return "3rd Stage"; default: return `${stage}th Stage` } })(); - if(remaining > 0) { + if (remaining > 0) { DCModifiers.push(new PTUModifier({ slug: "evolution-stage-modifier", label: stage, @@ -216,17 +215,17 @@ class PokeballItem extends PTUItemItem { // Rarity mods // If shiny subtract 10 - if(target.actor.system?.shiny) { + if (target.actor.system?.shiny) { DCModifiers.push(new PTUModifier({ slug: "shiny-modifier", label: "Shiny Modifier", modifier: -10 })); } - + // If legendary subtract 30 // TODO: Add legendary property on pokemon actors - if(target.actor.legendary) { + if (target.actor.legendary) { DCModifiers.push(new PTUModifier({ slug: "legendary-modifier", label: "Legendary Modifier", @@ -238,8 +237,8 @@ class PokeballItem extends PTUItemItem { // For each persistent condition add +15 // For each other condition add +8 const conditions = target.actor.conditions; - for(const condition of conditions?.contents) { - if(condition.persistent) { + for (const condition of conditions?.contents) { + if (condition.persistent) { DCModifiers.push(new PTUModifier({ slug: condition.slug, label: `${condition.name} (persistent condition)`, @@ -258,7 +257,7 @@ class PokeballItem extends PTUItemItem { // Injury mods // For each injury add +5 const injuries = target.actor.system.health?.injuries; - if(injuries) { + if (injuries) { DCModifiers.push(new PTUModifier({ slug: "injury-modifier", label: "Injury Modifier", @@ -269,8 +268,8 @@ class PokeballItem extends PTUItemItem { // Stage mods // For each combat stage in a stat below 0 add +2 to the capture DC // For each combat stage in a stat above 0 add -2 to the capture DC - for(const stat of Object.values(target.actor.system.stats)) { - if(stat.stage?.total < 0) { + for (const stat of Object.values(target.actor.system.stats)) { + if (stat.stage?.total < 0) { DCModifiers.push(new PTUModifier({ slug: stat.slug, label: `${stat.label} Stage Modifier`, @@ -288,7 +287,7 @@ class PokeballItem extends PTUItemItem { } DCModifiers.push( - ...extractModifiers(target.actor.synthetics, ["capture-dc", ...selectors], { injectables: this , test: context.options}) + ...extractModifiers(target.actor.synthetics, ["capture-dc", ...selectors], { injectables: this, test: context.options }) ) const DCCheck = new CheckModifier( @@ -304,7 +303,7 @@ class PokeballItem extends PTUItemItem { rollModifiers.push(new PTUModifier({ slug: "level-modifier", label: "Level Modifier", - modifier: -this.actor.system.level.current + modifier: (["data-revamp", "short-track"].includes(game.settings.get("ptu", "variant.trainerAdvancement")) ? 2 : ["long-track"].includes(game.settings.get("ptu", "variant.trainerAdvancement")) ? 0.5 : 1) * -this.actor.system.level.current })); // Item mods @@ -323,7 +322,7 @@ class PokeballItem extends PTUItemItem { type: "capture-calculation", actor: context.self.actor, token: context.self.token, - targets: [{...context.target, dc: {slug: 'capture-dc', value: 100 + DCCheck.totalModifier}, options: context.options ?? []}], + targets: [{ ...context.target, dc: { slug: 'capture-dc', value: 100 + DCCheck.totalModifier }, options: context.options ?? [] }], item: context.self.item, domains: selectors, options: context.options, @@ -354,7 +353,7 @@ class PokeballItem extends PTUItemItem { return roll; //DC Calculation - + // Roll calc // Roll 1d100 // on nat 100 succeed @@ -377,7 +376,7 @@ class PokeballItem extends PTUItemItem { async requestGmRoll(event, args) { //TODO - if(true) return this.rollCapture(event, args); + if (true) return this.rollCapture(event, args); } async rollCapture(event, args) { @@ -388,7 +387,7 @@ class PokeballItem extends PTUItemItem { async applyCapture(args) { const trainers = game.users.contents.map(c => c.character).filter(c => c?.type === "character"); // If the trainer is not in the list, add them to the front - if(!trainers.includes(this.actor)) trainers.unshift(this.actor); + if (!trainers.includes(this.actor)) trainers.unshift(this.actor); // If the current trainer is in the list, add them to the front else trainers.unshift(trainers.splice(trainers.indexOf(this.actor), 1)[0]); @@ -407,17 +406,17 @@ class PokeballItem extends PTUItemItem { obj[name] = value; return obj; }, {}); - + const trainer = game.actors.get(formData.trainer); - if(!trainer) return ui.notifications.error("PTU.Dialog.CaptureSuccess.TrainerNotFound", { localize: true }); + if (!trainer) return ui.notifications.error("PTU.Dialog.CaptureSuccess.TrainerNotFound", { localize: true }); - const party = new PTUPartySheet({actor: trainer}); + const party = new PTUPartySheet({ actor: trainer }); const location = formData.location; - if(!["party", "box", "available"].includes(location)) return ui.notifications.error("PTU.Dialog.CaptureSuccess.LocationNotFound", { localize: true }); + if (!["party", "box", "available"].includes(location)) return ui.notifications.error("PTU.Dialog.CaptureSuccess.LocationNotFound", { localize: true }); const pokemon = await fromUuid(args.targets[0].actor); - if(!pokemon) return ui.notifications.error("PTU.Dialog.CaptureSuccess.PokemonNotFound", { localize: true }); + if (!pokemon) return ui.notifications.error("PTU.Dialog.CaptureSuccess.PokemonNotFound", { localize: true }); const user = game.users.find(u => u.character?.id === trainer.id); @@ -429,7 +428,7 @@ class PokeballItem extends PTUItemItem { }, "system.pokeball": this.name } - if(location !== "available") { + if (location !== "available") { pokemonUpdateData["flags.ptu.party"] = { trainer: trainer.id, boxed: location === "box", @@ -450,7 +449,7 @@ class PokeballItem extends PTUItemItem { await Actor.updateDocuments([pokemonUpdateData, trainerUpdateData]); await ChatMessage.create({ - content: `${await TextEditor.enrichHTML(game.i18n.format("PTU.Dialog.CaptureSuccess.ChatMessage", { pokemon: pokemon.link, trainer: trainer.link, location: (party.folders?.[location === "available" ? "root" : location]?.link ?? location) }), {async: true})}`, + content: `${await TextEditor.enrichHTML(game.i18n.format("PTU.Dialog.CaptureSuccess.ChatMessage", { pokemon: pokemon.link, trainer: trainer.link, location: (party.folders?.[location === "available" ? "root" : location]?.link ?? location) }), { async: true })}`, speaker: ChatMessage.getSpeaker({ actor: trainer }), }) } diff --git a/src/module/item/move/document.js b/src/module/item/move/document.js index 2b80bd6df..1396336b5 100644 --- a/src/module/item/move/document.js +++ b/src/module/item/move/document.js @@ -12,6 +12,10 @@ class PTUMove extends PTUItem { /** @override */ get rollOptions() { const options = super.rollOptions; + if(this.isDamaging && this.damageBase.isStab) { + options.all['move:is-stab'] = true; + options.item['move:is-stab'] = true; + } if (this.isDamaging && this.damageBase.isStab && !!options.all[`move:damage-base:${this.damageBase.preStab}`]) { delete this.flags.ptu.rollOptions.all[`move:damage-base:${this.damageBase.preStab}`]; delete this.flags.ptu.rollOptions.item[`move:damage-base:${this.damageBase.preStab}`]; @@ -22,6 +26,10 @@ class PTUMove extends PTUItem { options.all[`move:damage-base:${this.damageBase.postStab}`] = true; options.item[`move:damage-base:${this.damageBase.postStab}`] = true; } + for(const keyword of this.system.keywords) { + options.all[`move:${sluggify(keyword)}`] = true; + options.item[`move:${sluggify(keyword)}`] = true; + } return options; } @@ -37,7 +45,7 @@ class PTUMove extends PTUItem { } get isFiveStrike() { - return !!this.rollOptions.item["move:range:five-strike"]; + return (!!this.rollOptions.item["move:range:five-strike"]) || (!!this.rollOptions.item["move:five-strike"]); } get damageBase() { @@ -76,7 +84,7 @@ class PTUMove extends PTUItem { if (!isNaN(Number(this.system.ac))) rollOptions.all[`move:ac:${this.system.ac}`] = true; rollOptions.item = rollOptions.all; - this.flags.ptu = mergeObject(this.flags.ptu, {rollOptions}); + this.flags.ptu = foundry.utils.mergeObject(this.flags.ptu, {rollOptions}); this.flags.ptu.rollOptions.attack = Object.keys(this.flags.ptu.rollOptions.all).reduce((obj, key) => { obj[key.replace("move:", "attack:").replace("item:", "attack:")] = true; return obj; diff --git a/src/module/item/move/sheet.js b/src/module/item/move/sheet.js index edcf6c422..8f97e5dee 100644 --- a/src/module/item/move/sheet.js +++ b/src/module/item/move/sheet.js @@ -2,7 +2,7 @@ import { PTUItemSheet } from "../index.js"; class PTUMoveSheet extends PTUItemSheet { static get defaultOptions() { - return mergeObject(super.defaultOptions, { + return foundry.utils.mergeObject(super.defaultOptions, { template: "systems/ptu/static/templates/item/move-sheet.hbs" }); } diff --git a/src/module/item/sheet/rule-elements/ae-like-form.js b/src/module/item/sheet/rule-elements/ae-like-form.js index ff52ef423..528d73c7c 100644 --- a/src/module/item/sheet/rule-elements/ae-like-form.js +++ b/src/module/item/sheet/rule-elements/ae-like-form.js @@ -1,5 +1,4 @@ import { isObject } from '../../../../util/misc.js'; -import { tagify } from '../../../../util/tags.js'; import { isBracketedValue } from '../../../rules/rule-element/base.js'; import { RuleElementForm } from './base.js' diff --git a/src/module/item/sheet/rule-elements/apply-effect-form.js b/src/module/item/sheet/rule-elements/apply-effect-form.js new file mode 100644 index 000000000..21fa09ea8 --- /dev/null +++ b/src/module/item/sheet/rule-elements/apply-effect-form.js @@ -0,0 +1,80 @@ +import { isObject } from '../../../../util/misc.js'; +import { isBracketedValue } from '../../../rules/rule-element/base.js'; +import { RuleElementForm } from './base.js' + +class ApplyEffectForm extends RuleElementForm { + /** @override */ + get template() { + return "systems/ptu/static/templates/item/rules/apply-effect.hbs"; + } + + /** @override */ + async getData() { + const data = await super.getData(); + const valueMode = this.rule.even ? "even" : "range"; + + const uuid = this.rule.uuid ? String(this.rule.uuid) : null; + const granted = uuid ? await fromUuid(uuid) : null; + + if(this.rule.predicate === undefined) this.updateItem({predicate: []}) + + return { + ...data, + granted, + predicationIsMultiple: Array.isArray(this.rule.predicate) && this.rule.predicate.every(p => typeof p === "string"), + selectorIsArray: Array.isArray(this.rule.selectors), + value: { + mode: valueMode, + data: this.rule.value, + }, + }; + } + + /** @override */ + activateListeners(html) { + // Add events for toggle buttons + html.querySelector("[data-action=toggle-selector]")?.addEventListener("click", () => { + const selector = this.rule.selectors; + const newValue = Array.isArray(selector) ? selector.at(0) ?? "" : [selector ?? ""].filter((s) => !!s); + this.updateItem({ selectors: newValue }); + }); + + // Add events for toggle buttons + html.querySelector("[data-action=toggle-predicate]")?.addEventListener("click", () => { + const predicate = this.rule.predicate; + const newValue = Array.isArray(predicate) ? {"and": predicate.length ? predicate : []} : predicate?.["and"]?.length ? predicate["and"] : []; + this.updateItem({ predicate: newValue }); + }); + + // Add events for toggle buttons + html.querySelector("[data-action=toggle-range]")?.addEventListener("click", () => { + if (this.rule.even) { + this.updateItem({ range: 0, even: false }); + } else { + this.updateItem({ range: "even", even: true }); + } + }); + } + + /** @override */ + _updateObject(formData) { + + formData.value = this.coerceNumber(formData.value ?? ""); + + if(Array.isArray(formData.selectors)) { + formData.selectors = formData.selectors.map(s => s.value).filter(s => !!s) + } + if(Array.isArray(formData.predicate) && formData.predicate.every(p => !!p.value)) { + formData.predicate = formData.predicate.map(s => s.value).filter(s => !!s) + } + + // Remove empty string, null, or falsy values for certain optional parameters + for (const optional of ["label"]) { + if (!formData[optional]) { + delete formData[optional]; + } + } + } +} + +export { ApplyEffectForm }; \ No newline at end of file diff --git a/src/module/item/sheet/rule-elements/base.js b/src/module/item/sheet/rule-elements/base.js index 81d948fdc..0ca0edec0 100644 --- a/src/module/item/sheet/rule-elements/base.js +++ b/src/module/item/sheet/rule-elements/base.js @@ -35,7 +35,7 @@ class RuleElementForm { updateItem(updates) { const rules = this.item.toObject().system.rules; - rules[this.index] = mergeObject(this.rule, updates, { performDeletions: true }); + rules[this.index] = foundry.utils.mergeObject(this.rule, updates, { performDeletions: true }); this.item.update({ [`system.rules`]: rules }); } diff --git a/src/module/item/sheet/rule-elements/effectiveness-form.js b/src/module/item/sheet/rule-elements/effectiveness-form.js index 4899fced0..d789de747 100644 --- a/src/module/item/sheet/rule-elements/effectiveness-form.js +++ b/src/module/item/sheet/rule-elements/effectiveness-form.js @@ -1,4 +1,3 @@ -import { tagify } from '../../../../util/tags.js'; import { RuleElementForm } from './base.js' class EffectivenessForm extends RuleElementForm { diff --git a/src/module/item/sheet/rule-elements/ephemeral-effect-form.js b/src/module/item/sheet/rule-elements/ephemeral-effect-form.js index 22cacc153..0ff8acd9d 100644 --- a/src/module/item/sheet/rule-elements/ephemeral-effect-form.js +++ b/src/module/item/sheet/rule-elements/ephemeral-effect-form.js @@ -1,4 +1,3 @@ -import { tagify } from '../../../../util/tags.js'; import { RuleElementForm } from './index.js'; class EphemeralEffectForm extends RuleElementForm { @@ -18,7 +17,7 @@ class EphemeralEffectForm extends RuleElementForm { return { ...data, granted, - allowDuplicate: !!this.rule.allowDuplicate ?? true, + allowduplicate: !!this.rule.allowduplicate ?? true, selectorIsArray: Array.isArray(this.rule.selectors), predicationIsMultiple: Array.isArray(this.rule.predicate) && this.rule.predicate.every(p => typeof p === "string") }; diff --git a/src/module/item/sheet/rule-elements/flat-modifier-form.js b/src/module/item/sheet/rule-elements/flat-modifier-form.js index f58b0ee05..35a726735 100644 --- a/src/module/item/sheet/rule-elements/flat-modifier-form.js +++ b/src/module/item/sheet/rule-elements/flat-modifier-form.js @@ -1,5 +1,4 @@ import { isObject } from '../../../../util/misc.js'; -import { tagify } from '../../../../util/tags.js'; import { isBracketedValue } from '../../../rules/rule-element/base.js'; import { RuleElementForm } from './base.js' diff --git a/src/module/item/sheet/rule-elements/grant-item-form.js b/src/module/item/sheet/rule-elements/grant-item-form.js index 2d5e349a5..f200ba288 100644 --- a/src/module/item/sheet/rule-elements/grant-item-form.js +++ b/src/module/item/sheet/rule-elements/grant-item-form.js @@ -1,4 +1,3 @@ -import { tagify } from '../../../../util/tags.js'; import { RuleElementForm } from './index.js'; class GrantItemForm extends RuleElementForm { @@ -18,7 +17,7 @@ class GrantItemForm extends RuleElementForm { return { ...data, granted, - allowDuplicate: !!this.rule.allowDuplicate ?? true, + allowduplicate: !!this.rule.allowduplicate ?? true, predicationIsMultiple: Array.isArray(this.rule.predicate) && this.rule.predicate.every(p => typeof p === "string") }; } @@ -50,7 +49,7 @@ class GrantItemForm extends RuleElementForm { if (!formData.reevaluateOnUpdate) delete formData.reevaluateOnUpdate; // Optional but defaults to true - if (formData.allowDuplicate) delete formData.allowDuplicate; + if (formData.allowduplicate) delete formData.allowduplicate; if(Array.isArray(formData.predicate) && formData.predicate.every(p => !!p.value)) { formData.predicate = formData.predicate.map(s => s.value).filter(s => !!s) diff --git a/src/module/item/sheet/rule-elements/index.js b/src/module/item/sheet/rule-elements/index.js index f459196d6..966806121 100644 --- a/src/module/item/sheet/rule-elements/index.js +++ b/src/module/item/sheet/rule-elements/index.js @@ -1,4 +1,5 @@ import { AELikeForm } from "./ae-like-form.js" +import { ApplyEffectForm } from "./apply-effect-form.js" import { RuleElementForm } from "./base.js" import { EffectivenessForm } from "./effectiveness-form.js" import { EphemeralEffectForm } from "./ephemeral-effect-form.js" @@ -12,7 +13,8 @@ const RULE_ELEMENT_FORMS = { RollOption: RollOptionForm, ActiveEffectLike: AELikeForm, Effectiveness: EffectivenessForm, - EphemeralEffect: EphemeralEffectForm + EphemeralEffect: EphemeralEffectForm, + ApplyEffect: ApplyEffectForm } export { RULE_ELEMENT_FORMS, RuleElementForm} \ No newline at end of file diff --git a/src/module/item/sheet/sheet.js b/src/module/item/sheet/sheet.js index 7dc118dd6..a0549cb02 100644 --- a/src/module/item/sheet/sheet.js +++ b/src/module/item/sheet/sheet.js @@ -1,12 +1,11 @@ import { sluggify, sortStringRecord } from "../../../util/misc.js"; -import { tagify } from "../../../util/tags.js"; import { RuleElements } from "../../rules/index.js"; import { RULE_ELEMENT_FORMS, RuleElementForm } from "./rule-elements/index.js"; class PTUItemSheet extends ItemSheet { /** @override */ static get defaultOptions() { - return mergeObject(super.defaultOptions, { + return foundry.utils.mergeObject(super.defaultOptions, { classes: ["ptu", "sheet", "item"], width: 650, height: 510, @@ -15,6 +14,11 @@ class PTUItemSheet extends ItemSheet { }); } + /** @override */ + _canDragDrop(selector) { + return this.isEditable; + } + /** @override */ get template() { return `systems/ptu/static/templates/item/${this.object.type}-sheet.hbs`; @@ -28,8 +32,14 @@ class PTUItemSheet extends ItemSheet { this.object._updateIcon({update: true}); - data.referenceEffect = this.item.referenceEffect ? await TextEditor.enrichHTML(`@UUID[${duplicate(this.item.referenceEffect)}]`, {async: true}) : null; - data.itemEffect = this.item.system.effect ? await TextEditor.enrichHTML(duplicate(this.item.system.effect), {async: true}) : this.item.system.effect; + data.referenceEffect = this.item.referenceEffect ? await TextEditor.enrichHTML(`@UUID[${foundry.utils.duplicate(this.item.referenceEffect)}]`, {async: true}) : null; + data.itemEffect = this.item.system.effect ? await TextEditor.enrichHTML(foundry.utils.duplicate(this.item.system.effect), {async: true}) : this.item.system.effect; + data.itemCost = await (async () => { + const cost = parseInt(this.item.system.cost); + if(!cost) return this.item.system.cost || "-"; + + return TextEditor.enrichHTML(`@Poke[${this.item.uuid} noname]`, {async: true}) + })(); const rules = this.item.toObject().system.rules ?? []; this.ruleElementForms = {}; @@ -54,7 +64,7 @@ class PTUItemSheet extends ItemSheet { selected: this.selectedRuleElementType, types: sortStringRecord( Object.keys(RuleElements.all).reduce( - (result, key) => mergeObject(result, {[key]: `RULES.Types.${key}`}), + (result, key) => foundry.utils.mergeObject(result, {[key]: `RULES.Types.${key}`}), {} ) ) @@ -70,7 +80,7 @@ class PTUItemSheet extends ItemSheet { if(this.item.flags.ptu?.showInTokenPanel === undefined) { if(this.item.type === "item" && this.item.roll) data.item.flags.ptu.showInTokenPanel = true; - if (["move", "ability", "feat"].includes(this.item.type)) data.item.flags.ptu.showInTokenPanel = true; + if (["move", "ability", "feat", "effect"].includes(this.item.type)) data.item.flags.ptu.showInTokenPanel = true; } return data; @@ -79,6 +89,13 @@ class PTUItemSheet extends ItemSheet { async _onDrop(event) { const data = JSON.parse(event.dataTransfer.getData('text/plain')); + if(data.type === "pokedollar" && this.item.type === "item") { + const amount = parseInt(data.data.amount); + if(!amount) return; + + this.object.update({"system.cost": amount}); + } + if(data.type === "Item" && data.uuid) { const item = await fromUuid(data.uuid); if(!["effect", "condition".includes(item.type)]) return; @@ -105,10 +122,6 @@ class PTUItemSheet extends ItemSheet { activateListeners(html) { super.activateListeners(html); - for(const taggifyElement of html.find(".ptu-tagify")) { - tagify(taggifyElement); - } - html.find('select[data-action=select-rule-element]').on('change', (event) => { this.selectedRuleElementType = event.target.value; }); @@ -189,11 +202,14 @@ class PTUItemSheet extends ItemSheet { /** @override */ async _updateObject(event, formData) { - const expanded = expandObject(formData); + const expanded = foundry.utils.expandObject(formData); if(Array.isArray(expanded.system.prerequisites)) { expanded.system.prerequisites = expanded.system.prerequisites.map(s => s.value).filter(s => !!s) } + if(Array.isArray(expanded.system.keywords)) { + expanded.system.keywords = expanded.system.keywords.map(s => s.value).filter(s => !!s) + } if(expanded.system?.rules) { const rules = this.item.toObject().system.rules ?? []; @@ -221,7 +237,7 @@ class PTUItemSheet extends ItemSheet { if(!value) continue; - rules[idx] = mergeObject(rules[idx] ?? {}, value); + rules[idx] = foundry.utils.mergeObject(rules[idx] ?? {}, value); // Call specific subhandlers this.ruleElementForms[idx]?._updateObject(rules[idx]) @@ -246,7 +262,7 @@ class PTUItemSheet extends ItemSheet { expanded.system.rules = rules; } - return super._updateObject(event, flattenObject(expanded)); + return super._updateObject(event, foundry.utils.flattenObject(expanded)); } } diff --git a/src/module/item/species/sheet.js b/src/module/item/species/sheet.js index d83983095..8a701c612 100644 --- a/src/module/item/species/sheet.js +++ b/src/module/item/species/sheet.js @@ -151,14 +151,15 @@ class PTUSpeciesSheet extends PTUItemSheet { /** @override */ _onDragStart(event) { const li = event.currentTarget; - const { itemUuid, itemSlug, itemType, itemSubtype, itemIndex } = li.dataset; + const { itemUuid, itemSlug, itemType, itemSubtype, itemIndex, itemLevel } = li.dataset; event.dataTransfer.setData('text/plain', JSON.stringify({ uuid: itemUuid, slug: itemSlug, type: itemType, subtype: itemSubtype, - index: itemIndex + index: itemIndex, + level: itemLevel })); } @@ -222,7 +223,7 @@ class PTUSpeciesSheet extends PTUItemSheet { this.dragMove = null; return; } - moves.level.unshift({uuid: item.uuid, slug: item.slug, level: 1}); + moves.level.unshift({uuid: item.uuid, slug: item.slug, level: Number(data.level) || 1}); } return this.item.update({"system.moves": moves}); @@ -295,6 +296,22 @@ class PTUSpeciesSheet extends PTUItemSheet { const {itemType, itemIndex, zone, subZone} = event.currentTarget.dataset; let { itemSubtype } = event.currentTarget.dataset; + const localItem = this.item.system.moves[subtype][index]; + const realItem = await fromUuid(data.uuid); + if(localItem?.slug != realItem?.slug) { + if(!realItem || realItem.type != "move") return; + + const moves = this.item.system.moves; + if(itemSubtype == "level") { + moves[itemSubtype].unshift({slug: realItem.slug, uuid: realItem.uuid, level: Number(data.level) || 1}); + moves[itemSubtype] = moves[itemSubtype].sort((a, b) => a.level - b.level); + } + else { + moves[itemSubtype].push({slug: realItem.slug, uuid: realItem.uuid}); + } + return this.item.update({"system.moves": moves}); + } + if(itemType != "move") { if(zone != "move") return; @@ -331,7 +348,7 @@ class PTUSpeciesSheet extends PTUItemSheet { /** @override */ _updateObject(event, formData) { - const expanded = expandObject(formData); + const expanded = foundry.utils.expandObject(formData); const types = [...new Set(Object.values(expanded.system.type))].filter(type => type != "Untyped" && type != ""); if(types.length == 0) types.push("Untyped"); @@ -396,7 +413,7 @@ class PTUSpeciesSheet extends PTUItemSheet { expanded.system.evolutions = evolutions.sort((a, b) => a.level - b.level); } - return super._updateObject(event, flattenObject(expanded)); + return super._updateObject(event, foundry.utils.flattenObject(expanded)); } /** @override */ diff --git a/src/module/message/attack.js b/src/module/message/attack.js index db11c9bf4..0b355e764 100644 --- a/src/module/message/attack.js +++ b/src/module/message/attack.js @@ -78,6 +78,7 @@ class AttackMessagePTU extends ChatMessagePTU { const params = { event, options: this.context.options ?? [], + rollResult: this.context.rollResult ?? null, actor: this.actor, targets: this.targets, callback: () => { diff --git a/src/module/message/base.js b/src/module/message/base.js index 3a88c66f3..09df2b122 100644 --- a/src/module/message/base.js +++ b/src/module/message/base.js @@ -1,6 +1,6 @@ class ChatMessagePTU extends ChatMessage { constructor(data = {}, context = {}) { - data.flags = mergeObject(expandObject(data.flags ?? {}), { core: {}, ptu: {} }); + data.flags = foundry.utils.mergeObject(foundry.utils.expandObject(data.flags ?? {}), { core: {}, ptu: {} }); super(data, context); } @@ -128,7 +128,7 @@ class ChatMessagePTU extends ChatMessage { const options = actor.getRollOptions(["all", "attack-roll"]); - const rollArgs = { event, options }; + const rollArgs = { event, options, rollResult: this.context.rollResult ?? null, }; return attack.damage?.(rollArgs); } @@ -207,6 +207,7 @@ class ChatMessagePTU extends ChatMessage { activateListeners($html) { $html.find("button.use").on("click", async event => { event.preventDefault(); + event.stopImmediatePropagation(); const item = await fromUuid(this.flags.ptu.origin.item); if (!item) return; @@ -214,7 +215,7 @@ class ChatMessagePTU extends ChatMessage { }); $html.find("button.apply-capture").on("click", async event => { event.preventDefault(); - event.stopPropagation(); + event.stopImmediatePropagation(); const item = await fromUuid(this.flags.ptu.origin.uuid); if (!item) return; @@ -222,7 +223,7 @@ class ChatMessagePTU extends ChatMessage { }); $html.find("button.contested-check").on("click", async event => { event.preventDefault(); - event.stopPropagation(); + event.stopImmediatePropagation(); const total = this.rolls[0]?.total; const target = this.targets.find(t => t.actor.isOwner); diff --git a/src/module/message/damage.js b/src/module/message/damage.js index ac38c17a8..9c1893eb7 100644 --- a/src/module/message/damage.js +++ b/src/module/message/damage.js @@ -1,4 +1,5 @@ -import { extractEphemeralEffects } from "../rules/helpers.js"; +import { sluggify } from "../../util/misc.js"; +import { extractApplyEffects, extractEphemeralEffects } from "../rules/helpers.js"; import { DamageRoll } from "../system/damage/roll.js"; import { ChatMessagePTU } from "./base.js"; @@ -186,6 +187,23 @@ async function applyDamageFromMessage({ message, targets, mode = "full", addend const [effectiveness, multiplier] = getMAFromMode(mode); + const originAttackOptions = message.flags.ptu.attack ?? {}; + const originItem = (await fromUuid(originAttackOptions.actor))?.items.get(originAttackOptions.id) ?? null; + const itemDomains = []; + if (originItem) { + itemDomains.push( + `${originItem.id}-damage-received`, + `${originItem.slug}-damage-received`, + ) + if(originItem.type === "move") { + itemDomains.push( + `${originItem.system.category.toLocaleLowerCase(game.i18n.lang)}-damage-received`, + `${originItem.system.type.toLocaleLowerCase(game.i18n.lang)}-damage-received`, + `${sluggify(originItem.system.frequency)}-damage-received`, + ) + } + } + const messageRollOptions = message.flags.ptu.context?.options ?? []; const originRollOptions = messageRollOptions .filter(o => o.startsWith("self:")) @@ -204,13 +222,15 @@ async function applyDamageFromMessage({ message, targets, mode = "full", addend totalCritImmune: (multiplier * roll.critImmuneTotal) + addend, } + const domains = ["damage-received", ...itemDomains]; + const ephemeralEffects = [ ...await extractEphemeralEffects({ affects: "target", origin: message.actor, target: token.actor, item: message.item, - domains: ["damage-received"], + domains, options: messageRollOptions }), // Ephemeral Effects on the target that it wishes to apply to itself @@ -220,11 +240,25 @@ async function applyDamageFromMessage({ message, targets, mode = "full", addend origin: message.actor, target: token.actor, item: message.item, - domains: ["damage-received"], + domains, options: messageRollOptions }) ]; + const applyEffectsTarget = Object.values([ + ...await extractApplyEffects({ + affects: "target", + origin: message.actor, + target: token.actor, + item: message.item, + domains, + options: messageRollOptions, + roll: Number(message.flags.ptu.context.accuracyRollResult ?? 0) + }), + ].reduce((a, b) => { + if (!a[b.slug]) a[b.slug] = b; + return a; + }, {})); const contextClone = token.actor.getContextualClone(originRollOptions, ephemeralEffects); const applicationRollOptions = new Set([ @@ -241,6 +275,41 @@ async function applyDamageFromMessage({ message, targets, mode = "full", addend rollOptions: applicationRollOptions, skipIWR }); + + if (applyEffectsTarget.length > 0) { + const newItems = await contextClone.createEmbeddedDocuments("Item", applyEffectsTarget); + if (newItems.length > 0) + await ChatMessage.create({ + content: await renderTemplate("systems/ptu/static/templates/chat/damage/effects-applied.hbs", { target: contextClone, effects: newItems }), + speaker: ChatMessage.getSpeaker({ actor: contextClone }), + whisper: ChatMessage.getWhisperRecipients("GM") + }) + } + } + + const applyEffectsOrigin = Object.values([ + ...await extractApplyEffects({ + affects: "origin", + origin: message.actor, + target: message.actor, + item: message.item, + domains: ["damage-dealt", ...itemDomains.map(d => d.replace(/-received$/, "-dealt"))], + options: messageRollOptions, + roll: Number(message.flags.ptu.context.accuracyRollResult ?? 0) + }), + ].reduce((a, b) => { + if (!a[b.slug]) a[b.slug] = b; + return a; + }, {})); + + if (applyEffectsOrigin.length > 0) { + const newItems = await message.actor.createEmbeddedDocuments("Item", applyEffectsOrigin); + if (newItems.length > 0) + await ChatMessage.create({ + content: await renderTemplate("systems/ptu/static/templates/chat/damage/effects-applied.hbs", { target: message.actor, effects: newItems }), + speaker: ChatMessage.getSpeaker({ actor: message.actor }), + whisper: ChatMessage.getWhisperRecipients("GM") + }) } } diff --git a/src/module/migration/migrations/105-custom-species-migration.js b/src/module/migration/migrations/105-custom-species-migration.js index bdb50109c..da264d864 100644 --- a/src/module/migration/migrations/105-custom-species-migration.js +++ b/src/module/migration/migrations/105-custom-species-migration.js @@ -18,11 +18,14 @@ export class Migration105CustomSpeciesMigration extends MigrationBase { const species = []; for (const data of oldCS.data) { const specie = await CONFIG.PTU.Item.documentClasses.species.convertToPTUSpecies(data, { prepareOnly: true }); + specie._id = foundry.utils.randomID(); + const ownEvolution = specie.system.evolutions.at(1); + if(!!ownEvolution.uuid) ownEvolution.uuid = specie._id; specie.folder = folder.id; species.push(specie); } - const customSpecies = await Item.createDocuments(species); + const customSpecies = await Item.createDocuments(species, { keepId: true}); if (customSpecies.length > 0) { await game.settings.set("ptu", "customSpeciesData", { flags: { schemaVersion: this.version } }); } diff --git a/src/module/migration/migrations/112-agility-reference.js b/src/module/migration/migrations/112-agility-reference.js new file mode 100644 index 000000000..6ddd9fa7b --- /dev/null +++ b/src/module/migration/migrations/112-agility-reference.js @@ -0,0 +1,23 @@ +import { sluggify } from "../../../util/misc.js"; +import { MigrationBase } from "../base.js"; + +export class Migration112AgilityReference extends MigrationBase { + static version = 0.112; + + /** + * @type {MigrationBase['updateItem']} + */ + async updateItem(item, actor) { + if (item.type !== "species") return; + + for (const type of Object.keys(item.system.moves)) { + for (const move of item.system.moves[type]) { + if (move.slug === "agility") { move.uuid = "Compendium.ptu.moves.Item.aS33KlxhRCP0s5Zd"; continue; } + if (item.slug === "palafin" && move.slug === "wave-crash") { move.uuid = "Compendium.ptu.moves.Item.XNmCCgclMJf0MP6C"; continue; } + if (item.slug === "dipplin" && move.slug === "withdraw") { move.uuid = "Compendium.ptu.moves.Item.PLNrSnH8aJCbQSAq"; continue; } + if (item.slug === "dipplin" && move.slug === "astonish") { move.uuid = "Compendium.ptu.moves.Item.VtkRa05N67wR9RcG"; continue; } + if (item.slug === "dipplin" && move.slug === "syrupy-bomb") { move.uuid = "Compendium.ptu.moves.Item.oKd3bholSLtezIEv"; continue; } + } + } + } +} \ No newline at end of file diff --git a/src/module/migration/migrations/113-keywords.js b/src/module/migration/migrations/113-keywords.js new file mode 100644 index 000000000..e0b535297 --- /dev/null +++ b/src/module/migration/migrations/113-keywords.js @@ -0,0 +1,33 @@ +import { sluggify } from "../../../util/misc.js"; +import { MigrationBase } from "../base.js"; + +export class Migration113Keywords extends MigrationBase { + static version = 0.113; + + items = { + feat: undefined, + move: undefined, + item: undefined + } + + /** + * @type {MigrationBase['updateItem']} + */ + async updateItem(item, actor) { + if(!["feat", "move", "item"].includes(item.type)) return; + + const items = this.items[item.type] ??= await game.packs.get(`ptu.${item.type}s`).getDocuments(); + + const entry = items.find(entry => entry.slug === (item.slug || sluggify(item.name))); + if(!entry) return; + + item.system.keywords = entry.system.keywords; + if(item.type === 'feat') { + delete item.system.tags; + item.system["-=tags"] = null; + } + if(item.type === "move") { + item.system.range = entry.system.range; + } + } +} \ No newline at end of file diff --git a/src/module/migration/migrations/114-hardened.js b/src/module/migration/migrations/114-hardened.js new file mode 100644 index 000000000..38eeb1829 --- /dev/null +++ b/src/module/migration/migrations/114-hardened.js @@ -0,0 +1,19 @@ +import { sluggify } from "../../../util/misc.js"; +import { MigrationBase } from "../base.js"; + +export class Migration114Hardened extends MigrationBase { + static version = 0.114; + + /** + * @type {MigrationBase['updateItem']} + */ + async updateItem(item, actor) { + const rules = item.system.rules; + if(!rules) return; + + const rule = rules.find(rule => rule.path === "system.modifiers.resistanceSteps.mod"); + if(!rule) return; + + if(rule.value === 0.5) rule.value = 1; + } +} \ No newline at end of file diff --git a/src/module/migration/migrations/115-rules-automation.js b/src/module/migration/migrations/115-rules-automation.js new file mode 100644 index 000000000..d4703aaca --- /dev/null +++ b/src/module/migration/migrations/115-rules-automation.js @@ -0,0 +1,55 @@ +import { sluggify } from "../../../util/misc.js"; +import { MigrationBase } from "../base.js"; + +export class Migration115RulesAutomation extends MigrationBase { + static version = 0.115; + requiresFlush = true; + + /** + * @type {MigrationBase['updateItem']} + */ + async updateItem(item, actor) { + if(item.type !== "move") return; + const moves = this.moves ??= await game.packs.get("ptu.moves").getDocuments(); + const slug = item.system.slug || sluggify(item.name); + + const move = (() => { + // Try to look up by source ID + const sourceId = item.flags?.core?.sourceId; + if(sourceId) { + const idPart = sourceId.split(".").at(-1); + const move = moves.find(move => move.id === idPart); + if(move) return move; + } + + // Try to look up by slug + return moves.find(move => move.slug === slug); + })(); + if(!move) return; + + if(move.system.referenceEffect && !item.system.referenceEffect) { + item.system.referenceEffect = move.system.referenceEffect; + } + + if(move.system.rules?.length == 0) return; + + const oldRules = item.system.rules ?? []; + const newRules = []; + for(const rule of move.system.rules) { + const anyMatch = oldRules.some(oldRule => foundry.utils.objectsEqual(oldRule, rule)); + if(anyMatch) continue; + + newRules.push(rule); + } + + item.system.rules = oldRules.concat(newRules); + + if(!this.once) { + this.once = true; + await ChatMessage.create({ + speaker: { alias: "System" }, + content: "All moves have been updated to include newly created rules automation.
If you had manually created rules automation before, they have been added in addition, therefore you may now find duplicate automation on your moves.
Please double check this in case you added custom automation to core moves!
" + }) + } + } +} \ No newline at end of file diff --git a/src/module/migration/migrations/index.js b/src/module/migration/migrations/index.js index 7a1ab780c..cb5c838b7 100644 --- a/src/module/migration/migrations/index.js +++ b/src/module/migration/migrations/index.js @@ -8,4 +8,8 @@ export { Migration107ActiveEffectsToEffectItem} from './107-active-effects-to-ef export { Migration108SpritesFix} from './108-sprites-fix.js'; export { Migration109PokeballFix} from './109-pokeball-fix.js'; export { Migration110ActorLink} from './110-actor-link.js'; -export { Migration111PrereqItems} from './111-prereq-items.js'; \ No newline at end of file +export { Migration111PrereqItems} from './111-prereq-items.js'; +export { Migration112AgilityReference} from './112-agility-reference.js'; +export { Migration113Keywords} from './113-keywords.js'; +export { Migration114Hardened} from './114-hardened.js'; +export { Migration115RulesAutomation} from './115-rules-automation.js'; \ No newline at end of file diff --git a/src/module/migration/runner/base.js b/src/module/migration/runner/base.js index a1fd8b99d..767765d82 100644 --- a/src/module/migration/runner/base.js +++ b/src/module/migration/runner/base.js @@ -11,7 +11,7 @@ class MigrationRunnerBase { /** @type {MigrationBase[]} */ migrations = [] - static LATEST_SCHEMA_VERSION = 0.111; + static LATEST_SCHEMA_VERSION = 0.115; static MINIMUM_SAFE_VERSION = 0.100; static RECOMMENDED_SAFE_VERSION = 0.101; @@ -73,7 +73,7 @@ class MigrationRunnerBase { * @param {MigrationBase[]} migrations */ async getUpdatedActor(actorSource, migrations) { - const currentActor = deepClone(actorSource); + const currentActor = foundry.utils.deepClone(actorSource); for (const migration of migrations) { for (const currentItem of currentActor.items) { @@ -106,7 +106,7 @@ class MigrationRunnerBase { * @param {MigrationBase[]} migrations */ async getUpdatedItem(itemSource, migrations) { - const current = deepClone(itemSource); + const current = foundry.utils.deepClone(itemSource); for (const migration of migrations) { await migration.preUpdateItem?.(current); @@ -127,7 +127,7 @@ class MigrationRunnerBase { * @param {MigrationBase[]} migrations */ async getUpdatedTable(tableSource, migrations) { - const current = deepClone(tableSource); + const current = foundry.utils.deepClone(tableSource); for (const migration of migrations) { try { @@ -145,7 +145,7 @@ class MigrationRunnerBase { * @param {MigrationBase[]} migrations */ async getUpdatedMacro(macroSource, migrations) { - const current = deepClone(macroSource); + const current = foundry.utils.deepClone(macroSource); for (const migration of migrations) { try { @@ -163,7 +163,7 @@ class MigrationRunnerBase { * @param {MigrationBase[]} migrations */ async getUpdatedJournalEntry(journalSource, migrations) { - const clone = deepClone(journalSource); + const clone = foundry.utils.deepClone(journalSource); for (const migration of migrations) { try { @@ -194,7 +194,7 @@ class MigrationRunnerBase { * @param {MigrationBase[]} migrations */ async getUpdatedUser(userData, migrations) { - const current = deepClone(userData); + const current = foundry.utils.deepClone(userData); for (const migration of migrations) { try { await migration.updateUser?.(current); diff --git a/src/module/migration/runner/index.js b/src/module/migration/runner/index.js index 74055a392..548cf897e 100644 --- a/src/module/migration/runner/index.js +++ b/src/module/migration/runner/index.js @@ -184,7 +184,7 @@ class MigrationRunner extends MigrationRunnerBase { try { const updated = await this.getUpdatedJournalEntry(journalEntry.toObject(), migrations); - const changes = diffObject(journalEntry.toObject(), updated); + const changes = foundry.utils.diffObject(journalEntry.toObject(), updated); if (Object.keys(changes).length > 0) { await journalEntry.update(changes, { noHook: true }); } @@ -204,7 +204,7 @@ class MigrationRunner extends MigrationRunnerBase { try { const updatedMacro = await this.getUpdatedMacro(macro.toObject(), migrations); - const changes = diffObject(macro.toObject(), updatedMacro); + const changes = foundry.utils.diffObject(macro.toObject(), updatedMacro); if (Object.keys(changes).length > 0) { await macro.update(changes, { noHook: true }); } @@ -224,7 +224,7 @@ class MigrationRunner extends MigrationRunnerBase { try { const updatedMacro = await this.getUpdatedTable(table.toObject(), migrations); - const changes = diffObject(table.toObject(), updatedMacro); + const changes = foundry.utils.diffObject(table.toObject(), updatedMacro); if (Object.keys(changes).length > 0) { table.update(changes, { noHook: true }); } @@ -244,7 +244,7 @@ class MigrationRunner extends MigrationRunnerBase { try { const updatedToken = await this.getUpdatedToken(token, migrations); - const changes = diffObject(token.toObject(), updatedToken); + const changes = foundry.utils.diffObject(token.toObject(), updatedToken); if (Object.keys(changes).length > 0) { try { @@ -272,7 +272,7 @@ class MigrationRunner extends MigrationRunnerBase { try { const baseUser = user.toObject(); const updatedUser = await this.getUpdatedUser(baseUser, migrations); - const changes = diffObject(user.toObject(), updatedUser); + const changes = foundry.utils.diffObject(user.toObject(), updatedUser); if (Object.keys(changes).length > 0) { await user.update(changes, { noHook: true }); } diff --git a/src/module/rules/helpers.js b/src/module/rules/helpers.js index cb1ace2c7..11df6e8fa 100644 --- a/src/module/rules/helpers.js +++ b/src/module/rules/helpers.js @@ -44,18 +44,33 @@ async function extractEphemeralEffects({ affects, origin, target, item, domains, ).flatMap(e => e ?? []) } +async function extractApplyEffects({ affects, origin, target, item, domains, options, roll }) { + if (!(origin && target)) return []; + + const [effectsFrom, effectsTo] = affects === "target" ? [origin, target] : [target, origin]; + const fullOptions = [...options, ...effectsTo.getSelfRollOptions(affects)]; + const resolvables = item?.type == "move" ? { move: item } : {}; + return ( + await Promise.all( + domains + .flatMap(s => effectsFrom.synthetics.applyEffects[s]?.[affects] ?? []) + .map(d => d({ test: fullOptions, resolvables, roll })) + ) + ).flatMap(e => e ?? []) +} + function extractRollSubstitutions(substitutions, domains, rollOptions) { return domains - .flatMap((d) => deepClone(substitutions?.[d] ?? [])) + .flatMap((d) => foundry.utils.deepClone(substitutions?.[d] ?? [])) .filter((s) => s.predicate?.test(rollOptions) ?? true); } -async function processPreUpdateActorHooks(changed,{ pack }){ +async function processPreUpdateActorHooks(changed, { pack }) { const actorId = String(changed._id); const actor = pack ? await game.packs.get(pack)?.getDocument(actorId) : game.actors.get(actorId); if (!(actor instanceof CONFIG.PTU.Actor.documentClass)) return; - if(actor.prototypeToken.actorLink !== true) { + if (actor.prototypeToken.actorLink !== true) { changed.prototypeToken ??= {}; changed.prototypeToken.actorLink = true; } @@ -69,8 +84,7 @@ async function processPreUpdateActorHooks(changed,{ pack }){ await Promise.all( rules.map( (r) => - actor.items.has(r.item.id) ? r.preUpdateActor() : new Promise(() => ({ create: [], delete: [] })) - ) + actor.items.has(r.item.id) ? r.preUpdateActor() : { create: [], delete: [] }) ) ).reduce( (combined, cd) => ({ @@ -85,7 +99,7 @@ async function processPreUpdateActorHooks(changed,{ pack }){ await actor.deleteEmbeddedDocuments("Item", createDeletes.delete, { render: false }); } -export { extractEphemeralEffects, extractDamageDice, extractNotes, extractModifierAdjustments, extractRollSubstitutions, extractModifiers, processPreUpdateActorHooks} +export { extractEphemeralEffects, extractApplyEffects, extractDamageDice, extractNotes, extractModifierAdjustments, extractRollSubstitutions, extractModifiers, processPreUpdateActorHooks } globalThis.extractEphemeralEffects = extractEphemeralEffects; globalThis.extractDamageDice = extractDamageDice; diff --git a/src/module/rules/index.js b/src/module/rules/index.js index 0dde39d71..f8f7998b2 100644 --- a/src/module/rules/index.js +++ b/src/module/rules/index.js @@ -13,6 +13,7 @@ import { TempHPRuleElement } from "./rule-element/temp-hp.js"; import { EffectivenessRuleElement } from "./rule-element/effectiveness.js"; import { EphemeralEffectRuleElement } from "./rule-element/ephemeral-effect.js"; import { ActionPointsRuleElement } from "./rule-element/ap.js"; +import { ApplyEffectRuleElement } from "./rule-element/apply-effect.js"; class RuleElements { static builtin = { @@ -29,7 +30,8 @@ class RuleElements { "TempHP": TempHPRuleElement, "Effectiveness": EffectivenessRuleElement, "EphemeralEffect": EphemeralEffectRuleElement, - "ActionPoint": ActionPointsRuleElement + "ActionPoint": ActionPointsRuleElement, + "ApplyEffect": ApplyEffectRuleElement, } static custom = {} diff --git a/src/module/rules/rule-element/ae-like.js b/src/module/rules/rule-element/ae-like.js index 4769b1ef8..cbb8501e7 100644 --- a/src/module/rules/rule-element/ae-like.js +++ b/src/module/rules/rule-element/ae-like.js @@ -17,8 +17,9 @@ class AELikeRuleElement extends RuleElementPTU { ...super.defineSchema(), mode: new foundry.data.fields.StringField({ type: String, required: true, choices: Object.keys(AELikeRuleElement.CHANGE_MODES), initial: undefined }), path: new foundry.data.fields.StringField({ type: String, required: true, nullable: false, blank: false, initial: undefined }), - phase: new foundry.data.fields.StringField({ type: String, required: false, nullable: false, choices: deepClone(AELikeRuleElement.PHASES), initial: "applyAEs" }), - value: new ResolvableValueField({ required: true, nullable: true, initial: undefined }) + phase: new foundry.data.fields.StringField({ type: String, required: false, nullable: false, choices: foundry.utils.deepClone(AELikeRuleElement.PHASES), initial: "applyAEs" }), + value: new ResolvableValueField({ required: true, nullable: true, initial: undefined }), + priority: new foundry.data.fields.NumberField({ required: false, nullable: true, initial: undefined }), } } @@ -40,7 +41,7 @@ class AELikeRuleElement extends RuleElementPTU { typeof this.path === "string" && this.path.length > 0 && [this.path, this.path.replace(/\.\w+$/, ""), this.path.replace(/\.?\w+\.\w+$/, "")].some( - (path) => typeof getProperty(actor, path) !== undefined + (path) => typeof foundry.utils.getProperty(actor, path) !== undefined ); if (!pathIsValid) return this._warn("path"); } @@ -101,7 +102,7 @@ class AELikeRuleElement extends RuleElementPTU { apply(rollOptions) { this.validateData(); - if (!this.test(rollOptions)) return; + if (!this.test(rollOptions ?? this.actor.getRollOptions())) return; const path = this.resolveInjectedProperties(this.path); @@ -109,7 +110,7 @@ class AELikeRuleElement extends RuleElementPTU { if (/\bundefined\b/.test(path)) return; const { actor } = this; - const current = getProperty(actor, path); + const current = foundry.utils.getProperty(actor, path); const change = this.resolveValue(this.value) const newValue = this.getNewValue(current, change); if (this.ignored) return; @@ -125,7 +126,7 @@ class AELikeRuleElement extends RuleElementPTU { return; } try { - setProperty(actor, path, newValue); + foundry.utils.setProperty(actor, path, newValue); this._logChange(change); } catch (error) { console.warn(error); @@ -212,7 +213,7 @@ class AELikeRuleElement extends RuleElementPTU { const { changes } = this.actor.system; const realPath = this.resolveInjectedProperties(this.path); const entries = (changes[realPath] ??= {}); - entries[randomID()] = { mode: this.mode, value, sourceId: this.item.uuid, source: this.item.name.includes(":") ? this.item.name.split(":")[1].trim() : this.item.name }; + entries[foundry.utils.randomID()] = { mode: this.mode, value, sourceId: this.item.isGlobal ? (this.item.flags?.core?.sourceId ?? this.item.uuid) : this.item.uuid, source: this.item.name.includes(":") ? this.item.name.split(":")[1].trim() : this.item.name}; } _warn(property) { diff --git a/src/module/rules/rule-element/ap.js b/src/module/rules/rule-element/ap.js index f509f24fa..235c30d6c 100644 --- a/src/module/rules/rule-element/ap.js +++ b/src/module/rules/rule-element/ap.js @@ -24,8 +24,8 @@ export class ActionPointsRuleElement extends RuleElementPTU { // drained and bound values are already included. Therefore, the maxAp before this rule can only be calced by adding // this rules values up to it again. const maxApBeforeRule = (Number(this.actor.system.ap.max) || 0) + this.drainedValue + this.boundValue; - const currentApAfterRule = Math.clamped(currentApBeforeRule - this.drainedValue - this.boundValue, 0, maxApBeforeRule - this.drainedValue - this.boundValue); - mergeObject(actorUpdates, { + const currentApAfterRule = Math.clamp(currentApBeforeRule - this.drainedValue - this.boundValue, 0, maxApBeforeRule - this.drainedValue - this.boundValue); + foundry.utils.mergeObject(actorUpdates, { "system.ap.value": currentApAfterRule }); } @@ -42,9 +42,9 @@ export class ActionPointsRuleElement extends RuleElementPTU { const maxApBeforeDeletion = Number(this.actor.system.ap.max) || 0; // Regain previously Bound AP - const currentApAfterDeletion = Math.clamped(currentApBeforeDeletion + this.boundValue, 0, maxApBeforeDeletion + this.drainedValue + this.boundValue); + const currentApAfterDeletion = Math.clamp(currentApBeforeDeletion + this.boundValue, 0, maxApBeforeDeletion + this.drainedValue + this.boundValue); - mergeObject(actorUpdates, { + foundry.utils.mergeObject(actorUpdates, { "system.ap.value": currentApAfterDeletion }); } diff --git a/src/module/rules/rule-element/apply-effect.js b/src/module/rules/rule-element/apply-effect.js new file mode 100644 index 000000000..b73333a54 --- /dev/null +++ b/src/module/rules/rule-element/apply-effect.js @@ -0,0 +1,105 @@ +import { sluggify } from "../../../util/misc.js"; +import { PTUModifier } from "../../actor/modifiers.js"; +import { ResolvableValueField } from "../../system/schema-data-fields.js"; +import { RuleElementPTU } from "./base.js"; + +class ApplyEffectRuleElement extends RuleElementPTU { + constructor(source, item, options) { + super(source, item, options); + } + + /** @override */ + static defineSchema() { + const { fields } = foundry.data; + + return { + ...super.defineSchema(), + uuid: new foundry.data.fields.StringField({ required: true, nullable: false, blank: false, initial: undefined }), + affects: new fields.StringField({ required: true, choices: ["target", "origin"], initial: "target" }), + selectors: new fields.ArrayField( + new fields.StringField({ required: true, blank: false, initial: undefined }), + ), + range: new ResolvableValueField({ required: false, nullable: false, initial: undefined }), + even: new fields.BooleanField({ required: false, nullable: false, initial: false }), + }; + } + + /** @override */ + beforePrepareData() { + if (this.ignored) return; + + const uuid = this.resolveInjectedProperties(this.uuid); + + const selectors = this.selectors.map(s => this.resolveInjectedProperties(s)).filter(s => !!s); + if (selectors.length === 0) { + return this.failValidation("must have at least one selector"); + } + + for (const selector of selectors) { + const construct = async (options = {}) => { + if (!this.test(options.test ?? this.actor.getRollOptions())) { + return null; + } + + const grantedItem = await (async () => { + try { + return (await fromUuid(uuid))?.clone(this.overwrites ?? {}) ?? null; + } + catch (error) { + console.error(error); + return null; + } + })(); + if (!(grantedItem instanceof CONFIG.PTU.Item.documentClass)) return null; + + if (this.even && Number(options.roll) % 2 !== 0) { + return null; + } + + const effectRange = (() => { + const modifiers = [ + new PTUModifier({ + slug: "effect-range", + label: "Effect Range", + modifier: this.actor?.system?.modifiers?.effectRange?.total ?? 0, + }) + ] + if (this.actor?.synthetics) { + modifiers.push( + ...extractModifiers(this.actor?.synthetics, [ + "effect-range", + this.item?.id ? `${this.item.id}-effect-range` : [], + this.item?.slug ? `${this.item.slug}-effect-range` : [], + this.item?.system?.category ? `${sluggify(this.item.system.category)}-effect-range` : [], + this.item?.system?.type ? `${sluggify(this.item.system.type)}-effect-range` : [], + this.item?.system?.frequency ? `${sluggify(this.item.system.frequency)}-effect-range` : [], + ].flat(), { test: options.test ?? this.actor.getRollOptions() }) + ) + } + + return Number(Object.values( + modifiers.reduce((acc, mod) => { + if (!mod.ignored && !acc[mod.slug]) acc[mod.slug] = mod.modifier; + return acc; + }, {}) + ).reduce((acc, mod) => acc + mod, 0)) || 0; + })(); + + if (this.range && (Number(options.roll) + effectRange) < Number(this.range)) { + return null; + } + + const itemObject = grantedItem.toObject(); + itemObject.system.effect ??= ""; + itemObject.system.effect += `Applied by ${this.label ?? this.item.name} from ${this.actor.name}`; + + return itemObject; + } + + const synthetics = ((this.actor.synthetics.applyEffects[selector] ??= { target: [], origin: [] })); + synthetics[this.affects].push(construct); + } + } +} + +export { ApplyEffectRuleElement } \ No newline at end of file diff --git a/src/module/rules/rule-element/base.js b/src/module/rules/rule-element/base.js index 67bc15e27..88e343afa 100644 --- a/src/module/rules/rule-element/base.js +++ b/src/module/rules/rule-element/base.js @@ -62,11 +62,15 @@ class RuleElementPTU extends foundry.abstract.DataModel { this.ignored = true; } else if(item instanceof CONFIG.PTU.Item.documentClass) { - this.ignored ??= false; + this.ignored ??= false } else { this.ignored = true; } + + if(this.item?.enabled !== undefined) { + this.ignored = !this.item.enabled; + } } /** @override */ @@ -196,6 +200,7 @@ class RuleElementPTU extends foundry.abstract.DataModel { test(options) { if(this.ignored) return false; + if(!this.item.enabled) return false; if(this.predicate.length === 0) return true; const optionSet = new Set([ @@ -242,7 +247,7 @@ class RuleElementPTU extends foundry.abstract.DataModel { const property = prop.replace(regex, replaceFunc.bind(this)); - const value = getProperty(data, property); + const value = foundry.utils.getProperty(data, property); if (value === undefined) { this.failValidation(`Failed to resolve injected property "${source}"`); } @@ -291,9 +296,9 @@ class RuleElementPTU extends foundry.abstract.DataModel { const {actor, item} = this; switch(source) { - case "actor": return Number(getProperty(actor, field.substring(seperator + 1))) || 0; - case "item": return Number(getProperty(item, field.substring(seperator + 1))) || 0; - case "rule": return Number(getProperty(this, field.substring(seperator + 1))) || 0; + case "actor": return Number(foundry.utils.getProperty(actor, field.substring(seperator + 1))) || 0; + case "item": return Number(foundry.utils.getProperty(item, field.substring(seperator + 1))) || 0; + case "rule": return Number(foundry.utils.getProperty(this, field.substring(seperator + 1))) || 0; default: return 0; } })(); @@ -331,7 +336,7 @@ class RuleElementPTU extends foundry.abstract.DataModel { }; return value instanceof Object && defaultValue instanceof Object - ? mergeObject(defaultValue, value, {inplace: false}) + ? foundry.utils.mergeObject(defaultValue, value, {inplace: false}) : typeof value === "string" && evaluate ? saferEval(Roll.replaceFormulaData(value, {actor: this.actor, item: this.item, ...injectables})) : value; diff --git a/src/module/rules/rule-element/choice-set/rule-element.js b/src/module/rules/rule-element/choice-set/rule-element.js index b771dccbb..48a0d5926 100644 --- a/src/module/rules/rule-element/choice-set/rule-element.js +++ b/src/module/rules/rule-element/choice-set/rule-element.js @@ -23,6 +23,8 @@ class ChoiceSetRuleElement extends RuleElementPTU { this.choices.predicate = new PTUPredicate(this.choices.predicate ?? []); } + + this.prompt = typeof data.prompt === "string" ? this.resolveInjectedProperties(data.prompt) : data.prompt; // Assign the selection to a flag on the parent item if (this.selection !== null) { const resolvedFlag = this.resolveInjectedProperties(this.flag); @@ -169,7 +171,7 @@ class ChoiceSetRuleElement extends RuleElementPTU { } #choicesFromPath(path) { - const choiceObject = getProperty(CONFIG.PTU, path) ?? getProperty(this.actor, path) ?? {}; + const choiceObject = foundry.utils.getProperty(CONFIG.PTU, path) ?? foundry.utils.getProperty(this.actor, path) ?? {}; if(Array.isArray(choiceObject) && choiceObject.every((c) => isObject(c) && typeof c.value === "string")) { return choiceObject; } diff --git a/src/module/rules/rule-element/ephemeral-effect.js b/src/module/rules/rule-element/ephemeral-effect.js index 2ddb78eba..1d7fc007a 100644 --- a/src/module/rules/rule-element/ephemeral-effect.js +++ b/src/module/rules/rule-element/ephemeral-effect.js @@ -41,7 +41,7 @@ class EphemeralEffectRuleElement extends RuleElementPTU { return null; } const effect = (await fromUuid(uuid)); - if (!(effect instanceof BaseEffectPTU && ["condition", "effect"].includes(effect.type))) { + if (!(effect instanceof CONFIG.PTU.Item.baseEffect && ["condition", "effect"].includes(effect.type))) { this.failValidation(`unable to find effect or condition item with uuid "${uuid}"`); return null; } diff --git a/src/module/rules/rule-element/flat-modifier.js b/src/module/rules/rule-element/flat-modifier.js index 783f87a0e..d503b5f86 100644 --- a/src/module/rules/rule-element/flat-modifier.js +++ b/src/module/rules/rule-element/flat-modifier.js @@ -15,32 +15,39 @@ class FlatModifierRuleElement extends RuleElementPTU { return { ...super.defineSchema(), selectors: new fields.ArrayField( - new fields.StringField({required: true, blank: false, initial: undefined}), + new fields.StringField({ required: true, blank: false, initial: undefined }), ), min: new fields.NumberField({ required: false, nullable: false, initial: undefined }), max: new fields.NumberField({ required: false, nullable: false, initial: undefined }), force: new fields.BooleanField(), - hideIfDisabled: new fields.BooleanField({required: false, initial: true}), - value: new ResolvableValueField({required: false, nullable: false, initial: undefined}) + hideIfDisabled: new fields.BooleanField({ required: false, initial: true }), + value: new ResolvableValueField({ required: false, nullable: false, initial: undefined }) }; } /** @override */ beforePrepareData() { - if(this.ignored) return; + if (this.ignored) return; const slug = this.slug ?? sluggify(this.reducedLabel); const selectors = this.selectors.map(s => this.resolveInjectedProperties(s)).filter(s => !!s); - if(selectors.length === 0) { + if (selectors.length === 0) { return this.failValidation("must have at least one selector"); } - for(const selector of selectors) { + for (const selector of selectors) { const construct = (options = {}) => { - if(this.ignored) return null; - const resolvedValue = Number(this.resolveValue(this.value, 0, options)) || 0; - const finalValue = Math.clamped(resolvedValue, this.min ?? resolvedValue, this.max ?? resolvedValue); + const finalValue = (() => { + if (selector.includes("damage-dice")) { + options.evaluate = false; + return this.resolveValue(this.value, "", options); + } + + const resolvedValue = Number(this.resolveValue(this.value, 0, options)) || 0; + const finalValue = Math.clamp(resolvedValue, this.min ?? resolvedValue, this.max ?? resolvedValue); + return finalValue; + })(); const modifier = new PTUModifier({ slug, @@ -52,9 +59,9 @@ class FlatModifierRuleElement extends RuleElementPTU { source: this.item.uuid, hideIfDisabled: this.hideIfDisabled }) - if(options.test) modifier.test(options.test); + if (options.test) modifier.test(options.test); return modifier; - } + } const modifiers = (this.actor.synthetics.statisticsModifiers[selector] ??= []); modifiers.push(construct); diff --git a/src/module/rules/rule-element/grant-item/rule-element.js b/src/module/rules/rule-element/grant-item/rule-element.js index 1e0a65b1b..0e77cd023 100644 --- a/src/module/rules/rule-element/grant-item/rule-element.js +++ b/src/module/rules/rule-element/grant-item/rule-element.js @@ -7,14 +7,12 @@ class GrantItemRuleElement extends RuleElementPTU { constructor(source, item, options = {}) { super(source, item, options); - if(this.reevaluateOnUpdate) { + if (this.reevaluateOnUpdate) { this.replaceSelf = false; - this.allowDuplicate = false; + this.allowduplicate = false; } this.onDeleteActions = this.#getOnDeleteActions(source); - - this.grantedId = this.item.flags.ptu?.itemGrants?.[this.flag ?? ""]?.id ?? null; } /** @override */ @@ -25,7 +23,7 @@ class GrantItemRuleElement extends RuleElementPTU { flag: new foundry.data.fields.StringField({ required: true, nullable: true, initial: null }), reevaluateOnUpdate: new foundry.data.fields.BooleanField({ required: false, nullable: false, initial: false }), replaceSelf: new foundry.data.fields.BooleanField({ required: false, nullable: false, initial: false }), - allowDuplicate: new foundry.data.fields.BooleanField({ required: false, nullable: false, initial: true }), + allowduplicate: new foundry.data.fields.BooleanField({ required: false, nullable: false, initial: true }), onDeleteActions: new foundry.data.fields.ObjectField({ required: false, nullable: false, initial: undefined }), overwrites: new foundry.data.fields.ObjectField({ required: false, nullable: false, initial: undefined }) }; @@ -33,11 +31,15 @@ class GrantItemRuleElement extends RuleElementPTU { static ON_DELETE_ACTIONS = ["cascade", "detach", "restrict"]; + get grantedId() { + return this.item.flags.ptu?.itemGrants?.[this.flag]?.id ?? this.item.flags.ptu?.itemGrants?.[this.flag?.replace(/\d{1,2}$/, '')]?.id; + } + /** @override */ async preCreate(args) { - const { itemSource, pendingItems, context, ruleSource} = args; - - if(this.reevaluateOnUpdate && this.predicate.length === 0) { + const { itemSource, pendingItems, context, ruleSource } = args; + + if (this.reevaluateOnUpdate && this.predicate.length === 0) { ruleSource.ignored = true; return this.failValidation("`reevaluateOnUpdate` may only be used with a predicate."); } @@ -52,9 +54,9 @@ class GrantItemRuleElement extends RuleElementPTU { return null; } })(); - if(!(grantedItem instanceof PTUItem)) return; + if (!(grantedItem instanceof PTUItem)) return; - ruleSource.flag = + ruleSource.flag = typeof ruleSource.flag === "string" && ruleSource.flag.length > 0 ? ruleSource.flag : (() => { @@ -62,62 +64,66 @@ class GrantItemRuleElement extends RuleElementPTU { const flagPattern = new RegExp(`^${defaultFlag}\\d*$`); const itemGrants = itemSource.flags?.ptu?.itemGrants ?? {}; const nthGrant = Object.keys(itemGrants).filter((g => flagPattern.test(g))).length; - return nthGrant > 0 ? `${defaultFlag}${nthGrant+1}` : defaultFlag; + return nthGrant > 0 ? `${defaultFlag}${nthGrant + 1}` : defaultFlag; })(); this.flag = String(ruleSource.flag); - if(!this.test()) return; + if (!this.test()) return; const migrations = MigrationList.constructFromVersion(grantedItem.schemaVersion); - if(migrations.length) { + if (migrations.length) { await MigrationRunner.ensureSchemaVersion(grantedItem, migrations); } const existingItem = this.actor.items.find((i) => i.sourceId === uuid || (grantedItem.type === "condition" && i.slug === grantedItem.slug)); - if(!this.allowDuplicate && existingItem) { - if(this.replaceSelf) { + if (!this.allowduplicate && existingItem) { + if (this.replaceSelf) { pendingItems.splice(pendingItems.indexOf(existingItem), 1); } //this.#setGrantFlags(itemSource, existingItem); - return ui.notifications.warn(`Item ${grantedItem.name} is already granted to ${this.actor.name}.`); + return args.reevaluation ? null : ui.notifications.warn(`Item ${grantedItem.name} is already granted to ${this.actor.name}.`); } - itemSource._id ??= randomID(); + if (!this.actor?.allowedItemTypes.includes(grantedItem.type)) { + ui.notifications.error(`PTU | ${source.type.capitalize()}s cannot be added to ${actor.name}`); + return []; + } + + itemSource._id ??= foundry.utils.randomID(); const grantedSource = grantedItem.toObject(); - grantedSource._id = randomID(); + grantedSource._id = foundry.utils.randomID(); - if(["feat", "edge"].includes(grantedSource.type)) { + if (["feat", "edge"].includes(grantedSource.type) && this.item?.slug !== 'grant-training-feature') { grantedSource.system.free = true; } // Guarantee future alreadyGranted checks pass in all cases by re-assigning sourceId - grantedSource.flags = mergeObject(grantedSource.flags, { core: { sourceId: uuid } }); + grantedSource.flags = foundry.utils.mergeObject(grantedSource.flags, { core: { sourceId: uuid } }); // Create a temporary owned item and run its actor-data preparation and early-stage rule-element callbacks - const tempGranted = new PTUItem(deepClone(grantedSource), { parent: this.actor }); + const tempGranted = new PTUItem(foundry.utils.deepClone(grantedSource), { parent: this.actor }); tempGranted.prepareActorData?.(); - for(const rule of tempGranted.prepareRuleElements()) { + for (const rule of tempGranted.prepareRuleElements()) { rule.onApplyActiveEffects?.(); } - if(this.ignored) return; + if (this.ignored) return; // If the granted item is replacing the granting item, swap it out and return early - if(this.replaceSelf) { + if (this.replaceSelf) { pendingItems.findSplice((i) => i === itemSource, grantedSource); await this.#runGrantedItemPreCreates(args, tempGranted, grantedSource, context); return; } - this.grantedId = grantedSource._id; context.keepId = true; this.#setGrantFlags(itemSource, grantedSource); // Run the granted item's preCreate callbacks unless this is a pre-actor-update reevaluation - if(!args.reevaluation) { + if (!args.reevaluation) { await this.#runGrantedItemPreCreates(args, tempGranted, grantedSource, context); } @@ -126,19 +132,25 @@ class GrantItemRuleElement extends RuleElementPTU { /** @override */ async preUpdateActor() { - const noAction = { create: [], delete: []}; - if(!this.reevaluateOnUpdate) return noAction; - - if(this.grantedId && this.actor.items.has(this.grantedId)) { - if(!this.test()) { - return { create: [], delete: [this.grantedId] }; + const noAction = { create: [], delete: [] }; + if (!this.reevaluateOnUpdate) return noAction; + + let noId = false; + if (this.grantedId) { + if (this.actor.items.has(this.grantedId)) { + if (!this.test()) { + return { create: [], delete: [this.grantedId] }; + } + return noAction; } - return noAction; + } + else { + noId = true; } const itemSource = this.item.toObject(); const ruleSource = itemSource.system.rules[this.sourceIndex ?? -1]; - if(!ruleSource) return noAction; + if (!ruleSource) return noAction; const pendingItems = []; const context = { @@ -148,9 +160,18 @@ class GrantItemRuleElement extends RuleElementPTU { await this.preCreate({ itemSource, pendingItems, context, ruleSource, reevaluation: true }); - if(pendingItems.length > 0) { + if (noId) { + if (this.grantedId && this.actor.items.has(this.grantedId)) { + if (!this.test()) { + return { create: [], delete: [this.grantedId] }; + } + return noAction; + } + } + + if (pendingItems.length > 0) { const updatedGrants = itemSource.flags.ptu.itemGrants ?? {}; - await this.item.update({"flags.ptu.itemGrants": updatedGrants}, { render: false }); + await this.item.update({ "flags.ptu.itemGrants": updatedGrants }, { render: false }); return { create: pendingItems, delete: [] }; } return noAction; @@ -158,22 +179,22 @@ class GrantItemRuleElement extends RuleElementPTU { #getOnDeleteActions(source) { const actions = source.onDeleteActions; - if(typeof actions === "object") { + if (typeof actions === "object") { const ACTIONS = GrantItemRuleElement.ON_DELETE_ACTIONS; - return ACTIONS.includes(actions.granter) || ACTIONS.includes(actions.grantee) - ? actions : null; + return ACTIONS.includes(actions.granter) || ACTIONS.includes(actions.grantee) + ? actions : null; } } #setGrantFlags(granter, grantee) { - const flags = mergeObject(granter.flags ?? {}, { ptu: { itemGrants: { } } }); - if(!this.flag) throw new Error("GrantItemRuleElement#flag must be set before calling #setGrantFlags"); + const flags = foundry.utils.mergeObject(granter.flags ?? {}, { ptu: { itemGrants: {} } }); + if (!this.flag) throw new Error("GrantItemRuleElement#flag must be set before calling #setGrantFlags"); flags.ptu.itemGrants[this.flag] = { id: grantee instanceof PTUItem ? grantee.id : grantee._id, // The on-delete action determines what will happen to the granter item when the granted item is deleted: // Default to "detach" (do nothing). onDelete: this.onDeleteActions?.grantee ?? "detach" - } + } // The granted item records its granting item's ID at `flags.ptu.grantedBy` const grantedBy = { @@ -183,19 +204,19 @@ class GrantItemRuleElement extends RuleElementPTU { onDelete: this.onDeleteActions?.granter ?? "cascade" } - if(grantee instanceof PTUItem) { + if (grantee instanceof PTUItem) { // This is a previously granted item: update its grantedBy flag - grantee.update({"flags.ptu.grantedBy": grantedBy}, { render: false }); + grantee.update({ "flags.ptu.grantedBy": grantedBy }, { render: false }); } else { - grantee.flags = mergeObject(grantee.flags ?? {}, { ptu: { grantedBy } }); + grantee.flags = foundry.utils.mergeObject(grantee.flags ?? {}, { ptu: { grantedBy } }); } } async #runGrantedItemPreCreates(args, grantedItem, grantedSource, context) { - for(const rule of grantedItem.rules) { + for (const rule of grantedItem.rules) { const ruleSource = grantedSource.system.rules[grantedItem.rules.indexOf(rule)]; - await rule.preCreate?.({ ...args, itemSource: grantedSource, context, ruleSource}); + await rule.preCreate?.({ ...args, itemSource: grantedSource, context, ruleSource }); } } } diff --git a/src/module/rules/rule-element/roll-option.js b/src/module/rules/rule-element/roll-option.js index 2baa15894..b02fe9847 100644 --- a/src/module/rules/rule-element/roll-option.js +++ b/src/module/rules/rule-element/roll-option.js @@ -38,7 +38,7 @@ class RollOptionRuleElement extends RuleElementPTU { #resolveOption() { return this.resolveInjectedProperties(this.option) - .replace(/[^-:\w]/g, "") + ?.replace(/[^-:\w]/g, "") .replace(/:+/g, ":") .replace(/-+/g, "-") .trim(); diff --git a/src/module/rules/rule-element/temp-hp.js b/src/module/rules/rule-element/temp-hp.js index 8471292d1..5f87c4a35 100644 --- a/src/module/rules/rule-element/temp-hp.js +++ b/src/module/rules/rule-element/temp-hp.js @@ -13,7 +13,7 @@ export class TempHPRuleElement extends RuleElementPTU { onCreate(actorUpdates) { if(this.ignored) return; - const updatedActorData = mergeObject(this.actor._source, actorUpdates, {inplace: false}); + const updatedActorData = foundry.utils.mergeObject(this.actor._source, actorUpdates, {inplace: false}); const value = this.resolveValue(this.value) const rollOptions = Array.from(new Set(this.actor.getRollOptions())); @@ -21,9 +21,9 @@ export class TempHPRuleElement extends RuleElementPTU { if(!this.predicate.test(rollOptions)) return; if(typeof value !== "number") return this.failValidation("Temporary HP requires a non-zero value field"); - const currentTempHP = Number(getProperty(updatedActorData, "system.tempHp.value")) || 0; + const currentTempHP = Number(foundry.utils.getProperty(updatedActorData, "system.tempHp.value")) || 0; if(value > currentTempHP) { - mergeObject(actorUpdates, { + foundry.utils.mergeObject(actorUpdates, { "system.tempHp.value": value, "system.tempHp.max": value, "system.tempHp.source": this.item.uuid @@ -34,11 +34,11 @@ export class TempHPRuleElement extends RuleElementPTU { /** @override */ onDelete(actorUpdates) { - const updatedActorData = mergeObject(this.actor._source, actorUpdates, {inplace: false}); + const updatedActorData = foundry.utils.mergeObject(this.actor._source, actorUpdates, {inplace: false}); if(!this.removeOnDelete) return; - if(getProperty(updatedActorData, "system.tempHp.source") === this.item.uuid) { - mergeObject(actorUpdates, { + if(foundry.utils.getProperty(updatedActorData, "system.tempHp.source") === this.item.uuid) { + foundry.utils.mergeObject(actorUpdates, { "system.tempHp.value": 0, "system.tempHp.-=source": null }); diff --git a/src/module/rules/rule-element/token-image.js b/src/module/rules/rule-element/token-image.js index d626d1cfe..83bef1f31 100644 --- a/src/module/rules/rule-element/token-image.js +++ b/src/module/rules/rule-element/token-image.js @@ -27,7 +27,7 @@ export class TokenImageRuleElement extends RuleElementPTU { /** @override */ afterPrepareData() { let src = this.value; - if (!this.#srcIsValid(src)) src = this.resolveValue(this.value); + if (!this.#srcIsValid(src)) src = this.resolveValue(this.value, 0, {evaluate: false}); if (!this.test()) return; @@ -53,6 +53,7 @@ export class TokenImageRuleElement extends RuleElementPTU { #srcIsValid(src) { if (typeof src !== "string") return false; + if (src.includes("{")) return false; const extension = /(?<=\.)([a-z0-9]{3,4})(\?[a-zA-Z0-9]+)?$/i.exec(src)?.at(1); return !!extension && (extension in CONST.IMAGE_FILE_EXTENSIONS || extension in CONST.VIDEO_FILE_EXTENSIONS); } diff --git a/src/module/rules/rule-element/token-light.js b/src/module/rules/rule-element/token-light.js index 1a0ba2e75..662dcfad4 100644 --- a/src/module/rules/rule-element/token-light.js +++ b/src/module/rules/rule-element/token-light.js @@ -33,6 +33,6 @@ export class TokenLightRuleElement extends RuleElementPTU { /** @override */ afterPrepareData(){ if (!this.test()) return; - this.actor.synthetics.tokenOverrides.light = deepClone(this.value); + this.actor.synthetics.tokenOverrides.light = foundry.utils.deepClone(this.value); } } \ No newline at end of file diff --git a/src/module/system/check/attack.js b/src/module/system/check/attack.js index f8e47aceb..d35c3c507 100644 --- a/src/module/system/check/attack.js +++ b/src/module/system/check/attack.js @@ -62,6 +62,25 @@ class PTUAttackCheck extends PTUDiceCheck { this.modifiers = modifiers; + const critRangeModifiers = [ + new PTUModifier({ + slug: "crit-range", + label: "Crit Range", + modifier: this.actor.system.modifiers.critRange.total ?? 0 + }), + ]; + + critRangeModifiers.push(...extractModifiers(this.actor.synthetics, [ + "crit-range", + `${this.item.id}-crit-range`, + `${this.item.slug}-crit-range`, + `${sluggify(this.item.system.category)}-crit-range`, + `${sluggify(this.item.system.type)}-crit-range`, + `${sluggify(this.item.system.frequency)}-crit-range` + ], { test: this.options })); + + this.critRangeModifiers = critRangeModifiers; + return this; } @@ -71,6 +90,16 @@ class PTUAttackCheck extends PTUDiceCheck { */ prepareStatistic() { super.prepareStatistic(sluggify(game.i18n.format("PTU.Action.AttackRoll", { move: this.item.name }))); + + this.critMod = Math.max( + 0, + Object.values( + this.critRangeModifiers.reduce((acc, mod) => { + if(!mod.ignored && !acc[mod.slug]) acc[mod.slug] = mod.modifier; + return acc; + }, {}) + ).reduce((acc, mod) => acc + mod, 0) + ) return this; } @@ -112,8 +141,7 @@ class PTUAttackCheck extends PTUDiceCheck { /** @type {DcCollection} */ const dcs = (() => { const targets = new Map(); - const critMod = this.actor.system.modifiers?.critRange?.total ?? 0; - const critRange = Array.fromRange(1 + Math.max(critMod, 0), 20 - Math.max(critMod, 0)); + const critRange = Array.fromRange(1 + Math.max(this.critMod, 0), 20 - Math.max(this.critMod, 0)); /** @type {TargetContext[]} */ const contexts = this._contexts.size > 0 ? this._contexts : [{ actor: this.actor, options: this.options, token: this.token }] @@ -190,7 +218,7 @@ class PTUAttackCheck extends PTUDiceCheck { } } - for(const modifier of extractModifiers(context.actor.synthetics, ["evasion"], { test: context.options })) { + for (const modifier of extractModifiers(context.actor.synthetics, ["evasion"], { test: context.options })) { target.statistic.push(modifier); } @@ -344,7 +372,7 @@ class PTUAttackCheck extends PTUDiceCheck { ui.notifications.warn("PTU.Action.StatusAttackWhileRaging", { localize: true }); return false; } - if (this.conditionOptions.has("condition:disabled") && this.options.includes(`condition:disabled:${move.slug}`)) { + if (this.conditionOptions.has("condition:disabled") && this.options.has(`condition:disabled:${this.item.slug}`)) { ui.notifications.warn("PTU.Action.DisabledMove", { localize: true }); return false; } diff --git a/src/module/system/check/check.js b/src/module/system/check/check.js index a50d84d78..ec1937d32 100644 --- a/src/module/system/check/check.js +++ b/src/module/system/check/check.js @@ -214,6 +214,9 @@ class PTUDiceCheck { ? rollResult.terms.find(t => t instanceof NumericTerm) : rollResult.dice.find(d => d instanceof Die && d.faces === diceSize ))?.total ?? 1; + + options.rollResult = result; + const total = rollResult.total; const targets = []; if (options.dcs?.targets.size > 0) { @@ -276,12 +279,13 @@ class PTUDiceCheck { title, type, skipDialog, - isReroll + isReroll, + rollResult: result }, modifierName: this.statistic.slug, modifiers: this.statistic.modifiers.map(m => m.toObject()), origin: options.origin, - resolved: targets.length > 0 ? game.settings.get("ptu", "autoRollDamage") : false + resolved: targets.length > 0 ? game.settings.get("ptu", "autoRollDamage") : false, } } if (attack) flags.ptu.attack = attack; @@ -300,6 +304,7 @@ class PTUDiceCheck { return { rolls: this.rolls, targets, + rollResult: result, } } @@ -408,7 +413,7 @@ class PTUDiceCheck { */ class PTUCheck { static async roll(check, context, event, callback, diceStatistic = null) { - if (event) mergeObject(context, eventToRollParams(event)); + if (event) foundry.utils.mergeObject(context, eventToRollParams(event)); context.skipDialog ??= game.settings.get("ptu", "skipRollDialog"); context.createMessage ??= true; diff --git a/src/module/system/check/damage.js b/src/module/system/check/damage.js index 977cfa0c6..d42851fd8 100644 --- a/src/module/system/check/damage.js +++ b/src/module/system/check/damage.js @@ -7,10 +7,11 @@ import { CheckDialog } from "./dialogs/dialog.js"; class PTUDamageCheck extends PTUDiceCheck { - constructor({ source, targets, selectors, event, outcomes }) { + constructor({ source, targets, selectors, event, outcomes, accuracyRollResult }) { super({ source, targets, selectors, event }); this.outcomes = outcomes; + this.accuracyRollResult = accuracyRollResult; } get isSelfAttack() { @@ -66,7 +67,7 @@ class PTUDamageCheck extends PTUDiceCheck { modifier: this.item.damageBase.preStab, }) ] - if(this.item.damageBase.isStab) { + if (this.item.damageBase.isStab) { damageBaseModifiers.push( new PTUModifier({ slug: "stab", @@ -77,6 +78,7 @@ class PTUDamageCheck extends PTUDiceCheck { } const modifiers = [] + const diceModifiers = [] const damageBonus = isNaN(Number(this.item.system.damageBonus)) ? 0 : Number(this.item.system.damageBonus); if (damageBonus != 0) { @@ -141,9 +143,20 @@ class PTUDamageCheck extends PTUDiceCheck { `${sluggify(this.item.system.frequency)}-damage-base` ], { injectables: { move: this.item, item: this.item, actor: this.actor }, test: this.targetOptions }) ) + diceModifiers.push( + ...extractModifiers(this.actor.synthetics, [ + "damage-dice", + `${this.item.id}-damage-dice`, + `${this.item.slug}-damage-dice`, + `${sluggify(this.item.system.category)}-damage-dice`, + `${sluggify(this.item.system.type)}-damage-dice`, + `${sluggify(this.item.system.frequency)}-damage-dice` + ], { injectables: { move: this.item, item: this.item, actor: this.actor }, test: this.targetOptions }) + ) this.modifiers = modifiers; this.damageBaseModifiers = damageBaseModifiers; + this.diceModifiers = diceModifiers; return this; } @@ -155,10 +168,13 @@ class PTUDamageCheck extends PTUDiceCheck { prepareStatistic() { super.prepareStatistic(sluggify(game.i18n.format("PTU.Action.DamageRoll", { move: this.item.name }))); - this.damageBase = Object.values(this.damageBaseModifiers.reduce((a, b) => { - if (!b.ignored && !a[b.slug]) a[b.slug] = b.modifier; - return a; - }, {})).reduce((a, b) => a + b, 0); + this.damageBase = Math.max( + Object.values(this.damageBaseModifiers.reduce((a, b) => { + if (!b.ignored && !a[b.slug]) a[b.slug] = b.modifier; + return a; + }, {})).reduce((a, b) => a + b, 0), + 1 + ); this.targetOptions.add(`damage-base:${this.damageBase}`) return this; @@ -314,7 +330,20 @@ class PTUDamageCheck extends PTUDiceCheck { const totalModifiersPart = this.statistic.totalModifier?.signedString() ?? ""; options.modifierPart = totalModifiersPart; - const roll = new this.rollCls(`${diceString}${totalModifiersPart}`, {}, options); + // Add the dice modifier to the total modifier + const diceModifierParts = this.diceModifiers.reduce((a, b) => { + if (!b.ignored && !a[b.slug]) a[b.slug] = b.modifier; + return a; + }, {}); + + if (this.options.has("charge:bonus") && this.options.has("move:type:electric")) { + diceModifierParts["charge"] = `${diceString}+${diceModifier}`; + } + + const diceModifiers = Object.values(diceModifierParts).reduce((a, b) => `${a} + ${b}`, ""); + options.diceModifiers = diceModifiers; + + const roll = new this.rollCls(`${diceString}${totalModifiersPart}${diceModifiers}`, {}, options); const rollResult = await roll.evaluate({ async: true }); const critDice = `${diceString}+${diceString}`; @@ -325,7 +354,7 @@ class PTUDamageCheck extends PTUDiceCheck { } const hasCrit = Object.values(this.outcomes).some(o => o == "crit-hit") - const critRoll = new this.rollCls(`${critDice}${totalModifiersPartCrit}`, {}, { ...options, crit: { hit: true, show: hasCrit, nonCritValue: rollResult.total }, fudges }); + const critRoll = new this.rollCls(`${critDice}${totalModifiersPartCrit}${diceModifiers}`, {}, { ...options, crit: { hit: true, show: hasCrit, nonCritValue: rollResult.total }, fudges }); const critRollResult = await critRoll.evaluate({ async: true }); const flags = { @@ -346,6 +375,7 @@ class PTUDamageCheck extends PTUDiceCheck { skipDialog, isReroll, outcomes: this.outcomes, + accuracyRollResult: this.accuracyRollResult }, modifierName: this.statistic.slug, modifiers: this.statistic.modifiers.map(m => m.toObject()), @@ -355,6 +385,7 @@ class PTUDamageCheck extends PTUDiceCheck { } const extraTags = this.fiveStrikeResult > 0 ? [`Five Strike x${this.fiveStrikeResult}`] : []; + extraTags.push(...Object.entries(diceModifierParts).flatMap(([slug, mod]) => `${Handlebars.helpers.formatSlug(slug)} +${mod}`)) const message = await this.createMessage({ roll, rollMode, flags, inverse: false, critRoll, type, extraTags }); @@ -379,19 +410,22 @@ class PTUDamageCheck extends PTUDiceCheck { * @returns {Promise