Files
DeltaGreen/module/sheets/base-item-sheet.js
T
Aaron Lutomski c197b83ec5 Refactor and unify some item styling.
Restyle profession item.

Restyle skills tab.
2026-05-29 22:51:56 -07:00

293 lines
8.4 KiB
JavaScript

import { BASE_TEMPLATE_PATH } from "../config/index.js";
import DGSheetMixin from "./base-sheet.js";
import EffectsTabMixin from "./mixins/effects-tab-mixin.js";
import ProfessionItemMixin from "./mixins/profession-item-mixin.js";
import { getDGRollToken } from "../chat/dg-chat-card.js";
import { createDGRollFromDataset, processDGRoll } from "../roll/roll.js";
const { ItemSheetV2 } = foundry.applications.sheets;
const ITEM_PARTS_PATH = `${BASE_TEMPLATE_PATH}/item/parts`;
const TAB_NAV_TEMPLATE = "templates/generic/tab-navigation.hbs";
/** Tab group id used by item sheet templates (`data-group="primary"`). */
const ITEM_TAB_GROUP = "primary";
const EFFECTS_TAB = /** @type {const} */ ({
id: "effects",
label: "DG.Navigation.Item.effects",
});
const EFFECTS_PART = /** @type {const} */ ({
effects: {
template: `${ITEM_PARTS_PATH}/effects-tab.html`,
scrollable: [""],
},
});
const TABS_PART = /** @type {const} */ ({
tabs: { template: TAB_NAV_TEMPLATE },
});
/**
* @param {object} config
* @param {string} config.header
* @param {string} config.description
* @param {string} config.descriptionLabel
* @returns {object}
*/
function activeEffectItemLayout({ header, description, descriptionLabel }) {
return {
tabs: {
initial: "description",
tabs: [
{ id: "description", label: descriptionLabel },
EFFECTS_TAB,
],
},
parts: {
header: { template: header },
...TABS_PART,
description: { template: description, scrollable: [""] },
...EFFECTS_PART,
},
};
}
/** Tab and part layout per item type. */
const ITEM_SHEET_LAYOUT = /** @type {const} */ ({
weapon: activeEffectItemLayout({
header: `${ITEM_PARTS_PATH}/weapon-header.html`,
description: `${ITEM_PARTS_PATH}/weapon-description.html`,
descriptionLabel: "DG.ItemWindow.Weapons.Description",
}),
gear: activeEffectItemLayout({
header: `${ITEM_PARTS_PATH}/gear-header.html`,
description: `${ITEM_PARTS_PATH}/gear-description.html`,
descriptionLabel: "DG.ItemWindow.Gear.Description",
}),
armor: activeEffectItemLayout({
header: `${ITEM_PARTS_PATH}/armor-header.html`,
description: `${ITEM_PARTS_PATH}/armor-description.html`,
descriptionLabel: "DG.ItemWindow.Armor.Description",
}),
bond: activeEffectItemLayout({
header: `${ITEM_PARTS_PATH}/bond-header.html`,
description: `${ITEM_PARTS_PATH}/bond-description.html`,
descriptionLabel: "DG.ItemWindow.Bonds.Description",
}),
motivation: activeEffectItemLayout({
header: `${ITEM_PARTS_PATH}/motivation-header.html`,
description: `${ITEM_PARTS_PATH}/motivation-description.html`,
descriptionLabel: "DG.ItemWindow.Motivations.Description",
}),
tome: {
tabs: {
initial: "description",
tabs: [
{ id: "description", label: "DG.ItemWindow.Tome.Description" },
{
id: "handler",
label: "DG.ItemWindow.Tome.HandlerOnlyTabHeader",
gmOnly: true,
},
],
},
parts: {
header: { template: `${ITEM_PARTS_PATH}/tome-header.html` },
...TABS_PART,
description: {
template: `${ITEM_PARTS_PATH}/tome-description.html`,
scrollable: [""],
},
handler: {
template: `${ITEM_PARTS_PATH}/tome-handler.html`,
scrollable: [""],
},
},
},
ritual: {
tabs: {
initial: "description",
tabs: [
{ id: "description", label: "DG.ItemWindow.Tome.Description" },
{
id: "handler",
label: "DG.ItemWindow.Tome.HandlerOnlyTabHeader",
gmOnly: true,
},
],
},
parts: {
header: { template: `${ITEM_PARTS_PATH}/ritual-header.html` },
...TABS_PART,
description: {
template: `${ITEM_PARTS_PATH}/ritual-description.html`,
scrollable: [""],
},
handler: {
template: `${ITEM_PARTS_PATH}/ritual-handler.html`,
scrollable: [""],
},
},
},
profession: {
tabs: {
initial: "description",
tabs: [
{ id: "description", label: "DG.ItemWindow.Profession.Description" },
{ id: "skills", label: "DG.ItemWindow.Profession.SkillsTab" },
],
},
parts: {
header: { template: `${ITEM_PARTS_PATH}/profession-header.html` },
...TABS_PART,
description: {
template: `${ITEM_PARTS_PATH}/profession-description.html`,
scrollable: [""],
},
skills: {
template: `${ITEM_PARTS_PATH}/profession-skills-tab.html`,
scrollable: [".profession-skills-scroll"],
},
},
},
});
const ItemSheetBase = ProfessionItemMixin(
EffectsTabMixin(DGSheetMixin(ItemSheetV2)),
);
/**
* @extends {ItemSheetBase}
*/
export default class DGItemSheet extends ItemSheetBase {
/** @override */
static DEFAULT_OPTIONS = /** @type {const} */ ({
classes: ["item"],
position: { width: 520, height: 600 },
actions: {
roll: DGItemSheet._onRoll,
toggleTomeRevealed: DGItemSheet._toggleTomeRevealed,
addAutomaticSkill: DGItemSheet.addAutomaticSkill,
addOptionSkill: DGItemSheet.addOptionSkill,
removeAutomaticSkill: DGItemSheet.removeAutomaticSkill,
removeOptionSkill: DGItemSheet.removeOptionSkill,
createEffect: ItemSheetBase.createEffect,
openEffect: ItemSheetBase.openEffect,
deleteEffect: ItemSheetBase.deleteEffect,
toggleEffect: ItemSheetBase.toggleEffect,
},
});
/** @inheritdoc */
get item() {
return this.document;
}
/** @inheritdoc */
_configureRenderParts(options) {
const layout = ITEM_SHEET_LAYOUT[this.item.type];
if (!layout) return super._configureRenderParts(options);
const parts = foundry.utils.deepClone(layout.parts);
if (layout.tabs) {
const tabIds = layout.tabs.tabs
.filter((tab) => !tab.gmOnly || game.user.isGM)
.map((tab) => tab.id);
for (const partId of Object.keys(parts)) {
if (partId === "tabs" || partId === "header") continue;
if (!tabIds.includes(partId)) delete parts[partId];
}
}
Object.values(parts).forEach((p) => {
p.templates ??= [];
});
return parts;
}
/** @inheritdoc */
_getTabsConfig(group) {
const layout = ITEM_SHEET_LAYOUT[this.item.type];
if (!layout?.tabs || group !== ITEM_TAB_GROUP) {
return super._getTabsConfig(group);
}
const tabs = layout.tabs.tabs.filter((tab) => !tab.gmOnly || game.user.isGM);
return { ...layout.tabs, tabs };
}
/** @inheritdoc */
async _preparePartContext(partId, context, options) {
const partContext = await super._preparePartContext(partId, context, options);
if (partId === "tabs") partContext.tabClasses = "sheet-tabs";
return partContext;
}
/** @inheritdoc */
async _prepareContext(options) {
const context = await super._prepareContext(options);
const layout = ITEM_SHEET_LAYOUT[this.item.type];
if (layout?.tabs) {
const { initial } = layout.tabs;
const tabIds = layout.tabs.tabs
.filter((tab) => !tab.gmOnly || game.user.isGM)
.map((tab) => tab.id);
if (!tabIds.includes(this.tabGroups[ITEM_TAB_GROUP])) {
this.tabGroups[ITEM_TAB_GROUP] = initial;
}
context.tabs = this._prepareTabs(ITEM_TAB_GROUP);
}
context.owner = this.document.isOwner;
return context;
}
/** @inheritdoc */
async _onFirstRender(context, options) {
await super._onFirstRender(context, options);
this._setRightClickListeners();
}
/**
* Right-click on roll controls opens the modifier dialog.
* @returns {void}
*/
_setRightClickListeners() {
this.element.addEventListener("contextmenu", (event) => {
const target = event.target.closest("[data-action='roll']");
if (!target) return;
event.preventDefault();
DGItemSheet._onRoll.call(this, event, target);
});
}
/**
* @param {PointerEvent} event
* @param {HTMLElement} target
*/
static async _onRoll(event, target) {
if (event.which === 2) return;
const roll = createDGRollFromDataset(target.dataset, {
actor: this.actor,
item: this.item,
element: target,
sanityDamageSource: "item",
token: getDGRollToken(this.actor, this.token),
});
await processDGRoll(event, roll);
}
/**
* Toggle whether tome/ritual details are visible to players.
* @param {PointerEvent} event
*/
static _toggleTomeRevealed(event) {
event.preventDefault();
const revealed = !this.item.system.revealed;
this.item.update({ "system.revealed": revealed });
}
}