209 lines
7.2 KiB
JavaScript
209 lines
7.2 KiB
JavaScript
/**
|
||
* 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,
|
||
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 #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 })
|
||
}
|
||
}
|