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:
3
module/applications/_module.mjs
Normal file
3
module/applications/_module.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as CelestopolCharacterSheet } from "./sheets/character-sheet.mjs"
|
||||
export { default as CelestopolNPCSheet } from "./sheets/npc-sheet.mjs"
|
||||
export { CelestopolAnomalySheet, CelestopolAspectSheet, CelestopolAttributeSheet, CelestopolEquipmentSheet } from "./sheets/item-sheets.mjs"
|
||||
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
|
||||
}
|
||||
}
|
||||
130
module/config/system.mjs
Normal file
130
module/config/system.mjs
Normal file
@@ -0,0 +1,130 @@
|
||||
export const SYSTEM_ID = "fvtt-celestopol"
|
||||
|
||||
export const ASCII = `
|
||||
░█▀▀░█▀▀░█░░░█▀▀░█▀▀░▀█▀░█▀█░█▀█░█▀█░█░░
|
||||
░█░░░█▀▀░█░░░█▀▀░▀▀█░░█░░█░█░█▀▀░█░█░█░░
|
||||
░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░░▀░░▀▀▀░▀░░░▀▀▀░▀▀▀
|
||||
░░░░░░░░░░░░░░░░░░1922░░░░░░░░░░░░░░░░░░░
|
||||
`
|
||||
|
||||
/** Les 4 attributs principaux (stats). Chacun a une résistance (res) et 4 compétences. */
|
||||
export const STATS = {
|
||||
ame: { id: "ame", label: "CELESTOPOL.Stat.ame" },
|
||||
corps: { id: "corps", label: "CELESTOPOL.Stat.corps" },
|
||||
coeur: { id: "coeur", label: "CELESTOPOL.Stat.coeur" },
|
||||
esprit: { id: "esprit", label: "CELESTOPOL.Stat.esprit" },
|
||||
}
|
||||
|
||||
/** Compétences groupées par attribut. */
|
||||
export const SKILLS = {
|
||||
ame: {
|
||||
artifice: { id: "artifice", label: "CELESTOPOL.Skill.artifice", stat: "ame" },
|
||||
attraction: { id: "attraction", label: "CELESTOPOL.Skill.attraction", stat: "ame" },
|
||||
coercition: { id: "coercition", label: "CELESTOPOL.Skill.coercition", stat: "ame" },
|
||||
faveur: { id: "faveur", label: "CELESTOPOL.Skill.faveur", stat: "ame" },
|
||||
},
|
||||
corps: {
|
||||
echauffouree: { id: "echauffouree", label: "CELESTOPOL.Skill.echauffouree", stat: "corps" },
|
||||
effacement: { id: "effacement", label: "CELESTOPOL.Skill.effacement", stat: "corps" },
|
||||
mobilite: { id: "mobilite", label: "CELESTOPOL.Skill.mobilite", stat: "corps" },
|
||||
prouesse: { id: "prouesse", label: "CELESTOPOL.Skill.prouesse", stat: "corps" },
|
||||
},
|
||||
coeur: {
|
||||
appreciation: { id: "appreciation", label: "CELESTOPOL.Skill.appreciation", stat: "coeur" },
|
||||
arts: { id: "arts", label: "CELESTOPOL.Skill.arts", stat: "coeur" },
|
||||
inspiration: { id: "inspiration", label: "CELESTOPOL.Skill.inspiration", stat: "coeur" },
|
||||
traque: { id: "traque", label: "CELESTOPOL.Skill.traque", stat: "coeur" },
|
||||
},
|
||||
esprit: {
|
||||
instruction: { id: "instruction", label: "CELESTOPOL.Skill.instruction", stat: "esprit" },
|
||||
mtechnologique: { id: "mtechnologique", label: "CELESTOPOL.Skill.mtechnologique", stat: "esprit" },
|
||||
raisonnement: { id: "raisonnement", label: "CELESTOPOL.Skill.raisonnement", stat: "esprit" },
|
||||
traitement: { id: "traitement", label: "CELESTOPOL.Skill.traitement", stat: "esprit" },
|
||||
},
|
||||
}
|
||||
|
||||
/** Liste plate de toutes les compétences (utile pour les DataModels d'items). */
|
||||
export const ALL_SKILLS = Object.values(SKILLS).flatMap(group => Object.values(group))
|
||||
|
||||
/** Types d'anomalies (pouvoirs paranormaux). */
|
||||
export const ANOMALY_TYPES = {
|
||||
none: { id: "none", label: "CELESTOPOL.Anomaly.none" },
|
||||
entropie: { id: "entropie", label: "CELESTOPOL.Anomaly.entropie" },
|
||||
communicationaveclesmorts:{ id: "communicationaveclesmorts",label: "CELESTOPOL.Anomaly.communicationaveclesmorts" },
|
||||
illusion: { id: "illusion", label: "CELESTOPOL.Anomaly.illusion" },
|
||||
suggestion: { id: "suggestion", label: "CELESTOPOL.Anomaly.suggestion" },
|
||||
tarotdivinatoire: { id: "tarotdivinatoire", label: "CELESTOPOL.Anomaly.tarotdivinatoire" },
|
||||
telekinesie: { id: "telekinesie", label: "CELESTOPOL.Anomaly.telekinesie" },
|
||||
telepathie: { id: "telepathie", label: "CELESTOPOL.Anomaly.telepathie" },
|
||||
voyageastral: { id: "voyageastral", label: "CELESTOPOL.Anomaly.voyageastral" },
|
||||
}
|
||||
|
||||
/** Factions du monde de Célestopol. */
|
||||
export const FACTIONS = {
|
||||
pinkerton: { id: "pinkerton", label: "CELESTOPOL.Faction.pinkerton" },
|
||||
police: { id: "police", label: "CELESTOPOL.Faction.police" },
|
||||
okhrana: { id: "okhrana", label: "CELESTOPOL.Faction.okhrana" },
|
||||
lunanovatek: { id: "lunanovatek", label: "CELESTOPOL.Faction.lunanovatek" },
|
||||
oto: { id: "oto", label: "CELESTOPOL.Faction.oto" },
|
||||
syndicats: { id: "syndicats", label: "CELESTOPOL.Faction.syndicats" },
|
||||
vorovskoymir:{ id: "vorovskoymir",label: "CELESTOPOL.Faction.vorovskoymir" },
|
||||
cour: { id: "cour", label: "CELESTOPOL.Faction.cour" },
|
||||
}
|
||||
|
||||
/** Niveaux de blessures avec leur malus associé. */
|
||||
export const WOUND_LEVELS = [
|
||||
{ id: 0, label: "CELESTOPOL.Wound.none", malus: 0 },
|
||||
{ id: 1, label: "CELESTOPOL.Wound.anodin", malus: 0 },
|
||||
{ id: 2, label: "CELESTOPOL.Wound.derisoire", malus: 0 },
|
||||
{ id: 3, label: "CELESTOPOL.Wound.negligeable", malus: -1 },
|
||||
{ id: 4, label: "CELESTOPOL.Wound.superficiel", malus: -1 },
|
||||
{ id: 5, label: "CELESTOPOL.Wound.leger", malus: -2 },
|
||||
{ id: 6, label: "CELESTOPOL.Wound.modere", malus: -2 },
|
||||
{ id: 7, label: "CELESTOPOL.Wound.grave", malus: -3 },
|
||||
{ id: 8, label: "CELESTOPOL.Wound.dramatique", malus: -999 },
|
||||
]
|
||||
|
||||
/** Seuils de difficulté pour les jets de dés. */
|
||||
export const DIFFICULTY_CHOICES = {
|
||||
unknown: { id: "unknown", label: "CELESTOPOL.Difficulty.unknown", value: 0 },
|
||||
facile: { id: "facile", label: "CELESTOPOL.Difficulty.facile", value: 5 },
|
||||
normal: { id: "normal", label: "CELESTOPOL.Difficulty.normal", value: 7 },
|
||||
difficile:{ id: "difficile", label: "CELESTOPOL.Difficulty.difficile", value: 9 },
|
||||
ardu: { id: "ardu", label: "CELESTOPOL.Difficulty.ardu", value: 11 },
|
||||
extreme: { id: "extreme", label: "CELESTOPOL.Difficulty.extreme", value: 13 },
|
||||
}
|
||||
|
||||
/** Phases de la lune (dé de lune). */
|
||||
export const MOON_DICE_PHASES = {
|
||||
none: { id: "none", label: "CELESTOPOL.Moon.none", bonus: 0 },
|
||||
nouvellelune: { id: "nouvellelune", label: "CELESTOPOL.Moon.nouvellelune", bonus: 0 },
|
||||
premiercroissant: { id: "premiercroissant", label: "CELESTOPOL.Moon.premiercroissant", bonus: 1 },
|
||||
premierquartier: { id: "premierquartier", label: "CELESTOPOL.Moon.premierquartier", bonus: 1 },
|
||||
lunegibbeuse: { id: "lunegibbeuse", label: "CELESTOPOL.Moon.lunegibbeuse", bonus: 2 },
|
||||
lunevoutee: { id: "lunevoutee", label: "CELESTOPOL.Moon.lunevoutee", bonus: 2 },
|
||||
derniercroissant: { id: "derniercroissant", label: "CELESTOPOL.Moon.derniercroissant", bonus: 1 },
|
||||
dernierquartier: { id: "dernierquartier", label: "CELESTOPOL.Moon.dernierquartier", bonus: 1 },
|
||||
pleinelune: { id: "pleinelune", label: "CELESTOPOL.Moon.pleinelune", bonus: 3 },
|
||||
}
|
||||
|
||||
/** Types d'équipements. */
|
||||
export const EQUIPMENT_TYPES = {
|
||||
autre: { id: "autre", label: "CELESTOPOL.Equipment.autre" },
|
||||
arme: { id: "arme", label: "CELESTOPOL.Equipment.arme" },
|
||||
protection:{ id: "protection",label: "CELESTOPOL.Equipment.protection" },
|
||||
vehicule: { id: "vehicule", label: "CELESTOPOL.Equipment.vehicule" },
|
||||
}
|
||||
|
||||
export const SYSTEM = {
|
||||
id: SYSTEM_ID,
|
||||
ASCII,
|
||||
STATS,
|
||||
SKILLS,
|
||||
ALL_SKILLS,
|
||||
ANOMALY_TYPES,
|
||||
FACTIONS,
|
||||
WOUND_LEVELS,
|
||||
DIFFICULTY_CHOICES,
|
||||
MOON_DICE_PHASES,
|
||||
EQUIPMENT_TYPES,
|
||||
}
|
||||
4
module/documents/_module.mjs
Normal file
4
module/documents/_module.mjs
Normal 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"
|
||||
12
module/documents/actor.mjs
Normal file
12
module/documents/actor.mjs
Normal 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
|
||||
}
|
||||
}
|
||||
7
module/documents/chat-message.mjs
Normal file
7
module/documents/chat-message.mjs
Normal 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
11
module/documents/item.mjs
Normal 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
170
module/documents/roll.mjs
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
3
module/models/_module.mjs
Normal file
3
module/models/_module.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as CelestopolCharacter } from "./character.mjs"
|
||||
export { default as CelestopolNPC } from "./npc.mjs"
|
||||
export { CelestopolAnomaly, CelestopolAspect, CelestopolAttribute, CelestopolEquipment } from "./items.mjs"
|
||||
175
module/models/character.mjs
Normal file
175
module/models/character.mjs
Normal file
@@ -0,0 +1,175 @@
|
||||
import { SYSTEM } from "../config/system.mjs"
|
||||
|
||||
export default class CelestopolCharacter extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields
|
||||
const reqInt = { required: true, nullable: false, integer: true }
|
||||
const schema = {}
|
||||
|
||||
// Concept du personnage
|
||||
schema.concept = new fields.StringField({ required: true, nullable: false, initial: "" })
|
||||
|
||||
// Initiative (calculée mais stockée pour affichage)
|
||||
schema.initiative = new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
|
||||
|
||||
// Anomalie du personnage
|
||||
schema.anomaly = new fields.SchemaField({
|
||||
type: new fields.StringField({ required: true, nullable: false, initial: "none",
|
||||
choices: Object.keys(SYSTEM.ANOMALY_TYPES) }),
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
})
|
||||
|
||||
// Les 4 stats avec leurs compétences
|
||||
const skillField = (label) => new fields.SchemaField({
|
||||
label: new fields.StringField({ required: true, initial: label }),
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
})
|
||||
|
||||
const statField = (statId) => {
|
||||
const skills = SYSTEM.SKILLS[statId]
|
||||
const skillSchema = {}
|
||||
for (const [key, skill] of Object.entries(skills)) {
|
||||
skillSchema[key] = skillField(skill.label)
|
||||
}
|
||||
return new fields.SchemaField({
|
||||
label: new fields.StringField({ required: true, initial: SYSTEM.STATS[statId].label }),
|
||||
res: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
...skillSchema,
|
||||
})
|
||||
}
|
||||
|
||||
schema.stats = new fields.SchemaField({
|
||||
ame: statField("ame"),
|
||||
corps: statField("corps"),
|
||||
coeur: statField("coeur"),
|
||||
esprit: statField("esprit"),
|
||||
})
|
||||
|
||||
// Blessures (8 cases)
|
||||
const woundField = (idx) => new fields.SchemaField({
|
||||
checked: new fields.BooleanField({ required: true, initial: false }),
|
||||
malus: new fields.NumberField({ ...reqInt, initial: SYSTEM.WOUND_LEVELS[idx]?.malus ?? 0 }),
|
||||
})
|
||||
schema.blessures = new fields.SchemaField({
|
||||
lvl: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
b1: woundField(1), b2: woundField(2), b3: woundField(3), b4: woundField(4),
|
||||
b5: woundField(5), b6: woundField(6), b7: woundField(7), b8: woundField(8),
|
||||
})
|
||||
|
||||
// Destin (8 cases)
|
||||
const destField = () => new fields.SchemaField({
|
||||
checked: new fields.BooleanField({ required: true, initial: false }),
|
||||
})
|
||||
schema.destin = new fields.SchemaField({
|
||||
lvl: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
d1: destField(), d2: destField(), d3: destField(), d4: destField(),
|
||||
d5: destField(), d6: destField(), d7: destField(), d8: destField(),
|
||||
})
|
||||
|
||||
// Spleen (8 cases)
|
||||
schema.spleen = new fields.SchemaField({
|
||||
lvl: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
s1: destField(), s2: destField(), s3: destField(), s4: destField(),
|
||||
s5: destField(), s6: destField(), s7: destField(), s8: destField(),
|
||||
})
|
||||
|
||||
// Attributs de personnage (Entregent, Fortune, Rêve, Vision)
|
||||
const persoAttrField = () => new fields.SchemaField({
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||
max: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||
})
|
||||
schema.attributs = new fields.SchemaField({
|
||||
entregent: persoAttrField(),
|
||||
fortune: persoAttrField(),
|
||||
reve: persoAttrField(),
|
||||
vision: persoAttrField(),
|
||||
})
|
||||
|
||||
// Factions
|
||||
const factionField = () => new fields.SchemaField({
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0 }),
|
||||
})
|
||||
schema.factions = new fields.SchemaField({
|
||||
pinkerton: factionField(),
|
||||
police: factionField(),
|
||||
okhrana: factionField(),
|
||||
lunanovatek: factionField(),
|
||||
oto: factionField(),
|
||||
syndicats: factionField(),
|
||||
vorovskoymir: factionField(),
|
||||
cour: factionField(),
|
||||
perso1: new fields.SchemaField({
|
||||
label: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0 }),
|
||||
}),
|
||||
perso2: new fields.SchemaField({
|
||||
label: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0 }),
|
||||
}),
|
||||
})
|
||||
|
||||
// Préférences de jet (mémorisé entre sessions)
|
||||
schema.prefs = new fields.SchemaField({
|
||||
moonPhase: new fields.StringField({ required: true, nullable: false, initial: "none" }),
|
||||
difficulty: new fields.StringField({ required: true, nullable: false, initial: "normal" }),
|
||||
})
|
||||
|
||||
// Description & notes
|
||||
schema.description = new fields.HTMLField({ required: true, textSearch: true })
|
||||
schema.notes = new fields.HTMLField({ required: true, textSearch: true })
|
||||
|
||||
// Données biographiques
|
||||
schema.biodata = new fields.SchemaField({
|
||||
age: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
genre: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
taille: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
yeux: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
naissance: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
cheveux: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
origine: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
})
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
static LOCALIZATION_PREFIXES = ["CELESTOPOL.Character"]
|
||||
|
||||
prepareDerivedData() {
|
||||
super.prepareDerivedData()
|
||||
// L'initiative est basée sur la résistance Corps
|
||||
this.initiative = this.stats.corps.res
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule le malus de blessures actif.
|
||||
* @returns {number}
|
||||
*/
|
||||
getWoundMalus() {
|
||||
const lvl = Math.max(0, Math.min(8, this.blessures.lvl))
|
||||
return SYSTEM.WOUND_LEVELS[lvl]?.malus ?? 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Lance les dés pour une compétence donnée.
|
||||
* @param {string} statId - Id de la stat (ame, corps, coeur, esprit)
|
||||
* @param {string} skillId - Id de la compétence
|
||||
*/
|
||||
async roll(statId, skillId) {
|
||||
const { CelestopolRoll } = await import("../documents/roll.mjs")
|
||||
const skill = this.stats[statId][skillId]
|
||||
if (!skill) return null
|
||||
|
||||
return CelestopolRoll.prompt({
|
||||
actorId: this.parent.id,
|
||||
actorName: this.parent.name,
|
||||
actorImage: this.parent.img,
|
||||
statId,
|
||||
skillId,
|
||||
skillLabel: skill.label,
|
||||
skillValue: skill.value,
|
||||
woundMalus: this.getWoundMalus(),
|
||||
moonPhase: this.prefs.moonPhase,
|
||||
difficulty: this.prefs.difficulty,
|
||||
})
|
||||
}
|
||||
}
|
||||
98
module/models/items.mjs
Normal file
98
module/models/items.mjs
Normal file
@@ -0,0 +1,98 @@
|
||||
import { SYSTEM } from "../config/system.mjs"
|
||||
|
||||
/** Schéma partagé pour les bonus/malus par compétence (utilisé dans anomaly/aspect/attribute). */
|
||||
function skillScoresSchema() {
|
||||
const fields = foundry.data.fields
|
||||
const reqInt = { required: true, nullable: false, integer: true }
|
||||
const scoreField = () => new fields.SchemaField({
|
||||
bonus: new fields.NumberField({ ...reqInt, initial: 0 }),
|
||||
malus: new fields.NumberField({ ...reqInt, initial: 0 }),
|
||||
})
|
||||
|
||||
const statGroup = (statId) => {
|
||||
const skills = SYSTEM.SKILLS[statId]
|
||||
const schema = {}
|
||||
for (const key of Object.keys(skills)) {
|
||||
schema[key] = scoreField()
|
||||
}
|
||||
return new fields.SchemaField(schema)
|
||||
}
|
||||
|
||||
return new fields.SchemaField({
|
||||
ame: statGroup("ame"),
|
||||
corps: statGroup("corps"),
|
||||
coeur: statGroup("coeur"),
|
||||
esprit: statGroup("esprit"),
|
||||
})
|
||||
}
|
||||
|
||||
export class CelestopolAnomaly extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields
|
||||
const reqInt = { required: true, nullable: false, integer: true }
|
||||
return {
|
||||
subtype: new fields.StringField({ required: true, nullable: false, initial: "none",
|
||||
choices: Object.keys(SYSTEM.ANOMALY_TYPES) }),
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
reference: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
scores: skillScoresSchema(),
|
||||
description: new fields.HTMLField({ required: true, textSearch: true }),
|
||||
technique: new fields.HTMLField({ required: true, textSearch: true }),
|
||||
narratif: new fields.HTMLField({ required: true, textSearch: true }),
|
||||
notes: new fields.HTMLField({ required: true, textSearch: true }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class CelestopolAspect extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields
|
||||
const reqInt = { required: true, nullable: false, integer: true }
|
||||
return {
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
reference: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
scores: skillScoresSchema(),
|
||||
description: new fields.HTMLField({ required: true, textSearch: true }),
|
||||
technique: new fields.HTMLField({ required: true, textSearch: true }),
|
||||
narratif: new fields.HTMLField({ required: true, textSearch: true }),
|
||||
notes: new fields.HTMLField({ required: true, textSearch: true }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class CelestopolAttribute extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields
|
||||
const reqInt = { required: true, nullable: false, integer: true }
|
||||
return {
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
reference: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
scores: skillScoresSchema(),
|
||||
description: new fields.HTMLField({ required: true, textSearch: true }),
|
||||
technique: new fields.HTMLField({ required: true, textSearch: true }),
|
||||
narratif: new fields.HTMLField({ required: true, textSearch: true }),
|
||||
notes: new fields.HTMLField({ required: true, textSearch: true }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class CelestopolEquipment extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields
|
||||
const reqInt = { required: true, nullable: false, integer: true }
|
||||
return {
|
||||
subtype: new fields.StringField({ required: true, nullable: false, initial: "autre",
|
||||
choices: Object.keys(SYSTEM.EQUIPMENT_TYPES) }),
|
||||
reference: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
quantity: new fields.NumberField({ ...reqInt, initial: 1, min: 0 }),
|
||||
weight: new fields.NumberField({ required: true, nullable: false, initial: 0, min: 0 }),
|
||||
damage: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
range: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
speed: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
protection: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
crew: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
description:new fields.HTMLField({ required: true, textSearch: true }),
|
||||
notes: new fields.HTMLField({ required: true, textSearch: true }),
|
||||
}
|
||||
}
|
||||
}
|
||||
99
module/models/npc.mjs
Normal file
99
module/models/npc.mjs
Normal file
@@ -0,0 +1,99 @@
|
||||
import { SYSTEM } from "../config/system.mjs"
|
||||
|
||||
export default class CelestopolNPC extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields
|
||||
const reqInt = { required: true, nullable: false, integer: true }
|
||||
const schema = {}
|
||||
|
||||
schema.concept = new fields.StringField({ required: true, nullable: false, initial: "" })
|
||||
schema.initiative = new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
|
||||
|
||||
schema.anomaly = new fields.SchemaField({
|
||||
type: new fields.StringField({ required: true, nullable: false, initial: "none",
|
||||
choices: Object.keys(SYSTEM.ANOMALY_TYPES) }),
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
})
|
||||
|
||||
const skillField = (label) => new fields.SchemaField({
|
||||
label: new fields.StringField({ required: true, initial: label }),
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
})
|
||||
|
||||
const statField = (statId) => {
|
||||
const skills = SYSTEM.SKILLS[statId]
|
||||
const skillSchema = {}
|
||||
for (const [key, skill] of Object.entries(skills)) {
|
||||
skillSchema[key] = skillField(skill.label)
|
||||
}
|
||||
return new fields.SchemaField({
|
||||
label: new fields.StringField({ required: true, initial: SYSTEM.STATS[statId].label }),
|
||||
res: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
actuel: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }), // res + wound malus
|
||||
...skillSchema,
|
||||
})
|
||||
}
|
||||
|
||||
schema.stats = new fields.SchemaField({
|
||||
ame: statField("ame"),
|
||||
corps: statField("corps"),
|
||||
coeur: statField("coeur"),
|
||||
esprit: statField("esprit"),
|
||||
})
|
||||
|
||||
const woundField = (idx) => new fields.SchemaField({
|
||||
checked: new fields.BooleanField({ required: true, initial: false }),
|
||||
malus: new fields.NumberField({ ...reqInt, initial: SYSTEM.WOUND_LEVELS[idx]?.malus ?? 0 }),
|
||||
})
|
||||
schema.blessures = new fields.SchemaField({
|
||||
lvl: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
b1: woundField(1), b2: woundField(2), b3: woundField(3), b4: woundField(4),
|
||||
b5: woundField(5), b6: woundField(6), b7: woundField(7), b8: woundField(8),
|
||||
})
|
||||
|
||||
schema.prefs = new fields.SchemaField({
|
||||
moonPhase: new fields.StringField({ required: true, nullable: false, initial: "none" }),
|
||||
difficulty: new fields.StringField({ required: true, nullable: false, initial: "normal" }),
|
||||
})
|
||||
|
||||
schema.description = new fields.HTMLField({ required: true, textSearch: true })
|
||||
schema.notes = new fields.HTMLField({ required: true, textSearch: true })
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
static LOCALIZATION_PREFIXES = ["CELESTOPOL.NPC"]
|
||||
|
||||
prepareDerivedData() {
|
||||
super.prepareDerivedData()
|
||||
const malus = this.getWoundMalus()
|
||||
this.initiative = Math.max(0, this.stats.corps.res + malus)
|
||||
for (const stat of Object.values(this.stats)) {
|
||||
stat.actuel = Math.max(0, stat.res + malus)
|
||||
}
|
||||
}
|
||||
|
||||
getWoundMalus() {
|
||||
const lvl = Math.max(0, Math.min(8, this.blessures.lvl))
|
||||
return SYSTEM.WOUND_LEVELS[lvl]?.malus ?? 0
|
||||
}
|
||||
|
||||
async roll(statId, skillId) {
|
||||
const { CelestopolRoll } = await import("../documents/roll.mjs")
|
||||
const skill = this.stats[statId][skillId]
|
||||
if (!skill) return null
|
||||
|
||||
return CelestopolRoll.prompt({
|
||||
actorId: this.parent.id,
|
||||
actorName: this.parent.name,
|
||||
actorImage: this.parent.img,
|
||||
statId,
|
||||
skillId,
|
||||
skillLabel: skill.label,
|
||||
skillValue: skill.value,
|
||||
woundMalus: this.getWoundMalus(),
|
||||
moonPhase: this.prefs.moonPhase,
|
||||
difficulty: this.prefs.difficulty,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user