368 lines
14 KiB
TypeScript
368 lines
14 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 { ModifiableDataBaseTotal } from "../../common/common-data";
|
|
import { DS4 } from "../../config";
|
|
import { getCanvas, getGame } from "../../helpers";
|
|
import { DS4Item } from "../../item/item";
|
|
import { DS4Settings, getDS4Settings } from "../../settings";
|
|
import notifications from "../../ui/notifications";
|
|
import { isCheck } from "../actor-data-properties";
|
|
|
|
/**
|
|
* The base Sheet class for all DS4 Actors
|
|
*/
|
|
export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetData> {
|
|
/** @override */
|
|
static get defaultOptions(): ActorSheet.Options {
|
|
return foundry.utils.mergeObject(super.defaultOptions, {
|
|
classes: ["ds4", "sheet", "actor"],
|
|
height: 620,
|
|
scrollY: [".values", ".inventory", ".spells", ".abilities", ".effects", ".biography", ".description"],
|
|
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "values" }],
|
|
dragDrop: [
|
|
{ dragSelector: ".item-list .item", 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`;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
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.getSourceName(),
|
|
};
|
|
});
|
|
const enrichedEffects = await Promise.all(enrichedEffectPromises);
|
|
|
|
const data = {
|
|
...this.addTooltipsToData(await super.getData()),
|
|
config: DS4,
|
|
itemsByType,
|
|
enrichedEffects,
|
|
settings: getDS4Settings(),
|
|
};
|
|
return 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;
|
|
}
|
|
|
|
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);
|
|
|
|
// Everything below here is only needed if the sheet is editable
|
|
if (!this.options.editable) return;
|
|
|
|
// Add Inventory Item
|
|
html.find(".item-create").on("click", this.onItemCreate.bind(this));
|
|
|
|
// Update Inventory Item
|
|
html.find(".item-edit").on("click", (ev) => {
|
|
const li = $(ev.currentTarget).parents(".item");
|
|
const id = li.data("itemId");
|
|
const item = this.actor.items.get(id);
|
|
if (!item) {
|
|
throw new Error(getGame().i18n.format("DS4.ErrorActorDoesNotHaveItem", { id, actor: this.actor.name }));
|
|
}
|
|
if (!item.sheet) {
|
|
throw new Error(getGame().i18n.localize("DS4.ErrorUnexpectedError"));
|
|
}
|
|
item.sheet.render(true);
|
|
});
|
|
|
|
// Delete Inventory Item
|
|
html.find(".item-delete").on("click", (ev) => {
|
|
const li = $(ev.currentTarget).parents(".item");
|
|
this.actor.deleteEmbeddedDocuments("Item", [li.data("itemId")]);
|
|
li.slideUp(200, () => this.render(false));
|
|
});
|
|
|
|
html.find(".item-change").on("change", this.onItemChange.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));
|
|
}
|
|
|
|
/**
|
|
* Handle creating a new embedded Item for the actor using initial data defined in the HTML dataset
|
|
* @param event - The originating click event
|
|
*/
|
|
protected onItemCreate(event: JQuery.ClickEvent): void {
|
|
event.preventDefault();
|
|
const header = event.currentTarget;
|
|
|
|
const { type, ...data } = foundry.utils.deepClone(header.dataset);
|
|
|
|
const name = getGame().i18n.localize(`DS4.New${type.capitalize()}Name`);
|
|
|
|
const itemData = {
|
|
name: name,
|
|
type: type,
|
|
data: data,
|
|
};
|
|
|
|
DS4Item.create(itemData, { parent: this.actor });
|
|
}
|
|
|
|
/**
|
|
* 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 ev - The originating change event
|
|
*/
|
|
protected onItemChange(ev: JQuery.ChangeEvent): void {
|
|
ev.preventDefault();
|
|
const el: HTMLFormElement = $(ev.currentTarget).get(0);
|
|
const id = $(ev.currentTarget).parents(".item").data("itemId");
|
|
const item = this.actor.items.get(id);
|
|
if (!item) {
|
|
throw new Error(getGame().i18n.format("DS4.ErrorActorDoesNotHaveItem", { id, actor: this.actor.name }));
|
|
}
|
|
const itemObject = item.toObject();
|
|
const property: string | undefined = $(ev.currentTarget).data("property");
|
|
|
|
// Early return:
|
|
// Disabled => do nothing
|
|
if (el.disabled || el.getAttribute("disabled")) return;
|
|
// name not given => raise
|
|
if (property === undefined) {
|
|
throw TypeError("HTML element does not provide 'data-property' attribute");
|
|
}
|
|
|
|
// Set new value
|
|
const newValue = this.getValue(el);
|
|
foundry.utils.setProperty(itemObject, property, newValue);
|
|
item.update(itemObject);
|
|
}
|
|
|
|
/**
|
|
* Collect the value of a form element depending on the element's type
|
|
* The value is parsed to:
|
|
* - Checkbox: boolean
|
|
* - Text input: string
|
|
* - Number: number
|
|
* @param el - The input element to collect the value of
|
|
* @param inverted - Whether or not the value should be inverted
|
|
*/
|
|
private getValue(el: HTMLFormElement, inverted = false): boolean | string | number {
|
|
// One needs to differentiate between e.g. checkboxes (value="on") and select boxes etc.
|
|
// Checkbox:
|
|
if (el.type === "checkbox") {
|
|
const value: boolean = el.checked;
|
|
return inverted ? !value : value;
|
|
}
|
|
|
|
// Text input:
|
|
else if (el.type === "text") {
|
|
const value: string = el.value;
|
|
return value;
|
|
}
|
|
|
|
// Numbers:
|
|
else if (el.type === "number") {
|
|
const value = Number(el.value.trim());
|
|
return value;
|
|
}
|
|
|
|
// // Ranges:
|
|
// else if (el.type === "range") {
|
|
// const value: string = el.value.trim();
|
|
// return value;
|
|
// }
|
|
|
|
// // Radio Checkboxes (untested, cf. FormDataExtended.process)
|
|
// else if (el.type === "radio") {
|
|
// const chosen: HTMLFormElement = el.find((r: HTMLFormElement) => r["checked"]);
|
|
// const value: string = chosen ? chosen.value : null;
|
|
// return value;
|
|
// }
|
|
|
|
// // Multi-Select (untested, cf. FormDataExtended.process)
|
|
// else if (el.type === "select-multiple") {
|
|
// const value: Array<string> = [];
|
|
// el.options.array.forEach((opt: HTMLOptionElement) => {
|
|
// if (opt.selected) value.push(opt.value);
|
|
// });
|
|
// return value;
|
|
|
|
// unsupported:
|
|
else {
|
|
throw new TypeError("Binding of item property to this type of HTML element not supported; given: " + el);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
protected onCreateEffect(): void {
|
|
const createData = {
|
|
label: getGame().i18n.localize(`DS4.NewEffectLabel`),
|
|
icon: "icons/svg/aura.svg",
|
|
origin: this.actor.uuid,
|
|
};
|
|
ActiveEffect.create(createData, { parent: this.actor });
|
|
}
|
|
|
|
protected onEditEffect(event: JQuery.ClickEvent): void {
|
|
const id = $(event.currentTarget).parents(".effect").data("effectId");
|
|
const effect = this.actor.effects.get(id);
|
|
if (!effect) {
|
|
throw new Error(getGame().i18n.format("DS4.ErrorActorDoesNotHaveEffect", { id, actor: this.actor.name }));
|
|
}
|
|
effect.sheet.render(true);
|
|
}
|
|
|
|
protected onDeleteEffect(event: JQuery.ClickEvent): void {
|
|
const li = $(event.currentTarget).parents(".effect");
|
|
const id = li.data("effectId");
|
|
this.actor.deleteEmbeddedDocuments("ActiveEffect", [id]);
|
|
li.slideUp(200, () => this.render(false));
|
|
}
|
|
|
|
protected onChangeEffect(event: JQuery.ChangeEvent): void {
|
|
event.preventDefault();
|
|
const currentTarget = $(event.currentTarget);
|
|
const element: HTMLFormElement = currentTarget.get(0);
|
|
const id = currentTarget.parents(".effect").data("effectId");
|
|
const property: string | undefined = currentTarget.data("property");
|
|
const inverted = Boolean(currentTarget.data("inverted"));
|
|
|
|
if (element.disabled || element.getAttribute("disabled")) return;
|
|
if (property === undefined) {
|
|
throw TypeError("HTML element does not provide 'data-property' attribute");
|
|
}
|
|
const newValue = this.getValue(element, inverted);
|
|
this.actor.updateEmbeddedDocuments("ActiveEffect", [{ _id: id, [property]: newValue }]);
|
|
}
|
|
|
|
/**
|
|
* Handle clickable item rolls.
|
|
* @param event - The originating click event
|
|
*/
|
|
protected onRollItem(event: JQuery.ClickEvent): void {
|
|
event.preventDefault();
|
|
const id = $(event.currentTarget).parents(".item").data("itemId");
|
|
const item = this.actor.items.get(id);
|
|
if (!item) {
|
|
throw new Error(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);
|
|
|
|
if (!isCheck(check)) throw new Error(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));
|
|
}
|
|
|
|
/** @override */
|
|
protected async _onDropItem(event: DragEvent, data: ActorSheet.DropData.Item): Promise<unknown> {
|
|
const item = await DS4Item.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<ActorSheet.Options> {
|
|
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;
|
|
}
|