In the Active Effect Config, there are now additional inputs to configure the effect to be applied to items owned by the actor instead of the actor itself. It is possible to select the items to which to apply the effect via matching by name, or via a condition expression, that provides similar capabilities as the evaluation of mathematical expressions in rolls. Data from the Actor, Item, and Active Effect can be accessed similar to how properties are accessed in roll formulas (using the prefixes `@actor`, `@item`, and `@effect`). For example, in order to apply an effect to all ranged weapons, the conditions would be ```js '@item.type' === 'weapon' && '@item.data.attackType' === 'ranged' ```
434 lines
17 KiB
TypeScript
434 lines
17 KiB
TypeScript
// SPDX-FileCopyrightText: 2021 Johannes Loher
|
|
// SPDX-FileCopyrightText: 2021 Oliver Rümpelein
|
|
// SPDX-FileCopyrightText: 2021 Gesina Schwalbe
|
|
// SPDX-FileCopyrightText: 2021 Siegfried Krug
|
|
//
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
import { DS4ActiveEffect } from "../active-effect/active-effect";
|
|
import { disableOverriddenFields } from "../apps/sheet-helpers";
|
|
import { DS4 } from "../config";
|
|
import { getCanvas, getGame } from "../helpers";
|
|
import { getDS4Settings } from "../settings";
|
|
import notifications from "../ui/notifications";
|
|
import { enforce } from "../utils";
|
|
import { isCheck } from "./actor-data-properties-base";
|
|
|
|
import type { ModifiableDataBaseTotal } from "../common/common-data";
|
|
import type { DS4Settings } from "../settings";
|
|
import type { DS4Item } from "../item/item";
|
|
|
|
/**
|
|
* The base sheet class for all {@link DS4Actor}s.
|
|
*/
|
|
export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetData> {
|
|
static override get defaultOptions(): ActorSheet.Options {
|
|
return foundry.utils.mergeObject(super.defaultOptions, {
|
|
classes: ["sheet", "ds4-actor-sheet"],
|
|
height: 625,
|
|
scrollY: [".ds4-sheet-body"],
|
|
tabs: [{ navSelector: ".ds4-sheet-tab-nav", contentSelector: ".ds4-sheet-body", initial: "values" }],
|
|
dragDrop: [
|
|
{ dragSelector: ".item-list .item", dropSelector: null },
|
|
{ dragSelector: ".effect-list .effect", dropSelector: null },
|
|
{ dragSelector: ".ds4-check", dropSelector: null },
|
|
],
|
|
width: 650,
|
|
});
|
|
}
|
|
|
|
override get template(): string {
|
|
const basePath = "systems/ds4/templates/sheets/actor";
|
|
return `${basePath}/${this.actor.data.type}-sheet.hbs`;
|
|
}
|
|
|
|
override async getData(): Promise<DS4ActorSheetData> {
|
|
const itemsByType = Object.fromEntries(
|
|
Object.entries(this.actor.itemTypes).map(([itemType, items]) => {
|
|
return [itemType, items.map((item) => item.data).sort((a, b) => (a.sort || 0) - (b.sort || 0))];
|
|
}),
|
|
);
|
|
|
|
const enrichedEffectPromises = this.actor.effects.map(async (effect) => {
|
|
return {
|
|
...effect.toObject(),
|
|
sourceName: await effect.getCurrentSourceName(),
|
|
factor: effect.factor,
|
|
isEffectivelyEnabled: !effect.data.disabled && !effect.isSurpressed,
|
|
};
|
|
});
|
|
const enrichedEffects = await Promise.all(enrichedEffectPromises);
|
|
|
|
const data = {
|
|
...this.addTooltipsToData(await super.getData()),
|
|
config: DS4,
|
|
itemsByType,
|
|
enrichedEffects,
|
|
settings: getDS4Settings(),
|
|
};
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Adds tooltips to the attributes, traits, and combatValues of the actor data of the given {@link ActorSheet.Data}.
|
|
*/
|
|
protected addTooltipsToData(data: ActorSheet.Data): ActorSheet.Data {
|
|
const valueGroups = [data.data.data.attributes, data.data.data.traits, data.data.data.combatValues];
|
|
|
|
valueGroups.forEach((valueGroup) => {
|
|
Object.values(valueGroup).forEach((attribute: ModifiableDataBaseTotal<number> & { tooltip?: string }) => {
|
|
attribute.tooltip = this.getTooltipForValue(attribute);
|
|
});
|
|
});
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Generates a tooltip for a given attribute, trait, or combatValue.
|
|
*/
|
|
protected getTooltipForValue(value: ModifiableDataBaseTotal<number>): string {
|
|
return `${value.base} (${getGame().i18n.localize("DS4.TooltipBaseValue")}) + ${
|
|
value.mod
|
|
} (${getGame().i18n.localize("DS4.TooltipModifier")}) ➞ ${getGame().i18n.localize("DS4.TooltipEffects")} ➞ ${
|
|
value.total
|
|
}`;
|
|
}
|
|
|
|
override activateListeners(html: JQuery): void {
|
|
super.activateListeners(html);
|
|
|
|
if (!this.options.editable) return;
|
|
|
|
html.find(".control-item").on("click", this.onControlItem.bind(this));
|
|
html.find(".change-item").on("change", this.onChangeItem.bind(this));
|
|
|
|
html.find(".control-effect").on("click", this.onControlEffect.bind(this));
|
|
html.find(".change-effect").on("change", this.onChangeEffect.bind(this));
|
|
|
|
html.find(".rollable-item").on("click", this.onRollItem.bind(this));
|
|
html.find(".rollable-check").on("click", this.onRollCheck.bind(this));
|
|
|
|
html.find(".sort-items").on("click", this.onSortItems.bind(this));
|
|
|
|
disableOverriddenFields.call(this);
|
|
}
|
|
|
|
/**
|
|
* Handles a click on an element of this sheet to control an embedded item of the actor corresponding to this sheet.
|
|
*
|
|
* @param event - The originating click event
|
|
*/
|
|
protected onControlItem(event: JQuery.ClickEvent): void {
|
|
event.preventDefault();
|
|
const a = event.currentTarget;
|
|
switch (a.dataset["action"]) {
|
|
case "create":
|
|
return this.onCreateItem(event);
|
|
case "edit":
|
|
return this.onEditItem(event);
|
|
case "delete":
|
|
return this.onDeleteItem(event);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a new embedded item using the initial data defined in the HTML dataset of the clicked element.
|
|
*
|
|
* @param event - The originating click event
|
|
*/
|
|
protected onCreateItem(event: JQuery.ClickEvent): void {
|
|
const { type, ...data } = foundry.utils.deepClone(event.currentTarget.dataset);
|
|
const name = getGame().i18n.localize(`DS4.New${type.capitalize()}Name`);
|
|
const itemData = {
|
|
name: name,
|
|
type: type,
|
|
data: data,
|
|
};
|
|
Item.create(itemData, { parent: this.actor, pack: this.actor.pack ?? undefined });
|
|
}
|
|
|
|
/**
|
|
* Opens the sheet of the embedded item corresponding to the clicked element.
|
|
*
|
|
* @param event - The originating click event
|
|
*/
|
|
protected onEditItem(event: JQuery.ClickEvent): void {
|
|
const id = $(event.currentTarget)
|
|
.parents(embeddedDocumentListEntryProperties.Item.selector)
|
|
.data(embeddedDocumentListEntryProperties.Item.idDataAttribute);
|
|
const item = this.actor.items.get(id);
|
|
enforce(item, getGame().i18n.format("DS4.ErrorActorDoesNotHaveItem", { id, actor: this.actor.name }));
|
|
enforce(item.sheet);
|
|
item.sheet.render(true);
|
|
}
|
|
|
|
/**
|
|
* Deletes the embedded item corresponding to the clicked element.
|
|
*
|
|
* @param event - The originating click event
|
|
*/
|
|
protected onDeleteItem(event: JQuery.ClickEvent): void {
|
|
const li = $(event.currentTarget).parents(embeddedDocumentListEntryProperties.Item.selector);
|
|
this.actor.deleteEmbeddedDocuments("Item", [li.data(embeddedDocumentListEntryProperties.Item.idDataAttribute)]);
|
|
li.slideUp(200, () => this.render(false));
|
|
}
|
|
|
|
/**
|
|
* Applies a change to a property of an embedded item depending on the `data-property` attribute of the
|
|
* {@link HTMLInputElement} that has been changed and its new value.
|
|
*
|
|
* @param event - The originating change event
|
|
*/
|
|
protected onChangeItem(event: JQuery.ChangeEvent): void {
|
|
return this.onChangeEmbeddedDocument(event, "Item");
|
|
}
|
|
|
|
/**
|
|
* Handles a click on an element of this sheet to control an embedded effect of the actor corresponding to this
|
|
* sheet.
|
|
*
|
|
* @param event - The originating click event
|
|
*/
|
|
protected onControlEffect(event: JQuery.ClickEvent): void {
|
|
event.preventDefault();
|
|
const a = event.currentTarget;
|
|
switch (a.dataset["action"]) {
|
|
case "create":
|
|
return this.onCreateEffect();
|
|
case "edit":
|
|
return this.onEditEffect(event);
|
|
case "delete":
|
|
return this.onDeleteEffect(event);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a new embedded effect.
|
|
*
|
|
* @param event - The originating click event
|
|
*/
|
|
protected onCreateEffect(): void {
|
|
DS4ActiveEffect.createDefault(this.actor);
|
|
}
|
|
|
|
/**
|
|
* Opens the sheet of the embedded effect corresponding to the clicked element.
|
|
*
|
|
* @param event - The originating click event
|
|
*/
|
|
protected onEditEffect(event: JQuery.ClickEvent): void {
|
|
const id = $(event.currentTarget)
|
|
.parents(embeddedDocumentListEntryProperties.ActiveEffect.selector)
|
|
.data(embeddedDocumentListEntryProperties.ActiveEffect.idDataAttribute);
|
|
const effect = this.actor.effects.get(id);
|
|
enforce(effect, getGame().i18n.format("DS4.ErrorActorDoesNotHaveEffect", { id, actor: this.actor.name }));
|
|
effect.sheet?.render(true);
|
|
}
|
|
|
|
/**
|
|
* Deletes the embedded item corresponding to the clicked element.
|
|
*
|
|
* @param event - The originating click event
|
|
*/
|
|
protected onDeleteEffect(event: JQuery.ClickEvent): void {
|
|
const li = $(event.currentTarget).parents(embeddedDocumentListEntryProperties.ActiveEffect.selector);
|
|
const id = li.data(embeddedDocumentListEntryProperties.ActiveEffect.idDataAttribute);
|
|
this.actor.deleteEmbeddedDocuments("ActiveEffect", [id]);
|
|
li.slideUp(200, () => this.render(false));
|
|
}
|
|
|
|
/**
|
|
* Applies a change to a property of an embedded effect depending on the `data-property` attribute of the
|
|
* {@link HTMLInputElement} that has been changed and its new value.
|
|
*
|
|
* @param event - The originating change event
|
|
*/
|
|
protected onChangeEffect(event: JQuery.ChangeEvent): void {
|
|
return this.onChangeEmbeddedDocument(event, "ActiveEffect");
|
|
}
|
|
|
|
/**
|
|
* Applies a change to a property of an embedded document of the actor belonging to this sheet. The change depends
|
|
* on the `data-property` attribute of the {@link HTMLInputElement} that has been changed and its new value.
|
|
*
|
|
* @param event - The originating change event
|
|
* @param documentName - The name of the embedded document to be changed.
|
|
*/
|
|
protected onChangeEmbeddedDocument(event: JQuery.ChangeEvent, documentName: "Item" | "ActiveEffect"): void {
|
|
event.preventDefault();
|
|
const element = $(event.currentTarget).get(0);
|
|
enforce(element instanceof HTMLInputElement);
|
|
if (element.disabled) return;
|
|
|
|
const documentElement = element.closest(embeddedDocumentListEntryProperties[documentName].selector);
|
|
enforce(documentElement instanceof HTMLElement);
|
|
const id = documentElement.dataset[embeddedDocumentListEntryProperties[documentName].idDataAttribute];
|
|
const property = element.dataset["property"];
|
|
enforce(property !== undefined, TypeError("HTML element does not provide 'data-property' attribute"));
|
|
|
|
const newValue = this.parseValue(element);
|
|
this.actor.updateEmbeddedDocuments(documentName, [{ _id: id, [property]: newValue }]);
|
|
}
|
|
|
|
/**
|
|
* Parses the value of the given {@link HTMLInputElement} depending on the element's type
|
|
* The value is parsed to:
|
|
* - checkbox: `boolean`, if the attribute `data-inverted` is set to a truthy value, the parsed value is inverted
|
|
* - text input: `string`
|
|
* - number: `number`
|
|
*
|
|
* @param element - The input element to parse the value from
|
|
*/
|
|
protected parseValue(element: HTMLInputElement): boolean | string | number {
|
|
switch (element.type) {
|
|
case "checkbox": {
|
|
const inverted = Boolean(element.dataset["inverted"]);
|
|
const value: boolean = element.checked;
|
|
return inverted ? !value : value;
|
|
}
|
|
case "text": {
|
|
const value: string = element.value;
|
|
return value;
|
|
}
|
|
case "number": {
|
|
const value = Number(element.value.trim());
|
|
return value;
|
|
}
|
|
default: {
|
|
throw new TypeError(
|
|
"Binding of item property to this type of HTML element not supported; given: " + element,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle clickable item rolls.
|
|
* @param event - The originating click event
|
|
*/
|
|
protected onRollItem(event: JQuery.ClickEvent): void {
|
|
event.preventDefault();
|
|
const id = $(event.currentTarget)
|
|
.parents(embeddedDocumentListEntryProperties.Item.selector)
|
|
.data(embeddedDocumentListEntryProperties.Item.idDataAttribute);
|
|
const item = this.actor.items.get(id);
|
|
enforce(item, getGame().i18n.format("DS4.ErrorActorDoesNotHaveItem", { id, actor: this.actor.name }));
|
|
item.roll().catch((e) => notifications.error(e, { log: true }));
|
|
}
|
|
|
|
/**
|
|
* Handle clickable check rolls.
|
|
* @param event - The originating click event
|
|
*/
|
|
protected onRollCheck(event: JQuery.ClickEvent): void {
|
|
event.preventDefault();
|
|
const check = event.currentTarget.dataset["check"];
|
|
this.actor.rollCheck(check).catch((e) => notifications.error(e, { log: true }));
|
|
}
|
|
|
|
override _onDragStart(event: DragEvent): void {
|
|
const target = event.currentTarget as HTMLElement;
|
|
if (!(target instanceof HTMLElement)) return super._onDragStart(event);
|
|
|
|
const check = target.dataset.check;
|
|
if (!check) return super._onDragStart(event);
|
|
|
|
enforce(isCheck(check), getGame().i18n.format("DS4.ErrorCannotDragMissingCheck", { check }));
|
|
|
|
const dragData = {
|
|
actorId: this.actor.id,
|
|
sceneId: this.actor.isToken ? getCanvas().scene?.id : null,
|
|
tokenId: this.actor.isToken ? this.actor.token?.id : null,
|
|
type: "Check",
|
|
data: check,
|
|
};
|
|
|
|
event.dataTransfer?.setData("text/plain", JSON.stringify(dragData));
|
|
}
|
|
|
|
/**
|
|
* Sort items according to the item list header that has been clicked.
|
|
* @param event - The originating click event
|
|
*/
|
|
protected onSortItems(event: JQuery.ClickEvent<unknown, unknown, HTMLElement>): void {
|
|
event.preventDefault();
|
|
const target = event.currentTarget;
|
|
const type = target.parentElement?.dataset["type"];
|
|
enforce(type !== undefined, `Could not find property 'type' in the dataset of the parent of ${target}`);
|
|
const dataPath = target.dataset["dataPath"];
|
|
enforce(dataPath !== undefined, `Could not find property 'dataPath' in the dataset of ${target}`);
|
|
const items = this.actor.items.filter((item) => item.type === type);
|
|
items.sort((a, b) => a.data.sort - b.data.sort);
|
|
|
|
const sortFunction =
|
|
(invert: boolean) =>
|
|
(a: DS4Item, b: DS4Item): number => {
|
|
const propertyA = getProperty(a.data, dataPath);
|
|
const propertyB = getProperty(b.data, dataPath);
|
|
if (typeof propertyA === "string" || typeof propertyB === "string") {
|
|
return invert
|
|
? (propertyB ?? "").localeCompare(propertyA ?? "")
|
|
: (propertyA ?? "").localeCompare(propertyB ?? "");
|
|
} else {
|
|
return invert ? propertyB - propertyA : propertyA - propertyB;
|
|
}
|
|
};
|
|
|
|
const sortedItems = [...items].sort(sortFunction(false));
|
|
const wasSortedAlready = !sortedItems.find((item, index) => item !== items[index]);
|
|
|
|
if (wasSortedAlready) {
|
|
sortedItems.sort(sortFunction(true));
|
|
}
|
|
|
|
const updates = sortedItems.map((item, i) => ({
|
|
_id: item.id,
|
|
sort: (i + 1) * CONST.SORT_INTEGER_DENSITY,
|
|
}));
|
|
|
|
this.actor.updateEmbeddedDocuments("Item", updates);
|
|
}
|
|
|
|
protected override async _onDropItem(event: DragEvent, data: ActorSheet.DropData.Item): Promise<unknown> {
|
|
const item = await Item.fromDropData(data);
|
|
if (item && !this.actor.canOwnItemType(item.data.type)) {
|
|
notifications.warn(
|
|
getGame().i18n.format("DS4.WarningActorCannotOwnItem", {
|
|
actorName: this.actor.name,
|
|
actorType: this.actor.data.type,
|
|
itemName: item.name,
|
|
itemType: item.data.type,
|
|
}),
|
|
);
|
|
return false;
|
|
}
|
|
return super._onDropItem(event, data);
|
|
}
|
|
}
|
|
|
|
interface DS4ActorSheetData extends ActorSheet.Data {
|
|
config: typeof DS4;
|
|
itemsByType: Record<string, foundry.data.ItemData[]>;
|
|
enrichedEffects: EnrichedActiveEffectDataSource[];
|
|
settings: DS4Settings;
|
|
}
|
|
|
|
type ActiveEffectDataSource = foundry.data.ActiveEffectData["_source"];
|
|
|
|
interface EnrichedActiveEffectDataSource extends ActiveEffectDataSource {
|
|
sourceName: string;
|
|
}
|
|
|
|
/**
|
|
* This object contains information about specific properties embedded document list entries for each different type.
|
|
*/
|
|
const embeddedDocumentListEntryProperties = Object.freeze({
|
|
ActiveEffect: {
|
|
selector: ".effect",
|
|
idDataAttribute: "effectId",
|
|
},
|
|
Item: {
|
|
selector: ".item",
|
|
idDataAttribute: "itemId",
|
|
},
|
|
});
|