Skip to content

[0.7.x] Actors and Items

Johannes Loher edited this page May 23, 2021 · 1 revision

Using foundry-vtt-types to define Actor and Item classes

Using foundry-vtt-types for your system development allows you to easily get type checking on your actor and item data. After setting up the dev environment, you'll want to start with at least defining the basics of your items, as actors are dependent on item data.

Items

To start off, you'll want to define the data structure of items matching the schema defined in template.json. Each item type will have its own interface. Then each of those gets wrapped into a data interface with a defined type. Once all of the data interfaces are defined, you can then create an Item data Union type. Doing this allows item data to be narrowed by testing the item.data.type parameter.

This is what a basic item data definition could look like:

src/module/item-data.ts

interface WeaponData {
  damage: number;
  hands: string;
};

interface WeaponItemData extends Item.Data<WeaponData> {
  type: "weapon";
}

interface ArmorData {
  reduction: number;
  class: "light" | "medium" | "heavy";
}

interface ArmorItemData extends Item.Data<ArmorData> {
  type: "armor";
}

export type SystemItemData = WeaponItemData | ArmorItemData;

Once the data is set up, the custom item class can be defined. Because of how the data fields are defined, they can be type guarded to narrow the Union.

src/module/item.ts

import { SystemItemData } from "./item-data";

export class SystemItem extends Item<SystemItemData> {
  doWeaponThing(): void {
    if (this.data.type !== "weapon") return;
    console.log(this.data.data.damage);
  }
  doArmorThing(): void {
    if (this.data.type !== "armor") return;
    console.log(this.data.data.reduction);
  }
}

In the above example, trying to access this.data.data.damage in the doArmorThing() method would result in an error being shown by tsserver.

Actors

Once the basics of items are done, actors can be set up. The process is very similar to items. The main difference is that Actors are dependent on Item typing. Start off similarly by defining the data for the different actor types as defined in template.json. Then when defining the ActorData for each actor type, be sure to specify the same system item data that was used for the item class in addition to the actor schema.

src/module/actor-data.ts

import { SystemItemData } from "./item-data.ts";

interface CharData {
  pcOnlyField: string;
  // Schema from template.json
}

interface CharActorData extends Actor.Data<CharData, SystemItemData> {
  type: "character";
}

interface NpcData {
  npcOnlyField: string;
  // Schema from template.json
}

interface NpcActorData extends Actor.Data<NpcData, SystemItemData> {
  type: "npc";
}

export type SysActorData = CharActorData | NpcActorData;

Once the actor data is defined, the class can be created. Once again item information needs to be supplied, this time the custom item class. TypeScript will check to make sure that the item data on the provided actor data and item class are compatible.

src/module/actor.ts

import { SysActorData } from "./actor-data";
import { SystemItem } from "./item";

export class SystemActor extends Actor<SysActorData, SystemItem> {
  async doStuff(): Promise<void> {
    if (this.data.type !== "character") return;
    console.log(this.data.data.pcOnlyField);
  }
  getArmorBonus(): number {
    const armor = this.items.find(i => i.data.type === "armor");
    if (armor === null || armor.data.type !== "armor") return 0; // Second condition is to help typescript infer the type on armor.data
    return armor.data.data.reduction;
  }
}

Different actor data can be narrowed just like with the items.

Getting Foundry to use the new classes

Specifying the new classes to Foundry is pretty much the same as when using pure JavaScript.

src/system.ts

import { SystemActor } from "./module/actor";
import { SystemItem } from "./module/item";

Hooks.once("init", () => {
  CONFIG.Actor.entityClass = SystemActor;
  CONFIG.Item.entityClass = SystemItem;
});