mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-01-11 19:25:21 +01:00
* Add ability to sort inventories in player and party sheets * Format base actor sheet * Check item validity when creating on an actor * Block dragdrop on adversaries and environments * Support drag and drop in adversary and environment sheets * Fix regression with dropping to character sheet * Move vault when created handling to domain card preCreate
329 lines
12 KiB
JavaScript
329 lines
12 KiB
JavaScript
import { getDocFromElement, itemIsIdentical } from '../../../helpers/utils.mjs';
|
|
import DHBaseActorSettings from './actor-setting.mjs';
|
|
import DHApplicationMixin from './application-mixin.mjs';
|
|
|
|
const { ActorSheetV2 } = foundry.applications.sheets;
|
|
|
|
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
|
|
|
|
/**
|
|
* A base actor sheet extending {@link ActorSheetV2} via {@link DHApplicationMixin}
|
|
*/
|
|
export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
|
|
/** @inheritDoc */
|
|
static DEFAULT_OPTIONS = {
|
|
classes: ['actor'],
|
|
position: {
|
|
width: 480
|
|
},
|
|
form: {
|
|
submitOnChange: true
|
|
},
|
|
actions: {
|
|
openSettings: DHBaseActorSheet.#openSettings,
|
|
sendExpToChat: DHBaseActorSheet.#sendExpToChat,
|
|
increaseActionUses: event => DHBaseActorSheet.#modifyActionUses(event, true)
|
|
},
|
|
contextMenus: [
|
|
{
|
|
handler: DHBaseActorSheet.#getFeatureContextOptions,
|
|
selector: '[data-item-uuid][data-type="feature"]',
|
|
options: {
|
|
parentClassHooks: false,
|
|
fixed: true
|
|
}
|
|
}
|
|
],
|
|
dragDrop: [{ dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null }]
|
|
};
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**@type {typeof DHBaseActorSettings}*/
|
|
#settingSheet;
|
|
|
|
/**@returns {DHBaseActorSettings|null} */
|
|
get settingSheet() {
|
|
const SheetClass = this.document.system.metadata.settingSheet;
|
|
return (this.#settingSheet ??= SheetClass ? new SheetClass({ document: this.document }) : null);
|
|
}
|
|
|
|
get isVisible() {
|
|
const viewPermission = this.document.testUserPermission(game.user, this.options.viewPermission);
|
|
const limitedOnly = this.document.testUserPermission(game.user, this.options.viewPermission, { exact: true });
|
|
return limitedOnly ? this.document.system.metadata.hasLimitedView : viewPermission;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
/* Prepare Context */
|
|
/* -------------------------------------------- */
|
|
|
|
/**@inheritdoc */
|
|
async _prepareContext(_options) {
|
|
const context = await super._prepareContext(_options);
|
|
context.isNPC = this.document.isNPC;
|
|
context.useResourcePips = game.settings.get(
|
|
CONFIG.DH.id,
|
|
CONFIG.DH.SETTINGS.gameSettings.appearance
|
|
).useResourcePips;
|
|
context.showAttribution = !game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
|
|
.hideAttribution;
|
|
|
|
// Prepare inventory data
|
|
if (['party', 'character'].includes(this.document.type)) {
|
|
context.inventory = {
|
|
currencies: {},
|
|
weapons: this.document.itemTypes.weapon.sort((a, b) => a.sort - b.sort),
|
|
armor: this.document.itemTypes.armor.sort((a, b) => a.sort - b.sort),
|
|
consumables: this.document.itemTypes.consumable.sort((a, b) => a.sort - b.sort),
|
|
loot: this.document.itemTypes.loot.sort((a, b) => a.sort - b.sort)
|
|
};
|
|
const { title, ...currencies } = game.settings.get(
|
|
CONFIG.DH.id,
|
|
CONFIG.DH.SETTINGS.gameSettings.Homebrew
|
|
).currency;
|
|
for (const key in currencies) {
|
|
context.inventory.currencies[key] = {
|
|
...currencies[key],
|
|
field: context.systemFields.gold.fields[key],
|
|
value: context.source.system.gold[key]
|
|
};
|
|
}
|
|
}
|
|
|
|
return context;
|
|
}
|
|
|
|
/**@inheritdoc */
|
|
async _preparePartContext(partId, context, options) {
|
|
context = await super._preparePartContext(partId, context, options);
|
|
switch (partId) {
|
|
case 'effects':
|
|
await this._prepareEffectsContext(context, options);
|
|
break;
|
|
}
|
|
return context;
|
|
}
|
|
|
|
_configureRenderParts(options) {
|
|
const parts = super._configureRenderParts(options);
|
|
if (!this.document.system.metadata.hasLimitedView) return parts;
|
|
|
|
if (this.document.testUserPermission(game.user, 'LIMITED', { exact: true })) return { limited: parts.limited };
|
|
|
|
return Object.keys(parts).reduce((acc, key) => {
|
|
if (key !== 'limited') acc[key] = parts[key];
|
|
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
async _onRender(context, options) {
|
|
await super._onRender(context, options);
|
|
|
|
if (
|
|
this.document.system.metadata.hasLimitedView &&
|
|
this.document.testUserPermission(game.user, 'LIMITED', { exact: true })
|
|
) {
|
|
this.element.classList = `${this.element.classList} limited`;
|
|
}
|
|
}
|
|
|
|
/**@inheritdoc */
|
|
_attachPartListeners(partId, htmlElement, options) {
|
|
super._attachPartListeners(partId, htmlElement, options);
|
|
|
|
htmlElement.querySelectorAll('.item-button .action-uses-button').forEach(element => {
|
|
element.addEventListener('contextmenu', DHBaseActorSheet.#modifyActionUses);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Prepare render context for the Effect part.
|
|
* @param {ApplicationRenderContext} context
|
|
* @param {ApplicationRenderOptions} options
|
|
* @returns {Promise<void>}
|
|
* @protected
|
|
*/
|
|
async _prepareEffectsContext(context, _options) {
|
|
context.effects = {
|
|
actives: [],
|
|
inactives: []
|
|
};
|
|
|
|
for (const effect of this.actor.allApplicableEffects()) {
|
|
const list = effect.active ? context.effects.actives : context.effects.inactives;
|
|
list.push(effect);
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
/* Context Menu */
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Get the set of ContextMenu options for Features.
|
|
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
|
|
* @this {DHBaseActorSheet}
|
|
* @protected
|
|
*/
|
|
static #getFeatureContextOptions() {
|
|
return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true });
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
/* Application Clicks Actions */
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Open the Actor Setting Sheet
|
|
* @type {ApplicationClickAction}
|
|
*/
|
|
static async #openSettings() {
|
|
await this.settingSheet.render({ force: true });
|
|
}
|
|
|
|
/**
|
|
* Send Experience to Chat
|
|
* @type {ApplicationClickAction}
|
|
*/
|
|
static async #sendExpToChat(_, button) {
|
|
const experience = this.document.system.experiences[button.dataset.id];
|
|
const cls = getDocumentClass('ChatMessage');
|
|
|
|
const systemData = {
|
|
actor: { name: this.actor.name, img: this.actor.img },
|
|
author: game.users.get(game.user.id),
|
|
action: {
|
|
name: `${experience.name} ${experience.value.signedString()}`,
|
|
img: '/icons/sundries/misc/admission-ticket-blue.webp'
|
|
},
|
|
itemOrigin: {
|
|
name: game.i18n.localize('DAGGERHEART.GENERAL.Experience.single')
|
|
},
|
|
description: experience.description
|
|
};
|
|
|
|
const msg = {
|
|
user: game.user.id,
|
|
content: await foundry.applications.handlebars.renderTemplate(
|
|
'systems/daggerheart/templates/ui/chat/action.hbs',
|
|
systemData
|
|
),
|
|
title: game.i18n.localize('DAGGERHEART.ACTIONS.Config.displayInChat'),
|
|
speaker: cls.getSpeaker(),
|
|
flags: {
|
|
daggerheart: {
|
|
cssClass: 'dh-chat-message dh-style'
|
|
}
|
|
}
|
|
};
|
|
|
|
cls.create(msg);
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
static async #modifyActionUses(event, increase) {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
const actionId = event.target.dataset.itemUuid;
|
|
const action = await foundry.utils.fromUuid(actionId);
|
|
|
|
const newValue = (action.uses.value ?? 0) + (increase ? 1 : -1);
|
|
await action.update({ 'uses.value': Math.min(Math.max(newValue, 0), action.uses.max ?? 0) });
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
/* Application Drag/Drop */
|
|
/* -------------------------------------------- */
|
|
|
|
async _onDropItem(event, item) {
|
|
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
|
|
const physicalActorTypes = ['character', 'party'];
|
|
const originActor = item.actor;
|
|
if (
|
|
item.actor?.uuid === this.document.uuid ||
|
|
!originActor ||
|
|
!physicalActorTypes.includes(this.document.type)
|
|
) {
|
|
return super._onDropItem(event, item);
|
|
}
|
|
|
|
/* Handling transfer of inventoryItems */
|
|
if (item.system.metadata.isInventoryItem) {
|
|
if (item.system.metadata.isQuantifiable) {
|
|
const actorItem = originActor.items.get(data.originId);
|
|
const quantityTransfered =
|
|
actorItem.system.quantity === 1
|
|
? 1
|
|
: await game.system.api.applications.dialogs.ItemTransferDialog.configure(item);
|
|
|
|
if (quantityTransfered) {
|
|
if (quantityTransfered === actorItem.system.quantity) {
|
|
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
|
|
} else {
|
|
await actorItem.update({
|
|
'system.quantity': actorItem.system.quantity - quantityTransfered
|
|
});
|
|
}
|
|
|
|
const existingItem = this.document.items.find(x => itemIsIdentical(x, item));
|
|
if (existingItem) {
|
|
await existingItem.update({
|
|
'system.quantity': existingItem.system.quantity + quantityTransfered
|
|
});
|
|
} else {
|
|
const createData = item.toObject();
|
|
await this.document.createEmbeddedDocuments('Item', [
|
|
{
|
|
...createData,
|
|
system: {
|
|
...createData.system,
|
|
quantity: quantityTransfered
|
|
}
|
|
}
|
|
]);
|
|
}
|
|
}
|
|
} else {
|
|
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
|
|
await this.document.createEmbeddedDocuments('Item', [item.toObject()]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* On dragStart on the item.
|
|
* @param {DragEvent} event - The drag event
|
|
*/
|
|
async _onDragStart(event) {
|
|
const attackItem = event.currentTarget.closest('.inventory-item[data-type="attack"]');
|
|
if (attackItem) {
|
|
const attackData = {
|
|
type: 'Attack',
|
|
actorUuid: this.document.uuid,
|
|
img: this.document.system.attack.img,
|
|
fromInternal: true
|
|
};
|
|
event.dataTransfer.setData('text/plain', JSON.stringify(attackData));
|
|
event.dataTransfer.setDragImage(attackItem.querySelector('img'), 60, 0);
|
|
return;
|
|
}
|
|
|
|
const item = await getDocFromElement(event.target);
|
|
if (item) {
|
|
const dragData = {
|
|
originActor: this.document.uuid,
|
|
originId: item.id,
|
|
type: item.documentName,
|
|
uuid: item.uuid
|
|
};
|
|
event.dataTransfer.setData('text/plain', JSON.stringify(dragData));
|
|
}
|
|
|
|
super._onDragStart(event);
|
|
}
|
|
}
|