const { HandlebarsApplicationMixin } = foundry.applications.api import { BoLRoll } from "../../controllers/bol-rolls.js" import { BoLUtility } from "../../system/bol-utility.js" /** * Base Actor Sheet for BoL system using AppV2 * @extends {ActorSheetV2} */ export default class BoLBaseActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) { constructor(options = {}) { super(options) this.#dragDrop = this.#createDragDropHandlers() } #dragDrop /** @override */ static DEFAULT_OPTIONS = { classes: ["bol", "sheet", "actor"], position: { width: 836, height: 807, }, form: { submitOnChange: true, closeOnSubmit: false, }, window: { resizable: true, }, tabs: [ { navSelector: "nav[data-group=\"primary\"]", contentSelector: "section.sheet-body", initial: "stats", }, ], dragDrop: [{ dragSelector: ".items-list .item", dropSelector: null }], actions: {}, } /** Tab groups state */ tabGroups = { primary: "stats" } /** @override */ async _prepareContext() { const actor = this.document return { actor, system: actor.system, config: game.bol.config, isGM: game.user.isGM, isEditable: this.isEditable, owner: this.document.isOwner, cssClass: this.options.classes.join(" "), } } /** @override */ _onRender(context, options) { super._onRender(context, options) this.#dragDrop.forEach((d) => d.bind(this.element)) this._activateTabs() this._activateListeners() this._applyBackgroundImage() this._activateImageEdit() } /** * Apply background image to the actor form * @private */ _applyBackgroundImage() { const logoUrl = BoLUtility.getLogoActorSheet() const form = this.element.querySelector(".bol-actor-form") if (form) form.style.backgroundImage = `url(${logoUrl})` } /** * Activate image editing via FilePicker * @private */ _activateImageEdit() { const img = this.element.querySelector('[data-edit="img"]') if (img && this.isEditable) { img.style.cursor = "pointer" img.addEventListener("click", () => { new FilePicker({ type: "image", current: this.document.img, callback: (path) => this.document.update({ img: path }), }).browse() }) } } /** * Activate tab navigation * @private */ _activateTabs() { const nav = this.element.querySelector("nav[data-group]") if (!nav) return const group = nav.dataset.group const activeTab = this.tabGroups[group] || "stats" nav.querySelectorAll("[data-tab]").forEach((link) => { const tab = link.dataset.tab link.classList.toggle("active", tab === activeTab) link.addEventListener("click", (event) => { event.preventDefault() this.tabGroups[group] = tab this.render() }) }) this.element.querySelectorAll(`[data-group="${group}"][data-tab]`).forEach((content) => { content.classList.toggle("active", content.dataset.tab === activeTab) }) } /** * Activate event listeners (replaces activateListeners with vanilla DOM) * @private */ _activateListeners() { if (!this.isEditable) return // Item edit this.element.querySelectorAll(".item-edit").forEach((el) => { el.addEventListener("click", (ev) => { const li = ev.currentTarget.closest(".item") const item = this.actor.items.get(li?.dataset.itemId) item?.sheet.render(true) }) }) // Item delete this.element.querySelectorAll(".item-delete").forEach((el) => { el.addEventListener("click", (ev) => { const li = ev.currentTarget.closest(".item") const itemId = li?.dataset.itemId Dialog.confirm({ title: game.i18n.localize("BOL.ui.deletetitle"), content: game.i18n.localize("BOL.ui.confirmdelete"), yes: () => { this.actor.deleteEmbeddedDocuments("Item", [itemId]) li?.remove() }, no: () => {}, defaultYes: false, }) }) }) // Item equip/unequip this.element.querySelectorAll(".item-equip").forEach((el) => { el.addEventListener("click", (ev) => { const li = ev.currentTarget.closest(".item") const item = this.actor.items.get(li?.dataset.itemId) if (item) this.actor.toggleEquipItem(item) }) }) // Toggle fight option this.element.querySelectorAll(".toggle-fight-option").forEach((el) => { el.addEventListener("click", (ev) => { const li = ev.currentTarget.closest(".item") this.actor.toggleFightOption(li?.dataset.itemId) }) }) // Inc/dec alchemy points this.element.querySelectorAll(".inc-dec-btns-alchemy").forEach((el) => { el.addEventListener("click", (ev) => { const li = ev.currentTarget.closest(".item") this.actor.spendAlchemyPoint(li?.dataset.itemId, 1) }) }) // Inc/dec resource buttons this.element.querySelectorAll(".inc-dec-btns-resource").forEach((el) => { el.addEventListener("click", (ev) => { const dataset = ev.currentTarget.dataset this.actor.incDecResources(dataset.target, parseInt(dataset.incr)) }) }) // Generic inc/dec buttons for item fields this.element.querySelectorAll(".inc-dec-btns").forEach((el) => { el.addEventListener("click", (ev) => { const li = ev.currentTarget.closest(".item") if (!li) return const item = this.actor.items.get(li.dataset.itemId) if (!item) return const dataset = ev.currentTarget.dataset const operator = dataset.operator const target = dataset.target const incr = parseInt(dataset.incr) const min = parseInt(dataset.min) const max = parseInt(dataset.max) || 10000 // eslint-disable-next-line no-eval let value = eval("item." + target) || 0 if (operator === "minus") value = value >= min + incr ? value - incr : min if (operator === "plus") value = value <= max - incr ? value + incr : max item.update({ [target]: value }) }) }) // Rollable elements this.element.querySelectorAll(".rollable").forEach((el) => { el.addEventListener("click", (ev) => this._onRoll(ev)) }) } /** * Handle clickable rolls (replaces _onRoll with vanilla DOM) * @param {Event} event * @private */ _onRoll(event) { event.preventDefault() const element = event.currentTarget const dataset = element.dataset const rollType = dataset.rollType const li = element.closest(".item") const itemId = li?.dataset.itemId switch (rollType) { case "attribute": BoLRoll.attributeCheck(this.actor, dataset.key, event) break case "aptitude": BoLRoll.aptitudeCheck(this.actor, dataset.key, event) break case "weapon": BoLRoll.weaponCheck(this.actor, event) break case "spell": BoLRoll.spellCheck(this.actor, event) break case "alchemy": BoLRoll.alchemyCheck(this.actor, event) break case "protection": this.actor.rollProtection(itemId) break case "damage": this.actor.rollWeaponDamage(itemId) break case "aptitudexp": this.actor.incAptitudeXP(dataset.key) break case "attributexp": this.actor.incAttributeXP(dataset.key) break case "careerxp": this.actor.incCareerXP(itemId) break case "horoscope-minor": BoLRoll.horoscopeCheck(this.actor, event, "minor") break case "horoscope-major": BoLRoll.horoscopeCheck(this.actor, event, "major") break case "horoscope-major-group": BoLRoll.horoscopeCheck(this.actor, event, "majorgroup") break case "bougette": this.actor.rollBougette() break default: break } } // #region Drag-and-Drop #createDragDropHandlers() { return (this.options.dragDrop || []).map((dragDrop) => new foundry.applications.ux.DragDrop.implementation({ ...dragDrop, permissions: { dragstart: this._canDragStart.bind(this), drop: this._canDragDrop.bind(this), }, callbacks: { dragstart: this._onDragStart.bind(this), dragover: this._onDragOver.bind(this), drop: this._onDrop.bind(this), }, }) ) } _canDragStart(selector) { return this.isEditable } _canDragDrop(selector) { return this.isEditable } // #endregion }