From f038509910ac1df6e05fc90c024ed862a5c871e0 Mon Sep 17 00:00:00 2001 From: Johannes Loher Date: Wed, 24 Mar 2021 09:19:26 +0100 Subject: [PATCH] Add functionality for common checks, which can be affected by effects and be performed as macros --- src/lang/de.json | 30 +++++++++++- src/lang/en.json | 30 +++++++++++- src/module/actor/actor-prepared-data.ts | 8 ++++ src/module/actor/actor.ts | 64 +++++++++++++++++++++++-- src/module/config.ts | 29 +++++++++++ src/module/macros/macros.ts | 4 +- src/module/macros/roll-check.ts | 43 +++++++++++++++++ src/module/macros/roll-item.ts | 1 - 8 files changed, 202 insertions(+), 7 deletions(-) create mode 100644 src/module/macros/roll-check.ts diff --git a/src/lang/de.json b/src/lang/de.json index e19dbadd..1da1dea4 100644 --- a/src/lang/de.json +++ b/src/lang/de.json @@ -198,6 +198,7 @@ "DS4.ErrorCanvasIsNotInitialized": "Canvas ist noch nicht initialisiert.", "DS4.WarningItemMustBeEquippedToBeRolled": "Um für das Item '{name}' ({id}) vom Typ '{type}' zu würfeln, muss es ausgerüstet sein.", "DS4.WarningMustControlActorToUseRollItemMacro": "Um ein ein Item Würfel Makro zu nutzen muss ein Aktor kontolliert werden.", + "DS4.WarningMustControlActorToUseRollCheckMacro": "Um ein ein Proben Würfel Makro zu nutzen muss ein Aktor kontolliert werden.", "DS4.WarningControlledActorDoesNotHaveItem": "Der kontrollierte Aktor '{actorName}' ({actorId}) hat kein Item mit der ID '{itemId}'.", "DS4.WarningItemIsNotRollable": "Für das Item '{name}' ({id}) vom Typ '{type}' kann nicht gewürfelt werden.", "DS4.WarningMacrosCanOnlyBeCreatedForOwnedItems": "Makros können nur für besessene Items angelegt werden.", @@ -235,5 +236,32 @@ "DS4.TooltipModifier": "Modifikator", "DS4.TooltipEffects": "Effekte", "DS4.SettingUseSlayingDiceForAutomatedChecksName": "Slayende Würfel", - "DS4.SettingUseSlayingDiceForAutomatedChecksHint": "Benutze Slayende Würfel bei automatisierten Proben." + "DS4.SettingUseSlayingDiceForAutomatedChecksHint": "Benutze Slayende Würfel bei automatisierten Proben.", + "DS4.ChecksAppraise": "Schätzen", + "DS4.ChangeSpell": "Zauber Wechseln", + "DS4.ChecksClimb": "Klettern", + "DS4.ChecksCommunicate": "Verständigen", + "DS4.ChecksDecipherScript": "Inschrift Entziffern", + "DS4.ChecksDefend": "Abwehren", + "DS4.ChecksDefyPoison": "Gift Trotzen", + "DS4.ChecksDisableTraps": "Fallen Entschärfen", + "DS4.ChecksFeatOfStrength": "Kraftakt", + "DS4.ChecksFlirt": "Flirten", + "DS4.ChecksHaggle": "Feilschen", + "DS4.ChecksHide": "Verbergen", + "DS4.ChecksJump": "Springen", + "DS4.ChecksKnowledge": "Wissen", + "DS4.ChecksOpenLock": "Schlösser Öffnen", + "DS4.ChecksPerception": "Bemerken", + "DS4.ChecksPickPocket": "Tschendiebstahl", + "DS4.ChecksReadTracks": "Spuren Lesen", + "DS4.ChecksResistDisease": "Krankheit Trotzen", + "DS4.ChecksRide": "Reiten", + "DS4.ChecksSearch": "Suchen", + "DS4.ChecksSneak": "Sneak", + "DS4.ChecksStartFire": "Feuer Machen", + "DS4.ChecksSwim": "Schwimmen", + "DS4.ChecksWakeUp": "Erwachen", + "DS4.ChecksWorkMechanism": "Mechanismus Öffnen", + "DS4.ActorCheckFlavor": "{actor} würfelt eine {check} Probe." } diff --git a/src/lang/en.json b/src/lang/en.json index a3ae4f71..f5d456c7 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -198,6 +198,7 @@ "DS4.ErrorCanvasIsNotInitialized": "Canvas is not initialized yet.", "DS4.WarningItemMustBeEquippedToBeRolled": "To roll for item '{name}' ({id}) of type '{type}', it needs to be equipped.", "DS4.WarningMustControlActorToUseRollItemMacro": "You must control an actor to be able to use a roll item macro.", + "DS4.WarningMustControlActorToUseRollCheckMacro": "You must control an actor to be able to use a roll check macro.", "DS4.WarningControlledActorDoesNotHaveItem": "Your controlled actor '{actorName}' ({actorId}) does not have any item with the id '{itemId}'.", "DS4.WarningItemIsNotRollable": "Item '{name}' ({id}) of type '{type}' is not rollable.", "DS4.WarningMacrosCanOnlyBeCreatedForOwnedItems": "Macros can only be created for owned items.", @@ -235,5 +236,32 @@ "DS4.TooltipModifier": "Modifier", "DS4.TooltipEffects": "Effects", "DS4.SettingUseSlayingDiceForAutomatedChecksName": "Slaying Dice", - "DS4.SettingUseSlayingDiceForAutomatedChecksHint": "Use Slaying Dice for automated checks." + "DS4.SettingUseSlayingDiceForAutomatedChecksHint": "Use Slaying Dice for automated checks.", + "DS4.ChecksAppraise": "Appraise", + "DS4.ChangeSpell": "Change Spell", + "DS4.ChecksClimb": "Climb", + "DS4.ChecksCommunicate": "Communicate", + "DS4.ChecksDecipherScript": "Decipher Script", + "DS4.ChecksDefend": "Defend", + "DS4.ChecksDefyPoison": "Defy Poison", + "DS4.ChecksDisableTraps": "Disable Traps", + "DS4.ChecksFeatOfStrength": "Feat of Strength", + "DS4.ChecksFlirt": "Flirt", + "DS4.ChecksHaggle": "Haggle", + "DS4.ChecksHide": "Hide", + "DS4.ChecksJump": "Jump", + "DS4.ChecksKnowledge": "Knowledge", + "DS4.ChecksOpenLock": "Open Lock", + "DS4.ChecksPerception": "Perception", + "DS4.ChecksPickPocket": "Pick Pocket", + "DS4.ChecksReadTracks": "Read Tracks", + "DS4.ChecksResistDisease": "Resist Disease", + "DS4.ChecksRide": "Ride", + "DS4.ChecksSearch": "Search", + "DS4.ChecksSneak": "Sneak", + "DS4.ChecksStartFire": "Start Fire", + "DS4.ChecksSwim": "Swim", + "DS4.ChecksWakeUp": "Wake Up", + "DS4.ChecksWorkMechanism": "Work Mechanism", + "DS4.ActorCheckFlavor": "{actor} rolls a {check} check." } diff --git a/src/module/actor/actor-prepared-data.ts b/src/module/actor/actor-prepared-data.ts index fcbe0139..85080dad 100644 --- a/src/module/actor/actor-prepared-data.ts +++ b/src/module/actor/actor-prepared-data.ts @@ -1,4 +1,5 @@ import { ModifiableDataBaseTotal, ResourceDataBaseTotalMax } from "../common/common-data"; +import { DS4 } from "../config"; import { DS4ActorDataHelper, DS4CharacterDataDataBaseInfo, @@ -21,6 +22,7 @@ interface DS4ActorPreparedDataDataBase { traits: DS4ActorPreparedDataDataTraits; combatValues: DS4ActorPreparedDataDataCombatValues; rolling: DS4ActorPreparedDataDataRolling; + checks: DS4ActorPreparedDataDataChecks; } interface DS4ActorPreparedDataDataAttributes { @@ -54,6 +56,12 @@ interface DS4ActorPreparedDataDataRolling { minimumFumbleResult: number; } +export type Check = keyof typeof DS4.i18n.checks; + +type DS4ActorPreparedDataDataChecks = { + [key in Check]: number; +}; + // types interface DS4CharacterPreparedDataData extends DS4ActorPreparedDataDataBase { diff --git a/src/module/actor/actor.ts b/src/module/actor/actor.ts index a8515444..8123cd5b 100644 --- a/src/module/actor/actor.ts +++ b/src/module/actor/actor.ts @@ -2,8 +2,9 @@ import { ModifiableDataBaseTotal } from "../common/common-data"; import { DS4 } from "../config"; import { DS4Item } from "../item/item"; import { ItemType } from "../item/item-data"; +import { createCheckRoll } from "../rolls/check-factory"; import { DS4ActorData } from "./actor-data"; -import { DS4ActorPreparedData } from "./actor-prepared-data"; +import { Check, DS4ActorPreparedData } from "./actor-prepared-data"; /** * The Actor class for DS4 @@ -108,13 +109,20 @@ export class DS4Actor extends Actor */ prepareDerivedData(): void { this._prepareCombatValues(); + this._prepareChecks(); } /** * The list of properties that are derived from others, given in dot notation. */ get derivedDataProperties(): Array { - return Object.keys(DS4.i18n.combatValues).map((combatValue) => `data.combatValues.${combatValue}.total`); + return Object.keys(DS4.i18n.combatValues) + .map((combatValue) => `data.combatValues.${combatValue}.total`) + .concat( + Object.keys(DS4.i18n.checks) + .filter((check) => check !== "defend") + .map((check) => `data.checks.${check}`), + ); } /** @@ -122,6 +130,7 @@ export class DS4Actor extends Actor */ prepareFinalDerivedData(): void { this.data.data.combatValues.hitPoints.max = this.data.data.combatValues.hitPoints.total; + this.data.data.checks.defend = this.data.data.combatValues.defense.total; } /** @@ -129,7 +138,7 @@ export class DS4Actor extends Actor * given in dot notation. */ get finalDerivedDataProperties(): string[] { - return ["data.combatValues.hitPoints.max"]; + return ["data.combatValues.hitPoints.max", "data.checks.defend"]; } /** @@ -204,6 +213,42 @@ export class DS4Actor extends Actor .reduce((a, b) => a + b, 0); } + /** + * Prepares the check target numbers of checks for the actor. + */ + protected _prepareChecks(): void { + const data = this.data.data; + data.checks = { + appraise: data.attributes.mind.total + data.traits.intellect.total, + changeSpell: data.attributes.mind.total + data.traits.intellect.total, + climb: data.attributes.mobility.total + data.traits.strength.total, + communicate: data.attributes.mind.total + data.traits.dexterity.total, + decipherScript: data.attributes.mind.total + data.traits.intellect.total, + defend: 0, // assigned in prepareFinalDerivedData as it must always match data.combatValues.defense.total and is not changeable by effects + defyPoison: data.attributes.body.total + data.traits.constitution.total, + disableTraps: data.attributes.mind.total + data.traits.dexterity.total, + featOfStrength: data.attributes.body.total + data.traits.strength.total, + flirt: data.attributes.mind.total + data.traits.aura.total, + haggle: data.attributes.mind.total + Math.max(data.traits.intellect.total, data.traits.intellect.total), + hide: data.attributes.mobility.total + data.traits.agility.total, + jump: data.attributes.mobility.total + data.traits.agility.total, + knowledge: data.attributes.mind.total + data.traits.intellect.total, + openLock: data.attributes.mind.total + data.traits.dexterity.total, + perception: Math.max(data.attributes.mind.total + data.traits.intellect.total, 8), + pickPocket: data.attributes.mobility.total + data.traits.dexterity.total, + readTracks: data.attributes.mind.total + data.traits.intellect.total, + resistDisease: data.attributes.body.total + data.traits.constitution.total, + ride: data.attributes.mobility.total + Math.max(data.traits.agility.total, data.traits.aura.total), + search: Math.max(data.attributes.mind.total + data.traits.intellect.total, 8), + sneak: data.attributes.mobility.total + data.traits.agility.total, + startFire: data.attributes.mind.total + data.traits.dexterity.total, + swim: data.attributes.mobility.total + data.traits.strength.total, + wakeUp: data.attributes.mind.total + data.traits.intellect.total, + workMechanism: + data.attributes.mind.total + Math.max(data.traits.dexterity.total, data.traits.intellect.total), + }; + } + /** * Handle how changes to a Token attribute bar are applied to the Actor. * This only differs from the base implementation by also allowing negative values. @@ -226,4 +271,17 @@ export class DS4Actor extends Actor const allowed = Hooks.call("modifyTokenAttribute", { attribute, value, isDelta, isBar }, updates); return allowed !== false ? this.update(updates) : this; } + + /** + * Roll for a given check. + * @param check The check to perform + */ + async rollCheck(check: Check): Promise { + await createCheckRoll(this.data.data.checks[check], { + rollMode: game.settings.get("core", "rollMode") as Const.DiceRollMode, // TODO(types): Type this setting in upstream + maximumCoupResult: this.data.data.rolling.maximumCoupResult, + minimumFumbleResult: this.data.data.rolling.minimumFumbleResult, + flavor: game.i18n.format("DS4.ActorCheckFlavor", { actor: this.name, check: DS4.i18n.checks[check] }), + }); + } } diff --git a/src/module/config.ts b/src/module/config.ts index ead4be51..c2990358 100644 --- a/src/module/config.ts +++ b/src/module/config.ts @@ -281,6 +281,35 @@ export const DS4 = { days: "DS4.UnitDaysAbbr", custom: "DS4.UnitCustomAbbr", }, + + checks: { + appraise: "DS4.ChecksAppraise", + changeSpell: "DS4.ChangeSpell", + climb: "DS4.ChecksClimb", + communicate: "DS4.ChecksCommunicate", + decipherScript: "DS4.ChecksDecipherScript", + defend: "DS4.ChecksDefend", + defyPoison: "DS4.ChecksDefyPoison", + disableTraps: "DS4.ChecksDisableTraps", + featOfStrength: "DS4.ChecksFeatOfStrength", + flirt: "DS4.ChecksFlirt", + haggle: "DS4.ChecksHaggle", + hide: "DS4.ChecksHide", + jump: "DS4.ChecksJump", + knowledge: "DS4.ChecksKnowledge", + openLock: "DS4.ChecksOpenLock", + perception: "DS4.ChecksPerception", + pickPocket: "DS4.ChecksPickPocket", + readTracks: "DS4.ChecksReadTracks", + resistDisease: "DS4.ChecksResistDisease", + ride: "DS4.ChecksRide", + search: "DS4.ChecksSearch", + sneak: "DS4.ChecksSneak", + startFire: "DS4.ChecksStartFire", + swim: "DS4.ChecksSwim", + wakeUp: "DS4.ChecksWakeUp", + workMechanism: "DS4.ChecksWorkMechanism", + }, }, /** diff --git a/src/module/macros/macros.ts b/src/module/macros/macros.ts index 95b47854..e2a43e9b 100644 --- a/src/module/macros/macros.ts +++ b/src/module/macros/macros.ts @@ -1,5 +1,7 @@ +import { rollCheck } from "./roll-check"; import { rollItem } from "./roll-item"; export const macros = { - rollItem: rollItem, + rollCheck, + rollItem, }; diff --git a/src/module/macros/roll-check.ts b/src/module/macros/roll-check.ts new file mode 100644 index 00000000..93bcd400 --- /dev/null +++ b/src/module/macros/roll-check.ts @@ -0,0 +1,43 @@ +import { DS4Actor } from "../actor/actor"; +import { Check } from "../actor/actor-prepared-data"; +import { DS4 } from "../config"; +import { getCanvas } from "../helpers"; +import notifications from "../ui/notifications"; + +/** + * Creates a macro from a check drop. + * Get an existing roll check macro if one exists, otherwise create a new one. + * @param check - The name of the check to perform. + * @param slot - The hotbar slot to use. + */ +export async function createRollCheckMacro(check: Check, slot: string): Promise { + const command = `game.ds4.macros.rollCheck("${check}");`; + const macro = + game.macros?.entities.find((m) => m.name === DS4.i18n.checks[check] && m.data.command === command) ?? + (await Macro.create( + { + command, + name: DS4.i18n.checks[check], + type: "script", + // TODO: img, should be addressed in https://git.f3l.de/dungeonslayers/ds4/-/issues/79 + flags: { "ds4.checkMacro": true }, + }, + { displaySheet: false }, + )); + game.user?.assignHotbarMacro(macro, slot); +} + +/** + * Executes the roll check macro for the given check. + */ +export async function rollCheck(check: Check): Promise { + const speaker = ChatMessage.getSpeaker(); + const actor = (getCanvas().tokens.get(speaker.token ?? "")?.actor ?? game.actors?.get(speaker.actor ?? "")) as + | DS4Actor + | null + | undefined; + if (!actor) { + return notifications.warn(game.i18n.localize("DS4.WarningMustControlActorToUseRollCheckMacro")); + } + return actor.rollCheck(check); +} diff --git a/src/module/macros/roll-item.ts b/src/module/macros/roll-item.ts index 9d8af10e..3dac24b1 100644 --- a/src/module/macros/roll-item.ts +++ b/src/module/macros/roll-item.ts @@ -28,7 +28,6 @@ export async function createRollItemMacro(itemData: DS4ItemData, slot: string): /** * Executes the roll item macro for the given itemId. - * @param itemId */ export async function rollItem(itemId: string): Promise { const speaker = ChatMessage.getSpeaker();