/** * Célestopol 1922 — Système FoundryVTT * * Célestopol 1922 est un jeu de rôle édité par Antre-Monde Éditions. * Ce système FoundryVTT est une implémentation indépendante et n'est pas * affilié à Antre-Monde Éditions, * mais a été réalisé avec l'autorisation d'Antre-Monde Éditions. * * @author LeRatierBretonnien * @copyright 2025–2026 LeRatierBretonnien * @license CC BY-NC-SA 4.0 – https://creativecommons.org/licenses/by-nc-sa/4.0/ */ import { SYSTEM } from "../../config/system.mjs" const { HandlebarsApplicationMixin } = foundry.applications.api export default class CelestopolActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) { static SHEET_MODES = { EDIT: 0, PLAY: 1 } constructor(options = {}) { super(options) this.#dragDrop = this.#createDragDropHandlers() } #dragDrop /** @override */ static DEFAULT_OPTIONS = { classes: ["fvtt-celestopol", "actor"], position: { width: 900, height: "auto" }, form: { submitOnChange: true }, window: { resizable: true }, dragDrop: [{ dragSelector: '[data-drag="true"], .rollable', dropSelector: null }], actions: { editImage: CelestopolActorSheet.#onEditImage, sendBiographyPortrait: CelestopolActorSheet.#onSendBiographyPortrait, toggleSheet: CelestopolActorSheet.#onToggleSheet, edit: CelestopolActorSheet.#onItemEdit, delete: CelestopolActorSheet.#onItemDelete, attack: CelestopolActorSheet.#onAttack, rangedDefense: CelestopolActorSheet.#onRangedDefense, trackBox: CelestopolActorSheet.#onTrackBox, skillLevel: CelestopolActorSheet.#onSkillLevel, factionLevel: CelestopolActorSheet.#onFactionLevel, toggleArmure: CelestopolActorSheet.#onToggleArmure, }, } _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 } /** @override */ async _prepareContext() { return { fields: this.document.schema.fields, systemFields: this.document.system.schema.fields, actor: this.document, system: this.document.system, source: this.document.toObject(), isGM: game.user.isGM, isEditMode: this.isEditMode, isPlayMode: this.isPlayMode, isEditable: this.isEditable, woundLevels: SYSTEM.WOUND_LEVELS, } } /** @override */ _onRender(context, options) { this.#dragDrop.forEach(d => d.bind(this.element)) this.element.querySelectorAll(".rollable").forEach(el => { el.addEventListener("click", this._onRoll.bind(this)) }) } async _onRoll(event) { if (!this.isPlayMode) return const el = event.currentTarget const statId = el.dataset.statId const skillId = el.dataset.skillId if (!statId) return if (!skillId) { // Test de résistance (clic sur la zone TR de la stat) await this.document.system.rollResistance(statId) return } await this.document.system.roll(statId, skillId) } #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 true } _onDragOver() {} async _onDrop(event) { if (!this.isEditable) return const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event) if (data.type === "Item") { const item = await fromUuid(data.uuid) if (item) return this._onDropItem(item) } } async _onDropItem(item) { if (item.type === "anomaly" && this.document.itemTypes.anomaly.length > 0) { ui.notifications.warn(game.i18n.localize("CELESTOPOL.Anomaly.maxAnomaly")) return } await this.document.createEmbeddedDocuments("Item", [item.toObject()], { renderSheet: false }) } static async #onEditImage(event, _target) { const current = this.document.img const fp = new FilePicker({ current, type: "image", callback: (path) => this.document.update({ img: path }), top: this.position.top + 40, left: this.position.left + 10, }) return fp.browse() } static async #onSendBiographyPortrait() { const portrait = this.document.system?.portraitImage || "" if (!portrait) { ui.notifications.warn(game.i18n.localize("CELESTOPOL.Actor.portraitImageMissing")) return } const rawContent = `
` await ChatMessage.create({ speaker: ChatMessage.getSpeaker({ actor: this.document }), style: CONST.CHAT_MESSAGE_STYLES.OTHER, content: await foundry.applications.ux.TextEditor.implementation.enrichHTML(rawContent, { async: true }), }) } static #onToggleSheet() { const modes = this.constructor.SHEET_MODES this._sheetMode = this.isEditMode ? modes.PLAY : modes.EDIT this.render() } static async #onItemEdit(event, target) { const uuid = target.getAttribute("data-item-uuid") const id = target.getAttribute("data-item-id") const item = uuid ? await fromUuid(uuid) : this.document.items.get(id) item?.sheet.render(true) } static async #onItemDelete(event, target) { const uuid = target.getAttribute("data-item-uuid") const item = await fromUuid(uuid) await item?.deleteDialog() } static async #onAttack(_event, target) { const itemId = target.getAttribute("data-item-id") if (!itemId) return await this.document.system.rollAttack(itemId) } static async #onRangedDefense(_event, target) { const itemId = target.getAttribute("data-item-id") if (!itemId) return await this.document.system.rollRangedDefense(itemId) } /** Met à jour une jauge de piste (blessures/destin/spleen) par clic sur une case. */ static #onTrackBox(_event, target) { if (!this.isEditable) return const path = target.dataset.path const index = parseInt(target.dataset.index) const current = foundry.utils.getProperty(this.document, path) ?? 0 const newValue = (index <= current) ? index - 1 : index this.document.update({ [path]: Math.max(0, newValue) }) } /** Met à jour la valeur d'un domaine par clic sur un point de niveau. */ static #onSkillLevel(_event, target) { if (!this.isEditable) return const { statId, skillId } = target.dataset const index = parseInt(target.dataset.index) const current = this.document.system.stats[statId]?.[skillId]?.value ?? 0 const newValue = (index <= current) ? index - 1 : index this.document.update({ [`system.stats.${statId}.${skillId}.value`]: Math.max(0, newValue) }) } /** Met à jour le score d'une faction par clic sur un point. */ static async #onToggleArmure(_event, target) { const uuid = target.closest('[data-item-uuid]')?.dataset.itemUuid if (!uuid) return const item = await fromUuid(uuid) if (item?.type === "armure") await item.update({ "system.equipped": !item.system.equipped }) } static #onFactionLevel(_event, target) { if (!this.isEditable) return const factionId = target.dataset.faction const index = parseInt(target.dataset.index) // 0-8 const newValue = index - 4 // -4 à +4 const current = this.document.system.factions[factionId]?.value ?? 0 // Cliquer sur le dot actif (sauf neutre) remet à 0 const finalValue = (newValue === current && newValue !== 0) ? 0 : newValue this.document.update({ [`system.factions.${factionId}.value`]: finalValue }) } }