263 lines
9.8 KiB
JavaScript
263 lines
9.8 KiB
JavaScript
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)
|
|
}
|
|
}
|