Make weapons rollable from the character sheet

This commit is contained in:
Johannes Loher 2021-03-04 00:14:16 +01:00
parent c1c0f41743
commit 3d272f2b92
13 changed files with 173 additions and 66 deletions

View file

@ -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}'.",

View file

@ -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}'.",

View file

@ -124,8 +124,7 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Data<DS4Actor>> {
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<ActorSheet.Data<DS4Actor>> {
*/
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 */

View file

@ -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
*/

View file

@ -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<unknown> | null | undefined): boolean {
return (input?.length ?? 0) === 0;
}

View file

@ -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<unknown> | null | undefined): boolean => (input?.length ?? 0) === 0,
};

View file

@ -0,0 +1,26 @@
export default async function registerHandlebarsPartials(): Promise<void> {
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);
}

View file

@ -32,10 +32,13 @@ type DS4LanguageData = DS4ItemDataHelper<DS4LanguageDataData, "language">;
type DS4AlphabetData = DS4ItemDataHelper<DS4AlphabetDataData, "alphabet">;
type DS4SpecialCreatureAbilityData = DS4ItemDataHelper<DS4SpecialCreatureAbilityDataData, "specialCreatureAbility">;
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

View file

@ -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<DS4ItemData> {
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<DS4ItemData> {
}
return 1;
}
/**
* Roll a check for a action with this item.
*/
async roll(): Promise<void> {
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<void> {
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;
}
}

View file

@ -138,7 +138,7 @@ async function askGmModifier(
buttons: {
ok: {
icon: '<i class="fas fa-check"></i>',
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: '<i class="fas fa-times"></i>',
label: game.i18n.localize("DS4.RollDialogCancelButton"),
label: game.i18n.localize("DS4.GenericCancelButton"),
},
},
default: "ok",

View file

@ -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 {

View file

@ -17,7 +17,8 @@
{{/if}}
{{!-- image --}}
<div class="ds4-item-list__image" style="background-image: url('{{itemData.img}}')" title="{{itemData.name}}"></div>
<div class="ds4-item-list__image{{#if itemData.data.rollable}} ds4-item-list__image--rollable rollable-item{{/if}}"
style="background-image: url('{{itemData.img}}')" title="{{itemData.name}}"></div>
{{!-- amount --}}
{{#if hasQuantity}}

View file

@ -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.
--}}
<form class="ds4-simple-form">
<div class="form-group">
<label for="{{identifier}}">{{label}}</label>
<div class="form-fields">
<select name="{{identifier}}" id="{{identifier}}">
{{#each options as | value key |}}
<option value="{{key}}">{{value}}</option>
{{/each}}
</select>
</div>
</div>
</form>