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:
2026-03-28 09:28:34 +01:00
parent 446f9b7500
commit 64e23271df
54 changed files with 3433 additions and 4 deletions

View File

@@ -0,0 +1,4 @@
export { default as CelestopolActor } from "./actor.mjs"
export { default as CelestopolItem } from "./item.mjs"
export { default as CelestopolChatMessage } from "./chat-message.mjs"
export { CelestopolRoll } from "./roll.mjs"

View File

@@ -0,0 +1,12 @@
export default class CelestopolActor extends Actor {
/** @override */
prepareDerivedData() {
super.prepareDerivedData()
this.system.prepareDerivedData?.()
}
/** @override */
getRollData() {
return this.toObject(false).system
}
}

View File

@@ -0,0 +1,7 @@
export default class CelestopolChatMessage extends ChatMessage {
/** @override */
async getHTML() {
const html = await super.getHTML()
return html
}
}

11
module/documents/item.mjs Normal file
View File

@@ -0,0 +1,11 @@
export default class CelestopolItem extends Item {
/** @override */
prepareDerivedData() {
super.prepareDerivedData()
}
/** @override */
getRollData() {
return this.toObject(false).system
}
}

170
module/documents/roll.mjs Normal file
View File

@@ -0,0 +1,170 @@
import { SYSTEM } from "../config/system.mjs"
/**
* Système de dés de Célestopol 1922.
*
* Le jet de base est : (valeur compétence)d6 comparé à un seuil de difficulté.
* Le dé de lune ajoute un bonus selon la phase actuelle.
* Destin et Spleen modifient le nombre de dés.
*/
export class CelestopolRoll extends Roll {
static CHAT_TEMPLATE = "systems/fvtt-celestopol/templates/chat-message.hbs"
get resultType() { return this.options.resultType }
get isSuccess() { return this.resultType === "success" }
get isFailure() { return this.resultType === "failure" }
get actorId() { return this.options.actorId }
get actorName() { return this.options.actorName }
get actorImage() { return this.options.actorImage }
get skillLabel() { return this.options.skillLabel }
get difficulty() { return this.options.difficulty }
get moonBonus() { return this.options.moonBonus ?? 0 }
/**
* Ouvre le dialogue de configuration du jet via DialogV2 et exécute le jet.
* @param {object} options
* @returns {Promise<CelestopolRoll|null>}
*/
static async prompt(options = {}) {
const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
const fieldRollMode = new foundry.data.fields.StringField({
choices: rollModes,
blank: false,
default: "publicroll",
})
const dialogContext = {
actorName: options.actorName,
skillLabel: options.skillLabel,
skillValue: options.skillValue,
woundMalus: options.woundMalus ?? 0,
difficultyChoices:SYSTEM.DIFFICULTY_CHOICES,
moonPhaseChoices: SYSTEM.MOON_DICE_PHASES,
defaultDifficulty:options.difficulty ?? "normal",
defaultMoonPhase: options.moonPhase ?? "none",
rollModes,
fieldRollMode,
}
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-celestopol/templates/roll-dialog.hbs",
dialogContext
)
const title = `${game.i18n.localize("CELESTOPOL.Roll.title")}${game.i18n.localize(options.skillLabel ?? "")}`
const rollContext = await foundry.applications.api.DialogV2.wait({
window: { title },
classes: ["fvtt-celestopol", "roll-dialog"],
content,
buttons: [
{
label: game.i18n.localize("CELESTOPOL.Roll.roll"),
callback: (event, button) => {
return Array.from(button.form.elements).reduce((obj, input) => {
if (input.name) obj[input.name] = input.value
return obj
}, {})
},
},
],
rejectClose: false,
})
if (!rollContext) return null
const difficulty = rollContext.difficulty ?? "normal"
const diffConfig = SYSTEM.DIFFICULTY_CHOICES[difficulty] ?? SYSTEM.DIFFICULTY_CHOICES.normal
const moonPhase = rollContext.moonPhase ?? "none"
const moonConfig = SYSTEM.MOON_DICE_PHASES[moonPhase] ?? SYSTEM.MOON_DICE_PHASES.none
const modifier = parseInt(rollContext.modifier ?? 0) || 0
const woundMalus = options.woundMalus ?? 0
const skillValue = Math.max(0, (options.skillValue ?? 0) + woundMalus)
const nbDice = Math.max(1, skillValue)
const moonBonus = moonConfig.bonus ?? 0
const totalModifier = moonBonus + modifier
const formula = totalModifier !== 0
? `${nbDice}d6 + ${totalModifier}`
: `${nbDice}d6`
const rollData = {
...options,
difficulty,
difficultyValue: diffConfig.value,
moonPhase,
moonBonus,
modifier,
formula,
rollMode: rollContext.visibility ?? "publicroll",
}
const roll = new this(formula, {}, rollData)
await roll.evaluate()
roll.computeResult()
await roll.toMessage({}, { rollMode: rollData.rollMode })
// Mémoriser les préférences sur l'acteur
const actor = game.actors.get(options.actorId)
if (actor) {
await actor.update({
"system.prefs.moonPhase": moonPhase,
"system.prefs.difficulty": difficulty,
})
}
return roll
}
/** Détermine succès/échec selon le total vs le seuil. */
computeResult() {
const threshold = SYSTEM.DIFFICULTY_CHOICES[this.options.difficulty]?.value ?? 0
if (threshold === 0) {
this.options.resultType = "unknown"
} else if (this.total >= threshold) {
this.options.resultType = "success"
} else {
this.options.resultType = "failure"
}
}
/** @override */
async render(chatOptions = {}) {
const data = await this._getChatCardData(chatOptions.isPrivate)
return foundry.applications.handlebars.renderTemplate(this.constructor.CHAT_TEMPLATE, data)
}
async _getChatCardData(isPrivate) {
return {
css: [SYSTEM.id, "dice-roll"],
cssClass: [SYSTEM.id, "dice-roll"].join(" "),
actorId: this.actorId,
actingCharName: this.actorName,
actingCharImg: this.actorImage,
skillLabel: this.skillLabel,
formula: this.formula,
total: this.total,
resultType: this.resultType,
isSuccess: this.isSuccess,
isFailure: this.isFailure,
difficulty: this.options.difficulty,
difficultyValue:this.options.difficultyValue,
moonPhase: this.options.moonPhase,
moonBonus: this.moonBonus,
isPrivate,
tooltip: isPrivate ? "" : await this.getTooltip(),
results: this.dice[0]?.results ?? [],
}
}
/** @override */
async toMessage(messageData = {}, { rollMode, create = true } = {}) {
return super.toMessage(
{
flavor: `<strong>${game.i18n.localize(this.skillLabel ?? "")}</strong>`,
...messageData,
},
{ rollMode }
)
}
}