const { HandlebarsApplicationMixin } = foundry.applications.api import { HawkmoonUtility } from "../../hawkmoon-utility.js" import { HawkmoonAutomation } from "../../hawkmoon-automation.js" export default class HawkmoonActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) { /** * Different sheet modes. * @enum {number} */ static SHEET_MODES = { EDIT: 0, PLAY: 1 } constructor(options = {}) { super(options) this.#dragDrop = this.#createDragDropHandlers() this._sheetMode = this.constructor.SHEET_MODES.PLAY // Commencer en mode visualisation } #dragDrop /** @override */ static DEFAULT_OPTIONS = { classes: ["fvtt-hawkmoon-cyd", "sheet", "actor"], position: { width: 640, height: 720, }, window: { resizable: true, }, form: { submitOnChange: true, closeOnSubmit: false, }, dragDrop: [{ dragSelector: ".item-list .item", dropSelector: "form" }], actions: { editImage: HawkmoonActorSheet.#onEditImage, toggleSheet: HawkmoonActorSheet.#onToggleSheet, editItem: HawkmoonActorSheet.#onEditItem, deleteItem: HawkmoonActorSheet.#onDeleteItem, createItem: HawkmoonActorSheet.#onCreateItem, equipItem: HawkmoonActorSheet.#onEquipItem, modifyQuantity: HawkmoonActorSheet.#onModifyQuantity, modifyAdversite: HawkmoonActorSheet.#onModifyAdversite, rollInitiative: HawkmoonActorSheet.#onRollInitiative, rollAttribut: HawkmoonActorSheet.#onRollAttribut, rollCompetence: HawkmoonActorSheet.#onRollCompetence, rollArmeOffensif: HawkmoonActorSheet.#onRollArmeOffensif, rollArmeDegats: HawkmoonActorSheet.#onRollArmeDegats, rollAssommer: HawkmoonActorSheet.#onRollAssommer, rollCoupBas: HawkmoonActorSheet.#onRollCoupBas, rollImmobiliser: HawkmoonActorSheet.#onRollImmobiliser, rollRepousser: HawkmoonActorSheet.#onRollRepousser, rollDesengager: HawkmoonActorSheet.#onRollDesengager, }, } /** * Is the sheet currently in 'Play' mode? * @type {boolean} */ get isPlayMode() { // Initialize if not set if (this._sheetMode === undefined) this._sheetMode = this.constructor.SHEET_MODES.PLAY return this._sheetMode === this.constructor.SHEET_MODES.PLAY } /** * Is the sheet currently in 'Edit' mode? * @type {boolean} */ get isEditMode() { // Initialize if not set if (this._sheetMode === undefined) this._sheetMode = this.constructor.SHEET_MODES.PLAY return this._sheetMode === this.constructor.SHEET_MODES.EDIT } /** * Tab groups state * @type {object} */ tabGroups = { primary: "principal" } /** @override */ async _prepareContext() { const actor = this.document const context = { actor: actor, system: actor.system, source: actor.toObject(), fields: actor.schema.fields, systemFields: actor.system.schema.fields, isEditable: this.isEditable, isEditMode: this.isEditMode, isPlayMode: this.isPlayMode, isGM: game.user.isGM, config: CONFIG.HAWKMOON, enrichedDescription: await foundry.applications.ux.TextEditor.implementation.enrichHTML(actor.system.biodata?.description || "", { async: true }), enrichedHabitat: await foundry.applications.ux.TextEditor.implementation.enrichHTML(actor.system.biodata?.habitat || "", { async: true }), } return context } /** @override */ _onRender(context, options) { super._onRender(context, options) // Activate drag & drop handlers this.#dragDrop.forEach(d => d.bind(this.element)) // Manual tab navigation const html = this.element const tabLinks = html.querySelectorAll('a.item[data-tab]') const tabContents = html.querySelectorAll('.tab[data-tab]') // Hide all tabs initially tabContents.forEach(tab => { tab.classList.remove('active') tab.style.display = 'none' }) // Show active tab const activeTab = this.tabGroups.primary const activeTabContent = html.querySelector(`.tab[data-tab="${activeTab}"]`) if (activeTabContent) { activeTabContent.classList.add('active') activeTabContent.style.display = 'block' } // Activate the corresponding nav link tabLinks.forEach(link => { if (link.dataset.tab === activeTab) { link.classList.add('active') } else { link.classList.remove('active') } }) // Tab click handler tabLinks.forEach(link => { link.addEventListener('click', (event) => { event.preventDefault() const tab = link.dataset.tab // Update state this.tabGroups.primary = tab // Hide all tabs tabContents.forEach(t => { t.classList.remove('active') t.style.display = 'none' }) // Show selected tab const selectedTab = html.querySelector(`.tab[data-tab="${tab}"]`) if (selectedTab) { selectedTab.classList.add('active') selectedTab.style.display = 'block' } // Update nav links tabLinks.forEach(l => { if (l.dataset.tab === tab) { l.classList.add('active') } else { l.classList.remove('active') } }) }) }) // Inline item editing html.querySelectorAll('.edit-item-data').forEach(input => { input.addEventListener('change', (event) => { const li = event.target.closest('.item') const itemId = li.dataset.itemId const itemType = li.dataset.itemType const itemField = event.target.dataset.itemField const dataType = event.target.dataset.dtype const value = event.target.value this.actor.editItemField(itemId, itemType, itemField, dataType, value) }) }) } // #region Drag & Drop /** * Create drag-and-drop workflow handlers for this Application * @returns {DragDrop[]} An array of DragDrop handlers * @private */ #createDragDropHandlers() { return this.options.dragDrop.map((d) => { d.permissions = { dragstart: this._canDragStart.bind(this), drop: this._canDragDrop.bind(this), } d.callbacks = { dragstart: this._onDragStart.bind(this), drop: this._onDrop.bind(this), } return new foundry.applications.ux.DragDrop(d) }) } /** * Define whether a user is able to begin a dragstart workflow for a given drag selector * @param {string} selector The candidate HTML selector for dragging * @returns {boolean} Can the current user drag this selector? * @protected */ _canDragStart(selector) { return this.isEditable } /** * Define whether a user is able to conclude a drag-and-drop workflow for a given drop selector * @param {string} selector The candidate HTML selector for the drop target * @returns {boolean} Can the current user drop on this selector? * @protected */ _canDragDrop(selector) { return this.isEditable } /** * Callback actions which occur at the beginning of a drag start workflow. * @param {DragEvent} event The originating DragEvent * @protected */ _onDragStart(event) { const li = event.currentTarget.closest(".item") if (!li?.dataset.itemId) return const item = this.actor.items.get(li.dataset.itemId) if (!item) return const dragData = item.toDragData() event.dataTransfer.setData("text/plain", JSON.stringify(dragData)) } /** * Callback actions which occur when a dragged element is dropped on a target. * @param {DragEvent} event The originating DragEvent * @protected */ async _onDrop(event) { const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event) const actor = this.actor // Handle different data types switch (data.type) { case "Item": return this._onDropItem(event, data) case "Actor": return this._onDropActor(event, data) case "ActiveEffect": return this._onDropActiveEffect(event, data) } } /** * Handle dropping an Item on the actor sheet * @param {DragEvent} event * @param {object} data * @private */ async _onDropItem(event, data) { if (!this.actor.isOwner) return false let item = await fromUuid(data.uuid) if (item.pack) { item = await HawkmoonUtility.searchItem(item) } const autoresult = HawkmoonAutomation.processAutomations("on-drop", item, this.actor) if (autoresult.isValid) { // In AppV2, we need to get the item data differently const itemData = item.toObject ? item.toObject() : item return this.actor.createEmbeddedDocuments("Item", [itemData]) } else { ui.notifications.warn(autoresult.warningMessage) return false } } /** * Handle dropping an Actor on the sheet * @param {DragEvent} event * @param {object} data * @private */ async _onDropActor(event, data) { // To be implemented by subclasses if needed return false } /** * Handle dropping an ActiveEffect on the sheet * @param {DragEvent} event * @param {object} data * @private */ async _onDropActiveEffect(event, data) { // To be implemented by subclasses if needed return false } // #endregion // #region Action Handlers /** * Toggle between edit and play mode * @param {Event} event * @param {HTMLElement} target * @private */ static #onToggleSheet(event, target) { console.log("Toggle sheet clicked", this) const wasEditMode = this.isEditMode console.log("Current mode:", this._sheetMode, "isEditMode:", wasEditMode, "isPlayMode:", this.isPlayMode) this._sheetMode = wasEditMode ? this.constructor.SHEET_MODES.PLAY : this.constructor.SHEET_MODES.EDIT console.log("New mode set to:", this._sheetMode, "(", wasEditMode ? "PLAY" : "EDIT", ")") console.log("After change - isEditMode:", this.isEditMode, "isPlayMode:", this.isPlayMode) this.render({ force: true }) } /** * Edit the actor image * @param {Event} event * @param {HTMLElement} target * @private */ static async #onEditImage(event, target) { const fp = new FilePicker({ type: "image", current: this.actor.img, callback: (path) => { this.actor.update({ img: path }) }, }) return fp.browse() } /** * Edit an item * @param {Event} event * @param {HTMLElement} target * @private */ static async #onEditItem(event, target) { const li = target.closest(".item") const itemId = li?.dataset.itemId if (!itemId) return const item = this.actor.items.get(itemId) if (item) item.sheet.render(true) } /** * Delete an item * @param {Event} event * @param {HTMLElement} target * @private */ static async #onDeleteItem(event, target) { const li = target.closest(".item") await HawkmoonUtility.confirmDelete(this, li) } /** * Create a new item * @param {Event} event * @param {HTMLElement} target * @private */ static async #onCreateItem(event, target) { const itemType = target.dataset.type await this.actor.createEmbeddedDocuments("Item", [{ name: `Nouveau ${itemType}`, type: itemType }], { renderSheet: true }) } /** * Equip/unequip an item * @param {Event} event * @param {HTMLElement} target * @private */ static async #onEquipItem(event, target) { const li = target.closest(".item") const itemId = li?.dataset.itemId if (itemId) { await this.actor.equipItem(itemId) this.render() } } /** * Modify item quantity * @param {Event} event * @param {HTMLElement} target * @private */ static async #onModifyQuantity(event, target) { const li = target.closest(".item") const itemId = li?.dataset.itemId const value = Number(target.dataset.quantiteValue) if (itemId) { await this.actor.incDecQuantity(itemId, value) } } /** * Modify adversité * @param {Event} event * @param {HTMLElement} target * @private */ static async #onModifyAdversite(event, target) { const li = target.closest(".item") const adv = li?.dataset.adversite const value = Number(target.dataset.adversiteValue) if (adv) { await this.actor.incDecAdversite(adv, value) } } /** * Roll initiative * @param {Event} event * @param {HTMLElement} target * @private */ static async #onRollInitiative(event, target) { await this.actor.rollAttribut("adr", true) } /** * Roll attribut * @param {Event} event * @param {HTMLElement} target * @private */ static async #onRollAttribut(event, target) { const li = target.closest(".item") const attrKey = li?.dataset.attrKey if (attrKey) { await this.actor.rollAttribut(attrKey, false) } } /** * Roll competence * @param {Event} event * @param {HTMLElement} target * @private */ static async #onRollCompetence(event, target) { const li = target.closest(".item") const attrKey = target.dataset.attrKey const compId = li?.dataset.itemId if (attrKey && compId) { await this.actor.rollCompetence(attrKey, compId) } } /** * Roll arme offensif * @param {Event} event * @param {HTMLElement} target * @private */ static async #onRollArmeOffensif(event, target) { const li = target.closest(".item") const armeId = li?.dataset.itemId if (armeId) { await this.actor.rollArmeOffensif(armeId) } } /** * Roll arme degats * @param {Event} event * @param {HTMLElement} target * @private */ static async #onRollArmeDegats(event, target) { const li = target.closest(".item") const armeId = li?.dataset.itemId if (armeId) { await this.actor.rollArmeDegats(armeId) } } /** * Roll assommer * @param {Event} event * @param {HTMLElement} target * @private */ static async #onRollAssommer(event, target) { await this.actor.rollAssommer() } /** * Roll coup bas * @param {Event} event * @param {HTMLElement} target * @private */ static async #onRollCoupBas(event, target) { await this.actor.rollCoupBas() } /** * Roll immobiliser * @param {Event} event * @param {HTMLElement} target * @private */ static async #onRollImmobiliser(event, target) { await this.actor.rollImmobiliser() } /** * Roll repousser * @param {Event} event * @param {HTMLElement} target * @private */ static async #onRollRepousser(event, target) { await this.actor.rollRepousser() } /** * Roll désengager * @param {Event} event * @param {HTMLElement} target * @private */ static async #onRollDesengager(event, target) { await this.actor.rollDesengager() } // #endregion }