From 306c5b290664d5785c3305b576c7168d023b1068 Mon Sep 17 00:00:00 2001 From: Zithith Date: Wed, 23 Oct 2024 23:54:02 +0100 Subject: [PATCH 01/17] feat(graze damage override): added initial data model and input fields for override formula --- src/lang/en.json | 6 +++- src/style/sheets/item/module.scss | 23 +++++++++++++ .../item/components/details-damage.ts | 18 ++++++++++ src/system/data/item/mixins/damaging.ts | 5 +++ .../item/components/details-damage.hbs | 33 +++++++++++++++++++ 5 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/lang/en.json b/src/lang/en.json index 38633f8d..935814c9 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -572,7 +572,11 @@ }, "Damage": { "Title": "Damage", - "Formula": "Damage Formula" + "Formula": "Damage Formula", + "GrazeOverride": "Override Graze Damage Calculation", + "GrazeShow": "Show New Graze Damage Formula", + "GrazeHide": "Hide New Graze Damage Forumla", + "GrazeFormula": "New Formula" }, "Modality": { "Title": "Modality", diff --git a/src/style/sheets/item/module.scss b/src/style/sheets/item/module.scss index 18052765..c6ea0203 100644 --- a/src/style/sheets/item/module.scss +++ b/src/style/sheets/item/module.scss @@ -304,6 +304,29 @@ app-item-properties { } } +app-item-details-damage { + .form-group-stacked { + .header { + display: flex; + align-items: center; + + label { + font-weight: bold; + flex: 2; + } + + span { + flex: 3; + font-style: italic; + } + + .controls { + margin-right: 0.5rem; + } + } + } +} + app-item-details-equip { .form-group-stacked { .header { diff --git a/src/system/applications/item/components/details-damage.ts b/src/system/applications/item/components/details-damage.ts index 4684a651..7cf7cbbb 100644 --- a/src/system/applications/item/components/details-damage.ts +++ b/src/system/applications/item/components/details-damage.ts @@ -11,6 +11,20 @@ export class DetailsDamageComponent extends HandlebarsApplicationComponent< static TEMPLATE = 'systems/cosmere-rpg/templates/item/components/details-damage.hbs'; + /* eslint-disable @typescript-eslint/unbound-method */ + static ACTIONS = { + 'toggle-graze-collapsed': DetailsDamageComponent.onToggleGrazeCollapsed, + }; + /* eslint-enable @typescript-eslint/unbound-method */ + private grazeOverrideCollapsed = true; + + /* --- Actions --- */ + + private static onToggleGrazeCollapsed(this: DetailsDamageComponent) { + this.grazeOverrideCollapsed = !this.grazeOverrideCollapsed; + void this.render(); + } + /* --- Context --- */ public _prepareContext(params: never, context: BaseItemSheetRenderContext) { @@ -34,6 +48,10 @@ export class DetailsDamageComponent extends HandlebarsApplicationComponent< return { hasSkillTest, hasSkill, + grazeInputCollapsed: + this.grazeOverrideCollapsed || + this.application.item.system.damage.grazeOverrideFormula !== + undefined, typeSelectOptions: { none: '—', diff --git a/src/system/data/item/mixins/damaging.ts b/src/system/data/item/mixins/damaging.ts index b3dafea5..5c1d8063 100644 --- a/src/system/data/item/mixins/damaging.ts +++ b/src/system/data/item/mixins/damaging.ts @@ -5,6 +5,7 @@ export interface DamagingItemData { damage: { formula?: string; type?: DamageType; + grazeOverrideFormula?: string; skill?: Skill; attribute?: Attribute; }; @@ -22,6 +23,10 @@ export function DamagingItemMixin

() { nullable: true, blank: false, }), + grazeOverrideFormula: + new foundry.data.fields.StringField({ + nullable: true, + }), type: new foundry.data.fields.StringField({ nullable: true, choices: Object.keys(CONFIG.COSMERE.damageTypes), diff --git a/src/templates/item/components/details-damage.hbs b/src/templates/item/components/details-damage.hbs index 9e0d5fb8..aad88f88 100644 --- a/src/templates/item/components/details-damage.hbs +++ b/src/templates/item/components/details-damage.hbs @@ -61,4 +61,37 @@ {{/if}} {{/if}} + +

+
+ +
+ {{#if grazeInputCollapsed}} + + + + {{else}} + + + + {{/if}} +
+
+ {{#if (not grazeInputCollapsed)}} +
+ + +
+ {{/if}} +
{{/if}} \ No newline at end of file From 365dd385547fa1e4731f40659dcb4a1ac9d3c4c8 Mon Sep 17 00:00:00 2001 From: Zithith Date: Fri, 25 Oct 2024 23:40:24 +0100 Subject: [PATCH 02/17] feat(graze damage): moved override input into the hasSkillRoll condition per PR comments --- src/templates/item/components/details-damage.hbs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/templates/item/components/details-damage.hbs b/src/templates/item/components/details-damage.hbs index aad88f88..ef07d7e4 100644 --- a/src/templates/item/components/details-damage.hbs +++ b/src/templates/item/components/details-damage.hbs @@ -60,8 +60,6 @@ > {{/if}} -{{/if}} -
@@ -93,5 +91,6 @@ >
{{/if}} +{{/if}}
{{/if}} \ No newline at end of file From 98811bd93a1a0f3cc9b7a9e8e511c0a1be41f00f Mon Sep 17 00:00:00 2001 From: Zithith Date: Mon, 28 Oct 2024 00:16:30 +0000 Subject: [PATCH 03/17] feat(graze damage): Added separate roll for graze damage when overridden --- src/system/dice/damage-roll.ts | 2 ++ src/system/documents/actor.ts | 7 +++- src/system/documents/item.ts | 64 +++++++++++++++++++++++++++------- 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/src/system/dice/damage-roll.ts b/src/system/dice/damage-roll.ts index 2d9bb2c2..4ac585c3 100644 --- a/src/system/dice/damage-roll.ts +++ b/src/system/dice/damage-roll.ts @@ -15,6 +15,8 @@ export type DamageRollData< attribute: Attribute; }; attribute?: number; + grazeOverrideForumla?: string; + baseRoll?: string; }; export interface DamageRollOptions diff --git a/src/system/documents/actor.ts b/src/system/documents/actor.ts index f57f0b57..0d330220 100644 --- a/src/system/documents/actor.ts +++ b/src/system/documents/actor.ts @@ -614,7 +614,12 @@ export class CosmereActor< public async useItem( item: CosmereItem, options?: Omit, - ): Promise { + ): Promise< + | D20Roll + | [D20Roll, DamageRoll] + | [D20Roll, DamageRoll, DamageRoll] + | null + > { return item.use({ ...options, actor: this }); } diff --git a/src/system/documents/item.ts b/src/system/documents/item.ts index 5ea4b66f..1773365b 100644 --- a/src/system/documents/item.ts +++ b/src/system/documents/item.ts @@ -331,7 +331,7 @@ export class CosmereItem< */ public async rollDamage( options: CosmereItem.RollDamageOptions = {}, - ): Promise { + ): Promise<[DamageRoll, DamageRoll | undefined] | null> { if (!this.hasDamage() || !this.system.damage.formula) return null; // Get the actor to roll for (either assigned through option, the parent of this item, or the first controlled actor) @@ -371,6 +371,7 @@ export class CosmereItem< skillId, attributeId, actor, + this.system.damage.grazeOverrideFormula, ); // Perform the roll @@ -382,6 +383,26 @@ export class CosmereItem< data: rollData, }), ); + rollData.baseRoll = roll.result; + + // Roll the dice pool for graze damage silently if set. + let grazeRoll = undefined; + if (rollData.grazeOverrideForumla) { + grazeRoll = await damageRoll( + foundry.utils.mergeObject(options, { + formula: rollData.grazeOverrideForumla, + damageType: this.system.damage.type, + data: rollData, + }), + ); + // hide from DSN + grazeRoll.dice.forEach( + (die) => + (die.results[0] = Object.assign(die.results[0], { + hidden: true, + })), + ); + } if (roll && options.chatMessage !== false) { // Get the speaker @@ -396,7 +417,7 @@ export class CosmereItem< } // Return the roll - return roll; + return [roll, grazeRoll]; } /** @@ -405,7 +426,7 @@ export class CosmereItem< */ public async rollAttack( options: CosmereItem.RollAttackOptions = {}, - ): Promise<[D20Roll, DamageRoll] | null> { + ): Promise<[D20Roll, DamageRoll, DamageRoll | undefined] | null> { if (!this.hasActivation()) return null; if (!this.hasDamage() || !this.system.damage.formula) return null; @@ -457,7 +478,12 @@ export class CosmereItem< damageRoll: { ...options.damage, parts: this.system.damage.formula.split(' + '), - data: this.getDamageRollData(skillId, attributeId, actor), + data: this.getDamageRollData( + skillId, + attributeId, + actor, + this.system.damage.grazeOverrideFormula, + ), }, defaultAttribute: attributeId, defaultRollMode: options.rollMode, @@ -494,7 +520,7 @@ export class CosmereItem< }))!; // Roll the damage - const damageRoll = (await this.rollDamage({ + const [damageRoll, grazeRoll] = (await this.rollDamage({ ...options.damage, actor, skill: skillId, @@ -520,12 +546,12 @@ export class CosmereItem< user: game.user!.id, speaker, content: `

${flavor}

`, - rolls: [skillRoll, damageRoll], + rolls: [skillRoll, damageRoll, grazeRoll], })) as ChatMessage; } // Return the rolls - return [skillRoll, damageRoll]; + return [skillRoll, damageRoll, grazeRoll]; } /** @@ -534,7 +560,12 @@ export class CosmereItem< */ public async use( options: CosmereItem.UseOptions = {}, - ): Promise { + ): Promise< + | D20Roll + | [D20Roll, DamageRoll] + | [D20Roll, DamageRoll, DamageRoll] + | null + > { if (!this.hasActivation()) return null; // Set up post roll actions @@ -693,7 +724,9 @@ export class CosmereItem< if (!attackResult) return null; // Add the rolls to the list - rolls.push(...attackResult); + rolls.push( + ...attackResult.filter((roll) => roll !== undefined), + ); // Set the flavor flavor = flavor @@ -705,14 +738,15 @@ export class CosmereItem< )})`; } else { if (hasDamage) { - const damageRoll = await this.rollDamage({ + const [damageRoll, grazeRoll] = (await this.rollDamage({ ...options, actor, chatMessage: false, - }); + }))!; if (!damageRoll) return null; rolls.push(damageRoll); + if (grazeRoll) rolls.push(grazeRoll); } if (this.system.activation.type === ActivationType.SkillTest) { @@ -755,7 +789,9 @@ export class CosmereItem< // Return the result return hasDamage - ? (rolls as [D20Roll, DamageRoll]) + ? (rolls as + | [D20Roll, DamageRoll] + | [D20Roll, DamageRoll, DamageRoll]) : (rolls[0] as D20Roll); } else { // NOTE: Use boolean or operator (`||`) here instead of nullish coalescing (`??`), @@ -921,6 +957,7 @@ export class CosmereItem< skillId: Skill | undefined, attributeId: Attribute | undefined, actor: CosmereActor, + grazeOverride: string | undefined, ): DamageRollData { const skill = skillId ? actor.system.skills[skillId] : undefined; const attribute = attributeId @@ -945,6 +982,9 @@ export class CosmereItem< } : undefined, attribute: attribute?.value, + // strimming empty strings as per line 778 + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + grazeOverrideForumla: grazeOverride || undefined, }; } } From b43b612401a53481d30f1fe72f116606d7e9a82d Mon Sep 17 00:00:00 2001 From: Zithith Date: Mon, 28 Oct 2024 00:31:02 +0000 Subject: [PATCH 04/17] feat(graze damage): fixed item sheet display toggle --- .../applications/item/components/details-damage.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/system/applications/item/components/details-damage.ts b/src/system/applications/item/components/details-damage.ts index 7cf7cbbb..9d1ce228 100644 --- a/src/system/applications/item/components/details-damage.ts +++ b/src/system/applications/item/components/details-damage.ts @@ -45,13 +45,15 @@ export class DetailsDamageComponent extends HandlebarsApplicationComponent< const hasSkill = hasSkillTest && this.application.item.system.activation.skill; + this.grazeOverrideCollapsed = this.application.item.system.damage + .grazeOverrideFormula + ? this.application.item.system.damage.grazeOverrideFormula === '' + : this.grazeOverrideCollapsed; + return { hasSkillTest, hasSkill, - grazeInputCollapsed: - this.grazeOverrideCollapsed || - this.application.item.system.damage.grazeOverrideFormula !== - undefined, + grazeInputCollapsed: this.grazeOverrideCollapsed, typeSelectOptions: { none: '—', From e402daf217d05b706c9a2539eb8af423aa5074cb Mon Sep 17 00:00:00 2001 From: Zithith Date: Sat, 2 Nov 2024 23:03:42 +0000 Subject: [PATCH 05/17] feat(graze damage): Added support for multiple damage rolls with attributable sources. Improved base graze damage calculation --- src/system/dice/damage-roll.ts | 25 +++++++++++- src/system/dice/index.ts | 1 + src/system/documents/actor.ts | 9 ++--- src/system/documents/item.ts | 72 ++++++++++++++++------------------ 4 files changed, 61 insertions(+), 46 deletions(-) diff --git a/src/system/dice/damage-roll.ts b/src/system/dice/damage-roll.ts index 4ac585c3..577a341b 100644 --- a/src/system/dice/damage-roll.ts +++ b/src/system/dice/damage-roll.ts @@ -15,8 +15,7 @@ export type DamageRollData< attribute: Attribute; }; attribute?: number; - grazeOverrideForumla?: string; - baseRoll?: string; + baseRoll?: number; }; export interface DamageRollOptions @@ -36,6 +35,16 @@ export interface DamageRollOptions * @default AdvantageMode.None */ advantageMode?: AdvantageMode; + + /** + * Where did this damage come from? + */ + source?: string; + + /** + * Nested Roll item for graze damage + */ + graze?: DamageRoll; } export class DamageRoll extends foundry.dice.Roll { @@ -63,6 +72,18 @@ export class DamageRoll extends foundry.dice.Roll { return this.options.mod; } + get source(): string | undefined { + return this.options.source; + } + + get graze(): DamageRoll | undefined { + return this.options.graze; + } + + set graze(roll: DamageRoll) { + this.options.graze = roll; + } + public get hasMod() { return this.options.mod !== undefined; } diff --git a/src/system/dice/index.ts b/src/system/dice/index.ts index 08868eed..00b4d102 100644 --- a/src/system/dice/index.ts +++ b/src/system/dice/index.ts @@ -117,6 +117,7 @@ export async function damageRoll( allowStrings: config.allowStrings, maximize: config.maximize, minimize: config.minimize, + source: config.source, }); // Evaluate the roll diff --git a/src/system/documents/actor.ts b/src/system/documents/actor.ts index 0d330220..f838a6c5 100644 --- a/src/system/documents/actor.ts +++ b/src/system/documents/actor.ts @@ -614,12 +614,9 @@ export class CosmereActor< public async useItem( item: CosmereItem, options?: Omit, - ): Promise< - | D20Roll - | [D20Roll, DamageRoll] - | [D20Roll, DamageRoll, DamageRoll] - | null - > { + ): Promise { + // Checks for relevant Active Effects triggers/manual toggles will go here + // E.g. permanent/conditional: attack bonuses, damage riders, auto opportunity/complications, etc. return item.use({ ...options, actor: this }); } diff --git a/src/system/documents/item.ts b/src/system/documents/item.ts index 1773365b..43f0e9ee 100644 --- a/src/system/documents/item.ts +++ b/src/system/documents/item.ts @@ -331,7 +331,7 @@ export class CosmereItem< */ public async rollDamage( options: CosmereItem.RollDamageOptions = {}, - ): Promise<[DamageRoll, DamageRoll | undefined] | null> { + ): Promise { if (!this.hasDamage() || !this.system.damage.formula) return null; // Get the actor to roll for (either assigned through option, the parent of this item, or the first controlled actor) @@ -371,26 +371,39 @@ export class CosmereItem< skillId, attributeId, actor, - this.system.damage.grazeOverrideFormula, ); // Perform the roll const roll = await damageRoll( foundry.utils.mergeObject(options, { - formula: this.system.damage.formula, + formula: `${this.system.damage.formula}+${rollData.mod}`, damageType: this.system.damage.type, mod: rollData.mod, data: rollData, + source: this.name, }), ); - rollData.baseRoll = roll.result; + // We want to store the unmodded damage + // Get the flat damage value + let unmoddedDamage = + roll.terms[0] instanceof foundry.dice.terms.NumericTerm + ? roll.terms[0].total + : 0; + // get any rolled die + unmoddedDamage += roll.dice + .map((die) => die.total ?? 0) + .reduce((sum, die) => sum + die, 0); + // store it in the roll + rollData.baseRoll = unmoddedDamage; // Roll the dice pool for graze damage silently if set. let grazeRoll = undefined; - if (rollData.grazeOverrideForumla) { + const grazeFormula = + this.system.damage.grazeOverrideFormula ?? `${rollData.baseRoll}`; + if (grazeFormula) { grazeRoll = await damageRoll( foundry.utils.mergeObject(options, { - formula: rollData.grazeOverrideForumla, + formula: grazeFormula, damageType: this.system.damage.type, data: rollData, }), @@ -403,6 +416,8 @@ export class CosmereItem< })), ); } + if (!grazeRoll) return null; + roll.graze = grazeRoll; if (roll && options.chatMessage !== false) { // Get the speaker @@ -417,7 +432,7 @@ export class CosmereItem< } // Return the roll - return [roll, grazeRoll]; + return [roll]; } /** @@ -426,7 +441,7 @@ export class CosmereItem< */ public async rollAttack( options: CosmereItem.RollAttackOptions = {}, - ): Promise<[D20Roll, DamageRoll, DamageRoll | undefined] | null> { + ): Promise<[D20Roll, DamageRoll[]] | null> { if (!this.hasActivation()) return null; if (!this.hasDamage() || !this.system.damage.formula) return null; @@ -478,12 +493,7 @@ export class CosmereItem< damageRoll: { ...options.damage, parts: this.system.damage.formula.split(' + '), - data: this.getDamageRollData( - skillId, - attributeId, - actor, - this.system.damage.grazeOverrideFormula, - ), + data: this.getDamageRollData(skillId, attributeId, actor), }, defaultAttribute: attributeId, defaultRollMode: options.rollMode, @@ -520,7 +530,7 @@ export class CosmereItem< }))!; // Roll the damage - const [damageRoll, grazeRoll] = (await this.rollDamage({ + const damageRolls = (await this.rollDamage({ ...options.damage, actor, skill: skillId, @@ -546,12 +556,12 @@ export class CosmereItem< user: game.user!.id, speaker, content: `

${flavor}

`, - rolls: [skillRoll, damageRoll, grazeRoll], + rolls: [skillRoll, damageRolls], })) as ChatMessage; } // Return the rolls - return [skillRoll, damageRoll, grazeRoll]; + return [skillRoll, damageRolls ?? []]; } /** @@ -560,12 +570,7 @@ export class CosmereItem< */ public async use( options: CosmereItem.UseOptions = {}, - ): Promise< - | D20Roll - | [D20Roll, DamageRoll] - | [D20Roll, DamageRoll, DamageRoll] - | null - > { + ): Promise { if (!this.hasActivation()) return null; // Set up post roll actions @@ -724,9 +729,7 @@ export class CosmereItem< if (!attackResult) return null; // Add the rolls to the list - rolls.push( - ...attackResult.filter((roll) => roll !== undefined), - ); + rolls.push(attackResult[0], ...attackResult[1]); // Set the flavor flavor = flavor @@ -738,15 +741,14 @@ export class CosmereItem< )})`; } else { if (hasDamage) { - const [damageRoll, grazeRoll] = (await this.rollDamage({ + const damageRolls = await this.rollDamage({ ...options, actor, chatMessage: false, - }))!; - if (!damageRoll) return null; + }); + if (!damageRolls) return null; - rolls.push(damageRoll); - if (grazeRoll) rolls.push(grazeRoll); + rolls.push(...damageRolls); } if (this.system.activation.type === ActivationType.SkillTest) { @@ -789,9 +791,7 @@ export class CosmereItem< // Return the result return hasDamage - ? (rolls as - | [D20Roll, DamageRoll] - | [D20Roll, DamageRoll, DamageRoll]) + ? (rolls as [D20Roll, ...DamageRoll[]]) : (rolls[0] as D20Roll); } else { // NOTE: Use boolean or operator (`||`) here instead of nullish coalescing (`??`), @@ -957,7 +957,6 @@ export class CosmereItem< skillId: Skill | undefined, attributeId: Attribute | undefined, actor: CosmereActor, - grazeOverride: string | undefined, ): DamageRollData { const skill = skillId ? actor.system.skills[skillId] : undefined; const attribute = attributeId @@ -982,9 +981,6 @@ export class CosmereItem< } : undefined, attribute: attribute?.value, - // strimming empty strings as per line 778 - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - grazeOverrideForumla: grazeOverride || undefined, }; } } From 3d7a6be61b86090d4def3728159427c1549d8b52 Mon Sep 17 00:00:00 2001 From: Zithith Date: Sat, 2 Nov 2024 23:33:15 +0000 Subject: [PATCH 06/17] feat(graze damage): reverted base damage calculation to a simpler apporach for now --- src/system/documents/item.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/system/documents/item.ts b/src/system/documents/item.ts index 89d3d4ad..2d84baa8 100644 --- a/src/system/documents/item.ts +++ b/src/system/documents/item.ts @@ -382,18 +382,10 @@ export class CosmereItem< source: this.name, }), ); - // We want to store the unmodded damage - // Get the flat damage value - let unmoddedDamage = - roll.terms[0] instanceof foundry.dice.terms.NumericTerm - ? roll.terms[0].total - : 0; - // get any rolled die - unmoddedDamage += roll.dice - .map((die) => die.total ?? 0) - .reduce((sum, die) => sum + die, 0); - // store it in the roll - rollData.baseRoll = unmoddedDamage; + // We want to store the unmodded damage for use in graze calcs + // This isn't a particularly perfect solution, but it's functional + // only undoing the automatic addition of the selected attribute + rollData.baseRoll = roll.total - rollData.mod; // Roll the dice pool for graze damage silently if set. let grazeRoll = undefined; From 72f2ddb25ec2ad2d0ff79cdc34539583c1d3a581 Mon Sep 17 00:00:00 2001 From: Zithith Date: Sat, 2 Nov 2024 23:38:06 +0000 Subject: [PATCH 07/17] chore: added missed null value check --- src/system/documents/item.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/system/documents/item.ts b/src/system/documents/item.ts index 2d84baa8..1540cb96 100644 --- a/src/system/documents/item.ts +++ b/src/system/documents/item.ts @@ -385,7 +385,7 @@ export class CosmereItem< // We want to store the unmodded damage for use in graze calcs // This isn't a particularly perfect solution, but it's functional // only undoing the automatic addition of the selected attribute - rollData.baseRoll = roll.total - rollData.mod; + rollData.baseRoll = (roll.total ?? 0) - (rollData.mod ?? 0); // Roll the dice pool for graze damage silently if set. let grazeRoll = undefined; From c73527396c1b156969100ba87b77e863e6c22c21 Mon Sep 17 00:00:00 2001 From: Zithith Date: Sun, 3 Nov 2024 00:22:52 +0000 Subject: [PATCH 08/17] feat(graze damage): added hint description to graze override input --- src/lang/en.json | 3 ++- src/templates/item/components/details-damage.hbs | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lang/en.json b/src/lang/en.json index 106bbd16..bc797cb6 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -583,7 +583,8 @@ "GrazeOverride": "Override Graze Damage Calculation", "GrazeShow": "Show New Graze Damage Formula", "GrazeHide": "Hide New Graze Damage Forumla", - "GrazeFormula": "New Formula" + "GrazeFormula": "New Formula", + "GrazeHint": "Formula entered here will be the entirety of the calculation for graze damage. Any die rolls will be rolled separately from the main damage roll. You can access the value of the main roll using @baseRoll or the selected attribute mod with @mod" }, "Modality": { "Title": "Modality", diff --git a/src/templates/item/components/details-damage.hbs b/src/templates/item/components/details-damage.hbs index ef07d7e4..8837c05c 100644 --- a/src/templates/item/components/details-damage.hbs +++ b/src/templates/item/components/details-damage.hbs @@ -89,6 +89,9 @@ readonly {{/if}} > +

+ {{localize "COSMERE.Item.Sheet.Damage.GrazeHint"}} +

{{/if}} {{/if}} From ebcbb4ca0627fd4abd6e0ca40b53e241172e675a Mon Sep 17 00:00:00 2001 From: Zithith Date: Sun, 3 Nov 2024 17:43:25 +0000 Subject: [PATCH 09/17] feat(graze damage): spread the damage rolls sent to chat cards --- src/system/documents/item.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/system/documents/item.ts b/src/system/documents/item.ts index 1540cb96..65a3be47 100644 --- a/src/system/documents/item.ts +++ b/src/system/documents/item.ts @@ -583,7 +583,7 @@ export class CosmereItem< user: game.user!.id, speaker, content: `

${flavor}

`, - rolls: [skillRoll, damageRolls], + rolls: [skillRoll, ...damageRolls], })) as ChatMessage; } From 64961a6a9d100a6e3b1480fa9974d8c49b45d254 Mon Sep 17 00:00:00 2001 From: Zithith Date: Sun, 10 Nov 2024 15:31:44 +0000 Subject: [PATCH 10/17] feat(graze damage): new approach to building graze rolls. Removed extra addition of modifier by apply damage function. Removed excess style declaration. --- src/style/sheets/item/module.scss | 5 ---- src/system/dice/damage-roll.ts | 41 +++++++++++++++++++++++++- src/system/dice/index.ts | 1 + src/system/documents/chat-message.ts | 10 +++---- src/system/documents/item.ts | 44 ++++++++++++++++------------ 5 files changed, 71 insertions(+), 30 deletions(-) diff --git a/src/style/sheets/item/module.scss b/src/style/sheets/item/module.scss index bb0aa92e..2d21faa0 100644 --- a/src/style/sheets/item/module.scss +++ b/src/style/sheets/item/module.scss @@ -310,11 +310,6 @@ app-item-details-damage { flex: 2; } - span { - flex: 3; - font-style: italic; - } - .controls { margin-right: 0.5rem; } diff --git a/src/system/dice/damage-roll.ts b/src/system/dice/damage-roll.ts index 577a341b..df8c9f8f 100644 --- a/src/system/dice/damage-roll.ts +++ b/src/system/dice/damage-roll.ts @@ -1,6 +1,7 @@ import { DamageType, Skill, Attribute } from '@system/types/cosmere'; import { CosmereActorRollData } from '@system/documents/actor'; import { AdvantageMode } from '@system/types/roll'; +import RollTerm from '@league-of-foundry-developers/foundry-vtt-types/src/foundry/client-esm/dice/terms/term.mjs'; export type DamageRollData< ActorRollData extends CosmereActorRollData = CosmereActorRollData, @@ -15,7 +16,11 @@ export type DamageRollData< attribute: Attribute; }; attribute?: number; - baseRoll?: number; + damage?: { + total: DamageRoll; + unmodded: DamageRoll; + dice: foundry.dice.terms.DiceTerm[]; + }; }; export interface DamageRollOptions @@ -102,6 +107,40 @@ export class DamageRoll extends foundry.dice.Roll { return this.options.advantageMode === AdvantageMode.Disadvantage; } + /* --- Helper Functions --- */ + + public removeTermSafely( + conditional: ( + value: RollTerm, + index: number, + obj: RollTerm[], + ) => boolean, + ) { + this.terms.findSplice(conditional); + if ( + this.terms[this.terms.length - 1] instanceof + foundry.dice.terms.OperatorTerm + ) + this.terms.pop(); + this.resetFormula(); + } + + public replaceDieResults(sourceDicePool: foundry.dice.terms.DiceTerm[]) { + sourceDicePool.forEach((die) => { + let numDiceToAlter = die.number ?? 0; + while (numDiceToAlter > 0) { + const nextDie = this.dice.find( + (newDie) => newDie.faces === die.faces, + ); + if (!nextDie) return; + nextDie.results = die.results; + numDiceToAlter--; + } + }); + this._total = this._evaluateTotal(); + // roll total doesn't update here! + } + /* --- Internal Functions --- */ private configureModifiers() { diff --git a/src/system/dice/index.ts b/src/system/dice/index.ts index 612a2455..f89d6ffb 100644 --- a/src/system/dice/index.ts +++ b/src/system/dice/index.ts @@ -4,6 +4,7 @@ import { D20Roll, D20RollOptions, D20RollData } from './d20-roll'; import { DamageRoll, DamageRollOptions, DamageRollData } from './damage-roll'; import { AdvantageMode } from '../types/roll'; import { determineConfigurationMode } from '../utils/generic'; +import { RollMode } from './types'; export * from './d20-roll'; export * from './damage-roll'; diff --git a/src/system/documents/chat-message.ts b/src/system/documents/chat-message.ts index c6ed41f4..c6b6c9d5 100644 --- a/src/system/documents/chat-message.ts +++ b/src/system/documents/chat-message.ts @@ -248,7 +248,7 @@ export class CosmereChatMessage extends ChatMessage { 'COSMERE.ChatMessage.Action.ApplyGraze', ), icon: 'fa-solid fa-shield-halved', - callback: this.onApplyDamage.bind(this, false), + callback: this.onApplyDamage.bind(this), }, ] : []), @@ -317,7 +317,7 @@ export class CosmereChatMessage extends ChatMessage { ); } - private onApplyDamage(includeMod = true) { + private onApplyDamage() { // Get selected actor const actor = (game.canvas!.tokens!.controlled?.[0]?.actor ?? game.user?.character) as CosmereActor | undefined; @@ -333,13 +333,13 @@ export class CosmereChatMessage extends ChatMessage { // Apply damage void actor.applyDamage( ...damageRolls.map((r) => ({ - amount: (r.total ?? 0) + (includeMod ? (r.mod ?? 0) : 0), + amount: r.total ?? 0, type: r.damageType, })), ); } - private onApplyHealing(includeMod = true) { + private onApplyHealing() { // Get selected actor const actor = (game.canvas!.tokens!.controlled?.[0]?.actor ?? game.user?.character) as CosmereActor | undefined; @@ -355,7 +355,7 @@ export class CosmereChatMessage extends ChatMessage { // Apply damage void actor.applyDamage( ...damageRolls.map((r) => ({ - amount: (r.total ?? 0) + (includeMod ? (r.mod ?? 0) : 0), + amount: r.total ?? 0, type: DamageType.Healing, })), ); diff --git a/src/system/documents/item.ts b/src/system/documents/item.ts index 65a3be47..b7a60c04 100644 --- a/src/system/documents/item.ts +++ b/src/system/documents/item.ts @@ -385,28 +385,34 @@ export class CosmereItem< // We want to store the unmodded damage for use in graze calcs // This isn't a particularly perfect solution, but it's functional // only undoing the automatic addition of the selected attribute - rollData.baseRoll = (roll.total ?? 0) - (rollData.mod ?? 0); + const unmoddedRoll = roll.clone(); + rollData.damage = { + total: roll, + unmodded: unmoddedRoll, + dice: roll.dice, + }; + + unmoddedRoll.removeTermSafely( + (term) => + term instanceof foundry.dice.terms.NumericTerm && + term.total === rollData.mod, + ); + await unmoddedRoll.evaluate(); + unmoddedRoll.replaceDieResults(roll.dice); // Roll the dice pool for graze damage silently if set. - let grazeRoll = undefined; const grazeFormula = - this.system.damage.grazeOverrideFormula ?? `${rollData.baseRoll}`; - if (grazeFormula) { - grazeRoll = await damageRoll( - foundry.utils.mergeObject(options, { - formula: grazeFormula, - damageType: this.system.damage.type, - data: rollData, - }), - ); - // hide from DSN - grazeRoll.dice.forEach( - (die) => - (die.results[0] = Object.assign(die.results[0], { - hidden: true, - })), - ); - } + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + this.system.damage.grazeOverrideFormula || '@damage.unmodded'; + const usesBaseDamage = grazeFormula.includes('@damage'); + const grazeRoll = await damageRoll( + foundry.utils.mergeObject(options, { + formula: grazeFormula, + damageType: this.system.damage.type, + data: rollData, + }), + ); + if (usesBaseDamage) grazeRoll.replaceDieResults(roll.dice); if (!grazeRoll) return null; roll.graze = grazeRoll; From bf761c3c494350181f5c1e2cab80ad982bcbed18 Mon Sep 17 00:00:00 2001 From: Zithith Date: Sun, 10 Nov 2024 15:33:17 +0000 Subject: [PATCH 11/17] chore: removed old comment --- src/system/dice/damage-roll.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/system/dice/damage-roll.ts b/src/system/dice/damage-roll.ts index df8c9f8f..8a660f20 100644 --- a/src/system/dice/damage-roll.ts +++ b/src/system/dice/damage-roll.ts @@ -138,7 +138,6 @@ export class DamageRoll extends foundry.dice.Roll { } }); this._total = this._evaluateTotal(); - // roll total doesn't update here! } /* --- Internal Functions --- */ From 5cd938288201fd8df677621fc3ade824edc0379f Mon Sep 17 00:00:00 2001 From: zithith <55356181+zithith@users.noreply.github.com> Date: Tue, 12 Nov 2024 00:57:15 +0000 Subject: [PATCH 12/17] feat(graze overrides): updated damage roll class to account for existing dice only option --- src/system/dice/damage-roll.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/system/dice/damage-roll.ts b/src/system/dice/damage-roll.ts index 8a660f20..65d8a015 100644 --- a/src/system/dice/damage-roll.ts +++ b/src/system/dice/damage-roll.ts @@ -19,7 +19,7 @@ export type DamageRollData< damage?: { total: DamageRoll; unmodded: DamageRoll; - dice: foundry.dice.terms.DiceTerm[]; + dice: DamageRoll; }; }; @@ -117,7 +117,7 @@ export class DamageRoll extends foundry.dice.Roll { ) => boolean, ) { this.terms.findSplice(conditional); - if ( + while ( this.terms[this.terms.length - 1] instanceof foundry.dice.terms.OperatorTerm ) From 5da4e434303b035fdaae6953a7b803da4ac95d8d Mon Sep 17 00:00:00 2001 From: zithith <55356181+zithith@users.noreply.github.com> Date: Tue, 12 Nov 2024 01:14:16 +0000 Subject: [PATCH 13/17] feat(graze damage): updated items to store damage.dice better and align our implementation following notes from Brotherwise --- src/system/documents/item.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/system/documents/item.ts b/src/system/documents/item.ts index b7a60c04..51564a9d 100644 --- a/src/system/documents/item.ts +++ b/src/system/documents/item.ts @@ -386,10 +386,11 @@ export class CosmereItem< // This isn't a particularly perfect solution, but it's functional // only undoing the automatic addition of the selected attribute const unmoddedRoll = roll.clone(); + const diceOnlyRoll = roll.clone(); rollData.damage = { total: roll, unmodded: unmoddedRoll, - dice: roll.dice, + dice: diceOnlyRoll, }; unmoddedRoll.removeTermSafely( @@ -400,10 +401,14 @@ export class CosmereItem< await unmoddedRoll.evaluate(); unmoddedRoll.replaceDieResults(roll.dice); + diceOnlyRoll.removeManyTermsSafely((term) => term !instanceof foundry.dice.terms.DiceTerm); + await diceOnlyRoll.evaluate(); + diceRollOnly.replaceDieResults(roll.dice); + // Roll the dice pool for graze damage silently if set. const grazeFormula = // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - this.system.damage.grazeOverrideFormula || '@damage.unmodded'; + this.system.damage.grazeOverrideFormula || '@damage.dice'; const usesBaseDamage = grazeFormula.includes('@damage'); const grazeRoll = await damageRoll( foundry.utils.mergeObject(options, { From d0a67ef115e9b95a7e5956eb1181c5543ddeecfb Mon Sep 17 00:00:00 2001 From: zithith <55356181+zithith@users.noreply.github.com> Date: Tue, 12 Nov 2024 07:26:14 +0000 Subject: [PATCH 14/17] feat(graze damage): added ability for damage rolls to filter multiple terms at once --- src/system/dice/damage-roll.ts | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/system/dice/damage-roll.ts b/src/system/dice/damage-roll.ts index 65d8a015..f6af6d8e 100644 --- a/src/system/dice/damage-roll.ts +++ b/src/system/dice/damage-roll.ts @@ -117,12 +117,18 @@ export class DamageRoll extends foundry.dice.Roll { ) => boolean, ) { this.terms.findSplice(conditional); - while ( - this.terms[this.terms.length - 1] instanceof - foundry.dice.terms.OperatorTerm - ) - this.terms.pop(); - this.resetFormula(); + this.cleanUpTerms(); + } + + public filterTermsSafely( + condition: ( + value: RollTerm, + index: number, + obj: RollTerm[], + ) => Boolean, + ) { + this.terms = this.termd.filter(condition); + this.cleanUpTerms(); } public replaceDieResults(sourceDicePool: foundry.dice.terms.DiceTerm[]) { @@ -142,6 +148,15 @@ export class DamageRoll extends foundry.dice.Roll { /* --- Internal Functions --- */ + private cleanUpTerms() { + while ( + this.terms[this.terms.length - 1] instanceof + foundry.dice.terms.OperatorTerm + ) + this.terms.pop(); + this.resetFormula(); + } + private configureModifiers() { // Find the first die term const dieTerm = this.terms.find( From dd45443f9f21caf6d655a417c22db0ab1155b265 Mon Sep 17 00:00:00 2001 From: zithith <55356181+zithith@users.noreply.github.com> Date: Tue, 12 Nov 2024 07:28:58 +0000 Subject: [PATCH 15/17] chore: updated damaging items to use new filter function rather than placeholder --- src/system/documents/item.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/system/documents/item.ts b/src/system/documents/item.ts index 51564a9d..f994b05a 100644 --- a/src/system/documents/item.ts +++ b/src/system/documents/item.ts @@ -401,7 +401,7 @@ export class CosmereItem< await unmoddedRoll.evaluate(); unmoddedRoll.replaceDieResults(roll.dice); - diceOnlyRoll.removeManyTermsSafely((term) => term !instanceof foundry.dice.terms.DiceTerm); + diceOnlyRoll.filterTermsSafely((term) => term instanceof foundry.dice.terms.DiceTerm || term instanceof foundry.dice.terms.OperatorTerm); await diceOnlyRoll.evaluate(); diceRollOnly.replaceDieResults(roll.dice); From 50edf61891d05b9b702a5c8035f662d7ba1271c4 Mon Sep 17 00:00:00 2001 From: Zithith Date: Tue, 12 Nov 2024 20:38:39 +0000 Subject: [PATCH 16/17] feat(graze damage): streamlined a bit of the damage option calculations, fixed typos and linting, updated code comments --- src/system/dice/damage-roll.ts | 8 ++------ src/system/documents/item.ts | 20 +++++++++----------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/system/dice/damage-roll.ts b/src/system/dice/damage-roll.ts index f6af6d8e..97522ead 100644 --- a/src/system/dice/damage-roll.ts +++ b/src/system/dice/damage-roll.ts @@ -121,13 +121,9 @@ export class DamageRoll extends foundry.dice.Roll { } public filterTermsSafely( - condition: ( - value: RollTerm, - index: number, - obj: RollTerm[], - ) => Boolean, + condition: (value: RollTerm, index: number, obj: RollTerm[]) => boolean, ) { - this.terms = this.termd.filter(condition); + this.terms = this.terms.filter(condition); this.cleanUpTerms(); } diff --git a/src/system/documents/item.ts b/src/system/documents/item.ts index f994b05a..6029579c 100644 --- a/src/system/documents/item.ts +++ b/src/system/documents/item.ts @@ -382,9 +382,8 @@ export class CosmereItem< source: this.name, }), ); - // We want to store the unmodded damage for use in graze calcs - // This isn't a particularly perfect solution, but it's functional - // only undoing the automatic addition of the selected attribute + + // Gather the formula options for graze rolls const unmoddedRoll = roll.clone(); const diceOnlyRoll = roll.clone(); rollData.damage = { @@ -392,20 +391,18 @@ export class CosmereItem< unmodded: unmoddedRoll, dice: diceOnlyRoll, }; - unmoddedRoll.removeTermSafely( (term) => term instanceof foundry.dice.terms.NumericTerm && term.total === rollData.mod, ); - await unmoddedRoll.evaluate(); - unmoddedRoll.replaceDieResults(roll.dice); - - diceOnlyRoll.filterTermsSafely((term) => term instanceof foundry.dice.terms.DiceTerm || term instanceof foundry.dice.terms.OperatorTerm); - await diceOnlyRoll.evaluate(); - diceRollOnly.replaceDieResults(roll.dice); + diceOnlyRoll.filterTermsSafely( + (term) => + term instanceof foundry.dice.terms.DiceTerm || + term instanceof foundry.dice.terms.OperatorTerm, + ); - // Roll the dice pool for graze damage silently if set. + // Make the graze pool and roll it const grazeFormula = // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing this.system.damage.grazeOverrideFormula || '@damage.dice'; @@ -417,6 +414,7 @@ export class CosmereItem< data: rollData, }), ); + // update with results from the basic roll if needed and store for display if (usesBaseDamage) grazeRoll.replaceDieResults(roll.dice); if (!grazeRoll) return null; roll.graze = grazeRoll; From 44470743e7e469da243960f70f3c21c74b0cb6cc Mon Sep 17 00:00:00 2001 From: zithith <55356181+zithith@users.noreply.github.com> Date: Tue, 12 Nov 2024 20:53:32 +0000 Subject: [PATCH 17/17] Update hint wording to reflect new variables --- src/lang/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/en.json b/src/lang/en.json index bc797cb6..5fd6a348 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -584,7 +584,7 @@ "GrazeShow": "Show New Graze Damage Formula", "GrazeHide": "Hide New Graze Damage Forumla", "GrazeFormula": "New Formula", - "GrazeHint": "Formula entered here will be the entirety of the calculation for graze damage. Any die rolls will be rolled separately from the main damage roll. You can access the value of the main roll using @baseRoll or the selected attribute mod with @mod" + "GrazeHint": "Formula entered here will be the entirety of the calculation for graze damage. Any die rolls will be rolled separately from the main damage roll. You can pull in all or part of the main roll using @damage.total/@damage.unmodded/@damage.dice (the latter being the system default) or the selected attribute mod with @mod" }, "Modality": { "Title": "Modality",