From 3d272f2b92c5b4c561ee53a80a4a2574fd22ca90 Mon Sep 17 00:00:00 2001 From: Johannes Loher Date: Thu, 4 Mar 2021 00:14:16 +0100 Subject: [PATCH] Make weapons rollable from the character sheet --- src/lang/de.json | 13 ++- src/lang/en.json | 9 +- src/module/actor/sheets/actor-sheet.ts | 17 +--- src/module/ds4.ts | 34 +------- src/module/handlebars-helpers.ts | 10 --- src/module/handlebars/handlebars-helpers.ts | 12 +++ src/module/handlebars/handlebars-partials.ts | 26 ++++++ src/module/item/item-data.ts | 5 +- src/module/item/item.ts | 82 ++++++++++++++++++- src/module/rolls/check-factory.ts | 4 +- src/scss/components/_item_list.scss | 5 ++ .../actor/partials/item-list-entry.hbs | 3 +- src/templates/common/simple-select-form.hbs | 19 +++++ 13 files changed, 173 insertions(+), 66 deletions(-) delete mode 100644 src/module/handlebars-helpers.ts create mode 100644 src/module/handlebars/handlebars-helpers.ts create mode 100644 src/module/handlebars/handlebars-partials.ts create mode 100644 src/templates/common/simple-select-form.hbs diff --git a/src/lang/de.json b/src/lang/de.json index 3180073..cbe1af5 100644 --- a/src/lang/de.json +++ b/src/lang/de.json @@ -15,8 +15,9 @@ "DS4.HeadingSpells": "Zaubersprüche", "DS4.HeadingDescription": "Beschreibung", "DS4.HeadingSpecialCreatureAbilities": "Besondere Fähigkeiten", - "DS4.AttackType": "Angriffstyp", - "DS4.AttackTypeAbbr": "AT", + "DS4.AttackType": "Angriffsart", + "DS4.AttackTypeAbbr": "AA", + "DS4.AttackTypeSelection": "Welche Angriffsart?", "DS4.WeaponBonus": "Waffenbonus", "DS4.WeaponBonusAbbr": "WB", "DS4.OpponentDefense": "Gegnerabwehr", @@ -184,6 +185,10 @@ "DS4.ErrorDiceCritOverlap": "Es gibt eine Überlappung zwischen Patzern und Immersiegen.", "DS4.ErrorExplodingRecursionLimitExceeded": "Die maximale Rekursionstiefe für slayende Würfelwürfe wurde überschritten.", "DS4.ErrorDuringMigration": "Fehler während der Aktualisierung des DS4 Systems von Migrationsversion {currentVersion} auf {targetVersion}. Der Fehler trat während der Ausführung des Migrationsskripts mit der Version {migrationVersion} auf. Spätere Migrationsskripte wurden nicht ausgeführt. Mehr Details finden Sie in der Entwicklerkonsole (F12).", + "DS4.ErrorCannotRollUnownedItem": "Für das Item '{name}' ({id}) kann nicht gewürfelt werden, da es keinem Aktor gehört.", + "DS4.ErrorRollingForItemTypeNotPossible": "Würfeln ist für items vom Typ '{type}' nicht möglich.", + "DS4.ErrorWrongItemType": "Ein Item vom Type '{expectedType}' wurde erwartet aber das Item '{name}' ({id}) ist vom Typ '{actualType}'.", + "DS4.ErrorUnexpectedAttackType": "Unerwartete Angriffsart '{actualType}', erwartete Angriffarten: {expectedTypes}", "DS4.InfoSystemUpdateStart": "Aktualisiere DS4 System von Migrationsversion {currentVersion} auf {targetVersion}. Bitte haben Sie etwas Geduld, schließen Sie nicht das Spiel und fahren Sie nicht den Server herunter.", "DS4.InfoSystemUpdateCompleted": "Aktualisierung des DS4 Systems von Migrationsversion {currentVersion} auf {targetVersion} erfolgreich!", "DS4.UnitRounds": "Runden", @@ -200,9 +205,9 @@ "DS4.UnitKilometersAbbr": "km", "DS4.UnitCustom": "individuell", "DS4.UnitCustomAbbr": " ", + "DS4.GenericOkButton": "OK", + "DS4.GenericCancelButton": "Abbrechen", "DS4.RollDialogDefaultTitle": "Proben-Optionen", - "DS4.RollDialogOkButton": "OK", - "DS4.RollDialogCancelButton": "Abbrechen", "DS4.ErrorUnexpectedHtmlType": "Typfehler: Erwartet wurde '{exType}', tatsächlich erhalten wurde '{realType}'.", "DS4.ErrorCouldNotFindForm": "Konnte HTML Element '{htmlElement}' nicht finden.", "DS4.ErrorActorDoesNotHaveItem": "Der Aktor '{actor}' hat kein Item mit der ID '{id}'.", diff --git a/src/lang/en.json b/src/lang/en.json index 0894350..f1436c4 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -17,6 +17,7 @@ "DS4.HeadingSpecialCreatureAbilities": "Special Abilities", "DS4.AttackType": "Attack Type", "DS4.AttackTypeAbbr": "AT", + "DS4.AttackTypeSelection": "Which Attack Type?", "DS4.WeaponBonus": "Weapon Bonus", "DS4.WeaponBonusAbbr": "WB", "DS4.OpponentDefense": "Opponent Defense", @@ -184,6 +185,10 @@ "DS4.ErrorDiceCritOverlap": "There's an overlap between Fumbles and Coups", "DS4.ErrorExplodingRecursionLimitExceeded": "Maximum recursion depth for exploding dice roll exceeded", "DS4.ErrorDuringMigration": "Error while migrating DS4 system from migration version {currentVersion} to {targetVersion}. The error occurred during execution of migration script with version {migrationVersion}. Later migrations have not been executed. For more details, please look at the development console (F12).", + "DS4.ErrorCannotRollUnownedItem": "Rolling for item '{name}' ({id})is not possible because it is not owned.", + "DS4.ErrorRollingForItemTypeNotPossible": "Rolling is not possible for items of type '{type}'.", + "DS4.ErrorWrongItemType": "Expected an item of type '{expectedType}' but item '{name}' ({id}) is of type '{actualType}'.", + "DS4.ErrorUnexpectedAttackType": "Unexpected attack type '{actualType}', expected it to be one of: {expectedTypes}", "DS4.InfoSystemUpdateStart": "Migrating DS4 system from migration version {currentVersion} to {targetVersion}. Please be patient and do not close your game or shut down your server.", "DS4.InfoSystemUpdateCompleted": "Migration of DS4 system from migration version {currentVersion} to {targetVersion} successful!", "DS4.UnitRounds": "Rounds", @@ -200,9 +205,9 @@ "DS4.UnitKilometersAbbr": "km", "DS4.UnitCustom": "Custom Unit", "DS4.UnitCustomAbbr": " ", + "DS4.GenericOkButton": "Ok", + "DS4.GenericCancelButton": "Cancel", "DS4.RollDialogDefaultTitle": "Roll Options", - "DS4.RollDialogOkButton": "Ok", - "DS4.RollDialogCancelButton": "Cancel", "DS4.ErrorUnexpectedHtmlType": "Type Error: Expected '{exType}' but got '{realType}'.", "DS4.ErrorCouldNotFindForm": "Could not find HTML element '{htmlElement}'.", "DS4.ErrorActorDoesNotHaveItem": "The actor '{actor}' does not have any item with the id '{id}'.", diff --git a/src/module/actor/sheets/actor-sheet.ts b/src/module/actor/sheets/actor-sheet.ts index aee139f..e75f1be 100644 --- a/src/module/actor/sheets/actor-sheet.ts +++ b/src/module/actor/sheets/actor-sheet.ts @@ -124,8 +124,7 @@ export class DS4ActorSheet extends ActorSheet> { html.find(".item-change").on("change", this._onItemChange.bind(this)); - // Rollable abilities. - html.find(".rollable").click(this._onRoll.bind(this)); + html.find(".rollable-item").on("click", this._onRoll.bind(this)); } /** @@ -239,17 +238,9 @@ export class DS4ActorSheet extends ActorSheet> { */ protected _onRoll(event: JQuery.ClickEvent): void { event.preventDefault(); - const element = event.currentTarget; - const dataset = element.dataset; - - if (dataset.roll) { - const roll = new Roll(dataset.roll, this.actor.data.data); - const label = dataset.label ? `Rolling ${dataset.label}` : ""; - roll.roll().toMessage({ - speaker: ChatMessage.getSpeaker({ actor: this.actor }), - flavor: label, - }); - } + const id = $(event.currentTarget).parents(".item").data("itemId"); + const item = this.actor.getOwnedItem(id); + item.roll(); } /** @override */ diff --git a/src/module/ds4.ts b/src/module/ds4.ts index c8243f4..67d0372 100644 --- a/src/module/ds4.ts +++ b/src/module/ds4.ts @@ -8,7 +8,8 @@ import { DS4CreatureActorSheet } from "./actor/sheets/creature-sheet"; import { createCheckRoll } from "./rolls/check-factory"; import { registerSystemSettings } from "./settings"; import { migration } from "./migrations"; -import handlebarsHelpers from "./handlebars-helpers"; +import registerHandlebarsHelpers from "./handlebars/handlebars-helpers"; +import registerHandlebarsPartials from "./handlebars/handlebars-partials"; Hooks.once("init", async () => { console.log(`DS4 | Initializing the DS4 Game System\n${DS4.ASCII}`); @@ -44,37 +45,6 @@ Hooks.once("init", async () => { registerHandlebarsHelpers(); }); -async function registerHandlebarsPartials() { - const templatePaths = [ - "systems/ds4/templates/item/partials/sheet-header.hbs", - "systems/ds4/templates/item/partials/description.hbs", - "systems/ds4/templates/item/partials/details.hbs", - "systems/ds4/templates/item/partials/effects.hbs", - "systems/ds4/templates/item/partials/body.hbs", - "systems/ds4/templates/actor/partials/items-overview.hbs", - "systems/ds4/templates/actor/partials/talents-abilities-overview.hbs", - "systems/ds4/templates/actor/partials/spells-overview.hbs", - "systems/ds4/templates/actor/partials/overview-add-button.hbs", - "systems/ds4/templates/actor/partials/overview-control-buttons.hbs", - "systems/ds4/templates/actor/partials/attributes-traits.hbs", - "systems/ds4/templates/actor/partials/combat-values.hbs", - "systems/ds4/templates/actor/partials/profile.hbs", - "systems/ds4/templates/actor/partials/character-progression.hbs", - "systems/ds4/templates/actor/partials/special-creature-abilities-overview.hbs", - "systems/ds4/templates/actor/partials/character-inventory.hbs", - "systems/ds4/templates/actor/partials/creature-inventory.hbs", - "systems/ds4/templates/actor/partials/talent-rank-equation.hbs", - "systems/ds4/templates/actor/partials/item-list-header.hbs", - "systems/ds4/templates/actor/partials/item-list-entry.hbs", - "systems/ds4/templates/actor/partials/currency.hbs", - ]; - return loadTemplates(templatePaths); -} - -function registerHandlebarsHelpers() { - Object.entries(handlebarsHelpers).forEach(([key, helper]) => Handlebars.registerHelper(key, helper)); -} - /** * This function runs after game data has been requested and loaded from the servers, so entities exist */ diff --git a/src/module/handlebars-helpers.ts b/src/module/handlebars-helpers.ts deleted file mode 100644 index 95144da..0000000 --- a/src/module/handlebars-helpers.ts +++ /dev/null @@ -1,10 +0,0 @@ -export default { htmlToPlainText, isEmpty }; - -function htmlToPlainText(input: string | null | undefined): string | null | undefined { - if (!input) return; - return $(input).text(); -} - -function isEmpty(input: Array | null | undefined): boolean { - return (input?.length ?? 0) === 0; -} diff --git a/src/module/handlebars/handlebars-helpers.ts b/src/module/handlebars/handlebars-helpers.ts new file mode 100644 index 0000000..fef9268 --- /dev/null +++ b/src/module/handlebars/handlebars-helpers.ts @@ -0,0 +1,12 @@ +export default function registerHandlebarsHelpers(): void { + Object.entries(helpers).forEach(([key, helper]) => Handlebars.registerHelper(key, helper)); +} + +const helpers = { + htmlToPlainText: (input: string | null | undefined): string | null | undefined => { + if (!input) return; + return $(input).text(); + }, + + isEmpty: (input: Array | null | undefined): boolean => (input?.length ?? 0) === 0, +}; diff --git a/src/module/handlebars/handlebars-partials.ts b/src/module/handlebars/handlebars-partials.ts new file mode 100644 index 0000000..98248c3 --- /dev/null +++ b/src/module/handlebars/handlebars-partials.ts @@ -0,0 +1,26 @@ +export default async function registerHandlebarsPartials(): Promise { + const templatePaths = [ + "systems/ds4/templates/item/partials/sheet-header.hbs", + "systems/ds4/templates/item/partials/description.hbs", + "systems/ds4/templates/item/partials/details.hbs", + "systems/ds4/templates/item/partials/effects.hbs", + "systems/ds4/templates/item/partials/body.hbs", + "systems/ds4/templates/actor/partials/items-overview.hbs", + "systems/ds4/templates/actor/partials/talents-abilities-overview.hbs", + "systems/ds4/templates/actor/partials/spells-overview.hbs", + "systems/ds4/templates/actor/partials/overview-add-button.hbs", + "systems/ds4/templates/actor/partials/overview-control-buttons.hbs", + "systems/ds4/templates/actor/partials/attributes-traits.hbs", + "systems/ds4/templates/actor/partials/combat-values.hbs", + "systems/ds4/templates/actor/partials/profile.hbs", + "systems/ds4/templates/actor/partials/character-progression.hbs", + "systems/ds4/templates/actor/partials/special-creature-abilities-overview.hbs", + "systems/ds4/templates/actor/partials/character-inventory.hbs", + "systems/ds4/templates/actor/partials/creature-inventory.hbs", + "systems/ds4/templates/actor/partials/talent-rank-equation.hbs", + "systems/ds4/templates/actor/partials/item-list-header.hbs", + "systems/ds4/templates/actor/partials/item-list-entry.hbs", + "systems/ds4/templates/actor/partials/currency.hbs", + ]; + await loadTemplates(templatePaths); +} diff --git a/src/module/item/item-data.ts b/src/module/item/item-data.ts index 5afd83b..c3cf9a9 100644 --- a/src/module/item/item-data.ts +++ b/src/module/item/item-data.ts @@ -32,10 +32,13 @@ type DS4LanguageData = DS4ItemDataHelper; type DS4AlphabetData = DS4ItemDataHelper; type DS4SpecialCreatureAbilityData = DS4ItemDataHelper; +export type AttackType = keyof typeof DS4["i18n"]["attackTypes"]; + interface DS4WeaponDataData extends DS4ItemDataDataBase, DS4ItemDataDataPhysical, DS4ItemDataDataEquipable { - attackType: "melee" | "ranged" | "meleeRanged"; + attackType: AttackType; weaponBonus: number; opponentDefense: number; + rollable?: boolean; } interface DS4ArmorDataData diff --git a/src/module/item/item.ts b/src/module/item/item.ts index 5c0a9f3..cbea743 100644 --- a/src/module/item/item.ts +++ b/src/module/item/item.ts @@ -1,4 +1,7 @@ -import { DS4ItemData } from "./item-data"; +import { DS4Actor } from "../actor/actor"; +import { DS4 } from "../config"; +import { createCheckRoll } from "../rolls/check-factory"; +import { AttackType, DS4ItemData } from "./item-data"; /** * The Item class for DS4 @@ -17,6 +20,9 @@ export class DS4Item extends Item { const data = this.data.data; data.rank.total = data.rank.base + data.rank.mod; } + if (this.data.type === "weapon") { + this.data.data.rollable = true; + } } isNonEquippedEuipable(): boolean { @@ -32,4 +38,78 @@ export class DS4Item extends Item { } return 1; } + + /** + * Roll a check for a action with this item. + */ + async roll(): Promise { + if (!this.isOwnedItem()) { + throw new Error(game.i18n.format("DS4.ErrorCannotRollUnownedItem", { name: this.name, id: this.id })); + } + if (this.data.type === "weapon") { + await this.rollWeapon(); + } else { + throw new Error(game.i18n.format("DS4.ErrorRollingForItemTypeNotPossible", { type: this.data.type })); + } + } + + private async rollWeapon(this: this & { readonly isOwned: true }): Promise { + if (!(this.data.type === "weapon")) { + throw new Error( + game.i18n.format("DS4.ErrorWrongItemType", { + actualType: this.data.type, + expectedType: "weapon", + id: this.id, + name: this.name, + }), + ); + } + + const owner = (this.actor as unknown) as DS4Actor; // TODO(types): Improve so that the concrete Actor type is known here + const weaponBonus = this.data.data.weaponBonus; + const combatValue = await this.getCombatValueKeyForAttackType(this.data.data.attackType); + const checkTargetValue = (owner.data.data.combatValues[combatValue].total as number) + weaponBonus; + await createCheckRoll(checkTargetValue, { rollMode: game.settings.get("core", "rollMode") }); // TODO: Get maxCritSuccess and minCritFailure from Actor once we store them there + } + + private async getCombatValueKeyForAttackType(attackType: AttackType): Promise<"meleeAttack" | "rangedAttack"> { + if (attackType === "meleeRanged") { + const { melee, ranged } = { ...DS4.i18n.attackTypes }; + const identifier = "attack-type-selection"; + const label = game.i18n.localize("DS4.AttackType"); + const answer = Dialog.prompt({ + title: game.i18n.localize("DS4.AttackTypeSelection"), + content: await renderTemplate("systems/ds4/templates/common/simple-select-form.hbs", { + label, + identifier, + options: { melee, ranged }, + }), + label: game.i18n.localize("DS4.GenericOkButton"), + callback: (html) => { + const selectedAttackType = html.find(`#${identifier}`).val(); + if (selectedAttackType !== "melee" && selectedAttackType !== "ranged") { + throw new Error( + game.i18n.format("DS4.ErrorUnexpectedAttackType", { + actualType: selectedAttackType, + expectedTypes: "'melee', 'ranged'", + }), + ); + } + return `${selectedAttackType}Attack` as const; + }, + render: () => undefined, // TODO(types): This is actually optional, remove when types are updated ) + options: { jQuery: true }, + }); + return answer; + } else { + return `${attackType}Attack` as const; + } + } + + /** + * Type-guarding variant to check if the item is owned. + */ + isOwnedItem(): this is this & { readonly isOwned: true } { + return this.isOwned; + } } diff --git a/src/module/rolls/check-factory.ts b/src/module/rolls/check-factory.ts index eb68789..2b3556b 100644 --- a/src/module/rolls/check-factory.ts +++ b/src/module/rolls/check-factory.ts @@ -138,7 +138,7 @@ async function askGmModifier( buttons: { ok: { icon: '', - label: game.i18n.localize("DS4.RollDialogOkButton"), + label: game.i18n.localize("DS4.GenericOkButton"), callback: (html) => { if (!("jquery" in html)) { throw new Error( @@ -160,7 +160,7 @@ async function askGmModifier( }, cancel: { icon: '', - label: game.i18n.localize("DS4.RollDialogCancelButton"), + label: game.i18n.localize("DS4.GenericCancelButton"), }, }, default: "ok", diff --git a/src/scss/components/_item_list.scss b/src/scss/components/_item_list.scss index afebd6c..280a468 100644 --- a/src/scss/components/_item_list.scss +++ b/src/scss/components/_item_list.scss @@ -60,6 +60,11 @@ background-position: center; background-repeat: no-repeat; background-size: 100%; + + &--rollable:hover { + background-image: url("../../../icons/svg/d20-black.svg") !important; + cursor: pointer; + } } &__editable { diff --git a/src/templates/actor/partials/item-list-entry.hbs b/src/templates/actor/partials/item-list-entry.hbs index f74093b..46caecf 100644 --- a/src/templates/actor/partials/item-list-entry.hbs +++ b/src/templates/actor/partials/item-list-entry.hbs @@ -17,7 +17,8 @@ {{/if}} {{!-- image --}} -
+
{{!-- amount --}} {{#if hasQuantity}} diff --git a/src/templates/common/simple-select-form.hbs b/src/templates/common/simple-select-form.hbs new file mode 100644 index 0000000..dc82f31 --- /dev/null +++ b/src/templates/common/simple-select-form.hbs @@ -0,0 +1,19 @@ +{{!-- +!-- Render a simple form with a single select element. It uses the default form classes of Foundry VTT. +!-- @param identifier: The identifier to use as id for the select element. Can be used to query the value later on. +!-- @param label: Text to display as the label for the select element. +!-- @param options: Key-value pairs that describe the options. The keys are used for the value attribute of the +options, the values are used as content. +--}} +
+
+ +
+ +
+
+