diff --git a/CHANGELOG.md b/CHANGELOG.md index 12c545e3..26efbbcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - notifications for errors in weapon macros created from source - a value replacing rule element ([#95](https://github.com/Wasteland-Ventures-Group/WV-VTT-module/issues/95)) +- weapon attack details on not executed attacks + ([#111](https://github.com/Wasteland-Ventures-Group/WV-VTT-module/issues/111)) ### Changed diff --git a/src/main/handlebars/chatMessages/weaponAttack.hbs b/src/main/handlebars/chatMessages/weaponAttack.hbs index 6d1d67f4..c4a48e2a 100644 --- a/src/main/handlebars/chatMessages/weaponAttack.hbs +++ b/src/main/handlebars/chatMessages/weaponAttack.hbs @@ -4,76 +4,77 @@

{{template.raw.mainHeading}}

{{template.raw.subHeading}}

-{{#if executed}} - {{#if weaponSystemData.notes}} -

{{weaponSystemData.notes}}

- {{/if}} +{{#if weaponSystemData.notes}} +

{{weaponSystemData.notes}}

+{{/if}} - {{!attack details}} -
- {{localize "wv.weapons.attacks.details"}} -
-
{{localize "wv.weapons.attacks.ranges"}}: {{template.raw.displayRanges}}
- {{#if details}} {{!backward compatibility}} -
{{localize "wv.weapons.modifiers.hit.range"}}: {{localize template.keys.rangeBracket}} ({{details.range.distance}})
-
-
{{localize "wv.sheets.actor.secondary.criticals.title"}}
-
    -
  • {{localize "wv.sheets.actor.secondary.criticals.success"}}: {{details.criticals.success}}
  • -
  • {{localize "wv.sheets.actor.secondary.criticals.failure"}}: {{details.criticals.failure}}
  • -
+{{!attack details}} +
+ {{localize "wv.weapons.attacks.details"}} +
+
{{localize "wv.weapons.attacks.ranges"}}: {{template.raw.displayRanges}}
+ {{#if details}} {{!backward compatibility}} +
{{localize "wv.weapons.modifiers.hit.range"}}: {{localize template.keys.rangeBracket}} ({{details.range.distance}})
+
+
{{localize "wv.sheets.actor.secondary.criticals.title"}}
+
    +
  • {{localize "wv.sheets.actor.secondary.criticals.success"}}: {{details.criticals.success}}
  • +
  • {{localize "wv.sheets.actor.secondary.criticals.failure"}}: {{details.criticals.failure}}
  • +
+
+
+
+ {{localize "wv.weapons.modifiers.listingTitles.hit"}} +
-
-
- {{localize "wv.weapons.modifiers.listingTitles.hit"}} -
-
-
- {{localize "wv.weapons.modifiers.base"}} - {{details.hit.base}} - {{#each details.hit.modifiers as |modifier|}} - {{localize modifier.key}} - {{modifier.amount}} - {{/each}} - {{localize "wv.weapons.modifiers.total"}} - {{details.hit.total}} -
+
+ {{localize "wv.weapons.modifiers.base"}} + {{details.hit.base}} + {{#each details.hit.modifiers as |modifier|}} + {{localize modifier.key}} + {{modifier.amount}} + {{/each}} + {{localize "wv.weapons.modifiers.total"}} + {{details.hit.total}}
-
-
- {{localize "wv.weapons.modifiers.listingTitles.damageBase"}} -
-
-
- {{localize "wv.weapons.modifiers.base"}} - {{details.damage.base.base}} - {{#each details.damage.base.modifiers as |modifier|}} - {{localize modifier.key}} - {{modifier.amount}} - {{/each}} - {{localize "wv.weapons.modifiers.total"}} - {{details.damage.base.total}} -
+
+
+
+ {{localize "wv.weapons.modifiers.listingTitles.damageBase"}} +
-
-
- {{localize "wv.weapons.modifiers.listingTitles.damageDice"}} -
-
-
- {{localize "wv.weapons.modifiers.base"}} - {{details.damage.dice.base}} - {{#each details.damage.dice.modifiers as |modifier|}} - {{localize modifier.key}} - {{modifier.amount}} - {{/each}} - {{localize "wv.weapons.modifiers.total"}} - {{details.damage.dice.total}} -
+
+ {{localize "wv.weapons.modifiers.base"}} + {{details.damage.base.base}} + {{#each details.damage.base.modifiers as |modifier|}} + {{localize modifier.key}} + {{modifier.amount}} + {{/each}} + {{localize "wv.weapons.modifiers.total"}} + {{details.damage.base.total}}
- {{/if}} -
+
+
+
+ {{localize "wv.weapons.modifiers.listingTitles.damageDice"}} +
+
+
+ {{localize "wv.weapons.modifiers.base"}} + {{details.damage.dice.base}} + {{#each details.damage.dice.modifiers as |modifier|}} + {{localize modifier.key}} + {{modifier.amount}} + {{/each}} + {{localize "wv.weapons.modifiers.total"}} + {{details.damage.dice.total}} +
+
+ {{/if}}
+
+ +{{#if executed}} {{!hit roll}}
diff --git a/src/main/lang/de.json b/src/main/lang/de.json index 41f7765b..5227b65c 100644 --- a/src/main/lang/de.json +++ b/src/main/lang/de.json @@ -294,6 +294,7 @@ }, "ranges": { "brackets": { + "outOfRange": "Außer Reichweite", "long": "Weit", "medium": "Mittel", "short": "Kurz", diff --git a/src/main/lang/en.json b/src/main/lang/en.json index f1b670fe..8d6f19c6 100644 --- a/src/main/lang/en.json +++ b/src/main/lang/en.json @@ -294,6 +294,7 @@ }, "ranges": { "brackets": { + "outOfRange": "Out of range", "long": "Long", "medium": "Medium", "short": "Short", diff --git a/src/main/typescript/hooks/renderChatMessage/decorateSystemMessage/decorateWeaponAttack.ts b/src/main/typescript/hooks/renderChatMessage/decorateSystemMessage/decorateWeaponAttack.ts index 2215673f..83777279 100644 --- a/src/main/typescript/hooks/renderChatMessage/decorateSystemMessage/decorateWeaponAttack.ts +++ b/src/main/typescript/hooks/renderChatMessage/decorateSystemMessage/decorateWeaponAttack.ts @@ -17,11 +17,37 @@ export default async function decorateWeaponAttack( const content = getContentElement(html); + const commonData: CommonWeaponAttackTemplateData = { + ...flags, + template: { + keys: { + rangeBracket: getRangeBracketKey(flags) + }, + raw: { + displayRanges: getDisplayRanges( + flags.weaponSystemData, + flags.ownerSpecials + ), + mainHeading: + flags.weaponName !== flags.weaponSystemData.name + ? flags.weaponName + : flags.weaponSystemData.name, + subHeading: + flags.weaponName !== flags.weaponSystemData.name + ? `${flags.weaponSystemData.name} - ${flags.attackName}` + : flags.attackName + } + } + }; + if (!flags.executed) { const data: NotExecutedAttackTemplateData = { ...flags, + ...commonData, template: { + ...commonData.template, keys: { + ...commonData.template.keys, notExecutedReason: getNotExecutedReasonKey(flags) } } @@ -32,7 +58,9 @@ export default async function decorateWeaponAttack( const data: ExecutedAttackTemplateData = { ...flags, + ...commonData, template: { + ...commonData.template, damage: { results: flags.rolls.damage.results.map((result) => { return { @@ -42,22 +70,8 @@ export default async function decorateWeaponAttack( }) }, keys: { - hit: getHitResultKey(flags), - rangeBracket: getRangeBracketKey(flags) - }, - raw: { - displayRanges: getDisplayRanges( - flags.weaponSystemData, - flags.ownerSpecials - ), - mainHeading: - flags.weaponName !== flags.weaponSystemData.name - ? flags.weaponName - : flags.weaponSystemData.name, - subHeading: - flags.weaponName !== flags.weaponSystemData.name - ? `${flags.weaponSystemData.name} - ${flags.attackName}` - : flags.attackName + ...commonData.template.keys, + hit: getHitResultKey(flags) } } }; @@ -87,8 +101,12 @@ function getHitResultKey(flags: ExecutedAttackFlags): string { } /** Get the i18n key for the range bracket. */ -function getRangeBracketKey(flags: ExecutedAttackFlags): string | undefined { +function getRangeBracketKey( + flags: CommonWeaponAttackFlags +): string | undefined { switch (flags.details?.range.bracket) { + case RangeBracket.OUT_OF_RANGE: + return "wv.weapons.ranges.brackets.outOfRange"; case RangeBracket.LONG: return "wv.weapons.ranges.brackets.long"; case RangeBracket.MEDIUM: @@ -104,24 +122,9 @@ function getRangeBracketKey(flags: ExecutedAttackFlags): string | undefined { export type WeaponAttackFlags = NotExecutedAttackFlags | ExecutedAttackFlags; /** The common weapon attack chat message flags */ -interface CommonWeaponAttackFlags { +export interface CommonWeaponAttackFlags { type: "weaponAttack"; - weaponName: string; - weaponImage: string | null; - weaponSystemData: WeaponDataProperties["data"]; attackName: string; -} - -/** The attack chat message flags for a unexecuted attack */ -export interface NotExecutedAttackFlags extends CommonWeaponAttackFlags { - executed: false; - reason?: "insufficientAp" | "outOfRange"; -} - -/** The attack chat message flags for an executed attack */ -export interface ExecutedAttackFlags extends CommonWeaponAttackFlags { - executed: true; - ownerSpecials?: Partial | undefined; details?: { criticals: { failure: number; @@ -137,6 +140,21 @@ export interface ExecutedAttackFlags extends CommonWeaponAttackFlags { distance: number; }; }; + ownerSpecials?: Partial | undefined; + weaponImage: string | null; + weaponName: string; + weaponSystemData: WeaponDataProperties["data"]; +} + +/** The attack chat message flags for a unexecuted attack */ +export type NotExecutedAttackFlags = CommonWeaponAttackFlags & { + executed: false; + reason?: "insufficientAp" | "outOfRange"; +}; + +/** The attack chat message flags for an executed attack */ +export type ExecutedAttackFlags = CommonWeaponAttackFlags & { + executed: true; rolls: { damage: { formula: string; @@ -150,7 +168,7 @@ export interface ExecutedAttackFlags extends CommonWeaponAttackFlags { total: number; }; }; -} +}; export interface ModifierFlags { amount: number; @@ -163,26 +181,10 @@ interface DetailsListingInfo { total: number; } -/** The data for rendering the not executed weapon attack template */ -interface NotExecutedAttackTemplateData extends NotExecutedAttackFlags { +/** The template data common for both executed and not executed attacks. */ +type CommonWeaponAttackTemplateData = CommonWeaponAttackFlags & { template: { keys: { - notExecutedReason: string; - }; - }; -} - -/** The data for rendering the executed weapon attack template */ -interface ExecutedAttackTemplateData extends ExecutedAttackFlags { - template: { - damage: { - results: { - class: string; - value: number; - }[]; - }; - keys: { - hit: string; rangeBracket: string | undefined; }; raw: { @@ -191,4 +193,30 @@ interface ExecutedAttackTemplateData extends ExecutedAttackFlags { subHeading: string; }; }; -} +}; + +/** The data for rendering the not executed weapon attack template */ +type NotExecutedAttackTemplateData = CommonWeaponAttackTemplateData & + NotExecutedAttackFlags & { + template: { + keys: { + notExecutedReason: string; + }; + }; + }; + +/** The data for rendering the executed weapon attack template */ +type ExecutedAttackTemplateData = CommonWeaponAttackTemplateData & + ExecutedAttackFlags & { + template: { + damage: { + results: { + class: string; + value: number; + }[]; + }; + keys: { + hit: string; + }; + }; + }; diff --git a/src/main/typescript/item/weapon/attack.ts b/src/main/typescript/item/weapon/attack.ts index e2ca1ec3..4fbc94cd 100644 --- a/src/main/typescript/item/weapon/attack.ts +++ b/src/main/typescript/item/weapon/attack.ts @@ -70,27 +70,26 @@ export default class Attack { alias }; - // Get range bracket and check bracket ------------------------------------- + // Create common chat message data ----------------------------------------- + const commonData: ChatMessageDataConstructorData = + this.createDefaultMessageData(speaker, options); + + // Get range bracket ------------------------------------------------------- const rangeBracket = ranges.getRangeBracket( this.weapon.systemData.ranges, range, specials ); - if (rangeBracket === ranges.RangeBracket.OUT_OF_RANGE) { - this.createOutOfRangeMessage(speaker, options); - return; - } - // Check AP and subtract in combat ----------------------------------------- - if (actor?.getActiveTokens(true).some((token) => token.inCombat)) { - const currentAp = actor.data.data.vitals.actionPoints.value; - const apUse = this.data.ap; - if (currentAp < apUse) { - this.createNotEnoughApMessage(speaker, options); - return; - } - actor.updateActionPoints(currentAp - apUse); - } + // Calculate damage dice --------------------------------------------------- + const strengthDamageDiceMod = this.getStrengthDamageDiceMod( + specials.strength + ); + const rangeDamageDiceMod = this.getRangeDamageDiceMod(rangeBracket); + const damageDice = this.getDamageDice( + strengthDamageDiceMod, + rangeDamageDiceMod + ); // Calculate hit roll target ----------------------------------------------- const rangeModifier = ranges.getRangeModifier( @@ -105,6 +104,40 @@ export default class Attack { critFailure ); + // Create common attack flags ---------------------------------------------- + const commonFlags: deco.CommonWeaponAttackFlags = + this.createCommonWeaponAttackFlags( + critFailure, + critSuccess, + strengthDamageDiceMod, + rangeDamageDiceMod, + damageDice, + skillTotal, + rangeModifier, + promptHitModifier, + hitTotal, + rangeBracket, + range, + specials + ); + + // Check range ------------------------------------------------------------- + if (rangeBracket === ranges.RangeBracket.OUT_OF_RANGE) { + this.createOutOfRangeMessage(commonData, commonFlags); + return; + } + + // Check AP and subtract in combat ----------------------------------------- + if (actor?.getActiveTokens(true).some((token) => token.inCombat)) { + const currentAp = actor.data.data.vitals.actionPoints.value; + const apUse = this.data.ap; + if (currentAp < apUse) { + this.createNotEnoughApMessage(commonData, commonFlags); + return; + } + actor.updateActionPoints(currentAp - apUse); + } + // Hit roll ---------------------------------------------------------------- const hitRoll = new Roll( Formulator.skill(hitTotal) @@ -112,62 +145,13 @@ export default class Attack { .toString() ).evaluate({ async: false }); - // Calculate damage dice --------------------------------------------------- - const strengthDamageDiceMod = this.getStrengthDamageDiceMod( - specials.strength - ); - const rangeDamageDiceMod = this.getRangeDamageDiceMod(rangeBracket); - const damageDice = this.getDamageDice( - strengthDamageDiceMod, - rangeDamageDiceMod - ); - // Damage roll ------------------------------------------------------------- const damageRoll = new Roll( Formulator.damage(this.data.damage.base, damageDice).toString() ).evaluate({ async: false }); - // Compose details --------------------------------------------------------- - const details: NonNullable = { - criticals: { - failure: critFailure, - success: critSuccess - }, - damage: { - base: { - base: this.data.damage.base, - modifiers: this.getDamageBaseModifierFlags(), - total: this.data.damage.base - }, - dice: { - base: this.data.damage.dice, - modifiers: this.getDamageDiceModifierFlags( - strengthDamageDiceMod, - rangeDamageDiceMod - ), - total: damageDice - } - }, - hit: { - base: skillTotal, - modifiers: this.getHitModifierFlags(rangeModifier, promptHitModifier), - total: hitTotal - }, - range: { - bracket: rangeBracket, - distance: range - } - }; - // Create attack message --------------------------------------------------- - this.createAttackMessage( - speaker, - specials, - details, - hitRoll, - damageRoll, - options - ); + this.createAttackMessage(commonData, commonFlags, hitRoll, damageRoll); } /** Get the system formula representation of the damage of this attack. */ @@ -385,99 +369,134 @@ export default class Attack { } /** Get the default ChatMessage flags for this Weapon Attack. */ - protected get defaultChatMessageFlags(): deco.WeaponAttackFlags { + protected createCommonWeaponAttackFlags( + critFailure: number, + critSuccess: number, + strengthDamageDiceMod: number, + rangeDamageDiceMod: number, + damageDice: number, + skillTotal: number, + rangeModifier: number, + promptHitModifier: number, + hitTotal: number, + rangeBracket: ranges.RangeBracket, + range: number, + ownerSpecials: Partial + ): Required { return { type: "weaponAttack", - weaponName: this.weapon.data.name, - weaponImage: this.weapon.img, - weaponSystemData: this.weapon.systemData, attackName: this.name, - executed: false + details: { + criticals: { + failure: critFailure, + success: critSuccess + }, + damage: { + base: { + base: this.data.damage.base, + modifiers: this.getDamageBaseModifierFlags(), + total: this.data.damage.base + }, + dice: { + base: this.data.damage.dice, + modifiers: this.getDamageDiceModifierFlags( + strengthDamageDiceMod, + rangeDamageDiceMod + ), + total: damageDice + } + }, + hit: { + base: skillTotal, + modifiers: this.getHitModifierFlags(rangeModifier, promptHitModifier), + total: hitTotal + }, + range: { + bracket: rangeBracket, + distance: range + } + }, + ownerSpecials, + weaponImage: this.weapon.img, + weaponName: this.weapon.data.name, + weaponSystemData: this.weapon.systemData }; } /** Create a weapon attack message, signaling out of range. */ protected createOutOfRangeMessage( - speaker: foundry.data.ChatMessageData["speaker"]["_source"], - options?: RollOptions + commonData: ChatMessageDataConstructorData, + commonFlags: deco.CommonWeaponAttackFlags ): void { + const flags: deco.NotExecutedAttackFlags = { + ...commonFlags, + executed: false, + reason: "outOfRange" + }; + ChatMessage.create({ - ...this.createDefaultMessageData(speaker, options), - flags: { - [CONSTANTS.systemId]: { - ...this.defaultChatMessageFlags, - executed: false, - reason: "outOfRange" - } - } + ...commonData, + flags: { [CONSTANTS.systemId]: flags } }); } /** Create a weapon attack message, signaling insufficient AP. */ protected createNotEnoughApMessage( - speaker: foundry.data.ChatMessageData["speaker"]["_source"], - options?: RollOptions + commonData: ChatMessageDataConstructorData, + commonFlags: deco.CommonWeaponAttackFlags ): void { + const flags: deco.NotExecutedAttackFlags = { + ...commonFlags, + executed: false, + reason: "insufficientAp" + }; + ChatMessage.create({ - ...this.createDefaultMessageData(speaker, options), - flags: { - [CONSTANTS.systemId]: { - ...this.defaultChatMessageFlags, - executed: false, - reason: "insufficientAp" - } - } + ...commonData, + flags: { [CONSTANTS.systemId]: flags } }); } /** Create a chat message for an executed attack. */ protected async createAttackMessage( - speaker: foundry.data.ChatMessageData["speaker"]["_source"], - specials: Partial, - details: NonNullable, + commonData: ChatMessageDataConstructorData, + commonFlags: deco.CommonWeaponAttackFlags, hitRoll: Roll, - damageRoll: Roll, - options?: RollOptions + damageRoll: Roll ): Promise { - const defaultData = this.createDefaultMessageData(speaker, options); const actorId = - defaultData.speaker?.actor instanceof WvActor - ? defaultData.speaker.actor.id - : defaultData.speaker?.actor; + commonData.speaker?.actor instanceof WvActor + ? commonData.speaker.actor.id + : commonData.speaker?.actor; await Promise.all([ - diceSoNice(hitRoll, defaultData.whisper ?? null, { actor: actorId }), - diceSoNice(damageRoll, defaultData.whisper ?? null, { actor: actorId }) + diceSoNice(hitRoll, commonData.whisper ?? null, { actor: actorId }), + diceSoNice(damageRoll, commonData.whisper ?? null, { actor: actorId }) ]); - const data: ChatMessageDataConstructorData = { - ...defaultData, - flags: { - [CONSTANTS.systemId]: { - ...this.defaultChatMessageFlags, - executed: true, - ownerSpecials: specials, - details: details, - rolls: { - damage: { - formula: damageRoll.formula, - results: - damageRoll.dice[0]?.results.map((result) => result.result) ?? - [], - total: damageRoll.total ?? this.data.damage.base - }, - hit: { - critical: hitRoll.dice[0]?.results[0]?.critical, - formula: hitRoll.formula, - result: hitRoll.dice[0]?.results[0]?.result ?? 0, - total: hitRoll.total ?? 0 - } - } + const flags: deco.ExecutedAttackFlags = { + ...commonFlags, + executed: true, + rolls: { + damage: { + formula: damageRoll.formula, + results: + damageRoll.dice[0]?.results.map((result) => result.result) ?? [], + total: damageRoll.total ?? this.data.damage.base + }, + hit: { + critical: hitRoll.dice[0]?.results[0]?.critical, + formula: hitRoll.formula, + result: hitRoll.dice[0]?.results[0]?.result ?? 0, + total: hitRoll.total ?? 0 } } }; - ChatMessage.create(data); + ChatMessage.create({ + ...commonData, + flags: { [CONSTANTS.systemId]: flags } + }); } } diff --git a/src/main/typescript/lang.d.ts b/src/main/typescript/lang.d.ts index 96e0881d..0d9c8a52 100644 --- a/src/main/typescript/lang.d.ts +++ b/src/main/typescript/lang.d.ts @@ -562,6 +562,8 @@ export interface LangSchema { ranges: { /** Labels for range bracket names */ brackets: { + /** The name of the out of range range bracket */ + outOfRange: string; /** The name of the long range bracket */ long: string; /** The name of the medium range bracket */