Merge branch '068-enable-strict-mode' into 050-basic-active-effects

This commit is contained in:
Johannes Loher 2021-02-08 03:24:36 +01:00
commit b74ee5ec7c
45 changed files with 3088 additions and 2119 deletions

View file

@ -1,20 +1,31 @@
import { ModifiableData, ResourceData, UsableResource } from "../common/common-data";
import { DS4 } from "../config";
import { DS4ItemData } from "../item/item-data";
export type DS4ActorDataType = DS4ActorDataCharacter | DS4ActorDataCreature;
export type DS4ActorData = DS4CharacterData | DS4CreatureData;
interface DS4ActorDataBase {
attributes: DS4ActorDataAttributes;
traits: DS4ActorDataTraits;
combatValues: DS4ActorDataCombatValues;
type ActorType = keyof typeof DS4.i18n.actorTypes;
interface DS4ActorDataHelper<T, U extends ActorType> extends Actor.Data<T, DS4ItemData> {
type: U;
}
interface DS4ActorDataAttributes {
type DS4CharacterData = DS4ActorDataHelper<DS4CharacterDataData, "character">;
type DS4CreatureData = DS4ActorDataHelper<DS4CreatureDataData, "creature">;
interface DS4ActorDataDataBase {
attributes: DS4ActorDataDataAttributes;
traits: DS4ActorDataDataTraits;
combatValues: DS4ActorDataDataCombatValues;
}
interface DS4ActorDataDataAttributes {
body: ModifiableData<number>;
mobility: ModifiableData<number>;
mind: ModifiableData<number>;
}
interface DS4ActorDataTraits {
interface DS4ActorDataDataTraits {
strength: ModifiableData<number>;
constitution: ModifiableData<number>;
agility: ModifiableData<number>;
@ -23,7 +34,7 @@ interface DS4ActorDataTraits {
aura: ModifiableData<number>;
}
interface DS4ActorDataCombatValues {
interface DS4ActorDataDataCombatValues {
hitPoints: ResourceData<number>;
defense: ModifiableData<number>;
initiative: ModifiableData<number>;
@ -34,34 +45,32 @@ interface DS4ActorDataCombatValues {
targetedSpellcasting: ModifiableData<number>;
}
interface DS4ActorDataCharacter extends DS4ActorDataBase {
baseInfo: DS4ActorDataCharacterBaseInfo;
progression: DS4ActorDataCharacterProgression;
language: DS4ActorDataCharacterLanguage;
profile: DS4ActorDataCharacterProfile;
currency: DS4ActorDataCharacterCurrency;
interface DS4CharacterDataData extends DS4ActorDataDataBase {
baseInfo: DS4CharacterDataDataBaseInfo;
progression: DS4CharacterDataDataProgression;
language: DS4CharacterDataDataLanguage;
profile: DS4CharacterDataDataProfile;
currency: DS4CharacterDataDataCurrency;
}
interface DS4ActorDataCharacterBaseInfo {
interface DS4CharacterDataDataBaseInfo {
race: string;
class: string;
heroClass: string;
culture: string;
}
interface DS4ActorDataCharacterProgression {
interface DS4CharacterDataDataProgression {
level: number;
experiencePoints: number;
talentPoints: UsableResource<number>;
progressPoints: UsableResource<number>;
}
interface DS4ActorDataCharacterLanguage {
interface DS4CharacterDataDataLanguage {
languages: string;
alphabets: string;
}
interface DS4ActorDataCharacterProfile {
interface DS4CharacterDataDataProfile {
biography: string;
gender: string;
birthday: string;
@ -74,21 +83,21 @@ interface DS4ActorDataCharacterProfile {
specialCharacteristics: string;
}
interface DS4ActorDataCharacterCurrency {
interface DS4CharacterDataDataCurrency {
gold: number;
silver: number;
copper: number;
}
interface DS4ActorDataCreature extends DS4ActorDataBase {
baseInfo: DS4ActorDataCreatureBaseInfo;
interface DS4CreatureDataData extends DS4ActorDataDataBase {
baseInfo: DS4CreatureDataDataBaseInfo;
}
type CreatureType = "animal" | "construct" | "humanoid" | "magicalEntity" | "plantBeing" | "undead";
type SizeCategory = "tiny" | "small" | "normal" | "large" | "huge" | "colossal";
interface DS4ActorDataCreatureBaseInfo {
interface DS4CreatureDataDataBaseInfo {
loot: string;
foeFactor: number;
creatureType: CreatureType;

View file

@ -1,15 +1,16 @@
import { ModifiableData } from "../common/common-data";
import { DS4 } from "../config";
import { DS4Item } from "../item/item";
import { DS4Armor, DS4EquippableItemDataType, DS4ItemDataType, DS4Shield, ItemType } from "../item/item-data";
import { DS4ActorDataType } from "./actor-data";
import { DS4ItemData, ItemType } from "../item/item-data";
import { DS4ActorData } from "./actor-data";
type DS4ActiveEffect = ActiveEffect<DS4ActorDataType, DS4ItemDataType, DS4Actor, DS4Item>;
export class DS4Actor extends Actor<DS4ActorDataType, DS4ItemDataType, DS4Item> {
/**
* The Actor class for DS4
*/
export class DS4Actor extends Actor<DS4ActorData, DS4Item> {
/** @override */
prepareData(): void {
this.data = duplicate(this._data);
this.data = duplicate(this._data) as DS4ActorData;
if (!this.data.img) this.data.img = CONST.DEFAULT_TOKEN;
if (!this.data.name) this.data.name = "New " + this.entity;
this.prepareBaseData();
@ -45,20 +46,23 @@ export class DS4Actor extends Actor<DS4ActorDataType, DS4ItemDataType, DS4Item>
* @param predicate The predicate that ActiveEffectChanges need to satisfy in order to be applied
*/
applyActiveEffectsFiltered(predicate: (change: ActiveEffectChange) => boolean): void {
const overrides = {};
const overrides: Record<string, unknown> = {};
// Organize non-disabled effects by their application priority
const changes = this.effects.reduce((changes: Array<ActiveEffectChange & { effect: DS4ActiveEffect }>, e) => {
if (e.data.disabled) return changes;
const changes = this.effects.reduce(
(changes: Array<ActiveEffectChange & { effect: ActiveEffect<DS4Actor> }>, e) => {
if (e.data.disabled) return changes;
return changes.concat(
e.data.changes.filter(predicate).map((c) => {
c = duplicate(c);
c.priority = c.priority ?? c.mode * 10;
return { ...c, effect: e };
}),
);
}, []);
return changes.concat(
e.data.changes.filter(predicate).map((c) => {
const duplicatedChange = duplicate(c) as ActiveEffect.Change;
duplicatedChange.priority = duplicatedChange.priority ?? duplicatedChange.mode * 10;
return { ...duplicatedChange, effect: e };
}),
);
},
[],
);
changes.sort((a, b) => a.priority - b.priority);
// Apply all changes
@ -68,7 +72,7 @@ export class DS4Actor extends Actor<DS4ActorDataType, DS4ItemDataType, DS4Item>
}
// Expand the set of final overrides
this["overrides"] = expandObject({ ...flattenObject(this["overrides"] ?? {}), ...overrides });
this.overrides = expandObject({ ...flattenObject(this.overrides ?? {}), ...overrides });
}
/** @override */
@ -78,7 +82,7 @@ export class DS4Actor extends Actor<DS4ActorDataType, DS4ItemDataType, DS4Item>
/** The list of properties that are derived from others, given in dot notation */
get derivedDataProperties(): Array<string> {
return Object.keys(DS4.combatValues)
return Object.keys(DS4.i18n.combatValues)
.map((combatValue) => `data.combatValues.${combatValue}.total`)
.concat("data.combatValues.hitPoints.max");
}
@ -110,7 +114,7 @@ export class DS4Actor extends Actor<DS4ActorDataType, DS4ItemDataType, DS4Item>
/**
* Checks whether or not the given item type can be owned by the actor.
* @param itemType the item type to check
* @param itemType - The item type to check
*/
canOwnItemType(itemType: ItemType): boolean {
return this.ownableItemTypes.includes(itemType);
@ -149,10 +153,13 @@ export class DS4Actor extends Actor<DS4ActorDataType, DS4ItemDataType, DS4Item>
*/
private _calculateArmorValueOfEquippedItems(): number {
return this.items
.filter((item) => ["armor", "shield"].includes(item.type))
.map((item) => item.data.data as DS4Armor | DS4Shield)
.filter((itemData) => itemData.equipped)
.map((itemData) => itemData.armorValue)
.map((item) => {
if (item.data.type === "armor" || item.data.type === "shield") {
return item.data.data.equipped ? item.data.data.armorValue : 0;
} else {
return 0;
}
})
.reduce((a, b) => a + b, 0);
}
@ -161,7 +168,7 @@ export class DS4Actor extends Actor<DS4ActorDataType, DS4ItemDataType, DS4Item>
* This only differs from the base implementation by also allowing negative values.
* @override
*/
async modifyTokenAttribute(attribute: string, value: number, isDelta = false, isBar = true): Promise<DS4Actor> {
async modifyTokenAttribute(attribute: string, value: number, isDelta = false, isBar = true): Promise<this> {
const current = getProperty(this.data.data, attribute);
// Determine the updates to make to the actor data
@ -180,59 +187,77 @@ export class DS4Actor extends Actor<DS4ActorDataType, DS4ItemDataType, DS4Item>
}
/** @override */
// TODO(types): Improve typing once it's fixed in upstream (arrays can be passed!)
createEmbeddedEntity(
embeddedName: string,
createData: Record<string, unknown> | Array<Record<string, unknown>>,
embeddedName: "OwnedItem",
data: DeepPartial<DS4ItemData>,
options?: Record<string, unknown>,
): Promise<this> {
): Promise<DS4ItemData>;
createEmbeddedEntity(
embeddedName: "ActiveEffect",
data: DeepPartial<DS4ItemData>,
options?: Record<string, unknown>,
): Promise<ActiveEffect.Data>;
createEmbeddedEntity(
embeddedName: "OwnedItem" | "ActiveEffect",
data: DeepPartial<DS4ItemData> | DeepPartial<ActiveEffect.Data>,
options?: Record<string, unknown>,
): Promise<DS4ItemData> | Promise<ActiveEffect.Data> {
if (embeddedName === "OwnedItem") {
this._preCreateOwnedItem((createData as unknown) as ItemData<DS4ItemDataType>);
this._preCreateOwnedItem(data);
return super.createEmbeddedEntity(embeddedName, data, options);
}
return super.createEmbeddedEntity(embeddedName, createData, options);
return super.createEmbeddedEntity(embeddedName, data, options);
}
/**
* If the item that is going to be created is equippable, set it to be non equipped and disable all ActiveEffects
* If the item that is going to be created is equipable, set it to be non equipped and disable all ActiveEffects
* contained in the item
* @param itemData The data of the item to be created
*/
private _preCreateOwnedItem(itemData: ItemData<DS4ItemDataType>): void {
if ("equipped" in itemData.data) {
itemData.effects = itemData.effects.map((effect) => ({ ...effect, disabled: true }));
const equippableUpdateData = itemData as ItemData<DS4EquippableItemDataType>;
equippableUpdateData.data.equipped = false;
protected _preCreateOwnedItem(itemData: DeepPartial<DS4ItemData>): void {
if (itemData.data && "equipped" in itemData.data) {
itemData.effects = itemData.effects?.map((effect) => ({ ...effect, disabled: true }));
itemData.data.equipped = false;
}
}
/** @override */
// TODO(types): Improve typing once it's fixed in upstream
updateEmbeddedEntity(embeddedName: string, data: unknown[], options?: Entity.UpdateOptions): Promise<unknown[]>;
updateEmbeddedEntity(embeddedName: string, data: unknown, options?: Entity.UpdateOptions): Promise<unknown>;
updateEmbeddedEntity(
embeddedName: string,
updateData: Record<string, unknown> | Array<Record<string, unknown>>,
updateData: unknown | unknown[],
options?: Record<string, unknown>,
): Promise<this> {
): Promise<unknown> {
if (embeddedName === "OwnedItem") {
this._preUpdateOwnedItem(updateData as Partial<ItemData<DS4ItemDataType>>);
this._preUpdateOwnedItem(updateData as DeepPartial<DS4ItemData> | Array<DeepPartial<DS4ItemData>>);
}
return super.updateEmbeddedEntity(embeddedName, updateData, options);
}
/**
* If the equipped flag of an item changed, update all ActiveEffects originating from that item accordingly.
* @param updateData The change that is going to be applied to the owned item
* If the equipped flag of one or more items changed, update all ActiveEffects originating from those items
* accordingly.
* @param updateData The change that is going to be applied to the owned item(s)
*/
private _preUpdateOwnedItem(updateData: Partial<ItemData<DS4ItemDataType>>): void {
if ("equipped" in updateData.data) {
const equippableUpdateData = updateData as Partial<ItemData<DS4EquippableItemDataType>>;
const origin = `Actor.${this.id}.OwnedItem.${updateData._id}`;
const effects = this.effects
.filter((e) => e.data.origin === origin)
.map((e) => {
const data = duplicate(e.data);
data.disabled = !equippableUpdateData.data.equipped;
return data;
});
if (effects.length > 0)
this.updateEmbeddedEntity("ActiveEffect", (effects as unknown) as Record<string, unknown>);
}
private _preUpdateOwnedItem(updateData: DeepPartial<DS4ItemData> | Array<DeepPartial<DS4ItemData>>): void {
const dataArray = updateData instanceof Array ? updateData : [updateData];
dataArray.forEach((data) => {
if (data.data && "equipped" in data.data) {
const equipped = data.data.equipped;
const origin = `Actor.${this.id}.OwnedItem.${data._id}`;
const effects = this.effects
.filter((e) => e.data.origin === origin)
.map((e) => {
const effectData = duplicate(e.data);
effectData.disabled = !(equipped ?? true);
return effectData;
});
if (effects.length > 0)
this.updateEmbeddedEntity("ActiveEffect", (effects as unknown) as Record<string, unknown>);
}
});
}
}

View file

@ -1,20 +1,38 @@
import { DS4 } from "../../config";
import { DS4Item } from "../../item/item";
import { DS4ItemDataType, ItemType } from "../../item/item-data";
import { DS4ItemData } from "../../item/item-data";
import { DS4Actor } from "../actor";
import { DS4ActorDataType } from "../actor-data";
/**
* Extend the basic ActorSheet with some very simple modifications
* @extends {ActorSheet}
* The base Sheet class for all DS4 Actors
*/
export class DS4ActorSheet extends ActorSheet<DS4ActorDataType, DS4Actor, DS4ItemDataType> {
export class DS4ActorSheet extends ActorSheet<ActorSheet.Data<DS4Actor>> {
// TODO(types): Improve mergeObject in upstream so that it isn't necessary to provide all parameters (see https://github.com/League-of-Foundry-Developers/foundry-vtt-types/issues/272)
/** @override */
static get defaultOptions(): FormApplicationOptions {
return mergeObject(super.defaultOptions, {
static get defaultOptions(): BaseEntitySheet.Options {
const superDefaultOptions = super.defaultOptions;
return mergeObject(superDefaultOptions, {
classes: ["ds4", "sheet", "actor"],
width: 745,
height: 600,
scrollY: [".sheet-body"],
template: superDefaultOptions.template,
viewPermission: superDefaultOptions.viewPermission,
closeOnSubmit: superDefaultOptions.closeOnSubmit,
submitOnChange: superDefaultOptions.submitOnChange,
submitOnClose: superDefaultOptions.submitOnClose,
editable: superDefaultOptions.editable,
baseApplication: superDefaultOptions.baseApplication,
top: superDefaultOptions.top,
left: superDefaultOptions.left,
popOut: superDefaultOptions.popOut,
minimizable: superDefaultOptions.minimizable,
resizable: superDefaultOptions.resizable,
id: superDefaultOptions.id,
dragDrop: superDefaultOptions.dragDrop,
filters: superDefaultOptions.filters,
title: superDefaultOptions.title,
tabs: superDefaultOptions.tabs,
});
}
@ -24,27 +42,23 @@ export class DS4ActorSheet extends ActorSheet<DS4ActorDataType, DS4Actor, DS4Ite
return `${path}/${this.actor.data.type}-sheet.hbs`;
}
/* -------------------------------------------- */
/**
* This method returns the data for the template of the actor sheet.
* It explicitly adds the items of the object sorted by type in the
* object itemsByType.
* @returns the data fed to the template of the actor sheet
* @returns The data fed to the template of the actor sheet
*/
getData(): ActorSheetData<DS4ActorDataType, DS4Actor> {
async getData(): Promise<ActorSheet.Data<DS4Actor>> {
const data = {
...super.getData(),
...(await super.getData()),
// Add the localization config to the data:
config: CONFIG.DS4,
config: DS4,
// Add the items explicitly sorted by type to the data:
itemsByType: this.actor.itemTypes,
};
return data;
}
/* -------------------------------------------- */
/** @override */
activateListeners(html: JQuery): void {
super.activateListeners(html);
@ -59,7 +73,7 @@ export class DS4ActorSheet extends ActorSheet<DS4ActorDataType, DS4Actor, DS4Ite
html.find(".item-edit").on("click", (ev) => {
const li = $(ev.currentTarget).parents(".item");
const item = this.actor.getOwnedItem(li.data("itemId"));
item.sheet.render(true);
item.sheet?.render(true);
});
// Delete Inventory Item
@ -75,20 +89,16 @@ export class DS4ActorSheet extends ActorSheet<DS4ActorDataType, DS4Actor, DS4Ite
html.find(".rollable").click(this._onRoll.bind(this));
}
/* -------------------------------------------- */
/**
* Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset
* @param {JQuery.ClickEvent} event The originating click event
* @private
* @param event - The originating click event
*/
private _onItemCreate(event: JQuery.ClickEvent): Promise<Item> {
protected _onItemCreate(event: JQuery.ClickEvent): Promise<DS4ItemData> {
event.preventDefault();
const header = event.currentTarget;
// Get the type of item to create.
const type = header.dataset.type;
// Grab any data associated with this control.
const data = duplicate(header.dataset);
const { type, ...data } = duplicate(header.dataset);
// Initialize a default name.
const name = `New ${type.capitalize()}`;
// Prepare the item object.
@ -97,8 +107,6 @@ export class DS4ActorSheet extends ActorSheet<DS4ActorDataType, DS4Actor, DS4Ite
type: type,
data: data,
};
// Remove the type from the dataset since it's in the itemData.type prop.
delete itemData.data.type;
// Finally, create the item!
return this.actor.createOwnedItem(itemData);
@ -108,15 +116,14 @@ export class DS4ActorSheet extends ActorSheet<DS4ActorDataType, DS4Actor, DS4Ite
* Handle changes to properties of an Owned Item from within character sheet.
* Can currently properly bind: see getValue().
* Assumes the item property is given as the value of the HTML element property 'data-property'.
* @param {JQuery.ChangeEvent<HTMLFormElement>} ev The originating change event
* @private
* @param ev - The originating change event
*/
private _onItemChange(ev: JQuery.ChangeEvent<HTMLFormElement>): void {
protected _onItemChange(ev: JQuery.ChangeEvent): void {
ev.preventDefault();
console.log("Current target:", $(ev.currentTarget).get(0)["name"]);
const el: HTMLFormElement = $(ev.currentTarget).get(0);
const id = $(ev.currentTarget).parents(".item").data("itemId");
const item = duplicate(this.actor.getOwnedItem(id)); // getOwnedItem is typed incorrectly, it actually returns a ItemData<DS4ItemDataType>, not an Item
const item = duplicate<DS4Item, "lenient">(this.actor.getOwnedItem(id));
const property: string | undefined = $(ev.currentTarget).data("property");
// Early return:
@ -139,7 +146,7 @@ export class DS4ActorSheet extends ActorSheet<DS4ActorDataType, DS4Actor, DS4Ite
* - Checkbox: boolean
* - Text input: string
* - Number: number
* @param el the input element to collect the value of
* @param el - The input element to collect the value of
*/
private getValue(el: HTMLFormElement): boolean | string | number {
// One needs to differentiate between e.g. checkboxes (value="on") and select boxes etc.
@ -190,10 +197,9 @@ export class DS4ActorSheet extends ActorSheet<DS4ActorDataType, DS4Actor, DS4Ite
/**
* Handle clickable rolls.
* @param {JQuery.ClickEvent} event The originating click event
* @private
* @param event - The originating click event
*/
private _onRoll(event: JQuery.ClickEvent): void {
protected _onRoll(event: JQuery.ClickEvent): void {
event.preventDefault();
const element = event.currentTarget;
const dataset = element.dataset;
@ -209,10 +215,13 @@ export class DS4ActorSheet extends ActorSheet<DS4ActorDataType, DS4Actor, DS4Ite
}
/** @override */
async _onDropItem(event: DragEvent, data: Parameters<typeof DS4Item.fromDropData>[0]): Promise<unknown> {
const item = await Item.fromDropData(data);
if (item && !this.actor.canOwnItemType(item.data.type as ItemType)) {
ui.notifications.warn(
protected async _onDropItem(
event: DragEvent,
data: { type: "Item" } & (DeepPartial<ActorSheet.OwnedItemData<DS4Actor>> | { pack: string } | { id: string }),
): Promise<boolean | undefined | ActorSheet.OwnedItemData<DS4Actor>> {
const item = ((await Item.fromDropData(data)) as unknown) as DS4Item;
if (item && !this.actor.canOwnItemType(item.data.type)) {
ui.notifications?.warn(
game.i18n.format("DS4.WarningActorCannotOwnItem", {
actorName: this.actor.name,
actorType: this.actor.data.type,

View file

@ -1,11 +1,34 @@
import { DS4ActorSheet } from "./actor-sheet";
/**
* The Sheet class for DS4 Character Actors
*/
export class DS4CharacterActorSheet extends DS4ActorSheet {
/** @override */
static get defaultOptions(): FormApplicationOptions {
return mergeObject(super.defaultOptions, {
static get defaultOptions(): BaseEntitySheet.Options {
const superDefaultOptions = super.defaultOptions;
return mergeObject(superDefaultOptions, {
classes: ["ds4", "sheet", "actor", "character"],
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "inventory" }],
template: superDefaultOptions.template,
viewPermission: superDefaultOptions.viewPermission,
closeOnSubmit: superDefaultOptions.closeOnSubmit,
submitOnChange: superDefaultOptions.submitOnChange,
submitOnClose: superDefaultOptions.submitOnClose,
editable: superDefaultOptions.editable,
baseApplication: superDefaultOptions.baseApplication,
top: superDefaultOptions.top,
left: superDefaultOptions.left,
popOut: superDefaultOptions.popOut,
minimizable: superDefaultOptions.minimizable,
resizable: superDefaultOptions.resizable,
id: superDefaultOptions.id,
dragDrop: superDefaultOptions.dragDrop,
filters: superDefaultOptions.filters,
title: superDefaultOptions.title,
width: superDefaultOptions.width,
height: superDefaultOptions.height,
scrollY: superDefaultOptions.scrollY,
});
}
}

View file

@ -1,11 +1,34 @@
import { DS4ActorSheet } from "./actor-sheet";
/**
* The Sheet class for DS4 Creature Actors
*/
export class DS4CreatureActorSheet extends DS4ActorSheet {
/** @override */
static get defaultOptions(): FormApplicationOptions {
return mergeObject(super.defaultOptions, {
static get defaultOptions(): BaseEntitySheet.Options {
const superDefaultOptions = super.defaultOptions;
return mergeObject(superDefaultOptions, {
classes: ["ds4", "sheet", "actor", "creature"],
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "inventory" }],
template: superDefaultOptions.template,
viewPermission: superDefaultOptions.viewPermission,
closeOnSubmit: superDefaultOptions.closeOnSubmit,
submitOnChange: superDefaultOptions.submitOnChange,
submitOnClose: superDefaultOptions.submitOnClose,
editable: superDefaultOptions.editable,
baseApplication: superDefaultOptions.baseApplication,
top: superDefaultOptions.top,
left: superDefaultOptions.left,
popOut: superDefaultOptions.popOut,
minimizable: superDefaultOptions.minimizable,
resizable: superDefaultOptions.resizable,
id: superDefaultOptions.id,
dragDrop: superDefaultOptions.dragDrop,
filters: superDefaultOptions.filters,
title: superDefaultOptions.title,
width: superDefaultOptions.width,
height: superDefaultOptions.height,
scrollY: superDefaultOptions.scrollY,
});
}
}