import { onManageActiveEffect } from "../../system/effects.mjs" const { HandlebarsApplicationMixin } = foundry.applications.api /** * Fiche de base pour tous les acteurs Vermine 2047 (ApplicationV2). * Remplace VermineActorSheet (AppV1). */ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) { // ── Mode édition / jeu ────────────────────────────────────────────── static SHEET_MODES = { EDIT: 0, PLAY: 1 } _sheetMode = this.constructor.SHEET_MODES.PLAY get isPlayMode() { return this._sheetMode === this.constructor.SHEET_MODES.PLAY } get isEditMode() { return this._sheetMode === this.constructor.SHEET_MODES.EDIT } // ── Options par défaut ────────────────────────────────────────────── static DEFAULT_OPTIONS = { classes: ["vermine2047", "actor"], position: { width: 800, height: "auto" }, form: { submitOnChange: true }, window: { resizable: true }, dragDrop: [{ dragSelector: ".item", dropSelector: null }], actions: { editImage: VermineBaseActorSheet.#onEditImage, toggleSheet: VermineBaseActorSheet.#onToggleSheet, edit: VermineBaseActorSheet.#onItemEdit, delete: VermineBaseActorSheet.#onItemDelete, create: VermineBaseActorSheet.#onItemCreate, roll: VermineBaseActorSheet.#onRollItem, clickRadio: VermineBaseActorSheet.#onClickRadioHexa, effectControl: VermineBaseActorSheet.#onEffectControl, chooseTotem: VermineBaseActorSheet.#onChooseTotem } } // ── Drag & Drop ───────────────────────────────────────────────────── #dragDrop constructor(options = {}) { super(options) this.#dragDrop = this.#createDragDropHandlers() } #createDragDropHandlers() { return this.options.dragDrop.map(d => { d.permissions = { dragstart: this._canDragStart.bind(this), drop: this._canDragDrop.bind(this) } d.callbacks = { dragover: this._onDragOver.bind(this), drop: this._onDrop.bind(this) } return new foundry.applications.ux.DragDrop.implementation(d) }) } _canDragStart() { return this.isEditable } _canDragDrop() { return this.isEditable } // ── Soumission du formulaire ──────────────────────────────────────── /** @override - coerce string values from HTML form inputs to numbers */ _prepareSubmitData(event, form, formData, updateData) { const fd = foundry.utils.deepClone(formData.object) for (const [key, value] of Object.entries(fd)) { if (!key.startsWith("system.") || typeof value === "number") continue const segments = key.slice(7).split(".") let node = this.document.system.schema for (const seg of segments) { if (node instanceof foundry.data.fields.SchemaField) node = node.fields[seg] else { node = undefined; break } } if (!(node instanceof foundry.data.fields.NumberField)) continue // Handle arrays from duplicate-named form inputs let raw = Array.isArray(value) ? value.filter(v => v !== "" && v !== null).pop() : value if (raw === undefined) continue if (typeof raw === "string" && raw.trim() === "") { fd[key] = 0; continue } const num = Number(typeof raw === "string" ? raw.trim() : raw) if (!isNaN(num)) fd[key] = num } return fd } // ── Contexte commun ───────────────────────────────────────────────── async _prepareContext() { const enrich = async (path) => { const val = foundry.utils.getProperty(this.document.system, path); return val ? await foundry.applications.ux.TextEditor.implementation.enrichHTML(val, { async: true }) : ""; }; return { fields: this.document.schema.fields, systemFields: this.document.system.schema.fields, actor: this.document, system: this.document.system, source: this.document.toObject(), config: CONFIG.VERMINE, rollData: this.document.getRollData(), isGM: game.user.isGM, isEditMode: this.isEditMode, isPlayMode: this.isPlayMode, isEditable: this.isEditable, enrichedNotes: await enrich("identity.notes"), enrichedBiography: await enrich("identity.biography"), enrichedRelations: await enrich("identity.relations") } } // ── Rendu ─────────────────────────────────────────────────────────── async _onRoll(event) { event.preventDefault() const el = event.currentTarget const type = el.dataset.type const label = el.dataset.label if (!type || !label) return const { default: RollDialog } = await import("../../system/dialogs/rollDialog.mjs") const dialog = await RollDialog.create({ actorId: this.document.id, rolltype: type, label }) if (dialog) dialog.render(true) } // ── Actions ───────────────────────────────────────────────────────── _onRender(context, options) { super._onRender(context, options) // Activate initial tabs (force to bypass changeTab's early-return when the // tab is already set as active in tabGroups — Foundry v12 doesn't call // changeTab on initial render, so the active class is never applied) for (const [group, tab] of Object.entries(this.tabGroups ?? {})) { this.changeTab(tab, group, {force: true}) } // Move toggle from hidden main tab to visible position (only for sheets where // .tab.main is not already displayed as a permanent sidebar via !important) const mainTab = this.element.querySelector(".tab.main") const tabs = this.element.querySelector('nav.tabs[data-application-part="tabs"]') if ( mainTab && tabs && getComputedStyle(mainTab).display === "none" ) { const existing = tabs.parentNode.querySelector('.sheet-header-toggle[data-moved]') if (existing) existing.remove() const toggle = mainTab.querySelector(".sheet-header-toggle") if (toggle) { toggle.dataset.moved = "true" tabs.parentNode.insertBefore(toggle, tabs) } } this.#dragDrop.forEach(d => d.bind(this.element)) this.element.querySelectorAll(".rollable").forEach(el => { el.addEventListener("click", this._onRoll.bind(this)) }) // Auto-fill empty number inputs on change to prevent validation errors this.element.addEventListener("change", e => { const input = e.target if (input?.type === "number" && !input.value && input.name && input !== document.activeElement) { input.value = "0" } }, { capture: true }) } /** @override */ async _onDropItem(event, item) { const doc = item instanceof foundry.abstract.Document ? item : await fromUuid(item.uuid) if (!doc) return const itemData = doc.toObject() await this.document.createEmbeddedDocuments("Item", [itemData], { renderSheet: false }) } static #onToggleSheet() { const modes = this.constructor.SHEET_MODES this._sheetMode = this.isEditMode ? modes.PLAY : modes.EDIT this.render() } static async #onEditImage(event, target) { const attr = target.dataset.edit ?? "img" const current = foundry.utils.getProperty(this.document, attr) const fp = new FilePicker({ current, type: "image", callback: (path) => this.document.update({ [attr]: path }), top: this.position.top + 40, left: this.position.left + 10 }) return fp.browse() } static async #onItemEdit(event, target) { const id = target.closest("[data-item-id]")?.dataset?.itemId const uuid = target.closest("[data-item-uuid]")?.dataset?.itemUuid let item if (uuid) item = await fromUuid(uuid) if (!item) item = this.document.items.get(id) item?.sheet.render(true) } static async #onItemDelete(event, target) { const itemUuid = target.closest("[data-item-uuid]")?.dataset?.itemUuid if (itemUuid) { const item = await fromUuid(itemUuid) await item?.deleteDialog() return } const id = target.closest("[data-item-id]")?.dataset?.itemId const item = this.document.items.get(id) await item?.deleteDialog() } static async #onItemCreate(event, target) { const type = target.dataset.type if (!type) return const name = game.i18n.localize("ITEMS.new_" + type) await this.document.createEmbeddedDocuments("Item", [{ name, type }]) } static async #onRollItem(event, target) { const id = target.closest("[data-item-id]")?.dataset?.itemId if (!id) return const item = this.document.items.get(id) item?.roll() } static #onClickRadioHexa(event, target) { event.preventDefault() event.stopPropagation() const input = target const update = {} let current = this.document const propTree = input.name.split(".") for (const prop of propTree) { current = current[prop] } if (current != input.value) { update[input.name] = parseInt(input.value) } else { update[input.name] = parseInt(input.value) - 1 } this.document.update(update) } static #onEffectControl(event, target) { onManageActiveEffect(event, this.document) } static async #onChooseTotem(event, target) { const { TotemPicker } = await import("../../system/applications.mjs") new TotemPicker(target, this.document).render(true) } }