feat: implémentation complète du système Célestopol 1922 pour FoundryVTT v13
- DataModels (character, npc, anomaly, aspect, attribute, equipment) - ApplicationV2 sheets (character 5 tabs, npc 3 tabs, 4 item sheets) - DialogV2 pour les jets de dés avec phase de lune - Templates Handlebars complets (fiches PJ/PNJ, items, jet, chat) - Styles LESS → CSS compilé (thème vert foncé / orange CopaseticNF) - i18n fr.json complet (clés CELESTOPOL.*) - Point d'entrée fvtt-celestopol.mjs avec hooks init/ready - Assets : polices CopaseticNF, images UI, icônes items - Mise à jour copilot-instructions.md avec l'architecture réelle Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
126
module/applications/sheets/base-actor-sheet.mjs
Normal file
126
module/applications/sheets/base-actor-sheet.mjs
Normal file
@@ -0,0 +1,126 @@
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
_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(),
|
||||
isEditMode: this.isEditMode,
|
||||
isPlayMode: this.isPlayMode,
|
||||
isEditable: this.isEditable,
|
||||
}
|
||||
}
|
||||
|
||||
/** @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 || !skillId) 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) {
|
||||
await this.document.createEmbeddedDocuments("Item", [item.toObject()], { renderSheet: false })
|
||||
}
|
||||
|
||||
static async #onEditImage(event, target) {
|
||||
const attr = target.dataset.edit
|
||||
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 #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()
|
||||
}
|
||||
}
|
||||
39
module/applications/sheets/base-item-sheet.mjs
Normal file
39
module/applications/sheets/base-item-sheet.mjs
Normal file
@@ -0,0 +1,39 @@
|
||||
const { HandlebarsApplicationMixin } = foundry.applications.api
|
||||
|
||||
export default class CelestopolItemSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ItemSheetV2) {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["fvtt-celestopol", "item"],
|
||||
position: { width: 580, height: "auto" },
|
||||
form: { submitOnChange: true },
|
||||
window: { resizable: true },
|
||||
actions: {
|
||||
editImage: CelestopolItemSheet.#onEditImage,
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
return {
|
||||
fields: this.document.schema.fields,
|
||||
systemFields: this.document.system.schema.fields,
|
||||
item: this.document,
|
||||
system: this.document.system,
|
||||
source: this.document.toObject(),
|
||||
isEditable: this.isEditable,
|
||||
}
|
||||
}
|
||||
|
||||
static async #onEditImage(event, target) {
|
||||
const attr = target.dataset.edit
|
||||
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()
|
||||
}
|
||||
}
|
||||
116
module/applications/sheets/character-sheet.mjs
Normal file
116
module/applications/sheets/character-sheet.mjs
Normal file
@@ -0,0 +1,116 @@
|
||||
import CelestopolActorSheet from "./base-actor-sheet.mjs"
|
||||
import { SYSTEM } from "../../config/system.mjs"
|
||||
|
||||
export default class CelestopolCharacterSheet extends CelestopolActorSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["character"],
|
||||
position: { width: 920, height: 660 },
|
||||
window: { contentClasses: ["character-content"] },
|
||||
actions: {
|
||||
createAnomaly: CelestopolCharacterSheet.#onCreateAnomaly,
|
||||
createAspect: CelestopolCharacterSheet.#onCreateAspect,
|
||||
createAttribute: CelestopolCharacterSheet.#onCreateAttribute,
|
||||
createEquipment: CelestopolCharacterSheet.#onCreateEquipment,
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
main: { template: "systems/fvtt-celestopol/templates/character-main.hbs" },
|
||||
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||
competences:{ template: "systems/fvtt-celestopol/templates/character-competences.hbs" },
|
||||
blessures: { template: "systems/fvtt-celestopol/templates/character-blessures.hbs" },
|
||||
factions: { template: "systems/fvtt-celestopol/templates/character-factions.hbs" },
|
||||
biography: { template: "systems/fvtt-celestopol/templates/character-biography.hbs" },
|
||||
}
|
||||
|
||||
tabGroups = { sheet: "competences" }
|
||||
|
||||
#getTabs() {
|
||||
const tabs = {
|
||||
competences:{ id: "competences", group: "sheet", icon: "fa-solid fa-dice-d6", label: "CELESTOPOL.Tab.competences" },
|
||||
blessures: { id: "blessures", group: "sheet", icon: "fa-solid fa-heart-crack", label: "CELESTOPOL.Tab.blessures" },
|
||||
factions: { id: "factions", group: "sheet", icon: "fa-solid fa-flag", label: "CELESTOPOL.Tab.factions" },
|
||||
biography: { id: "biography", group: "sheet", icon: "fa-solid fa-book", label: "CELESTOPOL.Tab.biography" },
|
||||
}
|
||||
for (const v of Object.values(tabs)) {
|
||||
v.active = this.tabGroups[v.group] === v.id
|
||||
v.cssClass = v.active ? "active" : ""
|
||||
}
|
||||
return tabs
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
context.stats = SYSTEM.STATS
|
||||
context.skills = SYSTEM.SKILLS
|
||||
context.anomalyTypes = SYSTEM.ANOMALY_TYPES
|
||||
context.factions = SYSTEM.FACTIONS
|
||||
context.woundLevels = SYSTEM.WOUND_LEVELS
|
||||
return context
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _preparePartContext(partId, context) {
|
||||
context.systemFields = this.document.system.schema.fields
|
||||
const doc = this.document
|
||||
|
||||
switch (partId) {
|
||||
case "main":
|
||||
break
|
||||
|
||||
case "competences":
|
||||
context.tab = context.tabs.competences
|
||||
context.anomalies = doc.itemTypes.anomaly
|
||||
context.aspects = doc.itemTypes.aspect
|
||||
context.attributes = doc.itemTypes.attribute
|
||||
break
|
||||
|
||||
case "blessures":
|
||||
context.tab = context.tabs.blessures
|
||||
break
|
||||
|
||||
case "factions":
|
||||
context.tab = context.tabs.factions
|
||||
break
|
||||
|
||||
case "biography":
|
||||
context.tab = context.tabs.biography
|
||||
context.equipments = doc.itemTypes.equipment
|
||||
context.equipments.sort((a, b) => a.name.localeCompare(b.name))
|
||||
context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
doc.system.description, { async: true })
|
||||
context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
doc.system.notes, { async: true })
|
||||
break
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
static #onCreateAnomaly() {
|
||||
this.document.createEmbeddedDocuments("Item", [{
|
||||
name: game.i18n.localize("CELESTOPOL.Item.newAnomaly"), type: "anomaly",
|
||||
}])
|
||||
}
|
||||
|
||||
static #onCreateAspect() {
|
||||
this.document.createEmbeddedDocuments("Item", [{
|
||||
name: game.i18n.localize("CELESTOPOL.Item.newAspect"), type: "aspect",
|
||||
}])
|
||||
}
|
||||
|
||||
static #onCreateAttribute() {
|
||||
this.document.createEmbeddedDocuments("Item", [{
|
||||
name: game.i18n.localize("CELESTOPOL.Item.newAttribute"), type: "attribute",
|
||||
}])
|
||||
}
|
||||
|
||||
static #onCreateEquipment() {
|
||||
this.document.createEmbeddedDocuments("Item", [{
|
||||
name: game.i18n.localize("CELESTOPOL.Item.newEquipment"), type: "equipment",
|
||||
}])
|
||||
}
|
||||
}
|
||||
83
module/applications/sheets/item-sheets.mjs
Normal file
83
module/applications/sheets/item-sheets.mjs
Normal file
@@ -0,0 +1,83 @@
|
||||
import CelestopolItemSheet from "./base-item-sheet.mjs"
|
||||
import { SYSTEM } from "../../config/system.mjs"
|
||||
|
||||
export class CelestopolAnomalySheet extends CelestopolItemSheet {
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["anomaly"],
|
||||
position: { width: 620, height: 560 },
|
||||
}
|
||||
static PARTS = {
|
||||
main: { template: "systems/fvtt-celestopol/templates/anomaly.hbs" },
|
||||
}
|
||||
async _prepareContext() {
|
||||
const ctx = await super._prepareContext()
|
||||
ctx.anomalyTypes = SYSTEM.ANOMALY_TYPES
|
||||
ctx.skills = SYSTEM.SKILLS
|
||||
ctx.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
this.document.system.description, { async: true })
|
||||
ctx.enrichedTechnique = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
this.document.system.technique, { async: true })
|
||||
ctx.enrichedNarratif = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
this.document.system.narratif, { async: true })
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
|
||||
export class CelestopolAspectSheet extends CelestopolItemSheet {
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["aspect"],
|
||||
position: { width: 620, height: 520 },
|
||||
}
|
||||
static PARTS = {
|
||||
main: { template: "systems/fvtt-celestopol/templates/aspect.hbs" },
|
||||
}
|
||||
async _prepareContext() {
|
||||
const ctx = await super._prepareContext()
|
||||
ctx.skills = SYSTEM.SKILLS
|
||||
ctx.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
this.document.system.description, { async: true })
|
||||
ctx.enrichedTechnique = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
this.document.system.technique, { async: true })
|
||||
ctx.enrichedNarratif = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
this.document.system.narratif, { async: true })
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
|
||||
export class CelestopolAttributeSheet extends CelestopolItemSheet {
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["attribute"],
|
||||
position: { width: 620, height: 520 },
|
||||
}
|
||||
static PARTS = {
|
||||
main: { template: "systems/fvtt-celestopol/templates/attribute.hbs" },
|
||||
}
|
||||
async _prepareContext() {
|
||||
const ctx = await super._prepareContext()
|
||||
ctx.skills = SYSTEM.SKILLS
|
||||
ctx.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
this.document.system.description, { async: true })
|
||||
ctx.enrichedTechnique = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
this.document.system.technique, { async: true })
|
||||
ctx.enrichedNarratif = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
this.document.system.narratif, { async: true })
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
|
||||
export class CelestopolEquipmentSheet extends CelestopolItemSheet {
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["equipment"],
|
||||
position: { width: 540, height: 420 },
|
||||
}
|
||||
static PARTS = {
|
||||
main: { template: "systems/fvtt-celestopol/templates/equipment.hbs" },
|
||||
}
|
||||
async _prepareContext() {
|
||||
const ctx = await super._prepareContext()
|
||||
ctx.equipmentTypes = SYSTEM.EQUIPMENT_TYPES
|
||||
ctx.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
this.document.system.description, { async: true })
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
57
module/applications/sheets/npc-sheet.mjs
Normal file
57
module/applications/sheets/npc-sheet.mjs
Normal file
@@ -0,0 +1,57 @@
|
||||
import CelestopolActorSheet from "./base-actor-sheet.mjs"
|
||||
import { SYSTEM } from "../../config/system.mjs"
|
||||
|
||||
export default class CelestopolNPCSheet extends CelestopolActorSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["npc"],
|
||||
position: { width: 760, height: 600 },
|
||||
window: { contentClasses: ["npc-content"] },
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
main: { template: "systems/fvtt-celestopol/templates/npc-main.hbs" },
|
||||
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||
competences:{ template: "systems/fvtt-celestopol/templates/npc-competences.hbs" },
|
||||
blessures: { template: "systems/fvtt-celestopol/templates/npc-blessures.hbs" },
|
||||
}
|
||||
|
||||
tabGroups = { sheet: "competences" }
|
||||
|
||||
#getTabs() {
|
||||
const tabs = {
|
||||
competences:{ id: "competences", group: "sheet", icon: "fa-solid fa-dice-d6", label: "CELESTOPOL.Tab.competences" },
|
||||
blessures: { id: "blessures", group: "sheet", icon: "fa-solid fa-heart-crack", label: "CELESTOPOL.Tab.blessures" },
|
||||
}
|
||||
for (const v of Object.values(tabs)) {
|
||||
v.active = this.tabGroups[v.group] === v.id
|
||||
v.cssClass = v.active ? "active" : ""
|
||||
}
|
||||
return tabs
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
context.stats = SYSTEM.STATS
|
||||
context.skills = SYSTEM.SKILLS
|
||||
context.woundLevels = SYSTEM.WOUND_LEVELS
|
||||
return context
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _preparePartContext(partId, context) {
|
||||
context.systemFields = this.document.system.schema.fields
|
||||
switch (partId) {
|
||||
case "competences":
|
||||
context.tab = context.tabs.competences
|
||||
break
|
||||
case "blessures":
|
||||
context.tab = context.tabs.blessures
|
||||
break
|
||||
}
|
||||
return context
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user