const { HandlebarsApplicationMixin } = foundry.applications.api import { HeritiersUtility } from "../../heritiers-utility.js" export default class HeritiersActorSheet 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 } #dragDrop /** @override */ static DEFAULT_OPTIONS = { classes: ["fvtt-les-heritiers", "sheet", "actor"], position: { width: 780, height: 840, }, window: { resizable: true, }, form: { submitOnChange: true, closeOnSubmit: false, }, dragDrop: [{ dragSelector: ".item-list .item", dropSelector: "form" }], actions: { editImage: HeritiersActorSheet.#onEditImage, toggleSheet: HeritiersActorSheet.#onToggleSheet, editItem: HeritiersActorSheet.#onEditItem, deleteItem: HeritiersActorSheet.#onDeleteItem, createItem: HeritiersActorSheet.#onCreateItem, equipItem: HeritiersActorSheet.#onEquipItem, modifyQuantity: HeritiersActorSheet.#onModifyQuantity, quantityIncrease: HeritiersActorSheet.#onQuantityIncrease, quantityDecrease: HeritiersActorSheet.#onQuantityDecrease, pvIncrease: HeritiersActorSheet.#onPvIncrease, pvDecrease: HeritiersActorSheet.#onPvDecrease, rollInitiative: HeritiersActorSheet.#onRollInitiative, rollCarac: HeritiersActorSheet.#onRollCarac, rollRang: HeritiersActorSheet.#onRollRang, rollRootCompetence: HeritiersActorSheet.#onRollRootCompetence, rollCompetence: HeritiersActorSheet.#onRollCompetence, rollSort: HeritiersActorSheet.#onRollSort, rollAttaqueArme: HeritiersActorSheet.#onRollAttaqueArme, rollAttaqueBrutaleArme: HeritiersActorSheet.#onRollAttaqueBrutaleArme, rollAttaqueChargeArme: HeritiersActorSheet.#onRollAttaqueChargeArme, rollAssomerArme: HeritiersActorSheet.#onRollAssomerArme, rollPouvoir: HeritiersActorSheet.#onRollPouvoir, toggleMasque: HeritiersActorSheet.#onToggleMasque, dialogRecupUsage: HeritiersActorSheet.#onDialogRecupUsage, }, } /** * Is the sheet currently in 'Play' mode? * @type {boolean} */ get isPlayMode() { 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() { 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: "competences" } /** @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.HERITIERS, 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 }), enrichedRevesetranges: await foundry.applications.ux.TextEditor.implementation.enrichHTML(actor.system.biodata?.revesetranges || "", { async: true }), enrichedSecretsdecouverts: await foundry.applications.ux.TextEditor.implementation.enrichHTML(actor.system.biodata?.secretsdecouverts || "", { async: true }), enrichedQuestions: await foundry.applications.ux.TextEditor.implementation.enrichHTML(actor.system.biodata?.questions || "", { async: true }), enrichedPlayernotes: await foundry.applications.ux.TextEditor.implementation.enrichHTML(actor.system.biodata?.playernotes || "", { 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('.sheet-tabs a.item[data-tab]') const tabContents = html.querySelectorAll('.sheet-body .tab[data-group="primary"]') // 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-group="primary"][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-group="primary"][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 if (itemId && itemType && itemField) { 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 HeritiersUtility.searchItem(item) } const itemData = item.toObject ? item.toObject() : item return this.actor.createEmbeddedDocuments("Item", [itemData]) } /** * Handle dropping an Actor on the sheet * @param {DragEvent} event * @param {object} data * @private */ async _onDropActor(event, data) { return false } /** * Handle dropping an ActiveEffect on the sheet * @param {DragEvent} event * @param {object} data * @private */ async _onDropActiveEffect(event, data) { return false } // #endregion // #region Action Handlers /** * Toggle between edit and play mode * @param {Event} event * @param {HTMLElement} target * @private */ static #onToggleSheet(event, target) { const wasEditMode = this.isEditMode this._sheetMode = wasEditMode ? this.constructor.SHEET_MODES.PLAY : this.constructor.SHEET_MODES.EDIT 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 HeritiersUtility.confirmDelete(this, li) } /** * Create a new item * @param {Event} event * @param {HTMLElement} target * @private */ static async #onCreateItem(event, target) { const itemType = target.dataset.type // Cas spécial pour les sorts avec une compétence spécifique if (itemType === "sort" && target.dataset.sortCompetence) { const sortCompetence = target.dataset.sortCompetence await this.actor.createEmbeddedDocuments('Item', [{ name: `Nouveau ${itemType} de ${sortCompetence}`, type: itemType, system: { competence: sortCompetence } }], { renderSheet: true }) return } 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) } } /** * Increase item quantity * @param {Event} event * @param {HTMLElement} target * @private */ static async #onQuantityIncrease(event, target) { const li = target.closest(".item") const itemId = li?.dataset.itemId if (itemId) { await this.actor.incDecQuantity(itemId, 1) } } /** * Decrease item quantity * @param {Event} event * @param {HTMLElement} target * @private */ static async #onQuantityDecrease(event, target) { const li = target.closest(".item") const itemId = li?.dataset.itemId if (itemId) { await this.actor.incDecQuantity(itemId, -1) } } /** * Increase PV * @param {Event} event * @param {HTMLElement} target * @private */ static async #onPvIncrease(event, target) { const currentPv = this.actor.system.pv.value || 0 const maxPv = this.actor.system.pv.max || 0 const newPv = Math.min(currentPv + 1, maxPv) await this.actor.update({ 'system.pv.value': newPv }) } /** * Decrease PV * @param {Event} event * @param {HTMLElement} target * @private */ static async #onPvDecrease(event, target) { const currentPv = this.actor.system.pv.value || 0 const newPv = Math.max(currentPv - 1, 0) await this.actor.update({ 'system.pv.value': newPv }) } /** * Roll initiative * @param {Event} event * @param {HTMLElement} target * @private */ static async #onRollInitiative(event, target) { await this.actor.rollInitiative() } /** * Roll caractéristique * @param {Event} event * @param {HTMLElement} target * @private */ static async #onRollCarac(event, target) { const key = target.dataset.key if (key) { await this.actor.rollCarac(key, false) } } /** * Roll rang * @param {Event} event * @param {HTMLElement} target * @private */ static async #onRollRang(event, target) { const key = target.dataset.rangKey if (key) { await this.actor.rollRang(key) } } /** * Roll root competence * @param {Event} event * @param {HTMLElement} target * @private */ static async #onRollRootCompetence(event, target) { const compKey = target.dataset.attrKey if (compKey) { await this.actor.rollRootCompetence(compKey) } } /** * Roll competence * @param {Event} event * @param {HTMLElement} target * @private */ static async #onRollCompetence(event, target) { const li = target.closest(".item") const compId = li?.dataset.itemId if (compId) { await this.actor.rollCompetence(compId) } } /** * Roll sort * @param {Event} event * @param {HTMLElement} target * @private */ static async #onRollSort(event, target) { const li = target.closest(".item") const sortId = li?.dataset.itemId if (sortId) { await this.actor.rollSort(sortId) } } /** * Roll attaque arme * @param {Event} event * @param {HTMLElement} target * @private */ static async #onRollAttaqueArme(event, target) { const li = target.closest(".item") const armeId = li?.dataset.itemId if (armeId) { await this.actor.rollAttaqueArme(armeId) } } /** * Roll attaque brutale arme * @param {Event} event * @param {HTMLElement} target * @private */ static async #onRollAttaqueBrutaleArme(event, target) { const li = target.closest(".item") const armeId = li?.dataset.itemId if (armeId) { await this.actor.rollAttaqueBrutaleArme(armeId) } } /** * Roll attaque charge arme * @param {Event} event * @param {HTMLElement} target * @private */ static async #onRollAttaqueChargeArme(event, target) { const li = target.closest(".item") const armeId = li?.dataset.itemId if (armeId) { await this.actor.rollAttaqueChargeArme(armeId) } } /** * Roll assomer arme * @param {Event} event * @param {HTMLElement} target * @private */ static async #onRollAssomerArme(event, target) { const li = target.closest(".item") const armeId = li?.dataset.itemId if (armeId) { await this.actor.rollAssomerArme(armeId) } } /** * Roll pouvoir * @param {Event} event * @param {HTMLElement} target * @private */ static async #onRollPouvoir(event, target) { const li = target.closest(".item") const pouvoirId = li?.dataset.itemId if (pouvoirId) { await this.actor.rollPouvoir(pouvoirId) } } /** * Toggle masque * @param {Event} event * @param {HTMLElement} target * @private */ static async #onToggleMasque(event, target) { await this.actor.toggleMasqueStatut() this.render() } /** * Dialog récupération usage * @param {Event} event * @param {HTMLElement} target * @private */ static async #onDialogRecupUsage(event, target) { new Dialog({ title: "Récupération des Points d'Usage", content: "
Combien de Points d'Usage souhaitez-vous récupérer ?
", buttons: { one: { icon: '', label: "1 Point", callback: () => { this.actor.recupUsage(1) } }, two: { icon: '', label: "2 Points", callback: () => { this.actor.recupUsage(2) } }, three: { icon: '', label: "3 Points", callback: () => { this.actor.recupUsage(3) } }, cancel: { icon: '', label: "Annuler" } }, default: "one" }).render(true) } // #endregion }