Files
vermine2047/module/applications/sheets/base-actor-sheet.mjs
T
uberwald 4ffaebf52b feat: lancer les armes via la boîte de dialogue avec compétence
Ajoute un champ compétence au modèle arme, un sélecteur dans la fiche
d'arme et ouvre la boîte de dialogue de jet pré-remplie au clic sur
l'icône dé (réservé aux acteurs disposant d'aptitudes/compétences).
2026-08-03 15:59:47 +02:00

274 lines
10 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)
if (!item) return
if (item.type === "weapon" && this.document.system.abilities && this.document.system.skills) {
const { default: RollDialog } = await import("../../system/dialogs/rollDialog.mjs")
const dialog = await RollDialog.create({
actorId: this.document.id,
rolltype: "skill",
label: item.system.skill
})
if (dialog) dialog.render(true)
return
}
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)
}
}